From 084e0a73dc81783ae33b514d1447e75de27a261a Mon Sep 17 00:00:00 2001 From: topjohnwu Date: Thu, 3 Feb 2022 03:50:40 -0800 Subject: [PATCH] Cleanup DownloadService --- .../magisk/core/download/DownloadService.kt | 197 ++++++++---------- .../magisk/core/download/ManagerHandler.kt | 67 ------ .../magisk/core/download/ModuleProcessor.kt | 38 ---- .../core/download/NotificationService.kt | 113 ++++++++++ .../topjohnwu/magisk/core/download/Subject.kt | 2 +- .../events/dialog/ManagerInstallDialog.kt | 2 +- .../topjohnwu/magisk/ui/home/HomeViewModel.kt | 4 +- .../topjohnwu/magisk/view/Notifications.kt | 2 +- 8 files changed, 210 insertions(+), 215 deletions(-) delete mode 100644 app/src/main/java/com/topjohnwu/magisk/core/download/ManagerHandler.kt delete mode 100644 app/src/main/java/com/topjohnwu/magisk/core/download/ModuleProcessor.kt create mode 100644 app/src/main/java/com/topjohnwu/magisk/core/download/NotificationService.kt diff --git a/app/src/main/java/com/topjohnwu/magisk/core/download/DownloadService.kt b/app/src/main/java/com/topjohnwu/magisk/core/download/DownloadService.kt index 868d4dd76..3b111f6cc 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/download/DownloadService.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/download/DownloadService.kt @@ -1,71 +1,63 @@ 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.net.Uri 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.PhoenixActivity import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.StubApk 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.utils.ProgressInputStream -import com.topjohnwu.magisk.di.ServiceLocator -import com.topjohnwu.magisk.ktx.synchronized -import com.topjohnwu.magisk.view.Notifications -import com.topjohnwu.magisk.view.Notifications.mgr +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.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.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import okhttp3.ResponseBody import timber.log.Timber +import java.io.File 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().synchronized() 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 { - intent.getParcelableExtra(SUBJECT_KEY)?.let { doDownload(it) } + intent.getParcelableExtra(SUBJECT_KEY)?.let { download(it) } return START_NOT_STICKY } - override fun onTaskRemoved(rootIntent: Intent?) { - super.onTaskRemoved(rootIntent) - notifications.forEach { mgr.cancel(it.key) } - notifications.clear() - } - override fun onDestroy() { super.onDestroy() job.cancel() } - // -- Download logic - - private fun doDownload(subject: Subject) { + private fun download(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) - is Subject.Module -> stream.toModule(subject.file, assets.open("module_installer.sh")) + is Subject.App -> handleApp(stream, subject) + is Subject.Module -> handleModule(stream, subject.file) } val activity = ActivityTracker.foreground if (activity != null && subject.autoStart) { @@ -83,88 +75,88 @@ class DownloadService : BaseService() { } } - private fun ResponseBody.toProgressStream(subject: Subject): InputStream { - val max = contentLength() - val total = max.toFloat() / 1048576 - val id = subject.notifyId + private suspend fun handleApp(stream: InputStream, subject: Subject.App) { + fun write(output: OutputStream) { + val external = subject.externalFile.outputStream() + 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()) { - 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 handleModule(src: InputStream, file: Uri) { + val input = ZipInputStream(src.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")) + 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 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.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 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) + 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() } } companion object { - private const val SUBJECT_KEY = "download_subject" + private const val SUBJECT_KEY = "subject" private const val REQUEST_CODE = 1 - private val progressBroadcast = MutableLiveData?>() - fun observeProgress(owner: LifecycleOwner, callback: (Float, Subject) -> Unit) { progressBroadcast.value = null 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) = context.intent().putExtra(SUBJECT_KEY, subject) @@ -200,5 +188,4 @@ class DownloadService : BaseService() { } } } - } diff --git a/app/src/main/java/com/topjohnwu/magisk/core/download/ManagerHandler.kt b/app/src/main/java/com/topjohnwu/magisk/core/download/ManagerHandler.kt deleted file mode 100644 index f5a607e99..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/core/download/ManagerHandler.kt +++ /dev/null @@ -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() -} diff --git a/app/src/main/java/com/topjohnwu/magisk/core/download/ModuleProcessor.kt b/app/src/main/java/com/topjohnwu/magisk/core/download/ModuleProcessor.kt deleted file mode 100644 index 5c0f57232..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/core/download/ModuleProcessor.kt +++ /dev/null @@ -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) - } - } - } - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/core/download/NotificationService.kt b/app/src/main/java/com/topjohnwu/magisk/core/download/NotificationService.kt new file mode 100644 index 000000000..2161ecd6a --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/core/download/NotificationService.kt @@ -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().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?>() + + private fun broadcast(progress: Float, subject: Subject) { + progressBroadcast.postValue(progress to subject) + } + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/core/download/Subject.kt b/app/src/main/java/com/topjohnwu/magisk/core/download/Subject.kt index 42d1405e8..001abdd2a 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/download/Subject.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/download/Subject.kt @@ -56,7 +56,7 @@ sealed class Subject : Parcelable { } @Parcelize - class Manager( + class App( private val json: MagiskJson = Info.remote.magisk, val stub: StubJson = Info.remote.stub, override val notifyId: Int = Notifications.nextId() diff --git a/app/src/main/java/com/topjohnwu/magisk/events/dialog/ManagerInstallDialog.kt b/app/src/main/java/com/topjohnwu/magisk/events/dialog/ManagerInstallDialog.kt index f220d3875..c3a881cf9 100644 --- a/app/src/main/java/com/topjohnwu/magisk/events/dialog/ManagerInstallDialog.kt +++ b/app/src/main/java/com/topjohnwu/magisk/events/dialog/ManagerInstallDialog.kt @@ -29,7 +29,7 @@ class ManagerInstallDialog : MarkDownDialog() { setCancelable(true) setButton(MagiskDialog.ButtonType.POSITIVE) { text = R.string.install - onClick { DownloadService.start(context, Subject.Manager()) } + onClick { DownloadService.start(context, Subject.App()) } } setButton(MagiskDialog.ButtonType.NEGATIVE) { text = android.R.string.cancel diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt index ea9a20cd5..ca89d3bd9 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt @@ -10,7 +10,7 @@ import com.topjohnwu.magisk.arch.* import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Info 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.databinding.itemBindingOf import com.topjohnwu.magisk.databinding.set @@ -112,7 +112,7 @@ class HomeViewModel( }.publish() fun onProgressUpdate(progress: Float, subject: Subject) { - if (subject is Manager) + if (subject is App) stateManagerProgress = progress.times(100f).roundToInt() } diff --git a/app/src/main/java/com/topjohnwu/magisk/view/Notifications.kt b/app/src/main/java/com/topjohnwu/magisk/view/Notifications.kt index c5c54f169..59179b449 100644 --- a/app/src/main/java/com/topjohnwu/magisk/view/Notifications.kt +++ b/app/src/main/java/com/topjohnwu/magisk/view/Notifications.kt @@ -48,7 +48,7 @@ object Notifications { } fun managerUpdate(context: Context) { - val intent = DownloadService.getPendingIntent(context, Subject.Manager()) + val intent = DownloadService.getPendingIntent(context, Subject.App()) val builder = updateBuilder(context) .setContentTitle(context.getString(R.string.magisk_update_title))