From 4eb4097b9bf5487444deef089cdd53c597bde0b2 Mon Sep 17 00:00:00 2001 From: topjohnwu Date: Thu, 12 Dec 2024 15:29:33 -0800 Subject: [PATCH] Split file processing into its own class --- .../com/topjohnwu/magisk/core/JobService.kt | 3 +- .../java/com/topjohnwu/magisk/core/Service.kt | 3 +- .../magisk/core/download/DownloadEngine.kt | 157 ++---------------- .../magisk/core/download/DownloadProcessor.kt | 122 ++++++++++++++ .../magisk/core/download/Interfaces.kt | 15 ++ 5 files changed, 159 insertions(+), 141 deletions(-) create mode 100644 app/core/src/main/java/com/topjohnwu/magisk/core/download/DownloadProcessor.kt create mode 100644 app/core/src/main/java/com/topjohnwu/magisk/core/download/Interfaces.kt diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/JobService.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/JobService.kt index 1cb5b082c..3631bef16 100644 --- a/app/core/src/main/java/com/topjohnwu/magisk/core/JobService.kt +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/JobService.kt @@ -11,6 +11,7 @@ import androidx.core.content.getSystemService import com.topjohnwu.magisk.core.base.BaseJobService import com.topjohnwu.magisk.core.di.ServiceLocator import com.topjohnwu.magisk.core.download.DownloadEngine +import com.topjohnwu.magisk.core.download.DownloadSession import com.topjohnwu.magisk.core.download.Subject import com.topjohnwu.magisk.view.Notifications import kotlinx.coroutines.Dispatchers @@ -25,7 +26,7 @@ class JobService : BaseJobService() { @TargetApi(value = 34) inner class Session( private var params: JobParameters - ) : DownloadEngine.Session { + ) : DownloadSession { override val context get() = this@JobService val engine = DownloadEngine(this) diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/Service.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/Service.kt index e68f15548..c567b0e1e 100644 --- a/app/core/src/main/java/com/topjohnwu/magisk/core/Service.kt +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/Service.kt @@ -7,9 +7,10 @@ import androidx.core.app.ServiceCompat import androidx.core.content.IntentCompat import com.topjohnwu.magisk.core.base.BaseService import com.topjohnwu.magisk.core.download.DownloadEngine +import com.topjohnwu.magisk.core.download.DownloadSession import com.topjohnwu.magisk.core.download.Subject -class Service : BaseService(), DownloadEngine.Session { +class Service : BaseService(), DownloadSession { private var mEngine: DownloadEngine? = null override val context get() = this diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/download/DownloadEngine.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/download/DownloadEngine.kt index ee10d1355..343031fb7 100644 --- a/app/core/src/main/java/com/topjohnwu/magisk/core/download/DownloadEngine.kt +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/download/DownloadEngine.kt @@ -7,7 +7,6 @@ import android.app.PendingIntent import android.app.job.JobInfo import android.app.job.JobScheduler import android.content.Context -import android.net.Uri import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity @@ -16,7 +15,6 @@ import androidx.collection.isNotEmpty import androidx.core.content.getSystemService import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.MutableLiveData -import com.topjohnwu.magisk.StubApk import com.topjohnwu.magisk.core.AppContext import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.JobService @@ -25,30 +23,16 @@ import com.topjohnwu.magisk.core.base.IActivityExtension import com.topjohnwu.magisk.core.cmp import com.topjohnwu.magisk.core.di.ServiceLocator import com.topjohnwu.magisk.core.intent -import com.topjohnwu.magisk.core.isRunningAsStub -import com.topjohnwu.magisk.core.ktx.cachedFile -import com.topjohnwu.magisk.core.ktx.copyAll -import com.topjohnwu.magisk.core.ktx.copyAndClose import com.topjohnwu.magisk.core.ktx.set -import com.topjohnwu.magisk.core.ktx.withInOut -import com.topjohnwu.magisk.core.ktx.writeTo -import com.topjohnwu.magisk.core.tasks.AppMigration -import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream import com.topjohnwu.magisk.core.utils.ProgressInputStream -import com.topjohnwu.magisk.utils.APKInstall import com.topjohnwu.magisk.view.Notifications import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import okhttp3.ResponseBody -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry -import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream -import org.apache.commons.compress.archivers.zip.ZipFile import timber.log.Timber -import java.io.IOException import java.io.InputStream -import java.io.OutputStream /** * This class drives the execution of file downloads and notification management. @@ -67,16 +51,7 @@ import java.io.OutputStream * For API 23 - 33, we use a foreground service as a session. * For API 34 and higher, we use user-initiated job services as a session. */ -class DownloadEngine( - private val session: Session -) { - - interface Session { - val context: Context - - fun attachNotification(id: Int, builder: Notification.Builder) - fun onDownloadComplete() - } +class DownloadEngine(session: DownloadSession) : DownloadSession by session, DownloadNotifier { companion object { const val ACTION = "com.topjohnwu.magisk.DOWNLOAD" @@ -140,6 +115,7 @@ class DownloadEngine( } } + @SuppressLint("MissingPermission") fun start(context: Context, subject: Subject) { if (Build.VERSION.SDK_INT >= 34) { val scheduler = context.getSystemService()!! @@ -163,16 +139,18 @@ class DownloadEngine( } } + private val notifications = SparseArrayCompat() + private var attachedId = -1 + private val job = Job() + private val processor = DownloadProcessor(this) + private val network get() = ServiceLocator.networkService + fun download(subject: Subject) { notifyUpdate(subject.notifyId) CoroutineScope(job + Dispatchers.IO).launch { try { val stream = network.fetchFile(subject.url).toProgressStream(subject) - when (subject) { - is Subject.App -> handleApp(stream, subject) - is Subject.Module -> handleModule(stream, subject.file) - else -> stream.copyAndClose(subject.file.outputStream()) - } + processor.handle(stream, subject) val activity = AppContext.foregroundActivity if (activity != null && subject.autoLaunch) { notifyRemove(subject.notifyId) @@ -190,16 +168,13 @@ class DownloadEngine( @Synchronized fun reattach() { val builder = notifications[attachedId] ?: return - session.attachNotification(attachedId, builder) + attachNotification(attachedId, builder) } - private val notifications = SparseArrayCompat() - private var attachedId = -1 - - private val job = Job() - - private val context get() = session.context - private val network get() = ServiceLocator.networkService + private fun attach(id: Int, notification: Notification.Builder) { + attachedId = id + attachNotification(id, notification) + } private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int { val notification = notifyRemove(id)?.also(editor) ?: return -1 @@ -226,19 +201,14 @@ class DownloadEngine( subject.pendingIntent(context)?.let { intent -> it.setContentIntent(intent) } } - private fun attachNotification(id: Int, notification: Notification.Builder) { - attachedId = id - session.attachNotification(id, notification) - } - @Synchronized - private fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit = {}) { + override fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit) { val notification = (notifications[id] ?: Notifications.startProgress("").also { notifications[id] = it }).apply(editor) if (attachedId < 0) - attachNotification(id, notification) + attach(id, notification) else Notifications.mgr.notify(id, notification.build()) } @@ -258,11 +228,11 @@ class DownloadEngine( // There are still remaining notifications, pick one and attach to the session val anotherId = notifications.keyAt(0) val notification = notifications.valueAt(0) - attachNotification(anotherId, notification) + attach(anotherId, notification) } else { // No more notifications left, terminate the session attachedId = -1 - session.onDownloadComplete() + onDownloadComplete() } } } @@ -271,97 +241,6 @@ class DownloadEngine( return n } - private suspend fun handleApp(stream: InputStream, subject: Subject.App) { - val external = subject.file.outputStream() - - if (isRunningAsStub) { - val updateApk = StubApk.update(context) - try { - // Download full APK to stub update path - stream.copyAndClose(TeeOutputStream(external, updateApk.outputStream())) - - // Also upgrade stub - notifyUpdate(subject.notifyId) { - it.setProgress(0, 0, true) - .setContentTitle(context.getString(R.string.hide_app_title)) - .setContentText("") - } - - // Extract stub - val apk = context.cachedFile("stub.apk") - ZipFile.Builder().setFile(updateApk).get().use { zf -> - apk.delete() - zf.getInputStream(zf.getEntry("assets/stub.apk")).writeTo(apk) - } - - // Patch and install - subject.intent = AppMigration.upgradeStub(context, apk) - ?: throw IOException("HideAPK patch error") - apk.delete() - } catch (e: Exception) { - // If any error occurred, do not let stub load the new APK - updateApk.delete() - throw e - } - } else { - val session = APKInstall.startSession(context) - stream.copyAndClose(TeeOutputStream(external, session.openStream(context))) - subject.intent = session.waitIntent() - } - } - - private suspend fun handleModule(src: InputStream, file: Uri) { - val tmp = context.cachedFile("module.zip") - try { - // First download the entire zip into cache so we can process it - src.writeTo(tmp) - - val input = ZipFile.Builder().setFile(tmp).get() - val output = ZipArchiveOutputStream(file.outputStream()) - withInOut(input, output) { zin, zout -> - zout.putArchiveEntry(ZipArchiveEntry("META-INF/")) - zout.closeArchiveEntry() - zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/")) - zout.closeArchiveEntry() - zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/")) - zout.closeArchiveEntry() - zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/android/")) - zout.closeArchiveEntry() - - zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/android/update-binary")) - context.assets.open("module_installer.sh").use { it.copyAll(zout) } - zout.closeArchiveEntry() - - zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/android/updater-script")) - zout.write("#MAGISK\n".toByteArray()) - zout.closeArchiveEntry() - - // Then simply copy all entries to output - zin.copyRawEntries(zout) { entry -> !entry.name.startsWith("META-INF") } - } - } finally { - tmp.delete() - } - } - - 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() - } - } - private fun ResponseBody.toProgressStream(subject: Subject): InputStream { val max = contentLength() val total = max.toFloat() / 1048576 diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/download/DownloadProcessor.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/download/DownloadProcessor.kt new file mode 100644 index 000000000..97a6f25ac --- /dev/null +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/download/DownloadProcessor.kt @@ -0,0 +1,122 @@ +package com.topjohnwu.magisk.core.download + +import android.net.Uri +import com.topjohnwu.magisk.StubApk +import com.topjohnwu.magisk.core.R +import com.topjohnwu.magisk.core.isRunningAsStub +import com.topjohnwu.magisk.core.ktx.cachedFile +import com.topjohnwu.magisk.core.ktx.copyAll +import com.topjohnwu.magisk.core.ktx.copyAndClose +import com.topjohnwu.magisk.core.ktx.withInOut +import com.topjohnwu.magisk.core.ktx.writeTo +import com.topjohnwu.magisk.core.tasks.AppMigration +import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream +import com.topjohnwu.magisk.utils.APKInstall +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream +import org.apache.commons.compress.archivers.zip.ZipFile +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream + +class DownloadProcessor(notifier: DownloadNotifier) : DownloadNotifier by notifier { + + suspend fun handle(stream: InputStream, subject: Subject) { + when (subject) { + is Subject.App -> handleApp(stream, subject) + is Subject.Module -> handleModule(stream, subject.file) + else -> stream.copyAndClose(subject.file.outputStream()) + } + } + + suspend fun handleApp(stream: InputStream, subject: Subject.App) { + val external = subject.file.outputStream() + + if (isRunningAsStub) { + val updateApk = StubApk.update(context) + try { + // Download full APK to stub update path + stream.copyAndClose(TeeOutputStream(external, updateApk.outputStream())) + + // Also upgrade stub + notifyUpdate(subject.notifyId) { + it.setProgress(0, 0, true) + .setContentTitle(context.getString(R.string.hide_app_title)) + .setContentText("") + } + + // Extract stub + val apk = context.cachedFile("stub.apk") + ZipFile.Builder().setFile(updateApk).get().use { zf -> + apk.delete() + zf.getInputStream(zf.getEntry("assets/stub.apk")).writeTo(apk) + } + + // Patch and install + subject.intent = AppMigration.upgradeStub(context, apk) + ?: throw IOException("HideAPK patch error") + apk.delete() + } catch (e: Exception) { + // If any error occurred, do not let stub load the new APK + updateApk.delete() + throw e + } + } else { + val session = APKInstall.startSession(context) + stream.copyAndClose(TeeOutputStream(external, session.openStream(context))) + subject.intent = session.waitIntent() + } + } + + suspend fun handleModule(src: InputStream, file: Uri) { + val tmp = context.cachedFile("module.zip") + try { + // First download the entire zip into cache so we can process it + src.writeTo(tmp) + + val input = ZipFile.Builder().setFile(tmp).get() + val output = ZipArchiveOutputStream(file.outputStream()) + withInOut(input, output) { zin, zout -> + zout.putArchiveEntry(ZipArchiveEntry("META-INF/")) + zout.closeArchiveEntry() + zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/")) + zout.closeArchiveEntry() + zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/")) + zout.closeArchiveEntry() + zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/android/")) + zout.closeArchiveEntry() + + zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/android/update-binary")) + context.assets.open("module_installer.sh").use { it.copyAll(zout) } + zout.closeArchiveEntry() + + zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/android/updater-script")) + zout.write("#MAGISK\n".toByteArray()) + zout.closeArchiveEntry() + + // Then simply copy all entries to output + zin.copyRawEntries(zout) { entry -> !entry.name.startsWith("META-INF") } + } + } finally { + tmp.delete() + } + } + + 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() + } + } +} diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/download/Interfaces.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/download/Interfaces.kt new file mode 100644 index 000000000..bdc68a7e9 --- /dev/null +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/download/Interfaces.kt @@ -0,0 +1,15 @@ +package com.topjohnwu.magisk.core.download + +import android.app.Notification +import android.content.Context + +interface DownloadSession { + val context: Context + fun attachNotification(id: Int, builder: Notification.Builder) + fun onDownloadComplete() +} + +interface DownloadNotifier { + val context: Context + fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit = {}) +}