diff --git a/app/shared/src/main/AndroidManifest.xml b/app/shared/src/main/AndroidManifest.xml
index dc8a97d32..db974ea69 100644
--- a/app/shared/src/main/AndroidManifest.xml
+++ b/app/shared/src/main/AndroidManifest.xml
@@ -10,6 +10,7 @@
+
diff --git a/app/src/main/java/com/topjohnwu/magisk/core/Const.kt b/app/src/main/java/com/topjohnwu/magisk/core/Const.kt
index 882a3c3ee..a8344db2e 100644
--- a/app/src/main/java/com/topjohnwu/magisk/core/Const.kt
+++ b/app/src/main/java/com/topjohnwu/magisk/core/Const.kt
@@ -35,7 +35,8 @@ object Const {
}
object ID {
- const val JOB_SERVICE_ID = 7
+ const val DOWNLOAD_JOB_ID = 6
+ const val CHECK_UPDATE_JOB_ID = 7
}
object Url {
diff --git a/app/src/main/java/com/topjohnwu/magisk/core/JobService.kt b/app/src/main/java/com/topjohnwu/magisk/core/JobService.kt
index 424176783..bfe770f10 100644
--- a/app/src/main/java/com/topjohnwu/magisk/core/JobService.kt
+++ b/app/src/main/java/com/topjohnwu/magisk/core/JobService.kt
@@ -1,5 +1,8 @@
package com.topjohnwu.magisk.core
+import android.annotation.SuppressLint
+import android.annotation.TargetApi
+import android.app.Notification
import android.app.job.JobInfo
import android.app.job.JobParameters
import android.app.job.JobScheduler
@@ -8,38 +11,79 @@ import androidx.core.content.getSystemService
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.core.base.BaseJobService
import com.topjohnwu.magisk.core.di.ServiceLocator
+import com.topjohnwu.magisk.core.download.DownloadManager
+import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.view.Notifications
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
+import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.util.concurrent.TimeUnit
class JobService : BaseJobService() {
- private val job = Job()
- private val svc get() = ServiceLocator.networkService
+ private var mSession: Session? = null
+ private var mDm: DownloadManager? = null
- override fun onStartJob(params: JobParameters): Boolean {
- val coroutineScope = CoroutineScope(Dispatchers.IO + job)
- coroutineScope.launch {
- doWork()
+ @TargetApi(value = 34)
+ inner class Session(
+ var params: JobParameters
+ ) : DownloadManager.Session {
+
+ override val context get() = this@JobService
+
+ override fun attach(id: Int, builder: Notification.Builder) {
+ setNotification(params, id, builder.build(), JOB_END_NOTIFICATION_POLICY_REMOVE)
+ }
+
+ override fun stop() {
jobFinished(params, false)
}
- return false
}
- private suspend fun doWork() {
- svc.fetchUpdate()?.let {
- Info.remote = it
- if (Info.env.isActive && BuildConfig.VERSION_CODE < it.magisk.versionCode)
- Notifications.updateAvailable()
+ @SuppressLint("NewApi")
+ override fun onStartJob(params: JobParameters): Boolean {
+ return when (params.jobId) {
+ Const.ID.CHECK_UPDATE_JOB_ID -> checkUpdate(params)
+ Const.ID.DOWNLOAD_JOB_ID -> downloadFile(params)
+ else -> false
}
}
- override fun onStopJob(params: JobParameters): Boolean {
- job.cancel()
- return false
+ override fun onStopJob(params: JobParameters?) = false
+
+ @TargetApi(value = 34)
+ private fun downloadFile(params: JobParameters): Boolean {
+ params.transientExtras.classLoader = Subject::class.java.classLoader
+ val subject = params.transientExtras
+ .getParcelable(DownloadManager.SUBJECT_KEY, Subject::class.java) ?:
+ return false
+
+ val session = mSession?.also {
+ it.params = params
+ } ?: run {
+ Session(params).also { mSession = it }
+ }
+
+ val dm = mDm?.also {
+ it.reattach()
+ } ?: run {
+ DownloadManager(session).also { mDm = it }
+ }
+
+ dm.download(subject)
+ return true
+ }
+
+ private fun checkUpdate(params: JobParameters): Boolean {
+ GlobalScope.launch(Dispatchers.IO) {
+ ServiceLocator.networkService.fetchUpdate()?.let {
+ Info.remote = it
+ if (Info.env.isActive && BuildConfig.VERSION_CODE < it.magisk.versionCode)
+ Notifications.updateAvailable()
+ jobFinished(params, false)
+ }
+ }
+ return true
}
companion object {
@@ -47,14 +91,14 @@ class JobService : BaseJobService() {
val scheduler = context.getSystemService() ?: return
if (Config.checkUpdate) {
val cmp = JobService::class.java.cmp(context.packageName)
- val info = JobInfo.Builder(Const.ID.JOB_SERVICE_ID, cmp)
+ val info = JobInfo.Builder(Const.ID.CHECK_UPDATE_JOB_ID, cmp)
.setPeriodic(TimeUnit.HOURS.toMillis(12))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.setRequiresDeviceIdle(true)
.build()
scheduler.schedule(info)
} else {
- scheduler.cancel(Const.ID.JOB_SERVICE_ID)
+ scheduler.cancel(Const.ID.CHECK_UPDATE_JOB_ID)
}
}
}
diff --git a/app/src/main/java/com/topjohnwu/magisk/core/Receiver.kt b/app/src/main/java/com/topjohnwu/magisk/core/Receiver.kt
index a91925590..c6acff23a 100644
--- a/app/src/main/java/com/topjohnwu/magisk/core/Receiver.kt
+++ b/app/src/main/java/com/topjohnwu/magisk/core/Receiver.kt
@@ -3,8 +3,11 @@ package com.topjohnwu.magisk.core
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
+import androidx.core.content.IntentCompat
import com.topjohnwu.magisk.core.base.BaseReceiver
import com.topjohnwu.magisk.core.di.ServiceLocator
+import com.topjohnwu.magisk.core.download.DownloadManager
+import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.superuser.Shell
@@ -35,6 +38,12 @@ open class Receiver : BaseReceiver() {
}
when (intent.action ?: return) {
+ DownloadManager.ACTION -> {
+ IntentCompat.getParcelableExtra(
+ intent, DownloadManager.SUBJECT_KEY, Subject::class.java)?.let {
+ DownloadManager.start(context, it)
+ }
+ }
Intent.ACTION_PACKAGE_REPLACED -> {
// This will only work pre-O
if (Config.suReAuth)
diff --git a/app/src/main/java/com/topjohnwu/magisk/core/download/DownloadManager.kt b/app/src/main/java/com/topjohnwu/magisk/core/download/DownloadManager.kt
index 20de8f0be..157e052c5 100644
--- a/app/src/main/java/com/topjohnwu/magisk/core/download/DownloadManager.kt
+++ b/app/src/main/java/com/topjohnwu/magisk/core/download/DownloadManager.kt
@@ -4,19 +4,26 @@ import android.Manifest
import android.annotation.SuppressLint
import android.app.Notification
import android.app.PendingIntent
+import android.app.job.JobInfo
+import android.app.job.JobScheduler
import android.content.Context
import android.net.Uri
import android.os.Build
+import android.os.Bundle
import androidx.collection.SparseArrayCompat
import androidx.collection.isNotEmpty
+import androidx.core.content.getSystemService
import androidx.core.net.toFile
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.StubApk
import com.topjohnwu.magisk.core.ActivityTracker
+import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
+import com.topjohnwu.magisk.core.JobService
import com.topjohnwu.magisk.core.base.BaseActivity
+import com.topjohnwu.magisk.core.cmp
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.intent
import com.topjohnwu.magisk.core.isRunningAsStub
@@ -55,7 +62,7 @@ class DownloadManager(
interface Session {
val context: Context
- fun attach(id: Int, notification: Notification.Builder)
+ fun attach(id: Int, builder: Notification.Builder)
fun stop()
}
@@ -79,9 +86,16 @@ class DownloadManager(
}
private fun createIntent(context: Context, subject: Subject) =
- context.intent()
- .setAction(ACTION)
- .putExtra(SUBJECT_KEY, subject)
+ if (Build.VERSION.SDK_INT >= 34) {
+ context.intent()
+ .setAction(ACTION)
+ .putExtra(SUBJECT_KEY, subject)
+ } else {
+ context.intent()
+ .setAction(ACTION)
+ .putExtra(SUBJECT_KEY, subject)
+ }
+
@SuppressLint("InlinedApi")
fun getPendingIntent(context: Context, subject: Subject): PendingIntent {
@@ -89,7 +103,13 @@ class DownloadManager(
PendingIntent.FLAG_UPDATE_CURRENT or
PendingIntent.FLAG_ONE_SHOT
val intent = createIntent(context, subject)
- return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ return if (Build.VERSION.SDK_INT >= 34) {
+ // On API 34+, download tasks are handled with a user-initiated job.
+ // However, there is no way to schedule a new job directly with a pending intent.
+ // As a workaround, we send the subject to a broadcast receiver and have it
+ // schedule the job for us.
+ PendingIntent.getBroadcast(context, REQUEST_CODE, intent, flag)
+ } else if (Build.VERSION.SDK_INT >= 26) {
PendingIntent.getForegroundService(context, REQUEST_CODE, intent, flag)
} else {
PendingIntent.getService(context, REQUEST_CODE, intent, flag)
@@ -97,15 +117,29 @@ class DownloadManager(
}
@SuppressLint("InlinedApi")
- fun start(activity: BaseActivity, subject: Subject) {
+ fun startWithActivity(activity: BaseActivity, subject: Subject) {
activity.withPermission(Manifest.permission.POST_NOTIFICATIONS) {
// Always download regardless of notification permission status
- val app = activity.applicationContext
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- app.startForegroundService(createIntent(app, subject))
- } else {
- app.startService(createIntent(app, subject))
- }
+ start(activity.applicationContext, subject)
+ }
+ }
+
+ fun start(context: Context, subject: Subject) {
+ if (Build.VERSION.SDK_INT >= 34) {
+ val scheduler = context.getSystemService()!!
+ val cmp = JobService::class.java.cmp(context.packageName)
+ val extras = Bundle()
+ extras.putParcelable(SUBJECT_KEY, subject)
+ val info = JobInfo.Builder(Const.ID.DOWNLOAD_JOB_ID, cmp)
+ .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
+ .setUserInitiated(true)
+ .setTransientExtras(extras)
+ .build()
+ scheduler.schedule(info)
+ } else if (Build.VERSION.SDK_INT >= 26) {
+ context.startForegroundService(createIntent(context, subject))
+ } else {
+ context.startService(createIntent(context, subject))
}
}
}
@@ -140,6 +174,12 @@ class DownloadManager(
}
}
+ @Synchronized
+ fun reattach() {
+ val builder = notifications[attachedId] ?: return
+ session.attach(attachedId, builder)
+ }
+
private val notifications = SparseArrayCompat()
private var attachedId = -1
@@ -205,15 +245,12 @@ class DownloadManager(
// There are still remaining notifications, pick one and attach to the session
val anotherId = notifications.keyAt(0)
val notification = notifications.valueAt(0)
- // Attaching a new notification will automatically remove the current one
attachNotification(anotherId, notification)
} else {
// No more notifications left, terminate the session
attachedId = -1
session.stop()
}
-
- return n
}
}
diff --git a/app/src/main/java/com/topjohnwu/magisk/core/download/Subject.kt b/app/src/main/java/com/topjohnwu/magisk/core/download/Subject.kt
index c0e93fe50..d75b035b1 100644
--- a/app/src/main/java/com/topjohnwu/magisk/core/download/Subject.kt
+++ b/app/src/main/java/com/topjohnwu/magisk/core/download/Subject.kt
@@ -81,7 +81,7 @@ sealed class Subject : Parcelable {
override val notifyId: Int = Notifications.nextId(),
override val title: String = UUID.randomUUID().toString().substring(0, 6)
) : Subject() {
- override val url get() = "http://link.testfile.org/150MB"
+ override val url get() = "https://link.testfile.org/250MB"
override val file get() = File("/dev/null").toUri()
override val autoLaunch get() = false
}
diff --git a/app/src/main/java/com/topjohnwu/magisk/dialog/ManagerInstallDialog.kt b/app/src/main/java/com/topjohnwu/magisk/dialog/ManagerInstallDialog.kt
index 7d14d7df6..2ac866bc5 100644
--- a/app/src/main/java/com/topjohnwu/magisk/dialog/ManagerInstallDialog.kt
+++ b/app/src/main/java/com/topjohnwu/magisk/dialog/ManagerInstallDialog.kt
@@ -29,7 +29,7 @@ class ManagerInstallDialog : MarkDownDialog() {
setCancelable(true)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = R.string.install
- onClick { DownloadManager.start(activity, Subject.App()) }
+ onClick { DownloadManager.startWithActivity(activity, Subject.App()) }
}
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel
diff --git a/app/src/main/java/com/topjohnwu/magisk/dialog/OnlineModuleInstallDialog.kt b/app/src/main/java/com/topjohnwu/magisk/dialog/OnlineModuleInstallDialog.kt
index 85a19f4dc..a5dba2949 100644
--- a/app/src/main/java/com/topjohnwu/magisk/dialog/OnlineModuleInstallDialog.kt
+++ b/app/src/main/java/com/topjohnwu/magisk/dialog/OnlineModuleInstallDialog.kt
@@ -24,7 +24,7 @@ class OnlineModuleInstallDialog(private val item: OnlineModule) : MarkDownDialog
fun download(install: Boolean) {
val action = if (install) Action.Flash else Action.Download
val subject = Subject.Module(item, action)
- DownloadManager.start(activity, subject)
+ DownloadManager.startWithActivity(activity, subject)
}
val title = context.getString(R.string.repo_install_title,
diff --git a/gradle.properties b/gradle.properties
index 49ef3cb87..25823aff9 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -26,6 +26,6 @@ android.injected.testOnly=false
android.nonFinalResIds=false
# Magisk
-magisk.stubVersion=38
+magisk.stubVersion=39
magisk.versionCode=27001
magisk.ondkVersion=r26.3