Split file processing into its own class

This commit is contained in:
topjohnwu 2024-12-12 15:29:33 -08:00 committed by John Wu
parent c512496847
commit 4eb4097b9b
5 changed files with 159 additions and 141 deletions

View File

@ -11,6 +11,7 @@ import androidx.core.content.getSystemService
import com.topjohnwu.magisk.core.base.BaseJobService import com.topjohnwu.magisk.core.base.BaseJobService
import com.topjohnwu.magisk.core.di.ServiceLocator import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.download.DownloadEngine 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.core.download.Subject
import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.magisk.view.Notifications
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -25,7 +26,7 @@ class JobService : BaseJobService() {
@TargetApi(value = 34) @TargetApi(value = 34)
inner class Session( inner class Session(
private var params: JobParameters private var params: JobParameters
) : DownloadEngine.Session { ) : DownloadSession {
override val context get() = this@JobService override val context get() = this@JobService
val engine = DownloadEngine(this) val engine = DownloadEngine(this)

View File

@ -7,9 +7,10 @@ import androidx.core.app.ServiceCompat
import androidx.core.content.IntentCompat import androidx.core.content.IntentCompat
import com.topjohnwu.magisk.core.base.BaseService import com.topjohnwu.magisk.core.base.BaseService
import com.topjohnwu.magisk.core.download.DownloadEngine 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.core.download.Subject
class Service : BaseService(), DownloadEngine.Session { class Service : BaseService(), DownloadSession {
private var mEngine: DownloadEngine? = null private var mEngine: DownloadEngine? = null
override val context get() = this override val context get() = this

View File

@ -7,7 +7,6 @@ import android.app.PendingIntent
import android.app.job.JobInfo import android.app.job.JobInfo
import android.app.job.JobScheduler import android.app.job.JobScheduler
import android.content.Context import android.content.Context
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
@ -16,7 +15,6 @@ import androidx.collection.isNotEmpty
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.topjohnwu.magisk.StubApk
import com.topjohnwu.magisk.core.AppContext import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.JobService 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.cmp
import com.topjohnwu.magisk.core.di.ServiceLocator import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.intent 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.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.core.utils.ProgressInputStream
import com.topjohnwu.magisk.utils.APKInstall
import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.magisk.view.Notifications
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 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 timber.log.Timber
import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream
/** /**
* This class drives the execution of file downloads and notification management. * 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 23 - 33, we use a foreground service as a session.
* For API 34 and higher, we use user-initiated job services as a session. * For API 34 and higher, we use user-initiated job services as a session.
*/ */
class DownloadEngine( class DownloadEngine(session: DownloadSession) : DownloadSession by session, DownloadNotifier {
private val session: Session
) {
interface Session {
val context: Context
fun attachNotification(id: Int, builder: Notification.Builder)
fun onDownloadComplete()
}
companion object { companion object {
const val ACTION = "com.topjohnwu.magisk.DOWNLOAD" const val ACTION = "com.topjohnwu.magisk.DOWNLOAD"
@ -140,6 +115,7 @@ class DownloadEngine(
} }
} }
@SuppressLint("MissingPermission")
fun start(context: Context, subject: Subject) { fun start(context: Context, subject: Subject) {
if (Build.VERSION.SDK_INT >= 34) { if (Build.VERSION.SDK_INT >= 34) {
val scheduler = context.getSystemService<JobScheduler>()!! val scheduler = context.getSystemService<JobScheduler>()!!
@ -163,16 +139,18 @@ class DownloadEngine(
} }
} }
private val notifications = SparseArrayCompat<Notification.Builder>()
private var attachedId = -1
private val job = Job()
private val processor = DownloadProcessor(this)
private val network get() = ServiceLocator.networkService
fun download(subject: Subject) { fun download(subject: Subject) {
notifyUpdate(subject.notifyId) notifyUpdate(subject.notifyId)
CoroutineScope(job + Dispatchers.IO).launch { CoroutineScope(job + Dispatchers.IO).launch {
try { try {
val stream = network.fetchFile(subject.url).toProgressStream(subject) val stream = network.fetchFile(subject.url).toProgressStream(subject)
when (subject) { processor.handle(stream, subject)
is Subject.App -> handleApp(stream, subject)
is Subject.Module -> handleModule(stream, subject.file)
else -> stream.copyAndClose(subject.file.outputStream())
}
val activity = AppContext.foregroundActivity val activity = AppContext.foregroundActivity
if (activity != null && subject.autoLaunch) { if (activity != null && subject.autoLaunch) {
notifyRemove(subject.notifyId) notifyRemove(subject.notifyId)
@ -190,16 +168,13 @@ class DownloadEngine(
@Synchronized @Synchronized
fun reattach() { fun reattach() {
val builder = notifications[attachedId] ?: return val builder = notifications[attachedId] ?: return
session.attachNotification(attachedId, builder) attachNotification(attachedId, builder)
} }
private val notifications = SparseArrayCompat<Notification.Builder>() private fun attach(id: Int, notification: Notification.Builder) {
private var attachedId = -1 attachedId = id
attachNotification(id, notification)
private val job = Job() }
private val context get() = session.context
private val network get() = ServiceLocator.networkService
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int { private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
val notification = notifyRemove(id)?.also(editor) ?: return -1 val notification = notifyRemove(id)?.also(editor) ?: return -1
@ -226,19 +201,14 @@ class DownloadEngine(
subject.pendingIntent(context)?.let { intent -> it.setContentIntent(intent) } subject.pendingIntent(context)?.let { intent -> it.setContentIntent(intent) }
} }
private fun attachNotification(id: Int, notification: Notification.Builder) {
attachedId = id
session.attachNotification(id, notification)
}
@Synchronized @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 { val notification = (notifications[id] ?: Notifications.startProgress("").also {
notifications[id] = it notifications[id] = it
}).apply(editor) }).apply(editor)
if (attachedId < 0) if (attachedId < 0)
attachNotification(id, notification) attach(id, notification)
else else
Notifications.mgr.notify(id, notification.build()) Notifications.mgr.notify(id, notification.build())
} }
@ -258,11 +228,11 @@ class DownloadEngine(
// There are still remaining notifications, pick one and attach to the session // There are still remaining notifications, pick one and attach to the session
val anotherId = notifications.keyAt(0) val anotherId = notifications.keyAt(0)
val notification = notifications.valueAt(0) val notification = notifications.valueAt(0)
attachNotification(anotherId, notification) attach(anotherId, notification)
} else { } else {
// No more notifications left, terminate the session // No more notifications left, terminate the session
attachedId = -1 attachedId = -1
session.onDownloadComplete() onDownloadComplete()
} }
} }
} }
@ -271,97 +241,6 @@ class DownloadEngine(
return n 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 { private fun ResponseBody.toProgressStream(subject: Subject): InputStream {
val max = contentLength() val max = contentLength()
val total = max.toFloat() / 1048576 val total = max.toFloat() / 1048576

View File

@ -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()
}
}
}

View File

@ -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 = {})
}