Cleanup DownloadService

This commit is contained in:
topjohnwu 2020-08-21 06:27:13 -07:00
parent 4b238a9cd0
commit abc5457136
12 changed files with 263 additions and 349 deletions

View File

@ -0,0 +1,201 @@
package com.topjohnwu.magisk.core.download
import android.app.Notification
import android.content.Intent
import android.os.IBinder
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.ForegroundTracker
import com.topjohnwu.magisk.core.base.BaseService
import com.topjohnwu.magisk.core.utils.ProgressInputStream
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.ktx.checkSum
import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.magisk.view.Notifications
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import okhttp3.ResponseBody
import org.koin.android.ext.android.inject
import org.koin.core.KoinComponent
import timber.log.Timber
import java.io.IOException
import java.io.InputStream
import java.util.*
import kotlin.collections.HashMap
import kotlin.random.Random.Default.nextInt
abstract class BaseDownloadService : BaseService(), KoinComponent {
private val hasNotifications get() = notifications.isNotEmpty()
private val notifications = Collections.synchronizedMap(HashMap<Int, Notification.Builder>())
private val coroutineScope = CoroutineScope(Dispatchers.IO)
val service: GithubRawServices by inject()
// -- Service overrides
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
intent?.getParcelableExtra<DownloadSubject>(ARG_URL)?.let { subject ->
update(subject.notifyID())
coroutineScope.launch {
try {
subject.startDownload()
} catch (e: IOException) {
Timber.e(e)
notifyFail(subject)
}
}
}
return START_REDELIVER_INTENT
}
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
notifications.forEach { cancel(it.key) }
notifications.clear()
}
override fun onDestroy() {
super.onDestroy()
coroutineScope.cancel()
}
// -- Download logic
private suspend fun DownloadSubject.startDownload() {
val skip = this is DownloadSubject.Magisk && file.exists() && file.checkSum("MD5", magisk.md5)
if (!skip) {
val stream = service.fetchFile(url).toProgressStream(this)
when (this) {
is DownloadSubject.Module -> // Download and process on-the-fly
stream.toModule(file, service.fetchInstaller().byteStream())
else ->
stream.writeTo(file)
}
}
val newId = notifyFinish(this)
if (ForegroundTracker.hasForeground)
onFinish(this, newId)
if (!hasNotifications)
stopSelf()
}
private fun ResponseBody.toProgressStream(subject: DownloadSubject): InputStream {
val max = contentLength()
val total = max.toFloat() / 1048576
val id = subject.notifyID()
update(id) { it.setContentTitle(subject.title) }
return ProgressInputStream(byteStream()) {
val progress = it.toFloat() / 1048576
update(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))
}
}
}
}
// --- Notification managements
fun DownloadSubject.notifyID() = hashCode()
private fun notifyFail(subject: DownloadSubject) = lastNotify(subject.notifyID()) {
broadcast(-1f, subject)
it.setContentText(getString(R.string.download_file_error))
.setSmallIcon(android.R.drawable.stat_notify_error)
.setOngoing(false)
}
private fun notifyFinish(subject: DownloadSubject) = lastNotify(subject.notifyID()) {
broadcast(1f, subject)
it.setIntent(subject)
.setContentText(getString(R.string.download_complete))
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setProgress(0, 0, false)
.setOngoing(false)
.setAutoCancel(true)
}
private fun create() = Notifications.progress(this, "")
fun update(id: Int, editor: (Notification.Builder) -> Unit = {}) {
val wasEmpty = !hasNotifications
val notification = notifications.getOrPut(id, ::create).also(editor)
if (wasEmpty)
updateForeground()
else
notify(id, notification.build())
}
private fun lastNotify(
id: Int,
editor: (Notification.Builder) -> Notification.Builder? = { null }
) : Int {
val notification = remove(id)?.run(editor) ?: return -1
val newId: Int = nextInt()
notify(newId, notification.build())
return newId
}
private fun remove(id: Int) = notifications.remove(id)?.also {
updateForeground()
cancel(id)
}
private fun notify(id: Int, notification: Notification) {
Notifications.mgr.notify(id, notification)
}
protected fun cancel(id: Int) {
Notifications.mgr.cancel(id)
}
private fun updateForeground() {
if (hasNotifications) {
val (id, notification) = notifications.entries.first()
startForeground(id, notification.build())
} else {
stopForeground(false)
}
}
// --- Implement custom logic
protected abstract suspend fun onFinish(subject: DownloadSubject, id: Int)
protected abstract fun Notification.Builder.setIntent(subject: DownloadSubject)
: Notification.Builder
// ---
companion object : KoinComponent {
const val ARG_URL = "arg_url"
private val progressBroadcast = MutableLiveData<Pair<Float, DownloadSubject>>()
fun observeProgress(owner: LifecycleOwner, callback: (Float, DownloadSubject) -> Unit) {
progressBroadcast.value = null
progressBroadcast.observe(owner) {
val (progress, subject) = it ?: return@observe
callback(progress, subject)
}
}
private fun broadcast(progress: Float, subject: DownloadSubject) {
progressBroadcast.postValue(progress to subject)
}
}
}

View File

@ -6,139 +6,84 @@ 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.webkit.MimeTypeMap
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.download.Configuration.* import com.topjohnwu.magisk.core.download.Configuration.*
import com.topjohnwu.magisk.core.download.Configuration.Flash.Secondary import com.topjohnwu.magisk.core.download.Configuration.Flash.Secondary
import com.topjohnwu.magisk.core.download.DownloadSubject.* import com.topjohnwu.magisk.core.download.DownloadSubject.*
import com.topjohnwu.magisk.core.intent import com.topjohnwu.magisk.core.intent
import com.topjohnwu.magisk.core.tasks.EnvFixTask import com.topjohnwu.magisk.core.tasks.EnvFixTask
import com.topjohnwu.magisk.ktx.chooser
import com.topjohnwu.magisk.ktx.exists
import com.topjohnwu.magisk.ktx.provide
import com.topjohnwu.magisk.ui.flash.FlashFragment import com.topjohnwu.magisk.ui.flash.FlashFragment
import com.topjohnwu.magisk.utils.APKInstall import com.topjohnwu.magisk.utils.APKInstall
import org.koin.core.get
import java.io.File
import kotlin.random.Random.Default.nextInt import kotlin.random.Random.Default.nextInt
/* More of a facade for [RemoteFileService], but whatever... */
@SuppressLint("Registered") @SuppressLint("Registered")
open class DownloadService : RemoteFileService() { open class DownloadService : BaseDownloadService() {
private val context get() = this private val context get() = this
private val File.type
get() = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(extension)
?: "resource/folder"
override suspend fun onFinished(subject: DownloadSubject, id: Int) = when (subject) { override suspend fun onFinish(subject: DownloadSubject, id: Int) = when (subject) {
is Magisk -> onFinished(subject, id) is Magisk -> subject.onFinish(id)
is Module -> onFinished(subject, id) is Module -> subject.onFinish(id)
is Manager -> onFinished(subject, id) is Manager -> subject.onFinish(id)
} }
private suspend fun onFinished( private suspend fun Magisk.onFinish(id: Int) = when (val conf = configuration) {
subject: Magisk, Uninstall -> FlashFragment.uninstall(file, id)
id: Int
) = when (val conf = subject.configuration) {
Uninstall -> FlashFragment.uninstall(subject.file, id)
EnvFix -> { EnvFix -> {
remove(id) cancel(id)
EnvFixTask(subject.file).exec() EnvFixTask(file).exec()
Unit Unit
} }
is Patch -> FlashFragment.patch(subject.file, conf.fileUri, id) is Patch -> FlashFragment.patch(file, conf.fileUri, id)
is Flash -> FlashFragment.flash(subject.file, conf is Secondary, id) is Flash -> FlashFragment.flash(file, conf is Secondary, id)
else -> Unit else -> Unit
} }
private fun onFinished( private fun Module.onFinish(id: Int) = when (configuration) {
subject: Module, is Flash -> FlashFragment.install(file, id)
id: Int
) = when (subject.configuration) {
is Flash -> FlashFragment.install(subject.file, id)
else -> Unit else -> Unit
} }
private suspend fun onFinished( private suspend fun Manager.onFinish(id: Int) {
subject: Manager, handleAPK(this)
id: Int cancel(id)
) {
handleAPK(subject)
remove(id)
when (subject.configuration) {
is APK.Upgrade -> APKInstall.install(this, subject.file)
is APK.Restore -> Unit
}
} }
// --- // --- Customize finish notification
override fun Notification.Builder.addActions(subject: DownloadSubject) override fun Notification.Builder.setIntent(subject: DownloadSubject)
= when (subject) { = when (subject) {
is Magisk -> addActions(subject) is Magisk -> setIntent(subject)
is Module -> addActions(subject) is Module -> setIntent(subject)
is Manager -> addActions(subject) is Manager -> setIntent(subject)
} }
private fun Notification.Builder.addActions(subject: Magisk) private fun Notification.Builder.setIntent(subject: Magisk)
= when (val conf = subject.configuration) { = when (val conf = subject.configuration) {
Download -> apply {
fileIntent(subject.file.parentFile!!)
.takeIf { it.exists(get()) }
?.let { addAction(0, R.string.download_open_parent, it.chooser()) }
fileIntent(subject.file)
.takeIf { it.exists(get()) }
?.let { addAction(0, R.string.download_open_self, it.chooser()) }
}
Uninstall -> setContentIntent(FlashFragment.uninstallIntent(context, subject.file)) Uninstall -> setContentIntent(FlashFragment.uninstallIntent(context, subject.file))
is Flash -> setContentIntent(FlashFragment.flashIntent(context, subject.file, conf is Secondary)) is Flash -> setContentIntent(FlashFragment.flashIntent(context, subject.file, conf is Secondary))
is Patch -> setContentIntent(FlashFragment.patchIntent(context, subject.file, conf.fileUri)) is Patch -> setContentIntent(FlashFragment.patchIntent(context, subject.file, conf.fileUri))
else -> this else -> setContentIntent(Intent())
} }
private fun Notification.Builder.addActions(subject: Module) private fun Notification.Builder.setIntent(subject: Module)
= when (subject.configuration) { = when (subject.configuration) {
Download -> this.apply {
fileIntent(subject.file.parentFile!!)
.takeIf { it.exists(get()) }
?.let { addAction(0, R.string.download_open_parent, it.chooser()) }
fileIntent(subject.file)
.takeIf { it.exists(get()) }
?.let { addAction(0, R.string.download_open_self, it.chooser()) }
}
is Flash -> setContentIntent(FlashFragment.installIntent(context, subject.file)) is Flash -> setContentIntent(FlashFragment.installIntent(context, subject.file))
else -> this else -> setContentIntent(Intent())
} }
private fun Notification.Builder.addActions(subject: Manager) private fun Notification.Builder.setIntent(subject: Manager)
= when (subject.configuration) { = when (subject.configuration) {
APK.Upgrade -> setContentIntent(APKInstall.installIntent(context, subject.file)) APK.Upgrade -> setContentIntent(APKInstall.installIntent(context, subject.file))
else -> this else -> setContentIntent(Intent())
} }
@Suppress("ReplaceSingleLineLet")
private fun Notification.Builder.setContentIntent(intent: Intent) = private fun Notification.Builder.setContentIntent(intent: Intent) =
setContentIntent( setContentIntent(
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT) PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
) )
@Suppress("ReplaceSingleLineLet")
private fun Notification.Builder.addAction(icon: Int, title: Int, intent: Intent) =
addAction(icon, getString(title),
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
)
// --- // ---
private fun fileIntent(file: File): Intent {
return Intent(Intent.ACTION_VIEW)
.setDataAndType(file.provide(this), file.type)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
class Builder { class Builder {
lateinit var subject: DownloadSubject lateinit var subject: DownloadSubject
} }
@ -150,7 +95,7 @@ open class DownloadService : RemoteFileService() {
val builder = Builder().apply(argBuilder) val builder = Builder().apply(argBuilder)
val intent = app.intent<DownloadService>().putExtra(ARG_URL, builder.subject) val intent = app.intent<DownloadService>().putExtra(ARG_URL, builder.subject)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= 26) {
app.startForegroundService(intent) app.startForegroundService(intent)
} else { } else {
app.startService(intent) app.startService(intent)

View File

@ -12,10 +12,11 @@ import com.topjohnwu.magisk.core.intent
import com.topjohnwu.magisk.core.isRunningAsStub import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.utils.PatchAPK import com.topjohnwu.magisk.core.utils.PatchAPK
import com.topjohnwu.magisk.ktx.writeTo import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.magisk.utils.APKInstall
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import java.io.File import java.io.File
private fun RemoteFileService.patch(apk: File, id: Int) { private fun DownloadService.patch(apk: File, id: Int) {
if (packageName == BuildConfig.APPLICATION_ID) if (packageName == BuildConfig.APPLICATION_ID)
return return
@ -31,7 +32,7 @@ private fun RemoteFileService.patch(apk: File, id: Int) {
patched.renameTo(apk) patched.renameTo(apk)
} }
private suspend fun RemoteFileService.upgrade(apk: File, id: Int) { private suspend fun DownloadService.upgrade(apk: File, id: Int) {
if (isRunningAsStub) { if (isRunningAsStub) {
// Move to upgrade location // Move to upgrade location
apk.copyTo(DynAPK.update(this), overwrite = true) apk.copyTo(DynAPK.update(this), overwrite = true)
@ -49,9 +50,10 @@ private suspend fun RemoteFileService.upgrade(apk: File, id: Int) {
} else { } else {
patch(apk, id) patch(apk, id)
} }
APKInstall.install(this, apk)
} }
private fun RemoteFileService.restore(apk: File, id: Int) { private fun DownloadService.restore(apk: File, id: Int) {
update(id) { update(id) {
it.setProgress(0, 0, true) it.setProgress(0, 0, true)
.setProgress(0, 0, true) .setProgress(0, 0, true)
@ -64,8 +66,8 @@ private fun RemoteFileService.restore(apk: File, id: Int) {
Shell.su("pm install $apk && pm uninstall $packageName").exec() Shell.su("pm install $apk && pm uninstall $packageName").exec()
} }
suspend fun RemoteFileService.handleAPK(subject: DownloadSubject.Manager) = suspend fun DownloadService.handleAPK(subject: DownloadSubject.Manager) =
when (subject.configuration) { when (subject.configuration) {
is Upgrade -> upgrade(subject.file, subject.hashCode()) is Upgrade -> upgrade(subject.file, subject.notifyID())
is Restore -> restore(subject.file, subject.hashCode()) is Restore -> restore(subject.file, subject.notifyID())
} }

View File

@ -1,96 +0,0 @@
package com.topjohnwu.magisk.core.download
import android.app.Notification
import android.content.Intent
import android.os.IBinder
import com.topjohnwu.magisk.core.base.BaseService
import com.topjohnwu.magisk.view.Notifications
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import org.koin.core.KoinComponent
import java.util.*
import kotlin.collections.HashMap
import kotlin.random.Random.Default.nextInt
abstract class NotificationService : BaseService(), KoinComponent {
private val hasNotifications get() = notifications.isNotEmpty()
private val notifications = Collections.synchronizedMap(HashMap<Int, Notification.Builder>())
val coroutineScope = CoroutineScope(Dispatchers.IO)
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
notifications.forEach { cancel(it.key) }
notifications.clear()
}
override fun onDestroy() {
super.onDestroy()
coroutineScope.cancel()
}
abstract fun createNotification(): Notification.Builder
// --
fun update(
id: Int,
body: (Notification.Builder) -> Unit = {}
) {
val wasEmpty = notifications.isEmpty()
val notification = notifications.getOrPut(id, ::createNotification).also(body)
if (wasEmpty)
updateForeground()
else
notify(id, notification.build())
}
protected fun lastNotify(
id: Int,
editBody: (Notification.Builder) -> Notification.Builder? = { null }
) : Int {
val currentNotification = remove(id)?.run(editBody)
var newId = -1
currentNotification?.let {
newId = nextInt(Int.MAX_VALUE)
notify(newId, it.build())
}
if (!hasNotifications) {
stopSelf()
}
return newId
}
protected fun remove(id: Int) = notifications.remove(id).also {
cancel(id)
updateForeground()
}
// ---
private fun notify(id: Int, notification: Notification) {
Notifications.mgr.notify(id, notification)
}
private fun cancel(id: Int) {
Notifications.mgr.cancel(id)
}
private fun updateForeground() {
if (hasNotifications) {
val first = notifications.entries.first()
startForeground(first.key, first.value.build())
} else {
stopForeground(true)
}
}
// --
override fun onBind(p0: Intent?): IBinder? = null
}

View File

@ -1,128 +0,0 @@
package com.topjohnwu.magisk.core.download
import android.app.Notification
import android.content.Intent
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.ForegroundTracker
import com.topjohnwu.magisk.core.download.DownloadSubject.Magisk
import com.topjohnwu.magisk.core.download.DownloadSubject.Module
import com.topjohnwu.magisk.core.utils.ProgressInputStream
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.ktx.checkSum
import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.magisk.view.Notifications
import kotlinx.coroutines.launch
import okhttp3.ResponseBody
import org.koin.android.ext.android.inject
import org.koin.core.KoinComponent
import timber.log.Timber
import java.io.IOException
import java.io.InputStream
abstract class RemoteFileService : NotificationService() {
val service: GithubRawServices by inject()
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
intent?.getParcelableExtra<DownloadSubject>(ARG_URL)?.let {
update(it.hashCode())
coroutineScope.launch {
try {
start(it)
} catch (e: IOException) {
Timber.e(e)
failNotify(it)
}
}
}
return START_REDELIVER_INTENT
}
override fun createNotification() = Notifications.progress(this, "")
// ---
private suspend fun start(subject: DownloadSubject) {
if (subject !is Magisk ||
!subject.file.exists() ||
!subject.file.checkSum("MD5", subject.magisk.md5)) {
val stream = service.fetchFile(subject.url).toProgressStream(subject)
when (subject) {
is Module ->
stream.toModule(subject.file, service.fetchInstaller().byteStream())
else ->
stream.writeTo(subject.file)
}
}
val newId = finishNotify(subject)
if (ForegroundTracker.hasForeground) {
onFinished(subject, newId)
}
}
private fun ResponseBody.toProgressStream(subject: DownloadSubject): InputStream {
val maxRaw = contentLength()
val max = maxRaw / 1_000_000f
val id = subject.hashCode()
update(id) { it.setContentTitle(subject.title) }
return ProgressInputStream(byteStream()) {
val progress = it / 1_000_000f
update(id) { notification ->
if (maxRaw > 0) {
send(progress / max, subject)
notification
.setProgress(maxRaw.toInt(), it.toInt(), false)
.setContentText("%.2f / %.2f MB".format(progress, max))
} else {
send(-1f, subject)
notification.setContentText("%.2f MB / ??".format(progress))
}
}
}
}
private fun failNotify(subject: DownloadSubject) = lastNotify(subject.hashCode()) {
send(0f, subject)
it.setContentText(getString(R.string.download_file_error))
.setSmallIcon(android.R.drawable.stat_notify_error)
.setOngoing(false)
}
private fun finishNotify(subject: DownloadSubject) = lastNotify(subject.hashCode()) {
send(1f, subject)
it.addActions(subject)
.setContentText(getString(R.string.download_complete))
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setProgress(0, 0, false)
.setOngoing(false)
.setAutoCancel(true)
}
// ---
protected abstract suspend fun onFinished(subject: DownloadSubject, id: Int)
protected abstract fun Notification.Builder.addActions(subject: DownloadSubject)
: Notification.Builder
companion object : KoinComponent {
const val ARG_URL = "arg_url"
private val internalProgressBroadcast = MutableLiveData<Pair<Float, DownloadSubject>>()
val progressBroadcast: LiveData<Pair<Float, DownloadSubject>> get() = internalProgressBroadcast
fun send(progress: Float, subject: DownloadSubject) {
internalProgressBroadcast.postValue(progress to subject)
}
fun reset() {
internalProgressBroadcast.value = null
}
}
}

View File

@ -1,6 +1,5 @@
package com.topjohnwu.magisk.core.utils package com.topjohnwu.magisk.core.utils
import com.topjohnwu.superuser.internal.UiThreadHandler
import java.io.FilterInputStream import java.io.FilterInputStream
import java.io.InputStream import java.io.InputStream
@ -16,7 +15,7 @@ class ProgressInputStream(
val cur = System.currentTimeMillis() val cur = System.currentTimeMillis()
if (cur - lastUpdate > 1000) { if (cur - lastUpdate > 1000) {
lastUpdate = cur lastUpdate = cur
UiThreadHandler.run { progressEmitter(bytesRead) } progressEmitter(bytesRead)
} }
} }

View File

@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.* import android.view.*
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseUIFragment import com.topjohnwu.magisk.arch.BaseUIFragment
import com.topjohnwu.magisk.core.download.BaseDownloadService
import com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding import com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding
import com.topjohnwu.magisk.events.RebootEvent import com.topjohnwu.magisk.events.RebootEvent
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
@ -18,6 +19,7 @@ class HomeFragment : BaseUIFragment<HomeViewModel, FragmentHomeMd2Binding>() {
super.onStart() super.onStart()
activity.title = resources.getString(R.string.section_home) activity.title = resources.getString(R.string.section_home)
setHasOptionsMenu(true) setHasOptionsMenu(true)
BaseDownloadService.observeProgress(this, viewModel::onProgressUpdate)
} }
override fun onCreateView( override fun onCreateView(

View File

@ -8,8 +8,8 @@ import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.* import com.topjohnwu.magisk.arch.*
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.DownloadSubject
import com.topjohnwu.magisk.core.download.DownloadSubject.Manager import com.topjohnwu.magisk.core.download.DownloadSubject.Manager
import com.topjohnwu.magisk.core.download.RemoteFileService
import com.topjohnwu.magisk.core.model.MagiskJson import com.topjohnwu.magisk.core.model.MagiskJson
import com.topjohnwu.magisk.core.model.ManagerJson import com.topjohnwu.magisk.core.model.ManagerJson
import com.topjohnwu.magisk.data.repository.MagiskRepository import com.topjohnwu.magisk.data.repository.MagiskRepository
@ -77,14 +77,6 @@ class HomeViewModel(
private var shownDialog = false private var shownDialog = false
init {
RemoteFileService.progressBroadcast.observeForever {
when (it?.second) {
is Manager -> stateManagerProgress = it.first.times(100f).roundToInt()
}
}
}
override fun refresh() = viewModelScope.launch { override fun refresh() = viewModelScope.launch {
notifyPropertyChanged(BR.showUninstall) notifyPropertyChanged(BR.showUninstall)
repoMagisk.fetchUpdate()?.apply { repoMagisk.fetchUpdate()?.apply {
@ -119,6 +111,12 @@ class HomeViewModel(
} }
}.publish() }.publish()
fun onProgressUpdate(progress: Float, subject: DownloadSubject) {
when (subject) {
is Manager -> stateManagerProgress = progress.times(100f).roundToInt()
}
}
fun onLinkPressed(link: String) = OpenInappLinkEvent(link).publish() fun onLinkPressed(link: String) = OpenInappLinkEvent(link).publish()
fun onDeletePressed() = UninstallDialog().publish() fun onDeletePressed() = UninstallDialog().publish()

View File

@ -4,6 +4,7 @@ import android.content.Intent
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseUIFragment import com.topjohnwu.magisk.arch.BaseUIFragment
import com.topjohnwu.magisk.core.download.BaseDownloadService
import com.topjohnwu.magisk.databinding.FragmentInstallMd2Binding import com.topjohnwu.magisk.databinding.FragmentInstallMd2Binding
import com.topjohnwu.magisk.events.RequestFileEvent import com.topjohnwu.magisk.events.RequestFileEvent
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
@ -24,6 +25,7 @@ class InstallFragment : BaseUIFragment<InstallViewModel, FragmentInstallMd2Bindi
// Allow markwon to run in viewmodel scope // Allow markwon to run in viewmodel scope
binding.releaseNotes.tag = viewModel.viewModelScope binding.releaseNotes.tag = viewModel.viewModelScope
BaseDownloadService.observeProgress(this, viewModel::onProgressUpdate)
} }
} }

View File

@ -11,7 +11,6 @@ import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.Configuration import com.topjohnwu.magisk.core.download.Configuration
import com.topjohnwu.magisk.core.download.DownloadService import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.download.DownloadSubject import com.topjohnwu.magisk.core.download.DownloadSubject
import com.topjohnwu.magisk.core.download.RemoteFileService
import com.topjohnwu.magisk.data.repository.StringRepository import com.topjohnwu.magisk.data.repository.StringRepository
import com.topjohnwu.magisk.events.RequestFileEvent import com.topjohnwu.magisk.events.RequestFileEvent
import com.topjohnwu.magisk.events.dialog.SecondSlotWarningDialog import com.topjohnwu.magisk.events.dialog.SecondSlotWarningDialog
@ -60,22 +59,21 @@ class InstallViewModel(
set(value) = set(value, field, { field = it }, BR.notes) set(value) = set(value, field, { field = it }, BR.notes)
init { init {
RemoteFileService.reset()
RemoteFileService.progressBroadcast.observeForever {
val (progress, subject) = it ?: return@observeForever
if (subject !is DownloadSubject.Magisk) {
return@observeForever
}
this.progress = progress.times(100).roundToInt()
if (this.progress >= 100) {
state = State.LOADED
}
}
viewModelScope.launch { viewModelScope.launch {
notes = stringRepo.getString(Info.remote.magisk.note) notes = stringRepo.getString(Info.remote.magisk.note)
} }
} }
fun onProgressUpdate(progress: Float, subject: DownloadSubject) {
if (subject !is DownloadSubject.Magisk) {
return
}
this.progress = progress.times(100).roundToInt()
if (this.progress >= 100) {
state = State.LOADED
}
}
fun step(nextStep: Int) { fun step(nextStep: Int) {
step = nextStep step = nextStep
} }

View File

@ -12,6 +12,7 @@ import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseUIFragment import com.topjohnwu.magisk.arch.BaseUIFragment
import com.topjohnwu.magisk.arch.ReselectionTarget import com.topjohnwu.magisk.arch.ReselectionTarget
import com.topjohnwu.magisk.arch.ViewEvent import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.core.download.BaseDownloadService
import com.topjohnwu.magisk.databinding.FragmentModuleMd2Binding import com.topjohnwu.magisk.databinding.FragmentModuleMd2Binding
import com.topjohnwu.magisk.events.InstallExternalModuleEvent import com.topjohnwu.magisk.events.InstallExternalModuleEvent
import com.topjohnwu.magisk.ktx.hideKeyboard import com.topjohnwu.magisk.ktx.hideKeyboard
@ -50,6 +51,7 @@ class ModuleFragment : BaseUIFragment<ModuleViewModel, FragmentModuleMd2Binding>
super.onStart() super.onStart()
setHasOptionsMenu(true) setHasOptionsMenu(true)
activity.title = resources.getString(R.string.modules) activity.title = resources.getString(R.string.modules)
BaseDownloadService.observeProgress(this, viewModel::onProgressUpdate)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

View File

@ -2,14 +2,12 @@ package com.topjohnwu.magisk.ui.module
import androidx.databinding.Bindable import androidx.databinding.Bindable
import androidx.databinding.ObservableArrayList import androidx.databinding.ObservableArrayList
import androidx.lifecycle.Observer
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.* import com.topjohnwu.magisk.arch.*
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.download.DownloadSubject import com.topjohnwu.magisk.core.download.DownloadSubject
import com.topjohnwu.magisk.core.download.RemoteFileService
import com.topjohnwu.magisk.core.model.module.Module import com.topjohnwu.magisk.core.model.module.Module
import com.topjohnwu.magisk.core.tasks.RepoUpdater import com.topjohnwu.magisk.core.tasks.RepoUpdater
import com.topjohnwu.magisk.data.database.RepoByNameDao import com.topjohnwu.magisk.data.database.RepoByNameDao
@ -47,7 +45,7 @@ class ModuleViewModel(
private val repoName: RepoByNameDao, private val repoName: RepoByNameDao,
private val repoUpdated: RepoByUpdatedDao, private val repoUpdated: RepoByUpdatedDao,
private val repoUpdater: RepoUpdater private val repoUpdater: RepoUpdater
) : BaseViewModel(), Queryable, Observer<Pair<Float, DownloadSubject>> { ) : BaseViewModel(), Queryable {
override val queryDelay = 1000L override val queryDelay = 1000L
private var queryJob: Job? = null private var queryJob: Job? = null
@ -125,9 +123,6 @@ class ModuleViewModel(
// --- // ---
init { init {
RemoteFileService.reset()
RemoteFileService.progressBroadcast.observeForever(this)
itemsInstalled.addOnListChangedCallback( itemsInstalled.addOnListChangedCallback(
onItemRangeInserted = { _, _, _ -> onItemRangeInserted = { _, _, _ ->
if (installSectionList.isEmpty()) if (installSectionList.isEmpty())
@ -152,13 +147,7 @@ class ModuleViewModel(
// --- // ---
override fun onCleared() { fun onProgressUpdate(progress: Float, subject: DownloadSubject) {
super.onCleared()
RemoteFileService.progressBroadcast.removeObserver(this)
}
override fun onChanged(it: Pair<Float, DownloadSubject>?) {
val (progress, subject) = it ?: return
if (subject !is DownloadSubject.Module) if (subject !is DownloadSubject.Module)
return return