Commit e660fabc authored by topjohnwu's avatar topjohnwu

Remove BaseDownloader

parent 2115bcd8
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.di.ServiceLocator
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 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 BaseDownloader : BaseService() {
private val hasNotifications get() = notifications.isNotEmpty()
private val notifications = Collections.synchronizedMap(HashMap<Int, Notification.Builder>())
private val coroutineScope = CoroutineScope(Dispatchers.IO)
val service get() = ServiceLocator.networkService
// -- Service overrides
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
intent.getParcelableExtra<Subject>(ACTION_KEY)?.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 Subject.startDownload() {
val stream = service.fetchFile(url).toProgressStream(this)
when (this) {
is Subject.Module -> // Download and process on-the-fly
stream.toModule(file, service.fetchInstaller().byteStream())
is Subject.Manager -> handleAPK(this, stream)
}
val newId = notifyFinish(this)
if (ForegroundTracker.hasForeground)
onFinish(this, newId)
if (!hasNotifications)
stopSelf()
}
private fun ResponseBody.toProgressStream(subject: Subject): 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 Subject.notifyID() = hashCode()
private fun notifyFail(subject: Subject) = lastNotify(subject.notifyID()) {
broadcast(-2f, subject)
it.setContentText(getString(R.string.download_file_error))
.setSmallIcon(android.R.drawable.stat_notify_error)
.setOngoing(false)
}
private fun notifyFinish(subject: Subject) = lastNotify(subject.notifyID()) {
broadcast(1f, subject)
it.setIntent(subject)
.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)
}
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
}
protected fun remove(id: Int) = notifications.remove(id)
?.also { updateForeground(); cancel(id) }
?: { cancel(id); null }()
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 (id, notification) = notifications.entries.first()
startForeground(id, notification.build())
} else {
stopForeground(false)
}
}
// --- Implement custom logic
protected abstract suspend fun onFinish(subject: Subject, id: Int)
protected abstract fun Notification.Builder.setIntent(subject: Subject): Notification.Builder
// ---
companion object {
const val ACTION_KEY = "download_action"
private val progressBroadcast = MutableLiveData<Pair<Float, 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 broadcast(progress: Float, subject: Subject) {
progressBroadcast.postValue(progress to subject)
}
}
}
package com.topjohnwu.magisk.core.download package com.topjohnwu.magisk.core.download
import android.annotation.SuppressLint
import android.app.Notification import android.app.Notification
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 androidx.core.net.toFile import androidx.core.net.toFile
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseUIActivity 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.download.Action.Flash import com.topjohnwu.magisk.core.download.Action.Flash
import com.topjohnwu.magisk.core.download.Subject.Manager import com.topjohnwu.magisk.core.download.Subject.Manager
import com.topjohnwu.magisk.core.download.Subject.Module 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.ProgressInputStream
import com.topjohnwu.magisk.di.ServiceLocator
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 com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.superuser.internal.UiThreadHandler import com.topjohnwu.superuser.internal.UiThreadHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import okhttp3.ResponseBody
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 import kotlin.random.Random.Default.nextInt
open class DownloadService : BaseDownloader() { class DownloadService : BaseService() {
private val context get() = this private val context get() = this
private val hasNotifications get() = notifications.isNotEmpty()
private val notifications = Collections.synchronizedMap(HashMap<Int, Notification.Builder>())
private val coroutineScope = CoroutineScope(Dispatchers.IO)
override suspend fun onFinish(subject: Subject, id: Int) = when (subject) { val service get() = ServiceLocator.networkService
is Module -> subject.onFinish(id) private val mgr get() = Notifications.mgr
is Manager -> subject.onFinish(id)
// -- Service overrides
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
intent.getParcelableExtra<Subject>(SUBJECT_KEY)?.let { subject ->
update(subject.notifyID())
coroutineScope.launch {
try {
subject.startDownload()
} catch (e: IOException) {
Timber.e(e)
notifyFail(subject)
}
}
}
return START_REDELIVER_INTENT
} }
private fun Module.onFinish(id: Int) = when (action) { override fun onTaskRemoved(rootIntent: Intent?) {
Flash -> { super.onTaskRemoved(rootIntent)
UiThreadHandler.run { notifications.forEach { mgr.cancel(it.key) }
(ForegroundTracker.foreground as? BaseUIActivity<*, *>) notifications.clear()
?.navigation?.navigate(FlashFragment.install(file, id)) }
override fun onDestroy() {
super.onDestroy()
coroutineScope.cancel()
}
// -- Download logic
private suspend fun Subject.startDownload() {
val stream = service.fetchFile(url).toProgressStream(this)
when (this) {
is Module -> // Download and process on-the-fly
stream.toModule(file, service.fetchInstaller().byteStream())
is Manager -> handleAPK(this, stream)
}
val newId = notifyFinish(this)
if (ForegroundTracker.hasForeground)
onFinish(this, newId)
if (!hasNotifications)
stopSelf()
}
private fun ResponseBody.toProgressStream(subject: Subject): 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))
}
} }
} }
else -> Unit
} }
private fun Manager.onFinish(id: Int) { // --- Notifications
remove(id)
APKInstall.install(context, file.toFile()) private 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)
}
private fun notifyFinish(subject: Subject) = finalNotify(subject.notifyID()) {
broadcast(1f, subject)
it.setIntent(subject)
.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)
}
private fun finalNotify(
id: Int,
editor: (Notification.Builder) -> Notification.Builder
) : Int {
val notification = remove(id)?.run(editor) ?: return -1
val newId = nextInt()
mgr.notify(newId, notification.build())
return newId
} }
// --- Customize finish notification fun create() = Notifications.progress(this, "")
override fun Notification.Builder.setIntent(subject: Subject) fun update(id: Int, editor: (Notification.Builder) -> Unit = {}) {
= when (subject) { val wasEmpty = !hasNotifications
val notification = notifications.getOrPut(id, ::create).also(editor)
if (wasEmpty)
updateForeground()
else
mgr.notify(id, notification.build())
}
private fun remove(id: Int): Notification.Builder? {
val n = notifications.remove(id)?.also { updateForeground() }
mgr.cancel(id)
return n
}
private fun updateForeground() {
if (hasNotifications) {
val (id, notification) = notifications.entries.first()
startForeground(id, notification.build())
} else {
stopForeground(false)
}
}
private fun Notification.Builder.setIntent(subject: Subject) = when (subject) {
is Module -> setIntent(subject) is Module -> setIntent(subject)
is Manager -> setIntent(subject) is Manager -> setIntent(subject)
} }
private fun Notification.Builder.setIntent(subject: Module) private fun Notification.Builder.setIntent(subject: Module)
= when (subject.action) { = when (subject.action) {
Flash -> setContentIntent(FlashFragment.installIntent(context, subject.file)) Flash -> setContentIntent(FlashFragment.installIntent(context, subject.file))
else -> setContentIntent(Intent()) else -> setContentIntent(Intent())
} }
private fun Notification.Builder.setIntent(subject: Manager) private fun Notification.Builder.setIntent(subject: Manager) =
= setContentIntent(APKInstall.installIntent(context, subject.file.toFile())) setContentIntent(APKInstall.installIntent(context, subject.file.toFile()))
@SuppressLint("InlinedApi")
private fun Notification.Builder.setContentIntent(intent: Intent) = private fun Notification.Builder.setContentIntent(intent: Intent) =
setContentIntent(PendingIntent.getActivity(context, nextInt(), intent, setContentIntent(PendingIntent.getActivity(context, nextInt(), intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT)) 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 val progressBroadcast = MutableLiveData<Pair<Float, 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 broadcast(progress: Float, subject: Subject) {
progressBroadcast.postValue(progress to subject)
}
private fun intent(context: Context, subject: Subject) = private fun intent(context: Context, subject: Subject) =
context.intent<DownloadService>().putExtra(ACTION_KEY, subject) context.intent<DownloadService>().putExtra(SUBJECT_KEY, subject)
@SuppressLint("InlinedApi")
fun pendingIntent(context: Context, subject: Subject): PendingIntent { fun pendingIntent(context: Context, subject: Subject): PendingIntent {
return if (Build.VERSION.SDK_INT >= 26) { return if (Build.VERSION.SDK_INT >= 26) {
PendingIntent.getForegroundService(context, nextInt(), intent(context, subject), PendingIntent.getForegroundService(context, nextInt(), intent(context, subject),
......
...@@ -22,7 +22,7 @@ private fun Context.patch(apk: File) { ...@@ -22,7 +22,7 @@ private fun Context.patch(apk: File) {
patched.renameTo(apk) patched.renameTo(apk)
} }
private fun BaseDownloader.notifyHide(id: Int) { private fun DownloadService.notifyHide(id: Int) {
update(id) { update(id) {
it.setProgress(0, 0, true) it.setProgress(0, 0, true)
.setContentTitle(getString(R.string.hide_app_title)) .setContentTitle(getString(R.string.hide_app_title))
...@@ -48,7 +48,7 @@ private class DupOutputStream( ...@@ -48,7 +48,7 @@ private class DupOutputStream(
} }
} }
suspend fun BaseDownloader.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 ext = subject.externalFile.outputStream()
val o = DupOutputStream(ext, output) val o = DupOutputStream(ext, output)
......
...@@ -52,6 +52,8 @@ sealed class Subject : Parcelable { ...@@ -52,6 +52,8 @@ sealed class Subject : Parcelable {
val externalFile get() = MediaStoreUtils.getFile("$title.apk").uri val externalFile get() = MediaStoreUtils.getFile("$title.apk").uri
} }
fun notifyID() = hashCode()
} }
sealed class Action : Parcelable { sealed class Action : Parcelable {
......
...@@ -6,7 +6,7 @@ import android.widget.ImageView ...@@ -6,7 +6,7 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
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.BaseDownloader import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding import com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding
import com.topjohnwu.magisk.di.viewModel import com.topjohnwu.magisk.di.viewModel
import com.topjohnwu.magisk.events.RebootEvent import com.topjohnwu.magisk.events.RebootEvent
...@@ -21,7 +21,7 @@ class HomeFragment : BaseUIFragment<HomeViewModel, FragmentHomeMd2Binding>() { ...@@ -21,7 +21,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)
BaseDownloader.observeProgress(this, viewModel::onProgressUpdate) DownloadService.observeProgress(this, viewModel::onProgressUpdate)
} }
private fun checkTitle(text: TextView, icon: ImageView) { private fun checkTitle(text: TextView, icon: ImageView) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment