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