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 {
// notifications
const val APK_UPDATE_NOTIFICATION_ID = 5
const val JOB_SERVICE_ID = 7
const val UPDATE_NOTIFICATION_CHANNEL = "update"
const val PROGRESS_NOTIFICATION_CHANNEL = "progress"

View File

@ -3,66 +3,47 @@ package com.topjohnwu.magisk.core.download
import android.annotation.SuppressLint
import android.app.Notification
import android.app.PendingIntent
import android.app.PendingIntent.*
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
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.core.ForegroundTracker
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.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.core.utils.ProgressInputStream
import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.ui.flash.FlashFragment
import com.topjohnwu.magisk.utils.APKInstall
import com.topjohnwu.magisk.ktx.copyAndClose
import com.topjohnwu.magisk.ktx.synchronized
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.superuser.internal.UiThreadHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import com.topjohnwu.magisk.view.Notifications.mgr
import kotlinx.coroutines.*
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
class DownloadService : BaseService() {
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)
private val notifications = HashMap<Int, Notification.Builder>().synchronized()
private val job = Job()
val service get() = ServiceLocator.networkService
private val mgr get() = Notifications.mgr
// -- 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
intent.getParcelableExtra<Subject>(SUBJECT_KEY)?.let { doDownload(it) }
return START_NOT_STICKY
}
override fun onTaskRemoved(rootIntent: Intent?) {
@ -73,29 +54,40 @@ class DownloadService : BaseService() {
override fun onDestroy() {
super.onDestroy()
coroutineScope.cancel()
job.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)
private fun doDownload(subject: Subject) {
update(subject.notifyId)
val coroutineScope = CoroutineScope(job + Dispatchers.IO)
coroutineScope.launch {
try {
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)
}
if (!hasNotifications)
stopSelf()
} catch (e: IOException) {
Timber.e(e)
notifyFail(subject)
}
}
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()
val id = subject.notifyId
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)
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()) {
private fun notifyFinish(subject: Subject) = finalNotify(subject.notifyId) {
broadcast(1f, subject)
it.setIntent(subject)
it.setContentIntent(subject.pendingIntent(this))
.setContentTitle(subject.title)
.setContentText(getString(R.string.download_complete))
.setSmallIcon(android.R.drawable.stat_sys_download_done)
@ -135,17 +127,14 @@ class DownloadService : BaseService() {
.setAutoCancel(true)
}
private fun finalNotify(
id: Int,
editor: (Notification.Builder) -> Notification.Builder
) : Int {
val notification = remove(id)?.run(editor) ?: return -1
val newId = nextInt()
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
val notification = remove(id)?.also(editor) ?: return -1
val newId = Notifications.nextId()
mgr.notify(newId, notification.build())
return newId
}
fun create() = Notifications.progress(this, "")
private fun create() = Notifications.progress(this, "")
fun update(id: Int, editor: (Notification.Builder) -> Unit = {}) {
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 {
private const val SUBJECT_KEY = "download_subject"
private const val REQUEST_CODE = 1
private val progressBroadcast = MutableLiveData<Pair<Float, Subject>?>()
@ -233,13 +182,13 @@ class DownloadService : BaseService() {
context.intent<DownloadService>().putExtra(SUBJECT_KEY, subject)
@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) {
PendingIntent.getForegroundService(context, nextInt(), intent(context, subject),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
getForegroundService(context, REQUEST_CODE, intent, flag)
} else {
PendingIntent.getService(context, nextInt(), intent(context, subject),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
getService(context, REQUEST_CODE, intent, flag)
}
}

View File

@ -1,6 +1,5 @@
package com.topjohnwu.magisk.core.download
import android.content.Context
import androidx.core.net.toFile
import com.topjohnwu.magisk.DynAPK
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.tasks.HideAPK
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.withStreams
import com.topjohnwu.magisk.ktx.writeTo
import java.io.File
import java.io.InputStream
import java.io.OutputStream
private fun Context.patch(apk: File) {
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 class TeeOutputStream(
private val o1: OutputStream,
private val o2: OutputStream
) : OutputStream() {
@ -50,20 +34,26 @@ private class DupOutputStream(
suspend fun DownloadService.handleAPK(subject: Subject.Manager, stream: InputStream) {
fun write(output: OutputStream) {
val ext = subject.externalFile.outputStream()
val o = DupOutputStream(ext, output)
withStreams(stream, o) { src, out -> src.copyTo(out) }
val external = subject.externalFile.outputStream()
stream.copyAndClose(TeeOutputStream(external, output))
}
if (isRunningAsStub) {
val apk = subject.file.toFile()
val id = subject.notifyID()
val id = subject.notifyId
write(DynAPK.update(this).outputStream())
if (Info.stub!!.version < subject.stub.versionCode) {
// 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)
patch(apk)
val patched = File(apk.parent, "patched.apk")
HideAPK.patch(this, apk, patched, packageName, applicationInfo.nonLocalizedLabel)
apk.delete()
patched.renameTo(apk)
} else {
// Simply relaunch the app
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
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Parcelable
import androidx.core.net.toFile
import androidx.core.net.toUri
import com.topjohnwu.magisk.core.Info
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.di.AppContext
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.Parcelize
private fun cachedFile(name: String) = AppContext.cachedFile(name).apply { delete() }.toUri()
enum class Action {
Flash,
Download
}
sealed class Subject : Parcelable {
abstract val url: String
abstract val file: Uri
abstract val action: Action
abstract val title: String
abstract val notifyId: Int
abstract fun pendingIntent(context: Context): PendingIntent
@Parcelize
class Module(
val module: OnlineModule,
override val action: Action
val action: Action,
override val notifyId: Int = Notifications.nextId()
) : Subject() {
override val url: String get() = module.zip_url
override val title: String get() = module.downloadFilename
@ -34,14 +50,19 @@ sealed class Subject : Parcelable {
override val file by lazy {
MediaStoreUtils.getFile(title).uri
}
override fun pendingIntent(context: Context) = when (action) {
Action.Flash -> FlashFragment.installIntent(context, file)
else -> Intent().toPending(context)
}
}
@Parcelize
class Manager(
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() {
override val action get() = Action.Download
override val title: String get() = "Magisk-${json.version}(${json.versionCode})"
override val url: String get() = json.link
@ -51,15 +72,14 @@ sealed class Subject : Parcelable {
}
val externalFile get() = MediaStoreUtils.getFile("$title.apk").uri
override fun pendingIntent(context: Context) =
APKInstall.installIntent(context, file.toFile()).toPending(context)
}
fun notifyID() = hashCode()
}
sealed class Action : Parcelable {
@Parcelize
object Flash : Action()
@Parcelize
object Download : Action()
@SuppressLint("InlinedApi")
protected fun Intent.toPending(context: Context): PendingIntent {
return PendingIntent.getActivity(context, notifyId, this,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT)
}
}

View File

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

View File

@ -14,8 +14,8 @@ class ModuleInstallDialog(private val item: OnlineModule) : DialogEvent() {
with(dialog) {
fun download(install: Boolean) {
val config = if (install) Action.Flash else Action.Download
val subject = Subject.Module(item, config)
val action = if (install) Action.Flash else Action.Download
val subject = Subject.Module(item, action)
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(
inStream: In,
outStream: Out,
@ -34,18 +31,19 @@ inline fun <In : InputStream, Out : OutputStream> withStreams(
}
}
fun <T> MutableList<T>.update(newList: List<T>) {
clear()
addAll(newList)
}
fun InputStream.copyAndClose(out: OutputStream) = withStreams(this, out) { i, o -> i.copyTo(o) }
fun InputStream.writeTo(file: File) = copyAndClose(file.outputStream())
operator fun <E> SparseArrayCompat<E>.set(key: Int, value: E) {
put(key, value)
}
fun <T> MutableList<T>.synchronized() = Collections.synchronizedList(this)
fun <T> MutableSet<T>.synchronized() = Collections.synchronizedSet(this)
fun <K, V> MutableMap<K, V>.synchronized() = Collections.synchronizedMap(this)
fun <T> MutableList<T>.synchronized(): MutableList<T> = Collections.synchronizedList(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) =
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 */
fun installIntent(context: Context, file: Uri, id: Int = -1) = FlashFragmentArgs(
fun installIntent(context: Context, file: Uri) = FlashFragmentArgs(
action = Const.Value.FLASH_ZIP,
additionalData = file,
dismissId = id
).let { createIntent(context, it) }
fun install(file: Uri, id: Int) = MainDirections.actionFlashFragment(
fun install(file: Uri) = MainDirections.actionFlashFragment(
action = Const.Value.FLASH_ZIP,
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.events.SnackbarEvent
import com.topjohnwu.magisk.ktx.*
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.superuser.CallbackList
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
@ -50,9 +49,7 @@ class FlashViewModel : BaseViewModel() {
}
fun startFlashing() {
val (action, uri, id) = args
if (id != -1)
Notifications.mgr.cancel(id)
val (action, uri) = args
viewModelScope.launch {
val result = when (action) {

View File

@ -70,4 +70,8 @@ class HomeFragment : BaseUIFragment<HomeViewModel, FragmentHomeMd2Binding>() {
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.graphics.drawable.toIcon
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.UPDATE_NOTIFICATION_CHANNEL
import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.di.AppContext
import com.topjohnwu.magisk.ktx.getBitmap
import java.util.concurrent.atomic.AtomicInteger
@Suppress("DEPRECATION")
object Notifications {
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) {
if (SDK_INT >= 26) {
var channel = NotificationChannel(UPDATE_NOTIFICATION_CHANNEL,
@ -46,7 +49,7 @@ object Notifications {
}
fun managerUpdate(context: Context) {
val intent = DownloadService.pendingIntent(context, Subject.Manager())
val intent = DownloadService.getPendingIntent(context, Subject.Manager())
val builder = updateBuilder(context)
.setContentTitle(context.getString(R.string.magisk_update_title))
@ -54,7 +57,7 @@ object Notifications {
.setAutoCancel(true)
.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 {
@ -69,4 +72,6 @@ object Notifications {
.setOngoing(true)
return builder
}
fun nextId() = nextId.incrementAndGet()
}

View File

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