Cleanup DownloadService

This commit is contained in:
topjohnwu 2022-02-03 03:50:40 -08:00
parent 10f991b8d0
commit 084e0a73dc
8 changed files with 210 additions and 215 deletions

View File

@ -1,71 +1,63 @@
package com.topjohnwu.magisk.core.download package com.topjohnwu.magisk.core.download
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Notification
import android.app.PendingIntent import android.app.PendingIntent
import android.app.PendingIntent.* import android.app.PendingIntent.*
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.IBinder import androidx.core.net.toFile
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData import com.topjohnwu.magisk.PhoenixActivity
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.StubApk
import com.topjohnwu.magisk.core.ActivityTracker import com.topjohnwu.magisk.core.ActivityTracker
import com.topjohnwu.magisk.core.base.BaseService import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.intent import com.topjohnwu.magisk.core.intent
import com.topjohnwu.magisk.core.utils.ProgressInputStream import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.di.ServiceLocator import com.topjohnwu.magisk.core.tasks.HideAPK
import com.topjohnwu.magisk.ktx.synchronized import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.magisk.ktx.copyAndClose
import com.topjohnwu.magisk.view.Notifications.mgr import com.topjohnwu.magisk.ktx.forEach
import com.topjohnwu.magisk.ktx.withStreams
import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.magisk.utils.APKInstall
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.ResponseBody
import timber.log.Timber import timber.log.Timber
import java.io.File
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
class DownloadService : BaseService() { class DownloadService : NotificationService() {
private val hasNotifications get() = notifications.isNotEmpty()
private val notifications = HashMap<Int, Notification.Builder>().synchronized()
private val job = Job() private val job = Job()
val service get() = ServiceLocator.networkService
// -- Service overrides
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
intent.getParcelableExtra<Subject>(SUBJECT_KEY)?.let { doDownload(it) } intent.getParcelableExtra<Subject>(SUBJECT_KEY)?.let { download(it) }
return START_NOT_STICKY return START_NOT_STICKY
} }
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
notifications.forEach { mgr.cancel(it.key) }
notifications.clear()
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
job.cancel() job.cancel()
} }
// -- Download logic private fun download(subject: Subject) {
private fun doDownload(subject: Subject) {
update(subject.notifyId) update(subject.notifyId)
val coroutineScope = CoroutineScope(job + Dispatchers.IO) val coroutineScope = CoroutineScope(job + Dispatchers.IO)
coroutineScope.launch { coroutineScope.launch {
try { try {
val stream = service.fetchFile(subject.url).toProgressStream(subject) val stream = service.fetchFile(subject.url).toProgressStream(subject)
when (subject) { when (subject) {
is Subject.Manager -> handleAPK(subject, stream) is Subject.App -> handleApp(stream, subject)
is Subject.Module -> stream.toModule(subject.file, assets.open("module_installer.sh")) is Subject.Module -> handleModule(stream, subject.file)
} }
val activity = ActivityTracker.foreground val activity = ActivityTracker.foreground
if (activity != null && subject.autoStart) { if (activity != null && subject.autoStart) {
@ -83,88 +75,88 @@ class DownloadService : BaseService() {
} }
} }
private fun ResponseBody.toProgressStream(subject: Subject): InputStream { private suspend fun handleApp(stream: InputStream, subject: Subject.App) {
val max = contentLength() fun write(output: OutputStream) {
val total = max.toFloat() / 1048576 val external = subject.externalFile.outputStream()
val id = subject.notifyId stream.copyAndClose(TeeOutputStream(external, output))
}
update(id) { it.setContentTitle(subject.title) } if (isRunningAsStub) {
val apk = subject.file.toFile()
val id = subject.notifyId
write(StubApk.update(this).outputStream())
if (Info.stub!!.version < subject.stub.versionCode) {
// Also upgrade stub
update(id) {
it.setProgress(0, 0, true)
.setContentTitle(getString(R.string.hide_app_title))
.setContentText("")
}
service.fetchFile(subject.stub.link).byteStream().writeTo(apk)
val patched = File(apk.parent, "patched.apk")
HideAPK.patch(this, apk, patched, packageName, applicationInfo.nonLocalizedLabel)
apk.delete()
patched.renameTo(apk)
} else {
val clz = Info.stub!!.classToComponent["PHOENIX"]!!
PhoenixActivity.rebirth(this, clz)
return
}
}
val receiver = APKInstall.register(this, null, null)
write(APKInstall.openStream(this, false))
subject.intent = receiver.waitIntent()
}
return ProgressInputStream(byteStream()) { private fun handleModule(src: InputStream, file: Uri) {
val progress = it.toFloat() / 1048576 val input = ZipInputStream(src.buffered())
update(id) { notification -> val output = ZipOutputStream(file.outputStream().buffered())
if (max > 0) {
broadcast(progress / total, subject) withStreams(input, output) { zin, zout ->
notification zout.putNextEntry(ZipEntry("META-INF/"))
.setProgress(max.toInt(), it.toInt(), false) zout.putNextEntry(ZipEntry("META-INF/com/"))
.setContentText("%.2f / %.2f MB".format(progress, total)) zout.putNextEntry(ZipEntry("META-INF/com/google/"))
} else { zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
broadcast(-1f, subject) zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
notification.setContentText("%.2f MB / ??".format(progress)) assets.open("module_installer.sh").copyTo(zout)
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
zout.write("#MAGISK\n".toByteArray())
zin.forEach { entry ->
val path = entry.name
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
zout.putNextEntry(ZipEntry(path))
if (!entry.isDirectory) {
zin.copyTo(zout)
}
} }
} }
} }
} }
// --- Notification management private class TeeOutputStream(
private val o1: OutputStream,
private fun notifyFail(subject: Subject) = finalNotify(subject.notifyId) { private val o2: OutputStream
broadcast(-2f, subject) ) : OutputStream() {
it.setContentText(getString(R.string.download_file_error)) override fun write(b: Int) {
.setSmallIcon(android.R.drawable.stat_notify_error) o1.write(b)
.setOngoing(false) o2.write(b)
} }
override fun write(b: ByteArray?, off: Int, len: Int) {
private fun notifyFinish(subject: Subject) = finalNotify(subject.notifyId) { o1.write(b, off, len)
broadcast(1f, subject) o2.write(b, off, len)
it.setContentTitle(subject.title) }
.setContentText(getString(R.string.download_complete)) override fun close() {
.setSmallIcon(android.R.drawable.stat_sys_download_done) o1.close()
.setProgress(0, 0, false) o2.close()
.setOngoing(false)
.setAutoCancel(true)
subject.pendingIntent(this)?.let { intent -> it.setContentIntent(intent) }
}
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
}
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
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)
} }
} }
companion object { companion object {
private const val SUBJECT_KEY = "download_subject" private const val SUBJECT_KEY = "subject"
private const val REQUEST_CODE = 1 private const val REQUEST_CODE = 1
private val progressBroadcast = MutableLiveData<Pair<Float, Subject>?>()
fun observeProgress(owner: LifecycleOwner, callback: (Float, Subject) -> Unit) { fun observeProgress(owner: LifecycleOwner, callback: (Float, Subject) -> Unit) {
progressBroadcast.value = null progressBroadcast.value = null
progressBroadcast.observe(owner) { progressBroadcast.observe(owner) {
@ -173,10 +165,6 @@ class DownloadService : BaseService() {
} }
} }
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(SUBJECT_KEY, subject) context.intent<DownloadService>().putExtra(SUBJECT_KEY, subject)
@ -200,5 +188,4 @@ class DownloadService : BaseService() {
} }
} }
} }
} }

View File

@ -1,67 +0,0 @@
package com.topjohnwu.magisk.core.download
import androidx.core.net.toFile
import com.topjohnwu.magisk.PhoenixActivity
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.StubApk
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.writeTo
import com.topjohnwu.magisk.utils.APKInstall
import java.io.File
import java.io.InputStream
import java.io.OutputStream
private class TeeOutputStream(
private val o1: OutputStream,
private val o2: OutputStream
) : OutputStream() {
override fun write(b: Int) {
o1.write(b)
o2.write(b)
}
override fun write(b: ByteArray?, off: Int, len: Int) {
o1.write(b, off, len)
o2.write(b, off, len)
}
override fun close() {
o1.close()
o2.close()
}
}
suspend fun DownloadService.handleAPK(subject: Subject.Manager, stream: InputStream) {
fun write(output: OutputStream) {
val external = subject.externalFile.outputStream()
stream.copyAndClose(TeeOutputStream(external, output))
}
if (isRunningAsStub) {
val apk = subject.file.toFile()
val id = subject.notifyId
write(StubApk.update(this).outputStream())
if (Info.stub!!.version < subject.stub.versionCode) {
// Also upgrade stub
update(id) {
it.setProgress(0, 0, true)
.setContentTitle(getString(R.string.hide_app_title))
.setContentText("")
}
service.fetchFile(subject.stub.link).byteStream().writeTo(apk)
val patched = File(apk.parent, "patched.apk")
HideAPK.patch(this, apk, patched, packageName, applicationInfo.nonLocalizedLabel)
apk.delete()
patched.renameTo(apk)
} else {
val clz = Info.stub!!.classToComponent["PHOENIX"]!!
PhoenixActivity.rebirth(this, clz)
return
}
}
val receiver = APKInstall.register(this, null, null)
write(APKInstall.openStream(this, false))
subject.intent = receiver.waitIntent()
}

View File

@ -1,38 +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")))
zin.forEach { entry ->
val path = entry.name
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
zout.putNextEntry(ZipEntry(path))
if (!entry.isDirectory) {
zin.copyTo(zout)
}
}
}
}
}

View File

@ -0,0 +1,113 @@
package com.topjohnwu.magisk.core.download
import android.app.Notification
import android.content.Intent
import android.os.IBinder
import androidx.lifecycle.MutableLiveData
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.base.BaseService
import com.topjohnwu.magisk.core.utils.ProgressInputStream
import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.ktx.synchronized
import com.topjohnwu.magisk.view.Notifications
import okhttp3.ResponseBody
import java.io.InputStream
open class NotificationService : BaseService() {
private val notifications = HashMap<Int, Notification.Builder>().synchronized()
protected val hasNotifications get() = notifications.isNotEmpty()
protected val service get() = ServiceLocator.networkService
override fun onBind(intent: Intent?): IBinder? = null
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
notifications.forEach { Notifications.mgr.cancel(it.key) }
notifications.clear()
}
protected 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))
}
}
}
}
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
val notification = remove(id)?.also(editor) ?: return -1
val newId = Notifications.nextId()
Notifications.mgr.notify(newId, notification.build())
return newId
}
protected 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)
}
protected fun notifyFinish(subject: Subject) = finalNotify(subject.notifyId) {
broadcast(1f, subject)
it.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)
subject.pendingIntent(this)?.let { intent -> it.setContentIntent(intent) }
}
private fun create() = Notifications.progress(this, "")
private fun updateForeground() {
if (hasNotifications) {
val (id, notification) = notifications.entries.first()
startForeground(id, notification.build())
} else {
stopForeground(false)
}
}
protected fun update(id: Int, editor: (Notification.Builder) -> Unit = {}) {
val wasEmpty = !hasNotifications
val notification = notifications.getOrPut(id, ::create).also(editor)
if (wasEmpty)
updateForeground()
else
Notifications.mgr.notify(id, notification.build())
}
protected fun remove(id: Int): Notification.Builder? {
val n = notifications.remove(id)?.also { updateForeground() }
Notifications.mgr.cancel(id)
return n
}
companion object {
@JvmStatic
protected val progressBroadcast = MutableLiveData<Pair<Float, Subject>?>()
private fun broadcast(progress: Float, subject: Subject) {
progressBroadcast.postValue(progress to subject)
}
}
}

View File

@ -56,7 +56,7 @@ sealed class Subject : Parcelable {
} }
@Parcelize @Parcelize
class Manager( class App(
private val json: MagiskJson = Info.remote.magisk, private val json: MagiskJson = Info.remote.magisk,
val stub: StubJson = Info.remote.stub, val stub: StubJson = Info.remote.stub,
override val notifyId: Int = Notifications.nextId() override val notifyId: Int = Notifications.nextId()

View File

@ -29,7 +29,7 @@ class ManagerInstallDialog : MarkDownDialog() {
setCancelable(true) setCancelable(true)
setButton(MagiskDialog.ButtonType.POSITIVE) { setButton(MagiskDialog.ButtonType.POSITIVE) {
text = R.string.install text = R.string.install
onClick { DownloadService.start(context, Subject.Manager()) } onClick { DownloadService.start(context, Subject.App()) }
} }
setButton(MagiskDialog.ButtonType.NEGATIVE) { setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel text = android.R.string.cancel

View File

@ -10,7 +10,7 @@ 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.Subject import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.download.Subject.Manager import com.topjohnwu.magisk.core.download.Subject.App
import com.topjohnwu.magisk.data.repository.NetworkService import com.topjohnwu.magisk.data.repository.NetworkService
import com.topjohnwu.magisk.databinding.itemBindingOf import com.topjohnwu.magisk.databinding.itemBindingOf
import com.topjohnwu.magisk.databinding.set import com.topjohnwu.magisk.databinding.set
@ -112,7 +112,7 @@ class HomeViewModel(
}.publish() }.publish()
fun onProgressUpdate(progress: Float, subject: Subject) { fun onProgressUpdate(progress: Float, subject: Subject) {
if (subject is Manager) if (subject is App)
stateManagerProgress = progress.times(100f).roundToInt() stateManagerProgress = progress.times(100f).roundToInt()
} }

View File

@ -48,7 +48,7 @@ object Notifications {
} }
fun managerUpdate(context: Context) { fun managerUpdate(context: Context) {
val intent = DownloadService.getPendingIntent(context, Subject.Manager()) val intent = DownloadService.getPendingIntent(context, Subject.App())
val builder = updateBuilder(context) val builder = updateBuilder(context)
.setContentTitle(context.getString(R.string.magisk_update_title)) .setContentTitle(context.getString(R.string.magisk_update_title))