Cleanup DownloadService
This commit is contained in:
parent
4b238a9cd0
commit
abc5457136
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -6,139 +6,84 @@ import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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.Flash.Secondary
|
||||
import com.topjohnwu.magisk.core.download.DownloadSubject.*
|
||||
import com.topjohnwu.magisk.core.intent
|
||||
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.utils.APKInstall
|
||||
import org.koin.core.get
|
||||
import java.io.File
|
||||
import kotlin.random.Random.Default.nextInt
|
||||
|
||||
/* More of a facade for [RemoteFileService], but whatever... */
|
||||
@SuppressLint("Registered")
|
||||
open class DownloadService : RemoteFileService() {
|
||||
open class DownloadService : BaseDownloadService() {
|
||||
|
||||
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) {
|
||||
is Magisk -> onFinished(subject, id)
|
||||
is Module -> onFinished(subject, id)
|
||||
is Manager -> onFinished(subject, id)
|
||||
override suspend fun onFinish(subject: DownloadSubject, id: Int) = when (subject) {
|
||||
is Magisk -> subject.onFinish(id)
|
||||
is Module -> subject.onFinish(id)
|
||||
is Manager -> subject.onFinish(id)
|
||||
}
|
||||
|
||||
private suspend fun onFinished(
|
||||
subject: Magisk,
|
||||
id: Int
|
||||
) = when (val conf = subject.configuration) {
|
||||
Uninstall -> FlashFragment.uninstall(subject.file, id)
|
||||
private suspend fun Magisk.onFinish(id: Int) = when (val conf = configuration) {
|
||||
Uninstall -> FlashFragment.uninstall(file, id)
|
||||
EnvFix -> {
|
||||
remove(id)
|
||||
EnvFixTask(subject.file).exec()
|
||||
cancel(id)
|
||||
EnvFixTask(file).exec()
|
||||
Unit
|
||||
}
|
||||
is Patch -> FlashFragment.patch(subject.file, conf.fileUri, id)
|
||||
is Flash -> FlashFragment.flash(subject.file, conf is Secondary, id)
|
||||
is Patch -> FlashFragment.patch(file, conf.fileUri, id)
|
||||
is Flash -> FlashFragment.flash(file, conf is Secondary, id)
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
private fun onFinished(
|
||||
subject: Module,
|
||||
id: Int
|
||||
) = when (subject.configuration) {
|
||||
is Flash -> FlashFragment.install(subject.file, id)
|
||||
private fun Module.onFinish(id: Int) = when (configuration) {
|
||||
is Flash -> FlashFragment.install(file, id)
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
private suspend fun onFinished(
|
||||
subject: Manager,
|
||||
id: Int
|
||||
) {
|
||||
handleAPK(subject)
|
||||
remove(id)
|
||||
when (subject.configuration) {
|
||||
is APK.Upgrade -> APKInstall.install(this, subject.file)
|
||||
is APK.Restore -> Unit
|
||||
}
|
||||
private suspend fun Manager.onFinish(id: Int) {
|
||||
handleAPK(this)
|
||||
cancel(id)
|
||||
}
|
||||
|
||||
// ---
|
||||
// --- Customize finish notification
|
||||
|
||||
override fun Notification.Builder.addActions(subject: DownloadSubject)
|
||||
override fun Notification.Builder.setIntent(subject: DownloadSubject)
|
||||
= when (subject) {
|
||||
is Magisk -> addActions(subject)
|
||||
is Module -> addActions(subject)
|
||||
is Manager -> addActions(subject)
|
||||
is Magisk -> setIntent(subject)
|
||||
is Module -> setIntent(subject)
|
||||
is Manager -> setIntent(subject)
|
||||
}
|
||||
|
||||
private fun Notification.Builder.addActions(subject: Magisk)
|
||||
private fun Notification.Builder.setIntent(subject: Magisk)
|
||||
= 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))
|
||||
is Flash -> setContentIntent(FlashFragment.flashIntent(context, subject.file, conf is Secondary))
|
||||
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) {
|
||||
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))
|
||||
else -> this
|
||||
else -> setContentIntent(Intent())
|
||||
}
|
||||
|
||||
private fun Notification.Builder.addActions(subject: Manager)
|
||||
private fun Notification.Builder.setIntent(subject: Manager)
|
||||
= when (subject.configuration) {
|
||||
APK.Upgrade -> setContentIntent(APKInstall.installIntent(context, subject.file))
|
||||
else -> this
|
||||
else -> setContentIntent(Intent())
|
||||
}
|
||||
|
||||
@Suppress("ReplaceSingleLineLet")
|
||||
private fun Notification.Builder.setContentIntent(intent: Intent) =
|
||||
setContentIntent(
|
||||
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 {
|
||||
lateinit var subject: DownloadSubject
|
||||
}
|
||||
@ -150,7 +95,7 @@ open class DownloadService : RemoteFileService() {
|
||||
val builder = Builder().apply(argBuilder)
|
||||
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)
|
||||
} else {
|
||||
app.startService(intent)
|
||||
|
@ -12,10 +12,11 @@ import com.topjohnwu.magisk.core.intent
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.utils.PatchAPK
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import com.topjohnwu.superuser.Shell
|
||||
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)
|
||||
return
|
||||
|
||||
@ -31,7 +32,7 @@ private fun RemoteFileService.patch(apk: File, id: Int) {
|
||||
patched.renameTo(apk)
|
||||
}
|
||||
|
||||
private suspend fun RemoteFileService.upgrade(apk: File, id: Int) {
|
||||
private suspend fun DownloadService.upgrade(apk: File, id: Int) {
|
||||
if (isRunningAsStub) {
|
||||
// Move to upgrade location
|
||||
apk.copyTo(DynAPK.update(this), overwrite = true)
|
||||
@ -49,9 +50,10 @@ private suspend fun RemoteFileService.upgrade(apk: File, id: Int) {
|
||||
} else {
|
||||
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) {
|
||||
it.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()
|
||||
}
|
||||
|
||||
suspend fun RemoteFileService.handleAPK(subject: DownloadSubject.Manager) =
|
||||
suspend fun DownloadService.handleAPK(subject: DownloadSubject.Manager) =
|
||||
when (subject.configuration) {
|
||||
is Upgrade -> upgrade(subject.file, subject.hashCode())
|
||||
is Restore -> restore(subject.file, subject.hashCode())
|
||||
is Upgrade -> upgrade(subject.file, subject.notifyID())
|
||||
is Restore -> restore(subject.file, subject.notifyID())
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import java.io.FilterInputStream
|
||||
import java.io.InputStream
|
||||
|
||||
@ -16,7 +15,7 @@ class ProgressInputStream(
|
||||
val cur = System.currentTimeMillis()
|
||||
if (cur - lastUpdate > 1000) {
|
||||
lastUpdate = cur
|
||||
UiThreadHandler.run { progressEmitter(bytesRead) }
|
||||
progressEmitter(bytesRead)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import android.os.Bundle
|
||||
import android.view.*
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.BaseUIFragment
|
||||
import com.topjohnwu.magisk.core.download.BaseDownloadService
|
||||
import com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding
|
||||
import com.topjohnwu.magisk.events.RebootEvent
|
||||
import com.topjohnwu.superuser.Shell
|
||||
@ -18,6 +19,7 @@ class HomeFragment : BaseUIFragment<HomeViewModel, FragmentHomeMd2Binding>() {
|
||||
super.onStart()
|
||||
activity.title = resources.getString(R.string.section_home)
|
||||
setHasOptionsMenu(true)
|
||||
BaseDownloadService.observeProgress(this, viewModel::onProgressUpdate)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
|
@ -8,8 +8,8 @@ import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.*
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
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.RemoteFileService
|
||||
import com.topjohnwu.magisk.core.model.MagiskJson
|
||||
import com.topjohnwu.magisk.core.model.ManagerJson
|
||||
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
||||
@ -77,14 +77,6 @@ class HomeViewModel(
|
||||
|
||||
private var shownDialog = false
|
||||
|
||||
init {
|
||||
RemoteFileService.progressBroadcast.observeForever {
|
||||
when (it?.second) {
|
||||
is Manager -> stateManagerProgress = it.first.times(100f).roundToInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun refresh() = viewModelScope.launch {
|
||||
notifyPropertyChanged(BR.showUninstall)
|
||||
repoMagisk.fetchUpdate()?.apply {
|
||||
@ -119,6 +111,12 @@ class HomeViewModel(
|
||||
}
|
||||
}.publish()
|
||||
|
||||
fun onProgressUpdate(progress: Float, subject: DownloadSubject) {
|
||||
when (subject) {
|
||||
is Manager -> stateManagerProgress = progress.times(100f).roundToInt()
|
||||
}
|
||||
}
|
||||
|
||||
fun onLinkPressed(link: String) = OpenInappLinkEvent(link).publish()
|
||||
|
||||
fun onDeletePressed() = UninstallDialog().publish()
|
||||
|
@ -4,6 +4,7 @@ import android.content.Intent
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.BaseUIFragment
|
||||
import com.topjohnwu.magisk.core.download.BaseDownloadService
|
||||
import com.topjohnwu.magisk.databinding.FragmentInstallMd2Binding
|
||||
import com.topjohnwu.magisk.events.RequestFileEvent
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
@ -24,6 +25,7 @@ class InstallFragment : BaseUIFragment<InstallViewModel, FragmentInstallMd2Bindi
|
||||
|
||||
// Allow markwon to run in viewmodel scope
|
||||
binding.releaseNotes.tag = viewModel.viewModelScope
|
||||
BaseDownloadService.observeProgress(this, viewModel::onProgressUpdate)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.download.Configuration
|
||||
import com.topjohnwu.magisk.core.download.DownloadService
|
||||
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.events.RequestFileEvent
|
||||
import com.topjohnwu.magisk.events.dialog.SecondSlotWarningDialog
|
||||
@ -60,21 +59,20 @@ class InstallViewModel(
|
||||
set(value) = set(value, field, { field = it }, BR.notes)
|
||||
|
||||
init {
|
||||
RemoteFileService.reset()
|
||||
RemoteFileService.progressBroadcast.observeForever {
|
||||
val (progress, subject) = it ?: return@observeForever
|
||||
viewModelScope.launch {
|
||||
notes = stringRepo.getString(Info.remote.magisk.note)
|
||||
}
|
||||
}
|
||||
|
||||
fun onProgressUpdate(progress: Float, subject: DownloadSubject) {
|
||||
if (subject !is DownloadSubject.Magisk) {
|
||||
return@observeForever
|
||||
return
|
||||
}
|
||||
this.progress = progress.times(100).roundToInt()
|
||||
if (this.progress >= 100) {
|
||||
state = State.LOADED
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
notes = stringRepo.getString(Info.remote.magisk.note)
|
||||
}
|
||||
}
|
||||
|
||||
fun step(nextStep: Int) {
|
||||
step = nextStep
|
||||
|
@ -12,6 +12,7 @@ import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.BaseUIFragment
|
||||
import com.topjohnwu.magisk.arch.ReselectionTarget
|
||||
import com.topjohnwu.magisk.arch.ViewEvent
|
||||
import com.topjohnwu.magisk.core.download.BaseDownloadService
|
||||
import com.topjohnwu.magisk.databinding.FragmentModuleMd2Binding
|
||||
import com.topjohnwu.magisk.events.InstallExternalModuleEvent
|
||||
import com.topjohnwu.magisk.ktx.hideKeyboard
|
||||
@ -50,6 +51,7 @@ class ModuleFragment : BaseUIFragment<ModuleViewModel, FragmentModuleMd2Binding>
|
||||
super.onStart()
|
||||
setHasOptionsMenu(true)
|
||||
activity.title = resources.getString(R.string.modules)
|
||||
BaseDownloadService.observeProgress(this, viewModel::onProgressUpdate)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
@ -2,14 +2,12 @@ package com.topjohnwu.magisk.ui.module
|
||||
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.databinding.ObservableArrayList
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.*
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
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.tasks.RepoUpdater
|
||||
import com.topjohnwu.magisk.data.database.RepoByNameDao
|
||||
@ -47,7 +45,7 @@ class ModuleViewModel(
|
||||
private val repoName: RepoByNameDao,
|
||||
private val repoUpdated: RepoByUpdatedDao,
|
||||
private val repoUpdater: RepoUpdater
|
||||
) : BaseViewModel(), Queryable, Observer<Pair<Float, DownloadSubject>> {
|
||||
) : BaseViewModel(), Queryable {
|
||||
|
||||
override val queryDelay = 1000L
|
||||
private var queryJob: Job? = null
|
||||
@ -125,9 +123,6 @@ class ModuleViewModel(
|
||||
// ---
|
||||
|
||||
init {
|
||||
RemoteFileService.reset()
|
||||
RemoteFileService.progressBroadcast.observeForever(this)
|
||||
|
||||
itemsInstalled.addOnListChangedCallback(
|
||||
onItemRangeInserted = { _, _, _ ->
|
||||
if (installSectionList.isEmpty())
|
||||
@ -152,13 +147,7 @@ class ModuleViewModel(
|
||||
|
||||
// ---
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
RemoteFileService.progressBroadcast.removeObserver(this)
|
||||
}
|
||||
|
||||
override fun onChanged(it: Pair<Float, DownloadSubject>?) {
|
||||
val (progress, subject) = it ?: return
|
||||
fun onProgressUpdate(progress: Float, subject: DownloadSubject) {
|
||||
if (subject !is DownloadSubject.Module)
|
||||
return
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user