Cleanup DownloadService

This commit is contained in:
topjohnwu 2021-11-06 17:45:41 -07:00
parent e660fabc57
commit 6dbd8baa7e
13 changed files with 126 additions and 210 deletions

View File

@ -39,8 +39,6 @@ object Const {
} }
object ID { object ID {
// notifications
const val APK_UPDATE_NOTIFICATION_ID = 5
const val JOB_SERVICE_ID = 7 const val JOB_SERVICE_ID = 7
const val UPDATE_NOTIFICATION_CHANNEL = "update" const val UPDATE_NOTIFICATION_CHANNEL = "update"
const val PROGRESS_NOTIFICATION_CHANNEL = "progress" const val PROGRESS_NOTIFICATION_CHANNEL = "progress"

View File

@ -3,66 +3,47 @@ package com.topjohnwu.magisk.core.download
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Notification 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.content.Intent
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import androidx.core.net.toFile
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseUIActivity
import com.topjohnwu.magisk.core.ForegroundTracker import com.topjohnwu.magisk.core.ForegroundTracker
import com.topjohnwu.magisk.core.base.BaseService import com.topjohnwu.magisk.core.base.BaseService
import com.topjohnwu.magisk.core.download.Action.Flash
import com.topjohnwu.magisk.core.download.Subject.Manager
import com.topjohnwu.magisk.core.download.Subject.Module
import com.topjohnwu.magisk.core.intent import com.topjohnwu.magisk.core.intent
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.core.utils.ProgressInputStream import com.topjohnwu.magisk.core.utils.ProgressInputStream
import com.topjohnwu.magisk.di.ServiceLocator import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.ui.flash.FlashFragment import com.topjohnwu.magisk.ktx.copyAndClose
import com.topjohnwu.magisk.utils.APKInstall import com.topjohnwu.magisk.ktx.synchronized
import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.superuser.internal.UiThreadHandler import com.topjohnwu.magisk.view.Notifications.mgr
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import okhttp3.ResponseBody import okhttp3.ResponseBody
import timber.log.Timber import timber.log.Timber
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.util.* import java.util.*
import kotlin.collections.HashMap import kotlin.collections.HashMap
import kotlin.random.Random.Default.nextInt
class DownloadService : BaseService() { class DownloadService : BaseService() {
private val context get() = this
private val hasNotifications get() = notifications.isNotEmpty() private val hasNotifications get() = notifications.isNotEmpty()
private val notifications = Collections.synchronizedMap(HashMap<Int, Notification.Builder>()) private val notifications = HashMap<Int, Notification.Builder>().synchronized()
private val coroutineScope = CoroutineScope(Dispatchers.IO) private val job = Job()
val service get() = ServiceLocator.networkService val service get() = ServiceLocator.networkService
private val mgr get() = Notifications.mgr
// -- Service overrides // -- Service overrides
override fun onBind(intent: Intent?): IBinder? = null override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
intent.getParcelableExtra<Subject>(SUBJECT_KEY)?.let { subject -> intent.getParcelableExtra<Subject>(SUBJECT_KEY)?.let { doDownload(it) }
update(subject.notifyID()) return START_NOT_STICKY
coroutineScope.launch {
try {
subject.startDownload()
} catch (e: IOException) {
Timber.e(e)
notifyFail(subject)
}
}
}
return START_REDELIVER_INTENT
} }
override fun onTaskRemoved(rootIntent: Intent?) { override fun onTaskRemoved(rootIntent: Intent?) {
@ -73,29 +54,40 @@ class DownloadService : BaseService() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
coroutineScope.cancel() job.cancel()
} }
// -- Download logic // -- Download logic
private suspend fun Subject.startDownload() { private fun doDownload(subject: Subject) {
val stream = service.fetchFile(url).toProgressStream(this) update(subject.notifyId)
when (this) { val coroutineScope = CoroutineScope(job + Dispatchers.IO)
is Module -> // Download and process on-the-fly coroutineScope.launch {
stream.toModule(file, service.fetchInstaller().byteStream()) try {
is Manager -> handleAPK(this, stream) val stream = service.fetchFile(subject.url).toProgressStream(subject)
when (subject) {
is Subject.Manager -> handleAPK(subject, stream)
else -> stream.copyAndClose(subject.file.outputStream())
}
if (ForegroundTracker.hasForeground) {
remove(subject.notifyId)
subject.pendingIntent(this@DownloadService).send()
} else {
notifyFinish(subject)
} }
val newId = notifyFinish(this)
if (ForegroundTracker.hasForeground)
onFinish(this, newId)
if (!hasNotifications) if (!hasNotifications)
stopSelf() stopSelf()
} catch (e: IOException) {
Timber.e(e)
notifyFail(subject)
}
}
} }
private fun ResponseBody.toProgressStream(subject: Subject): InputStream { private fun ResponseBody.toProgressStream(subject: Subject): InputStream {
val max = contentLength() val max = contentLength()
val total = max.toFloat() / 1048576 val total = max.toFloat() / 1048576
val id = subject.notifyID() val id = subject.notifyId
update(id) { it.setContentTitle(subject.title) } update(id) { it.setContentTitle(subject.title) }
@ -115,18 +107,18 @@ class DownloadService : BaseService() {
} }
} }
// --- Notifications // --- Notification management
private fun notifyFail(subject: Subject) = finalNotify(subject.notifyID()) { private fun notifyFail(subject: Subject) = finalNotify(subject.notifyId) {
broadcast(-2f, subject) broadcast(-2f, subject)
it.setContentText(getString(R.string.download_file_error)) it.setContentText(getString(R.string.download_file_error))
.setSmallIcon(android.R.drawable.stat_notify_error) .setSmallIcon(android.R.drawable.stat_notify_error)
.setOngoing(false) .setOngoing(false)
} }
private fun notifyFinish(subject: Subject) = finalNotify(subject.notifyID()) { private fun notifyFinish(subject: Subject) = finalNotify(subject.notifyId) {
broadcast(1f, subject) broadcast(1f, subject)
it.setIntent(subject) it.setContentIntent(subject.pendingIntent(this))
.setContentTitle(subject.title) .setContentTitle(subject.title)
.setContentText(getString(R.string.download_complete)) .setContentText(getString(R.string.download_complete))
.setSmallIcon(android.R.drawable.stat_sys_download_done) .setSmallIcon(android.R.drawable.stat_sys_download_done)
@ -135,17 +127,14 @@ class DownloadService : BaseService() {
.setAutoCancel(true) .setAutoCancel(true)
} }
private fun finalNotify( private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
id: Int, val notification = remove(id)?.also(editor) ?: return -1
editor: (Notification.Builder) -> Notification.Builder val newId = Notifications.nextId()
) : Int {
val notification = remove(id)?.run(editor) ?: return -1
val newId = nextInt()
mgr.notify(newId, notification.build()) mgr.notify(newId, notification.build())
return newId return newId
} }
fun create() = Notifications.progress(this, "") private fun create() = Notifications.progress(this, "")
fun update(id: Int, editor: (Notification.Builder) -> Unit = {}) { fun update(id: Int, editor: (Notification.Builder) -> Unit = {}) {
val wasEmpty = !hasNotifications val wasEmpty = !hasNotifications
@ -171,49 +160,9 @@ class DownloadService : BaseService() {
} }
} }
private fun Notification.Builder.setIntent(subject: Subject) = when (subject) {
is Module -> setIntent(subject)
is Manager -> setIntent(subject)
}
private fun Notification.Builder.setIntent(subject: Module)
= when (subject.action) {
Flash -> setContentIntent(FlashFragment.installIntent(context, subject.file))
else -> setContentIntent(Intent())
}
private fun Notification.Builder.setIntent(subject: Manager) =
setContentIntent(APKInstall.installIntent(context, subject.file.toFile()))
@SuppressLint("InlinedApi")
private fun Notification.Builder.setContentIntent(intent: Intent) =
setContentIntent(PendingIntent.getActivity(context, nextInt(), intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT))
// -- Post download processing
private fun onFinish(subject: Subject, id: Int) = when (subject) {
is Module -> subject.onFinish(id)
is Manager -> subject.onFinish(id)
}
private fun Module.onFinish(id: Int) = when (action) {
Flash -> {
UiThreadHandler.run {
(ForegroundTracker.foreground as? BaseUIActivity<*, *>)
?.navigation?.navigate(FlashFragment.install(file, id))
}
}
else -> Unit
}
private fun Manager.onFinish(id: Int) {
remove(id)
APKInstall.install(context, file.toFile())
}
companion object { companion object {
private const val SUBJECT_KEY = "download_subject" private const val SUBJECT_KEY = "download_subject"
private const val REQUEST_CODE = 1
private val progressBroadcast = MutableLiveData<Pair<Float, Subject>?>() private val progressBroadcast = MutableLiveData<Pair<Float, Subject>?>()
@ -233,13 +182,13 @@ class DownloadService : BaseService() {
context.intent<DownloadService>().putExtra(SUBJECT_KEY, subject) context.intent<DownloadService>().putExtra(SUBJECT_KEY, subject)
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
fun pendingIntent(context: Context, subject: Subject): PendingIntent { fun getPendingIntent(context: Context, subject: Subject): PendingIntent {
val flag = FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT or FLAG_ONE_SHOT
val intent = intent(context, subject)
return if (Build.VERSION.SDK_INT >= 26) { return if (Build.VERSION.SDK_INT >= 26) {
PendingIntent.getForegroundService(context, nextInt(), intent(context, subject), getForegroundService(context, REQUEST_CODE, intent, flag)
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
} else { } else {
PendingIntent.getService(context, nextInt(), intent(context, subject), getService(context, REQUEST_CODE, intent, flag)
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
} }
} }

View File

@ -1,6 +1,5 @@
package com.topjohnwu.magisk.core.download package com.topjohnwu.magisk.core.download
import android.content.Context
import androidx.core.net.toFile import androidx.core.net.toFile
import com.topjohnwu.magisk.DynAPK import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
@ -8,29 +7,14 @@ import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.isRunningAsStub import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.tasks.HideAPK import com.topjohnwu.magisk.core.tasks.HideAPK
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.ktx.copyAndClose
import com.topjohnwu.magisk.ktx.relaunchApp import com.topjohnwu.magisk.ktx.relaunchApp
import com.topjohnwu.magisk.ktx.withStreams
import com.topjohnwu.magisk.ktx.writeTo import com.topjohnwu.magisk.ktx.writeTo
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
private fun Context.patch(apk: File) { private class TeeOutputStream(
val patched = File(apk.parent, "patched.apk")
HideAPK.patch(this, apk, patched, packageName, applicationInfo.nonLocalizedLabel)
apk.delete()
patched.renameTo(apk)
}
private fun DownloadService.notifyHide(id: Int) {
update(id) {
it.setProgress(0, 0, true)
.setContentTitle(getString(R.string.hide_app_title))
.setContentText("")
}
}
private class DupOutputStream(
private val o1: OutputStream, private val o1: OutputStream,
private val o2: OutputStream private val o2: OutputStream
) : OutputStream() { ) : OutputStream() {
@ -50,20 +34,26 @@ private class DupOutputStream(
suspend fun DownloadService.handleAPK(subject: Subject.Manager, stream: InputStream) { suspend fun DownloadService.handleAPK(subject: Subject.Manager, stream: InputStream) {
fun write(output: OutputStream) { fun write(output: OutputStream) {
val ext = subject.externalFile.outputStream() val external = subject.externalFile.outputStream()
val o = DupOutputStream(ext, output) stream.copyAndClose(TeeOutputStream(external, output))
withStreams(stream, o) { src, out -> src.copyTo(out) }
} }
if (isRunningAsStub) { if (isRunningAsStub) {
val apk = subject.file.toFile() val apk = subject.file.toFile()
val id = subject.notifyID() val id = subject.notifyId
write(DynAPK.update(this).outputStream()) write(DynAPK.update(this).outputStream())
if (Info.stub!!.version < subject.stub.versionCode) { if (Info.stub!!.version < subject.stub.versionCode) {
// Also upgrade stub // Also upgrade stub
notifyHide(id) update(id) {
it.setProgress(0, 0, true)
.setContentTitle(getString(R.string.hide_app_title))
.setContentText("")
}
service.fetchFile(subject.stub.link).byteStream().writeTo(apk) service.fetchFile(subject.stub.link).byteStream().writeTo(apk)
patch(apk) val patched = File(apk.parent, "patched.apk")
HideAPK.patch(this, apk, patched, packageName, applicationInfo.nonLocalizedLabel)
apk.delete()
patched.renameTo(apk)
} else { } else {
// Simply relaunch the app // Simply relaunch the app
stopSelf() stopSelf()

View File

@ -1,43 +0,0 @@
package com.topjohnwu.magisk.core.download
import android.net.Uri
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.ktx.forEach
import com.topjohnwu.magisk.ktx.withStreams
import java.io.InputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
fun InputStream.toModule(file: Uri, installer: InputStream) {
val input = ZipInputStream(buffered())
val output = ZipOutputStream(file.outputStream().buffered())
withStreams(input, output) { zin, zout ->
zout.putNextEntry(ZipEntry("META-INF/"))
zout.putNextEntry(ZipEntry("META-INF/com/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
installer.copyTo(zout)
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
zout.write("#MAGISK\n".toByteArray(charset("UTF-8")))
var off = -1
zin.forEach { entry ->
if (off < 0) {
off = entry.name.indexOf('/') + 1
}
val path = entry.name.substring(off)
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
zout.putNextEntry(ZipEntry(path))
if (!entry.isDirectory) {
zin.copyTo(zout)
}
}
}
}
}

View File

@ -1,7 +1,12 @@
package com.topjohnwu.magisk.core.download package com.topjohnwu.magisk.core.download
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Parcelable import android.os.Parcelable
import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.model.MagiskJson import com.topjohnwu.magisk.core.model.MagiskJson
@ -10,22 +15,33 @@ import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.core.utils.MediaStoreUtils import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.di.AppContext import com.topjohnwu.magisk.di.AppContext
import com.topjohnwu.magisk.ktx.cachedFile import com.topjohnwu.magisk.ktx.cachedFile
import com.topjohnwu.magisk.ui.flash.FlashFragment
import com.topjohnwu.magisk.utils.APKInstall
import com.topjohnwu.magisk.view.Notifications
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
private fun cachedFile(name: String) = AppContext.cachedFile(name).apply { delete() }.toUri() private fun cachedFile(name: String) = AppContext.cachedFile(name).apply { delete() }.toUri()
enum class Action {
Flash,
Download
}
sealed class Subject : Parcelable { sealed class Subject : Parcelable {
abstract val url: String abstract val url: String
abstract val file: Uri abstract val file: Uri
abstract val action: Action
abstract val title: String abstract val title: String
abstract val notifyId: Int
abstract fun pendingIntent(context: Context): PendingIntent
@Parcelize @Parcelize
class Module( class Module(
val module: OnlineModule, val module: OnlineModule,
override val action: Action val action: Action,
override val notifyId: Int = Notifications.nextId()
) : Subject() { ) : Subject() {
override val url: String get() = module.zip_url override val url: String get() = module.zip_url
override val title: String get() = module.downloadFilename override val title: String get() = module.downloadFilename
@ -34,14 +50,19 @@ sealed class Subject : Parcelable {
override val file by lazy { override val file by lazy {
MediaStoreUtils.getFile(title).uri MediaStoreUtils.getFile(title).uri
} }
override fun pendingIntent(context: Context) = when (action) {
Action.Flash -> FlashFragment.installIntent(context, file)
else -> Intent().toPending(context)
}
} }
@Parcelize @Parcelize
class Manager( class Manager(
private val json: MagiskJson = Info.remote.magisk, private val json: MagiskJson = Info.remote.magisk,
val stub: StubJson = Info.remote.stub val stub: StubJson = Info.remote.stub,
override val notifyId: Int = Notifications.nextId()
) : Subject() { ) : Subject() {
override val action get() = Action.Download
override val title: String get() = "Magisk-${json.version}(${json.versionCode})" override val title: String get() = "Magisk-${json.version}(${json.versionCode})"
override val url: String get() = json.link override val url: String get() = json.link
@ -51,15 +72,14 @@ sealed class Subject : Parcelable {
} }
val externalFile get() = MediaStoreUtils.getFile("$title.apk").uri val externalFile get() = MediaStoreUtils.getFile("$title.apk").uri
override fun pendingIntent(context: Context) =
APKInstall.installIntent(context, file.toFile()).toPending(context)
} }
fun notifyID() = hashCode() @SuppressLint("InlinedApi")
protected fun Intent.toPending(context: Context): PendingIntent {
return PendingIntent.getActivity(context, notifyId, this,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT)
} }
sealed class Action : Parcelable {
@Parcelize
object Flash : Action()
@Parcelize
object Download : Action()
} }

View File

@ -5,7 +5,7 @@ import java.io.InputStream
class ProgressInputStream( class ProgressInputStream(
base: InputStream, base: InputStream,
val progressEmitter: (Long) -> Unit = {} val progressEmitter: (Long) -> Unit
) : FilterInputStream(base) { ) : FilterInputStream(base) {
private var bytesRead = 0L private var bytesRead = 0L
@ -40,4 +40,9 @@ class ProgressInputStream(
} }
return sz return sz
} }
override fun close() {
super.close()
progressEmitter(bytesRead)
}
} }

View File

@ -14,8 +14,8 @@ class ModuleInstallDialog(private val item: OnlineModule) : DialogEvent() {
with(dialog) { with(dialog) {
fun download(install: Boolean) { fun download(install: Boolean) {
val config = if (install) Action.Flash else Action.Download val action = if (install) Action.Flash else Action.Download
val subject = Subject.Module(item, config) val subject = Subject.Module(item, action)
DownloadService.start(context, subject) DownloadService.start(context, subject)
} }

View File

@ -19,9 +19,6 @@ inline fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
} }
} }
fun InputStream.writeTo(file: File) =
withStreams(this, file.outputStream()) { reader, writer -> reader.copyTo(writer) }
inline fun <In : InputStream, Out : OutputStream> withStreams( inline fun <In : InputStream, Out : OutputStream> withStreams(
inStream: In, inStream: In,
outStream: Out, outStream: Out,
@ -34,18 +31,19 @@ inline fun <In : InputStream, Out : OutputStream> withStreams(
} }
} }
fun <T> MutableList<T>.update(newList: List<T>) { fun InputStream.copyAndClose(out: OutputStream) = withStreams(this, out) { i, o -> i.copyTo(o) }
clear()
addAll(newList) fun InputStream.writeTo(file: File) = copyAndClose(file.outputStream())
}
operator fun <E> SparseArrayCompat<E>.set(key: Int, value: E) { operator fun <E> SparseArrayCompat<E>.set(key: Int, value: E) {
put(key, value) put(key, value)
} }
fun <T> MutableList<T>.synchronized() = Collections.synchronizedList(this) fun <T> MutableList<T>.synchronized(): MutableList<T> = Collections.synchronizedList(this)
fun <T> MutableSet<T>.synchronized() = Collections.synchronizedSet(this)
fun <K, V> MutableMap<K, V>.synchronized() = Collections.synchronizedMap(this) fun <T> MutableSet<T>.synchronized(): MutableSet<T> = Collections.synchronizedSet(this)
fun <K, V> MutableMap<K, V>.synchronized(): MutableMap<K, V> = Collections.synchronizedMap(this)
fun SimpleDateFormat.parseOrNull(date: String) = fun SimpleDateFormat.parseOrNull(date: String) =
runCatching { parse(date) }.onFailure { Timber.e(it) }.getOrNull() runCatching { parse(date) }.onFailure { Timber.e(it) }.getOrNull()

View File

@ -111,16 +111,14 @@ class FlashFragment : BaseUIFragment<FlashViewModel, FragmentFlashMd2Binding>()
/* Installing is understood as flashing modules / zips */ /* Installing is understood as flashing modules / zips */
fun installIntent(context: Context, file: Uri, id: Int = -1) = FlashFragmentArgs( fun installIntent(context: Context, file: Uri) = FlashFragmentArgs(
action = Const.Value.FLASH_ZIP, action = Const.Value.FLASH_ZIP,
additionalData = file, additionalData = file,
dismissId = id
).let { createIntent(context, it) } ).let { createIntent(context, it) }
fun install(file: Uri, id: Int) = MainDirections.actionFlashFragment( fun install(file: Uri) = MainDirections.actionFlashFragment(
action = Const.Value.FLASH_ZIP, action = Const.Value.FLASH_ZIP,
additionalData = file, additionalData = file,
dismissId = id
) )
} }

View File

@ -20,7 +20,6 @@ import com.topjohnwu.magisk.databinding.itemBindingOf
import com.topjohnwu.magisk.databinding.set import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.events.SnackbarEvent import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.ktx.* import com.topjohnwu.magisk.ktx.*
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.superuser.CallbackList import com.topjohnwu.superuser.CallbackList
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -50,9 +49,7 @@ class FlashViewModel : BaseViewModel() {
} }
fun startFlashing() { fun startFlashing() {
val (action, uri, id) = args val (action, uri) = args
if (id != -1)
Notifications.mgr.cancel(id)
viewModelScope.launch { viewModelScope.launch {
val result = when (action) { val result = when (action) {

View File

@ -70,4 +70,8 @@ class HomeFragment : BaseUIFragment<HomeViewModel, FragmentHomeMd2Binding>() {
return true return true
} }
override fun onResume() {
super.onResume()
viewModel.stateManagerProgress = 0
}
} }

View File

@ -8,19 +8,22 @@ import android.os.Build.VERSION.SDK_INT
import androidx.core.content.getSystemService 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.Const
import com.topjohnwu.magisk.core.Const.ID.PROGRESS_NOTIFICATION_CHANNEL import com.topjohnwu.magisk.core.Const.ID.PROGRESS_NOTIFICATION_CHANNEL
import com.topjohnwu.magisk.core.Const.ID.UPDATE_NOTIFICATION_CHANNEL import com.topjohnwu.magisk.core.Const.ID.UPDATE_NOTIFICATION_CHANNEL
import com.topjohnwu.magisk.core.download.DownloadService import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.download.Subject import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.di.AppContext import com.topjohnwu.magisk.di.AppContext
import com.topjohnwu.magisk.ktx.getBitmap import com.topjohnwu.magisk.ktx.getBitmap
import java.util.concurrent.atomic.AtomicInteger
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
object Notifications { object Notifications {
val mgr by lazy { AppContext.getSystemService<NotificationManager>()!! } val mgr by lazy { AppContext.getSystemService<NotificationManager>()!! }
private const val APK_UPDATE_NOTIFICATION_ID = 5
private val nextId = AtomicInteger(APK_UPDATE_NOTIFICATION_ID)
fun setup(context: Context) { fun setup(context: Context) {
if (SDK_INT >= 26) { if (SDK_INT >= 26) {
var channel = NotificationChannel(UPDATE_NOTIFICATION_CHANNEL, var channel = NotificationChannel(UPDATE_NOTIFICATION_CHANNEL,
@ -46,7 +49,7 @@ object Notifications {
} }
fun managerUpdate(context: Context) { fun managerUpdate(context: Context) {
val intent = DownloadService.pendingIntent(context, Subject.Manager()) val intent = DownloadService.getPendingIntent(context, Subject.Manager())
val builder = updateBuilder(context) val builder = updateBuilder(context)
.setContentTitle(context.getString(R.string.magisk_update_title)) .setContentTitle(context.getString(R.string.magisk_update_title))
@ -54,7 +57,7 @@ object Notifications {
.setAutoCancel(true) .setAutoCancel(true)
.setContentIntent(intent) .setContentIntent(intent)
mgr.notify(Const.ID.APK_UPDATE_NOTIFICATION_ID, builder.build()) mgr.notify(APK_UPDATE_NOTIFICATION_ID, builder.build())
} }
fun progress(context: Context, title: CharSequence): Notification.Builder { fun progress(context: Context, title: CharSequence): Notification.Builder {
@ -69,4 +72,6 @@ object Notifications {
.setOngoing(true) .setOngoing(true)
return builder return builder
} }
fun nextId() = nextId.incrementAndGet()
} }

View File

@ -51,11 +51,6 @@
app:argType="android.net.Uri" app:argType="android.net.Uri"
app:nullable="true" /> app:nullable="true" />
<argument
android:name="dismiss_id"
android:defaultValue="-1"
app:argType="integer" />
</fragment> </fragment>
<fragment <fragment