Isolate download logic from service lifecycle

This commit is contained in:
topjohnwu 2024-02-06 00:56:14 -08:00
parent 154121f3dd
commit c5d34670c4
9 changed files with 245 additions and 195 deletions

View File

@ -61,7 +61,7 @@
</receiver> </receiver>
<service <service
android:name=".core.download.DownloadService" android:name=".core.Service"
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />

View File

@ -0,0 +1,40 @@
package com.topjohnwu.magisk.core
import android.app.Notification
import android.content.Intent
import android.os.Build
import androidx.core.app.ServiceCompat
import androidx.core.content.IntentCompat
import com.topjohnwu.magisk.core.base.BaseService
import com.topjohnwu.magisk.core.download.DownloadManager
import com.topjohnwu.magisk.core.download.Subject
class Service : BaseService(), DownloadManager.Session {
private lateinit var dm: DownloadManager
override val context get() = this
override fun onCreate() {
super.onCreate()
dm = DownloadManager(this)
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
if (intent.action == DownloadManager.ACTION) {
IntentCompat
.getParcelableExtra(intent, DownloadManager.SUBJECT_KEY, Subject::class.java)
?.let { dm.download(it) }
}
return START_NOT_STICKY
}
override fun attach(id: Int, builder: Notification.Builder) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
builder.setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE)
startForeground(id, builder.build())
}
override fun stop() {
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
}
}

View File

@ -2,30 +2,41 @@ package com.topjohnwu.magisk.core.download
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Notification
import android.app.PendingIntent import android.app.PendingIntent
import android.app.PendingIntent.*
import android.content.Context import android.content.Context
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import androidx.collection.SparseArrayCompat
import androidx.collection.isNotEmpty
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.StubApk import com.topjohnwu.magisk.StubApk
import com.topjohnwu.magisk.core.ActivityTracker import com.topjohnwu.magisk.core.ActivityTracker
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.base.BaseActivity import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.intent import com.topjohnwu.magisk.core.intent
import com.topjohnwu.magisk.core.isRunningAsStub import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.ktx.* import com.topjohnwu.magisk.core.ktx.copyAndClose
import com.topjohnwu.magisk.core.ktx.forEach
import com.topjohnwu.magisk.core.ktx.selfLaunchIntent
import com.topjohnwu.magisk.core.ktx.set
import com.topjohnwu.magisk.core.ktx.withStreams
import com.topjohnwu.magisk.core.ktx.writeTo
import com.topjohnwu.magisk.core.tasks.HideAPK import com.topjohnwu.magisk.core.tasks.HideAPK
import com.topjohnwu.magisk.core.utils.MediaStoreUtils import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.core.utils.ProgressInputStream
import com.topjohnwu.magisk.utils.APKInstall import com.topjohnwu.magisk.utils.APKInstall
import com.topjohnwu.magisk.view.Notifications
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.ResponseBody
import timber.log.Timber import timber.log.Timber
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.IOException import java.io.IOException
@ -37,27 +48,77 @@ import java.util.zip.ZipFile
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
class DownloadService : NotificationService() { class DownloadManager(
private val session: Session
) {
private val job = Job() interface Session {
val context: Context
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { fun attach(id: Int, notification: Notification.Builder)
intent.getParcelableExtra<Subject>(SUBJECT_KEY)?.let { download(it) } fun stop()
return START_NOT_STICKY
} }
override fun onDestroy() { companion object {
job.cancel() const val ACTION = "com.topjohnwu.magisk.DOWNLOAD"
const val SUBJECT_KEY = "subject"
private const val REQUEST_CODE = 1
private val progressBroadcast = MutableLiveData<Pair<Float, Subject>?>()
private fun broadcast(progress: Float, subject: Subject) {
progressBroadcast.postValue(progress to subject)
}
fun observeProgress(owner: LifecycleOwner, callback: (Float, Subject) -> Unit) {
progressBroadcast.value = null
progressBroadcast.observe(owner) {
val (progress, subject) = it ?: return@observe
callback(progress, subject)
}
}
private fun createIntent(context: Context, subject: Subject) =
context.intent<com.topjohnwu.magisk.core.Service>()
.setAction(ACTION)
.putExtra(SUBJECT_KEY, subject)
@SuppressLint("InlinedApi")
fun getPendingIntent(context: Context, subject: Subject): PendingIntent {
val flag = PendingIntent.FLAG_IMMUTABLE or
PendingIntent.FLAG_UPDATE_CURRENT or
PendingIntent.FLAG_ONE_SHOT
val intent = createIntent(context, subject)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PendingIntent.getForegroundService(context, REQUEST_CODE, intent, flag)
} else {
PendingIntent.getService(context, REQUEST_CODE, intent, flag)
}
}
@SuppressLint("InlinedApi")
fun start(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))
}
}
}
} }
private fun download(subject: Subject) { fun download(subject: Subject) {
notifyUpdate(subject.notifyId) notifyUpdate(subject.notifyId)
CoroutineScope(job + Dispatchers.IO).launch { CoroutineScope(job + Dispatchers.IO).launch {
try { try {
val stream = service.fetchFile(subject.url).toProgressStream(subject) val stream = network.fetchFile(subject.url).toProgressStream(subject)
when (subject) { when (subject) {
is Subject.App -> handleApp(stream, subject) is Subject.App -> handleApp(stream, subject)
is Subject.Module -> handleModule(stream, subject.file) is Subject.Module -> handleModule(stream, subject.file)
is Subject.Test -> stream.copyAndClose(subject.file.outputStream())
} }
val activity = ActivityTracker.foreground val activity = ActivityTracker.foreground
if (activity != null && subject.autoLaunch) { if (activity != null && subject.autoLaunch) {
@ -67,15 +128,99 @@ class DownloadService : NotificationService() {
notifyFinish(subject) notifyFinish(subject)
} }
subject.postDownload?.invoke() subject.postDownload?.invoke()
if (!hasNotifications)
stopSelf()
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
notifyFail(subject) notifyFail(subject)
} }
synchronized(this@DownloadManager) {
if (notifications.isEmpty)
session.stop()
}
} }
} }
private val notifications = SparseArrayCompat<Notification.Builder>()
private var attachedId = -1
private val job = Job()
private val context get() = session.context
private val network get() = ServiceLocator.networkService
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
val notification = notifyRemove(id)?.also(editor) ?: return -1
val newId = Notifications.nextId()
Notifications.mgr.notify(newId, notification.build())
return newId
}
private fun notifyFail(subject: Subject) = finalNotify(subject.notifyId) {
broadcast(-2f, subject)
it.setContentText(context.getString(R.string.download_file_error))
.setSmallIcon(android.R.drawable.stat_notify_error)
.setOngoing(false)
}
private fun notifyFinish(subject: Subject) = finalNotify(subject.notifyId) {
broadcast(1f, subject)
it.setContentTitle(subject.title)
.setContentText(context.getString(R.string.download_complete))
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setProgress(0, 0, false)
.setOngoing(false)
.setAutoCancel(true)
subject.pendingIntent(context)?.let { intent -> it.setContentIntent(intent) }
}
private fun attachNotification(id: Int, notification: Notification.Builder) {
attachedId = id
session.attach(id, notification)
}
@Synchronized
private fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit = {}) {
val notification = (notifications[id] ?: Notifications.startProgress("").also {
notifications[id] = it
}).apply(editor)
if (attachedId < 0)
attachNotification(id, notification)
else
Notifications.mgr.notify(id, notification.build())
}
@Synchronized
private fun notifyRemove(id: Int): Notification.Builder? {
val idx = notifications.indexOfKey(id)
var n: Notification.Builder? = null
if (idx >= 0) {
n = notifications.valueAt(idx)
notifications.removeAt(idx)
// The cancelled notification is the one attached to the session, need special handling
if (attachedId == id) {
if (notifications.isNotEmpty()) {
// 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
}
}
Notifications.mgr.cancel(id)
return n
}
private fun handleApp(stream: InputStream, subject: Subject.App) { private fun handleApp(stream: InputStream, subject: Subject.App) {
fun writeTee(output: OutputStream) { fun writeTee(output: OutputStream) {
val uri = MediaStoreUtils.getFile("${subject.title}.apk").uri val uri = MediaStoreUtils.getFile("${subject.title}.apk").uri
@ -84,7 +229,7 @@ class DownloadService : NotificationService() {
} }
if (isRunningAsStub) { if (isRunningAsStub) {
val updateApk = StubApk.update(this) val updateApk = StubApk.update(context)
try { try {
// Download full APK to stub update path // Download full APK to stub update path
writeTee(updateApk.outputStream()) writeTee(updateApk.outputStream())
@ -97,7 +242,7 @@ class DownloadService : NotificationService() {
// Also upgrade stub // Also upgrade stub
notifyUpdate(subject.notifyId) { notifyUpdate(subject.notifyId) {
it.setProgress(0, 0, true) it.setProgress(0, 0, true)
.setContentTitle(getString(R.string.hide_app_title)) .setContentTitle(context.getString(R.string.hide_app_title))
.setContentText("") .setContentText("")
} }
@ -107,7 +252,7 @@ class DownloadService : NotificationService() {
zf.close() zf.close()
// Patch and install // Patch and install
subject.intent = HideAPK.upgrade(this, apk) subject.intent = HideAPK.upgrade(context, apk)
?: throw IOException("HideAPK patch error") ?: throw IOException("HideAPK patch error")
apk.delete() apk.delete()
} else { } else {
@ -116,7 +261,7 @@ class DownloadService : NotificationService() {
StubApk.restartProcess(it) StubApk.restartProcess(it)
} ?: run { } ?: run {
// Or else kill the current process after posting notification // Or else kill the current process after posting notification
subject.intent = selfLaunchIntent() subject.intent = context.selfLaunchIntent()
subject.postDownload = { Runtime.getRuntime().exit(0) } subject.postDownload = { Runtime.getRuntime().exit(0) }
} }
return return
@ -127,8 +272,8 @@ class DownloadService : NotificationService() {
throw e throw e
} }
} else { } else {
val session = APKInstall.startSession(this) val session = APKInstall.startSession(context)
writeTee(session.openStream(this)) writeTee(session.openStream(context))
subject.intent = session.waitIntent() subject.intent = session.waitIntent()
} }
} }
@ -143,7 +288,7 @@ class DownloadService : NotificationService() {
zout.putNextEntry(ZipEntry("META-INF/com/google/")) zout.putNextEntry(ZipEntry("META-INF/com/google/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/android/")) zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary")) zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
assets.open("module_installer.sh").copyTo(zout) context.assets.open("module_installer.sh").copyTo(zout)
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script")) zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
zout.write("#MAGISK\n".toByteArray()) zout.write("#MAGISK\n".toByteArray())
@ -178,41 +323,24 @@ class DownloadService : NotificationService() {
} }
} }
companion object { private fun ResponseBody.toProgressStream(subject: Subject): InputStream {
private const val SUBJECT_KEY = "subject" val max = contentLength()
private const val REQUEST_CODE = 1 val total = max.toFloat() / 1048576
val id = subject.notifyId
fun observeProgress(owner: LifecycleOwner, callback: (Float, Subject) -> Unit) { notifyUpdate(id) { it.setContentTitle(subject.title) }
progressBroadcast.value = null
progressBroadcast.observe(owner) {
val (progress, subject) = it ?: return@observe
callback(progress, subject)
}
}
private fun intent(context: Context, subject: Subject) = return ProgressInputStream(byteStream()) {
context.intent<DownloadService>().putExtra(SUBJECT_KEY, subject) val progress = it.toFloat() / 1048576
notifyUpdate(id) { notification ->
@SuppressLint("InlinedApi") if (max > 0) {
fun getPendingIntent(context: Context, subject: Subject): PendingIntent { broadcast(progress / total, subject)
val flag = FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT or FLAG_ONE_SHOT notification
val intent = intent(context, subject) .setProgress(max.toInt(), it.toInt(), false)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { .setContentText("%.2f / %.2f MB".format(progress, total))
getForegroundService(context, REQUEST_CODE, intent, flag)
} else {
getService(context, REQUEST_CODE, intent, flag)
}
}
@SuppressLint("InlinedApi")
fun start(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(intent(app, subject))
} else { } else {
app.startService(intent(app, subject)) broadcast(-1f, subject)
notification.setContentText("%.2f MB / ??".format(progress))
} }
} }
} }

View File

@ -1,128 +0,0 @@
package com.topjohnwu.magisk.core.download
import android.app.Notification
import android.content.Intent
import android.os.Build
import androidx.lifecycle.MutableLiveData
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.base.BaseService
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.ktx.synchronized
import com.topjohnwu.magisk.core.utils.ProgressInputStream
import com.topjohnwu.magisk.view.Notifications
import okhttp3.ResponseBody
import java.io.InputStream
open class NotificationService : BaseService() {
private val notifications = HashMap<Int, Notification.Builder>().synchronized()
protected val hasNotifications get() = notifications.isNotEmpty()
protected val service get() = ServiceLocator.networkService
private var attachedNotificationId = 0
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
notifications.forEach { Notifications.mgr.cancel(it.key) }
notifications.clear()
}
protected fun ResponseBody.toProgressStream(subject: Subject): InputStream {
val max = contentLength()
val total = max.toFloat() / 1048576
val id = subject.notifyId
notifyUpdate(id) { it.setContentTitle(subject.title) }
return ProgressInputStream(byteStream()) {
val progress = it.toFloat() / 1048576
notifyUpdate(id) { notification ->
if (max > 0) {
broadcast(progress / total, subject)
notification
.setProgress(max.toInt(), it.toInt(), false)
.setContentText("%.2f / %.2f MB".format(progress, total))
} else {
broadcast(-1f, subject)
notification.setContentText("%.2f MB / ??".format(progress))
}
}
}
}
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
val notification = notifyRemove(id)?.also(editor) ?: return -1
val newId = Notifications.nextId()
Notifications.mgr.notify(newId, notification.build())
return newId
}
protected fun notifyFail(subject: Subject) = finalNotify(subject.notifyId) {
broadcast(-2f, subject)
it.setContentText(getString(R.string.download_file_error))
.setSmallIcon(android.R.drawable.stat_notify_error)
.setOngoing(false)
}
protected fun notifyFinish(subject: Subject) = finalNotify(subject.notifyId) {
broadcast(1f, subject)
it.setContentTitle(subject.title)
.setContentText(getString(R.string.download_complete))
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setProgress(0, 0, false)
.setOngoing(false)
.setAutoCancel(true)
subject.pendingIntent(this)?.let { intent -> it.setContentIntent(intent) }
}
private fun attachNotification(id: Int, notification: Notification) {
attachedNotificationId = id
startForeground(id, notification)
}
private fun maybeDetachNotification(id: Int) : Boolean {
if (attachedNotificationId != id) return false
if (hasNotifications) {
val (anotherId, notification) = notifications.entries.first()
// Attaching a new notification will remove the current showing one
attachNotification(anotherId, notification.build())
return true
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE)
} else {
@Suppress("DEPRECATION")
stopForeground(true)
}
attachedNotificationId = 0
return true
}
protected fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit = {}) {
fun create() = Notifications.startProgress("")
val wasEmpty = !hasNotifications
val notification = notifications.getOrPut(id, ::create).also(editor).build()
if (wasEmpty)
attachNotification(id, notification)
else
Notifications.mgr.notify(id, notification)
}
protected fun notifyRemove(id: Int): Notification.Builder? {
val n = notifications.remove(id)
if (n == null || !maybeDetachNotification(id))
Notifications.mgr.cancel(id)
return n
}
companion object {
@JvmStatic
protected val progressBroadcast = MutableLiveData<Pair<Float, Subject>?>()
private fun broadcast(progress: Float, subject: Subject) {
progressBroadcast.postValue(progress to subject)
}
}
}

View File

@ -17,8 +17,8 @@ import com.topjohnwu.magisk.ui.flash.FlashFragment
import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.magisk.view.Notifications
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.io.File
private fun cachedFile(name: String) = AppContext.cachedFile(name).apply { delete() }.toUri() import java.util.UUID
enum class Action { enum class Action {
Flash, Flash,
@ -34,7 +34,7 @@ sealed class Subject : Parcelable {
open val autoLaunch: Boolean get() = true open val autoLaunch: Boolean get() = true
open val postDownload: (() -> Unit)? get() = null open val postDownload: (() -> Unit)? get() = null
abstract fun pendingIntent(context: Context): PendingIntent? open fun pendingIntent(context: Context): PendingIntent? = null
@Parcelize @Parcelize
class Module( class Module(
@ -65,7 +65,7 @@ sealed class Subject : Parcelable {
@IgnoredOnParcel @IgnoredOnParcel
override val file by lazy { override val file by lazy {
cachedFile("manager.apk") AppContext.cachedFile("manager.apk").apply { delete() }.toUri()
} }
@IgnoredOnParcel @IgnoredOnParcel
@ -76,6 +76,16 @@ sealed class Subject : Parcelable {
override fun pendingIntent(context: Context) = intent?.toPending(context) override fun pendingIntent(context: Context) = intent?.toPending(context)
} }
@Parcelize
class Test(
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 file get() = File("/dev/null").toUri()
override val autoLaunch get() = false
}
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
protected fun Intent.toPending(context: Context): PendingIntent { protected fun Intent.toPending(context: Context): PendingIntent {
return PendingIntent.getActivity(context, notifyId, this, return PendingIntent.getActivity(context, notifyId, this,

View File

@ -4,7 +4,7 @@ import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.di.AppContext import com.topjohnwu.magisk.core.di.AppContext
import com.topjohnwu.magisk.core.di.ServiceLocator import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.download.DownloadService import com.topjohnwu.magisk.core.download.DownloadManager
import com.topjohnwu.magisk.core.download.Subject import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.view.MagiskDialog import com.topjohnwu.magisk.view.MagiskDialog
import java.io.File import java.io.File
@ -29,7 +29,7 @@ class ManagerInstallDialog : MarkDownDialog() {
setCancelable(true) setCancelable(true)
setButton(MagiskDialog.ButtonType.POSITIVE) { setButton(MagiskDialog.ButtonType.POSITIVE) {
text = R.string.install text = R.string.install
onClick { DownloadService.start(activity, Subject.App()) } onClick { DownloadManager.start(activity, Subject.App()) }
} }
setButton(MagiskDialog.ButtonType.NEGATIVE) { setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel text = android.R.string.cancel

View File

@ -3,7 +3,7 @@ package com.topjohnwu.magisk.dialog
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.di.ServiceLocator import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.download.Action import com.topjohnwu.magisk.core.download.Action
import com.topjohnwu.magisk.core.download.DownloadService import com.topjohnwu.magisk.core.download.DownloadManager
import com.topjohnwu.magisk.core.download.Subject import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.model.module.OnlineModule import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.view.MagiskDialog import com.topjohnwu.magisk.view.MagiskDialog
@ -24,7 +24,7 @@ class OnlineModuleInstallDialog(private val item: OnlineModule) : MarkDownDialog
fun download(install: Boolean) { fun download(install: Boolean) {
val action = if (install) Action.Flash else Action.Download val action = if (install) Action.Flash else Action.Download
val subject = Subject.Module(item, action) val subject = Subject.Module(item, action)
DownloadService.start(activity, subject) DownloadManager.start(activity, subject)
} }
val title = context.getString(R.string.repo_install_title, val title = context.getString(R.string.repo_install_title,

View File

@ -14,7 +14,7 @@ import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.arch.viewModel import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.DownloadService import com.topjohnwu.magisk.core.download.DownloadManager
import com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding import com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding
class HomeFragment : BaseFragment<FragmentHomeMd2Binding>(), MenuProvider { class HomeFragment : BaseFragment<FragmentHomeMd2Binding>(), MenuProvider {
@ -25,7 +25,7 @@ class HomeFragment : BaseFragment<FragmentHomeMd2Binding>(), MenuProvider {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
activity?.setTitle(R.string.section_home) activity?.setTitle(R.string.section_home)
DownloadService.observeProgress(this, viewModel::onProgressUpdate) DownloadManager.observeProgress(this, viewModel::onProgressUpdate)
} }
private fun checkTitle(text: TextView, icon: ImageView) { private fun checkTitle(text: TextView, icon: ImageView) {

View File

@ -11,7 +11,7 @@ import androidx.core.content.getSystemService
import androidx.core.graphics.drawable.toIcon import androidx.core.graphics.drawable.toIcon
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.di.AppContext import com.topjohnwu.magisk.core.di.AppContext
import com.topjohnwu.magisk.core.download.DownloadService import com.topjohnwu.magisk.core.download.DownloadManager
import com.topjohnwu.magisk.core.download.Subject import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.ktx.getBitmap import com.topjohnwu.magisk.core.ktx.getBitmap
import com.topjohnwu.magisk.core.ktx.selfLaunchIntent import com.topjohnwu.magisk.core.ktx.selfLaunchIntent
@ -67,7 +67,7 @@ object Notifications {
fun updateAvailable() { fun updateAvailable() {
AppContext.apply { AppContext.apply {
val intent = DownloadService.getPendingIntent(this, Subject.App()) val intent = DownloadManager.getPendingIntent(this, Subject.App())
val bitmap = getBitmap(R.drawable.ic_magisk_outline) val bitmap = getBitmap(R.drawable.ic_magisk_outline)
val builder = if (SDK_INT >= Build.VERSION_CODES.O) { val builder = if (SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(this, UPDATE_CHANNEL) Notification.Builder(this, UPDATE_CHANNEL)