diff --git a/app/src/main/aidl/app/revanced/manager/patcher/ProgressEventParcel.aidl b/app/src/main/aidl/app/revanced/manager/patcher/ProgressEventParcel.aidl new file mode 100644 index 00000000..e9f6cf2c --- /dev/null +++ b/app/src/main/aidl/app/revanced/manager/patcher/ProgressEventParcel.aidl @@ -0,0 +1,4 @@ +// ProgressEventParcel.aidl +package app.revanced.manager.patcher; + +parcelable ProgressEventParcel; \ No newline at end of file diff --git a/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherEvents.aidl b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherEvents.aidl index 27a4f61b..fa11709a 100644 --- a/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherEvents.aidl +++ b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherEvents.aidl @@ -1,11 +1,12 @@ // IPatcherEvents.aidl package app.revanced.manager.patcher.runtime.process; +import app.revanced.manager.patcher.ProgressEventParcel; + // Interface for sending events back to the main app process. oneway interface IPatcherEvents { void log(String level, String msg); - void patchSucceeded(); - void progress(String name, String state, String msg); + void event(in ProgressEventParcel event); // The patching process has ended. The exceptionStackTrace is null if it finished successfully. void finished(String exceptionStackTrace); } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/PatcherProgress.kt b/app/src/main/java/app/revanced/manager/patcher/PatcherProgress.kt new file mode 100644 index 00000000..ce010a01 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/PatcherProgress.kt @@ -0,0 +1,78 @@ +package app.revanced.manager.patcher + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + + +@Parcelize +sealed class ProgressEvent : Parcelable { + abstract val stepId: StepId? + + data class Started(override val stepId: StepId) : ProgressEvent() + + data class Progress( + override val stepId: StepId, + val current: Long? = null, + val total: Long? = null, + val message: String? = null, + ) : ProgressEvent() + + data class Completed( + override val stepId: StepId, + ) : ProgressEvent() + + data class Failed( + override val stepId: StepId?, + val error: RemoteError, + ) : ProgressEvent() +} + +/** + * Parcelable wrapper for [ProgressEvent]. + * + * Required because AIDL does not support sealed classes. + */ +@Parcelize +data class ProgressEventParcel(val event: ProgressEvent) : Parcelable + +fun ProgressEventParcel.toEvent(): ProgressEvent = event +fun ProgressEvent.toParcel(): ProgressEventParcel = ProgressEventParcel(this) + +@Parcelize +sealed class StepId : Parcelable { + data object DownloadAPK : StepId() + data object LoadPatches : StepId() + data object ReadAPK : StepId() + data object ExecutePatches : StepId() + data class ExecutePatch(val index: Int) : StepId() + data object WriteAPK : StepId() + data object SignAPK : StepId() +} + +@Parcelize +data class RemoteError( + val type: String, + val message: String?, + val stackTrace: String, +) : Parcelable + +fun Exception.toRemoteError() = RemoteError( + type = this::class.java.name, + message = this.message, + stackTrace = this.stackTraceToString(), +) + + +inline fun runStep( + stepId: StepId, + onEvent: (ProgressEvent) -> Unit, + block: () -> T, +): T = try { + onEvent(ProgressEvent.Started(stepId)) + val value = block() + onEvent(ProgressEvent.Completed(stepId)) + value +} catch (error: Exception) { + onEvent(ProgressEvent.Failed(stepId, error.toRemoteError())) + throw error +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/Session.kt b/app/src/main/java/app/revanced/manager/patcher/Session.kt index dd5e7dc4..42f0e955 100644 --- a/app/src/main/java/app/revanced/manager/patcher/Session.kt +++ b/app/src/main/java/app/revanced/manager/patcher/Session.kt @@ -1,10 +1,9 @@ package app.revanced.manager.patcher -import android.content.Context import app.revanced.library.ApkUtils.applyTo -import app.revanced.manager.R +import app.revanced.manager.patcher.Session.Companion.component1 +import app.revanced.manager.patcher.Session.Companion.component2 import app.revanced.manager.patcher.logger.Logger -import app.revanced.manager.ui.model.State import app.revanced.patcher.Patcher import app.revanced.patcher.PatcherConfig import app.revanced.patcher.patch.Patch @@ -22,15 +21,10 @@ class Session( cacheDir: String, frameworkDir: String, aaptPath: String, - private val androidContext: Context, private val logger: Logger, private val input: File, - private val onPatchCompleted: suspend () -> Unit, - private val onProgress: (name: String?, state: State?, message: String?) -> Unit + private val onEvent: (ProgressEvent) -> Unit, ) : Closeable { - private fun updateProgress(name: String? = null, state: State? = null, message: String? = null) = - onProgress(name, state, message) - private val tempDir = File(cacheDir).resolve("patcher").also { it.mkdirs() } private val patcher = Patcher( PatcherConfig( @@ -42,86 +36,68 @@ class Session( ) private suspend fun Patcher.applyPatchesVerbose(selectedPatches: PatchList) { - var nextPatchIndex = 0 - - updateProgress( - name = androidContext.getString(R.string.executing_patch, selectedPatches[nextPatchIndex]), - state = State.RUNNING - ) - this().collect { (patch, exception) -> - if (patch !in selectedPatches) return@collect + val index = selectedPatches.indexOf(patch) + if (index == -1) return@collect if (exception != null) { - updateProgress( - name = androidContext.getString(R.string.failed_to_execute_patch, patch.name), - state = State.FAILED, - message = exception.stackTraceToString() + onEvent( + ProgressEvent.Failed( + StepId.ExecutePatch(index), + exception.toRemoteError(), + ) ) - logger.error("${patch.name} failed:") logger.error(exception.stackTraceToString()) throw exception } - nextPatchIndex++ - - onPatchCompleted() - - selectedPatches.getOrNull(nextPatchIndex)?.let { nextPatch -> - updateProgress( - name = androidContext.getString(R.string.executing_patch, nextPatch.name) + onEvent( + ProgressEvent.Completed( + StepId.ExecutePatch(index), ) - } + ) logger.info("${patch.name} succeeded") } - - updateProgress( - state = State.COMPLETED, - name = androidContext.resources.getQuantityString( - R.plurals.patches_executed, - selectedPatches.size, - selectedPatches.size - ) - ) } suspend fun run(output: File, selectedPatches: PatchList) { - updateProgress(state = State.COMPLETED) // Unpacking + runStep(StepId.ExecutePatches, onEvent) { + java.util.logging.Logger.getLogger("").apply { + handlers.forEach { + it.close() + removeHandler(it) + } - java.util.logging.Logger.getLogger("").apply { - handlers.forEach { - it.close() - removeHandler(it) + addHandler(logger.handler) } - addHandler(logger.handler) + with(patcher) { + logger.info("Merging integrations") + this += selectedPatches.toSet() + + logger.info("Applying patches...") + applyPatchesVerbose(selectedPatches.sortedBy { it.name }) + } } - with(patcher) { - logger.info("Merging integrations") - this += selectedPatches.toSet() + runStep(StepId.WriteAPK, onEvent) { + logger.info("Writing patched files...") + val result = patcher.get() - logger.info("Applying patches...") - applyPatchesVerbose(selectedPatches.sortedBy { it.name }) + val patched = tempDir.resolve("result.apk") + withContext(Dispatchers.IO) { + Files.copy(input.toPath(), patched.toPath(), StandardCopyOption.REPLACE_EXISTING) + } + result.applyTo(patched) + + logger.info("Patched apk saved to $patched") + + withContext(Dispatchers.IO) { + Files.move(patched.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING) + } } - - logger.info("Writing patched files...") - val result = patcher.get() - - val patched = tempDir.resolve("result.apk") - withContext(Dispatchers.IO) { - Files.copy(input.toPath(), patched.toPath(), StandardCopyOption.REPLACE_EXISTING) - } - result.applyTo(patched) - - logger.info("Patched apk saved to $patched") - - withContext(Dispatchers.IO) { - Files.move(patched.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING) - } - updateProgress(state = State.COMPLETED) // Saving } override fun close() { diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt index 50a96a1f..8b52f5d5 100644 --- a/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt @@ -1,11 +1,12 @@ package app.revanced.manager.patcher.runtime import android.content.Context +import app.revanced.manager.patcher.ProgressEvent import app.revanced.manager.patcher.Session +import app.revanced.manager.patcher.StepId import app.revanced.manager.patcher.logger.Logger import app.revanced.manager.patcher.patch.PatchBundle -import app.revanced.manager.patcher.worker.ProgressEventHandler -import app.revanced.manager.ui.model.State +import app.revanced.manager.patcher.runStep import app.revanced.manager.util.Options import app.revanced.manager.util.PatchSelection import java.io.File @@ -13,7 +14,7 @@ import java.io.File /** * Simple [Runtime] implementation that runs the patcher using coroutines. */ -class CoroutineRuntime(private val context: Context) : Runtime(context) { +class CoroutineRuntime(context: Context) : Runtime(context) { override suspend fun execute( inputFile: String, outputFile: String, @@ -21,47 +22,50 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) { selectedPatches: PatchSelection, options: Options, logger: Logger, - onPatchCompleted: suspend () -> Unit, - onProgress: ProgressEventHandler, + onEvent: (ProgressEvent) -> Unit, ) { - val selectedBundles = selectedPatches.keys - val bundles = bundles() - val uids = bundles.entries.associate { (key, value) -> value to key } + val patchList = runStep(StepId.LoadPatches, onEvent) { + val selectedBundles = selectedPatches.keys + val bundles = bundles() + val uids = bundles.entries.associate { (key, value) -> value to key } - val allPatches = - PatchBundle.Loader.patches(bundles.values, packageName) - .mapKeys { (b, _) -> uids[b]!! } - .filterKeys { it in selectedBundles } + val allPatches = + PatchBundle.Loader.patches(bundles.values, packageName) + .mapKeys { (b, _) -> uids[b]!! } + .filterKeys { it in selectedBundles } - val patchList = selectedPatches.flatMap { (bundle, selected) -> - allPatches[bundle]?.filter { it.name in selected } - ?: throw IllegalArgumentException("Patch bundle $bundle does not exist") - } + val patchList = selectedPatches.flatMap { (bundle, selected) -> + allPatches[bundle]?.filter { it.name in selected } + ?: throw IllegalArgumentException("Patch bundle $bundle does not exist") + } - // Set all patch options. - options.forEach { (bundle, bundlePatchOptions) -> - val patches = allPatches[bundle] ?: return@forEach - bundlePatchOptions.forEach { (patchName, configuredPatchOptions) -> - val patchOptions = patches.single { it.name == patchName }.options - configuredPatchOptions.forEach { (key, value) -> - patchOptions[key] = value + // Set all patch options. + options.forEach { (bundle, bundlePatchOptions) -> + val patches = allPatches[bundle] ?: return@forEach + bundlePatchOptions.forEach { (patchName, configuredPatchOptions) -> + val patchOptions = patches.single { it.name == patchName }.options + configuredPatchOptions.forEach { (key, value) -> + patchOptions[key] = value + } } } + + patchList } - onProgress(null, State.COMPLETED, null) // Loading patches + val session = runStep(StepId.ReadAPK, onEvent) { + Session( + cacheDir, + frameworkPath, + aaptPath, + logger, + File(inputFile), + onEvent, + ) + } - Session( - cacheDir, - frameworkPath, - aaptPath, - context, - logger, - File(inputFile), - onPatchCompleted = onPatchCompleted, - onProgress - ).use { session -> - session.run( + session.use { s -> + s.run( File(outputFile), patchList ) diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt index 2e026298..c8597eca 100644 --- a/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt @@ -10,12 +10,13 @@ import app.revanced.manager.BuildConfig import app.revanced.manager.patcher.runtime.process.IPatcherEvents import app.revanced.manager.patcher.runtime.process.IPatcherProcess import app.revanced.manager.patcher.LibraryResolver +import app.revanced.manager.patcher.ProgressEvent +import app.revanced.manager.patcher.ProgressEventParcel import app.revanced.manager.patcher.logger.Logger import app.revanced.manager.patcher.runtime.process.Parameters import app.revanced.manager.patcher.runtime.process.PatchConfiguration import app.revanced.manager.patcher.runtime.process.PatcherProcess -import app.revanced.manager.patcher.worker.ProgressEventHandler -import app.revanced.manager.ui.model.State +import app.revanced.manager.patcher.toEvent import app.revanced.manager.util.Options import app.revanced.manager.util.PM import app.revanced.manager.util.PatchSelection @@ -66,8 +67,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) { selectedPatches: PatchSelection, options: Options, logger: Logger, - onPatchCompleted: suspend () -> Unit, - onProgress: ProgressEventHandler, + onEvent: (ProgressEvent) -> Unit, ) = coroutineScope { // Get the location of our own Apk. val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo!!.sourceDir @@ -111,7 +111,6 @@ class ProcessRuntime(private val context: Context) : Runtime(context) { } val patching = CompletableDeferred() - val scope = this launch(Dispatchers.IO) { val binder = awaitBinderConnection() @@ -124,13 +123,10 @@ class ProcessRuntime(private val context: Context) : Runtime(context) { val eventHandler = object : IPatcherEvents.Stub() { override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg) - override fun patchSucceeded() { - scope.launch { onPatchCompleted() } + override fun event(event: ProgressEventParcel?) { + event?.let { onEvent(it.toEvent()) } } - override fun progress(name: String?, state: String?, msg: String?) = - onProgress(name, state?.let { enumValueOf(it) }, msg) - override fun finished(exceptionStackTrace: String?) { binder.exit() diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt index 7f4616bc..67da1ee0 100644 --- a/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt @@ -4,9 +4,9 @@ import android.content.Context import app.revanced.manager.data.platform.Filesystem import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.patcher.ProgressEvent import app.revanced.manager.patcher.aapt.Aapt import app.revanced.manager.patcher.logger.Logger -import app.revanced.manager.patcher.worker.ProgressEventHandler import app.revanced.manager.util.Options import app.revanced.manager.util.PatchSelection import kotlinx.coroutines.flow.first @@ -34,7 +34,6 @@ sealed class Runtime(context: Context) : KoinComponent { selectedPatches: PatchSelection, options: Options, logger: Logger, - onPatchCompleted: suspend () -> Unit, - onProgress: ProgressEventHandler, + onEvent: (ProgressEvent) -> Unit, ) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt index f117f201..ab9f5229 100644 --- a/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt @@ -8,12 +8,15 @@ import android.os.Build import android.os.Bundle import android.os.Looper import app.revanced.manager.BuildConfig +import app.revanced.manager.patcher.ProgressEvent import app.revanced.manager.patcher.Session +import app.revanced.manager.patcher.StepId import app.revanced.manager.patcher.logger.LogLevel import app.revanced.manager.patcher.logger.Logger import app.revanced.manager.patcher.patch.PatchBundle +import app.revanced.manager.patcher.runStep import app.revanced.manager.patcher.runtime.ProcessRuntime -import app.revanced.manager.ui.model.State +import app.revanced.manager.patcher.toParcel import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -24,7 +27,7 @@ import kotlin.system.exitProcess /** * The main class that runs inside the runner process launched by [ProcessRuntime]. */ -class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() { +class PatcherProcess() : IPatcherProcess.Stub() { private var eventBinder: IPatcherEvents? = null private val scope = @@ -46,6 +49,8 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() { override fun exit() = exitProcess(0) override fun start(parameters: Parameters, events: IPatcherEvents) { + fun onEvent(event: ProgressEvent) = events.event(event.toParcel()) + eventBinder = events scope.launch { @@ -56,38 +61,42 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() { logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB") - val allPatches = PatchBundle.Loader.patches(parameters.configurations.map { it.bundle }, parameters.packageName) - val patchList = parameters.configurations.flatMap { config -> - val patches = (allPatches[config.bundle] ?: return@flatMap emptyList()) + val patchList = runStep(StepId.LoadPatches, ::onEvent) { + val allPatches = PatchBundle.Loader.patches( + parameters.configurations.map { it.bundle }, + parameters.packageName + ) + + parameters.configurations.flatMap { config -> + val patches = (allPatches[config.bundle] ?: return@flatMap emptyList()) .filter { it.name in config.patches } .associateBy { it.name } - config.options.forEach { (patchName, opts) -> - val patchOptions = patches[patchName]?.options - ?: throw Exception("Patch with name $patchName does not exist.") + config.options.forEach { (patchName, opts) -> + val patchOptions = patches[patchName]?.options + ?: throw Exception("Patch with name $patchName does not exist.") - opts.forEach { (key, value) -> - patchOptions[key] = value + opts.forEach { (key, value) -> + patchOptions[key] = value + } } - } - patches.values + patches.values + } } - events.progress(null, State.COMPLETED.name, null) // Loading patches + val session = runStep(StepId.ReadAPK, ::onEvent) { + Session( + cacheDir = parameters.cacheDir, + aaptPath = parameters.aaptPath, + frameworkDir = parameters.frameworkDir, + logger = logger, + input = File(parameters.inputFile), + onEvent = ::onEvent, + ) + } - Session( - cacheDir = parameters.cacheDir, - aaptPath = parameters.aaptPath, - frameworkDir = parameters.frameworkDir, - androidContext = context, - logger = logger, - input = File(parameters.inputFile), - onPatchCompleted = { events.patchSucceeded() }, - onProgress = { name, state, message -> - events.progress(name, state?.name, message) - } - ).use { + session.use { it.run(File(parameters.outputFile), patchList) } @@ -119,7 +128,7 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() { } } - val ipcInterface = PatcherProcess(appContext) + val ipcInterface = PatcherProcess() appContext.sendBroadcast(Intent().apply { action = ProcessRuntime.CONNECT_TO_APP_ACTION diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt index 0708817d..7076e122 100644 --- a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt +++ b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt @@ -29,14 +29,17 @@ import app.revanced.manager.domain.repository.InstalledAppRepository import app.revanced.manager.domain.worker.Worker import app.revanced.manager.domain.worker.WorkerRepository import app.revanced.manager.network.downloader.LoadedDownloaderPlugin +import app.revanced.manager.patcher.ProgressEvent +import app.revanced.manager.patcher.StepId import app.revanced.manager.patcher.logger.Logger +import app.revanced.manager.patcher.runStep import app.revanced.manager.patcher.runtime.CoroutineRuntime import app.revanced.manager.patcher.runtime.ProcessRuntime +import app.revanced.manager.patcher.toRemoteError import app.revanced.manager.plugin.downloader.GetScope import app.revanced.manager.plugin.downloader.PluginHostApi import app.revanced.manager.plugin.downloader.UserInteractionException import app.revanced.manager.ui.model.SelectedApp -import app.revanced.manager.ui.model.State import app.revanced.manager.util.Options import app.revanced.manager.util.PM import app.revanced.manager.util.PatchSelection @@ -48,8 +51,6 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import java.io.File -typealias ProgressEventHandler = (name: String?, state: State?, message: String?) -> Unit - @OptIn(PluginHostApi::class) class PatcherWorker( context: Context, @@ -71,11 +72,9 @@ class PatcherWorker( val selectedPatches: PatchSelection, val options: Options, val logger: Logger, - val onDownloadProgress: suspend (Pair?) -> Unit, - val onPatchCompleted: suspend () -> Unit, val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult, val setInputFile: suspend (File) -> Unit, - val onProgress: ProgressEventHandler + val onEvent: (ProgressEvent) -> Unit, ) { val packageName get() = input.packageName } @@ -140,10 +139,6 @@ class PatcherWorker( } private suspend fun runPatcher(args: Args): Result { - - fun updateProgress(name: String? = null, state: State? = null, message: String? = null) = - args.onProgress(name, state, message) - val patchedApk = fs.tempDir.resolve("patched.apk") return try { @@ -163,51 +158,65 @@ class PatcherWorker( args.input.version, prefs.suggestedVersionSafeguard.get(), !prefs.disablePatchVersionCompatCheck.get(), - onDownload = args.onDownloadProgress - ).also { - args.setInputFile(it) - updateProgress(state = State.COMPLETED) // Download APK - } + onDownload = { progress -> + args.onEvent( + ProgressEvent.Progress( + stepId = StepId.DownloadAPK, + current = progress.first, + total = progress.second + ) + ) + } + ).also { args.setInputFile(it) } val inputFile = when (val selectedApp = args.input) { is SelectedApp.Download -> { - val (plugin, data) = downloaderPluginRepository.unwrapParceledData(selectedApp.data) + runStep(StepId.DownloadAPK, args.onEvent) { + val (plugin, data) = downloaderPluginRepository.unwrapParceledData( + selectedApp.data + ) - download(plugin, data) + download(plugin, data) + } } is SelectedApp.Search -> { - downloaderPluginRepository.loadedPluginsFlow.first() - .firstNotNullOfOrNull { plugin -> - try { - val getScope = object : GetScope { - override val pluginPackageName = plugin.packageName - override val hostPackageName = applicationContext.packageName - override suspend fun requestStartActivity(intent: Intent): Intent? { - val result = args.handleStartActivityRequest(plugin, intent) - return when (result.resultCode) { - Activity.RESULT_OK -> result.data - Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled() - else -> throw UserInteractionException.Activity.NotCompleted( - result.resultCode, - result.data - ) + runStep(StepId.DownloadAPK, args.onEvent) { + downloaderPluginRepository.loadedPluginsFlow.first() + .firstNotNullOfOrNull { plugin -> + try { + val getScope = object : GetScope { + override val pluginPackageName = plugin.packageName + override val hostPackageName = + applicationContext.packageName + + override suspend fun requestStartActivity(intent: Intent): Intent? { + val result = + args.handleStartActivityRequest(plugin, intent) + return when (result.resultCode) { + Activity.RESULT_OK -> result.data + Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled() + else -> throw UserInteractionException.Activity.NotCompleted( + result.resultCode, + result.data + ) + } } } - } - withContext(Dispatchers.IO) { - plugin.get( - getScope, - selectedApp.packageName, - selectedApp.version - ) - }?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version } - } catch (e: UserInteractionException.Activity.NotCompleted) { - throw e - } catch (_: UserInteractionException) { - null - }?.let { (data, _) -> download(plugin, data) } - } ?: throw Exception("App is not available.") + withContext(Dispatchers.IO) { + plugin.get( + getScope, + selectedApp.packageName, + selectedApp.version + ) + }?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version } + } catch (e: UserInteractionException.Activity.NotCompleted) { + throw e + } catch (_: UserInteractionException) { + null + }?.let { (data, _) -> download(plugin, data) } + } ?: throw Exception("App is not available.") + } } is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) } @@ -227,12 +236,12 @@ class PatcherWorker( args.selectedPatches, args.options, args.logger, - args.onPatchCompleted, - args.onProgress + args.onEvent, ) - keystoreManager.sign(patchedApk, File(args.output)) - updateProgress(state = State.COMPLETED) // Signing + runStep(StepId.SignAPK, args.onEvent) { + keystoreManager.sign(patchedApk, File(args.output)) + } Log.i(tag, "Patching succeeded".logFmt()) Result.success() @@ -241,11 +250,11 @@ class PatcherWorker( tag, "An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt() ) - updateProgress(state = State.FAILED, message = e.originalStackTrace) + args.onEvent(ProgressEvent.Failed(null, e.toRemoteError())) // Fallback if exception doesn't occur within step Result.failure() } catch (e: Exception) { Log.e(tag, "An exception occurred while patching".logFmt(), e) - updateProgress(state = State.FAILED, message = e.stackTraceToString()) + args.onEvent(ProgressEvent.Failed(null, e.toRemoteError())) // Fallback if exception doesn't occur within step Result.failure() } finally { patchedApk.delete() diff --git a/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt b/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt index 537dd84c..fd62ecca 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt @@ -39,11 +39,9 @@ import androidx.compose.ui.unit.dp import app.revanced.manager.R import app.revanced.manager.ui.component.ArrowButton import app.revanced.manager.ui.component.LoadingIndicator -import app.revanced.manager.ui.model.ProgressKey import app.revanced.manager.ui.model.State -import app.revanced.manager.ui.model.Step import app.revanced.manager.ui.model.StepCategory -import app.revanced.manager.ui.model.StepProgressProvider +import app.revanced.manager.ui.model.Step import java.util.Locale import kotlin.math.floor @@ -52,8 +50,6 @@ import kotlin.math.floor fun Steps( category: StepCategory, steps: List, - stepCount: Pair? = null, - stepProgressProvider: StepProgressProvider, isExpanded: Boolean = false, onExpand: () -> Unit, onClick: () -> Unit @@ -67,8 +63,17 @@ fun Steps( } } + val filteredSteps = remember(steps) { + val failedCount = steps.count { it.state == State.FAILED } + + steps.filter { step -> + // Show hidden steps if it's the only failed step. + !step.hide || (step.state == State.FAILED && failedCount == 1) + } + } + LaunchedEffect(state) { - if (state == State.RUNNING) + if (state == State.RUNNING || state == State.FAILED) onExpand() } @@ -92,13 +97,8 @@ fun Steps( Spacer(modifier = Modifier.weight(1f)) - val stepProgress = remember(stepCount, steps) { - stepCount?.let { (current, total) -> "$current/$total" } - ?: "${steps.count { it.state == State.COMPLETED }}/${steps.size}" - } - Text( - text = stepProgress, + text = "${filteredSteps.count { it.state == State.COMPLETED }}/${filteredSteps.size}", style = MaterialTheme.typography.labelSmall ) @@ -112,23 +112,20 @@ fun Steps( .fillMaxWidth() .padding(top = 10.dp) ) { - steps.forEachIndexed { index, step -> - val (progress, progressText) = when (step.progressKey) { - null -> null - ProgressKey.DOWNLOAD -> stepProgressProvider.downloadProgress?.let { (downloaded, total) -> - if (total != null) downloaded.toFloat() / total.toFloat() to "${downloaded.megaBytes}/${total.megaBytes} MB" - else null to "${downloaded.megaBytes} MB" - } + filteredSteps.forEachIndexed { index, step -> + val (progress, progressText) = step.progress?.let { (current, total) -> + if (total != null) current.toFloat() / total.toFloat() to "${current.megaBytes}/${total.megaBytes} MB" + else null to "${current.megaBytes} MB" } ?: (null to null) SubStep( - name = step.name, + name = step.title, state = step.state, message = step.message, progress = progress, progressText = progressText, isFirst = index == 0, - isLast = index == steps.lastIndex, + isLast = index == filteredSteps.lastIndex, ) } } diff --git a/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt b/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt index 3dbb390e..46403d72 100644 --- a/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt +++ b/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt @@ -3,6 +3,7 @@ package app.revanced.manager.ui.model import android.os.Parcelable import androidx.annotation.StringRes import app.revanced.manager.R +import app.revanced.manager.patcher.StepId import kotlinx.parcelize.Parcelize enum class StepCategory(@StringRes val displayName: Int) { @@ -15,19 +16,20 @@ enum class State { WAITING, RUNNING, FAILED, COMPLETED } -enum class ProgressKey { - DOWNLOAD -} - -interface StepProgressProvider { - val downloadProgress: Pair? -} - @Parcelize data class Step( - val name: String, + val id: StepId, + val title: String, val category: StepCategory, val state: State = State.WAITING, val message: String? = null, - val progressKey: ProgressKey? = null -) : Parcelable \ No newline at end of file + val progress: Pair? = null, + val hide: Boolean = false, +) : Parcelable + + +fun Step.withState( + state: State = this.state, + message: String? = this.message, + progress: Pair? = this.progress +) = copy(state = state, message = message, progress = progress) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt index c3839551..f3a9f059 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt @@ -87,7 +87,7 @@ fun PatcherScreen( val steps by remember { derivedStateOf { - viewModel.steps.groupBy { it.category } + viewModel.steps.groupBy { it.category }.toList() } } @@ -230,14 +230,12 @@ fun PatcherScreen( contentPadding = PaddingValues(16.dp) ) { items( - items = steps.toList(), + items = steps, key = { it.first } ) { (category, steps) -> Steps( category = category, steps = steps, - stepCount = if (category == StepCategory.PATCHING) viewModel.patchesProgress else null, - stepProgressProvider = viewModel, isExpanded = expandedCategory == category, onExpand = { expandCategory(category) }, onClick = { diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt index 0365d62c..194f4214 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt @@ -29,20 +29,22 @@ import app.revanced.manager.data.room.apps.installed.InstalledApp import app.revanced.manager.domain.installer.RootInstaller import app.revanced.manager.domain.repository.InstalledAppRepository import app.revanced.manager.domain.worker.WorkerRepository +import app.revanced.manager.patcher.ProgressEvent +import app.revanced.manager.patcher.StepId import app.revanced.manager.patcher.logger.LogLevel import app.revanced.manager.patcher.logger.Logger import app.revanced.manager.patcher.worker.PatcherWorker import app.revanced.manager.plugin.downloader.PluginHostApi import app.revanced.manager.plugin.downloader.UserInteractionException import app.revanced.manager.ui.model.InstallerModel -import app.revanced.manager.ui.model.ProgressKey import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.State -import app.revanced.manager.ui.model.Step import app.revanced.manager.ui.model.StepCategory -import app.revanced.manager.ui.model.StepProgressProvider +import app.revanced.manager.ui.model.Step import app.revanced.manager.ui.model.navigation.Patcher +import app.revanced.manager.ui.model.withState import app.revanced.manager.util.PM +import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.asCode import app.revanced.manager.util.saveableVar import app.revanced.manager.util.saver.snapshotStateListSaver @@ -80,7 +82,7 @@ import java.time.Duration @OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class) class PatcherViewModel( private val input: Patcher.ViewModelParams -) : ViewModel(), KoinComponent, StepProgressProvider, InstallerModel { +) : ViewModel(), KoinComponent, InstallerModel { private val app: Application by inject() private val fs: Filesystem by inject() private val pm: PM by inject() @@ -157,35 +159,15 @@ class PatcherViewModel( } } - private val patchCount = input.selectedPatches.values.sumOf { it.size } - private var completedPatchCount by savedStateHandle.saveable { - // SavedStateHandle.saveable only supports the boxed version. - @Suppress("AutoboxingStateCreation") mutableStateOf( - 0 - ) - } - val patchesProgress get() = completedPatchCount to patchCount - override var downloadProgress by savedStateHandle.saveable( - key = "downloadProgress", - stateSaver = autoSaver() - ) { - mutableStateOf?>(null) - } - private set val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) { - generateSteps( - app, - input.selectedApp - ).toMutableStateList() + generateSteps(app, input.selectedApp, input.selectedPatches).toMutableStateList() } - private var currentStepIndex = 0 val progress by derivedStateOf { - val current = steps.count { - it.state == State.COMPLETED && it.category != StepCategory.PATCHING - } + completedPatchCount + val steps = steps.filter { it.id != StepId.ExecutePatches } - val total = steps.size - 1 + patchCount + val current = steps.count { it.state == State.COMPLETED } + val total = steps.size current.toFloat() / total.toFloat() } @@ -201,12 +183,6 @@ class PatcherViewModel( input.selectedPatches, input.options, logger, - onDownloadProgress = { - withContext(Dispatchers.Main) { - downloadProgress = it - } - }, - onPatchCompleted = { withContext(Dispatchers.Main) { completedPatchCount += 1 } }, setInputFile = { withContext(Dispatchers.Main) { inputFile = it } }, handleStartActivityRequest = { plugin, intent -> withContext(Dispatchers.Main) { @@ -235,26 +211,10 @@ class PatcherViewModel( } } }, - onProgress = { name, state, message -> - viewModelScope.launch { - steps[currentStepIndex] = steps[currentStepIndex].run { - copy( - name = name ?: this.name, - state = state ?: this.state, - message = message ?: this.message - ) - } - - if (state == State.COMPLETED && currentStepIndex != steps.lastIndex) { - currentStepIndex++ - - steps[currentStepIndex] = - steps[currentStepIndex].copy(state = State.RUNNING) - } - } - } + onEvent = ::handleProgressEvent, ) - )) + ) + ) } val patcherSucceeded = @@ -308,6 +268,35 @@ class PatcherViewModel( } } + private fun handleProgressEvent(event: ProgressEvent) = viewModelScope.launch { + val stepIndex = steps.indexOfFirst { + event.stepId?.let { id -> id == it.id } + ?: (it.state == State.RUNNING || it.state == State.WAITING) + } + + if (stepIndex != -1) steps[stepIndex] = steps[stepIndex].run { + when (event) { + is ProgressEvent.Started -> withState(State.RUNNING) + + is ProgressEvent.Progress -> withState( + message = event.message ?: message, + progress = event.current?.let { event.current to event.total } ?: progress + ) + + is ProgressEvent.Completed -> withState(State.COMPLETED, progress = null) + + is ProgressEvent.Failed -> { + if (event.stepId == null && steps.any { it.state == State.FAILED }) return@launch + withState( + State.FAILED, + message = event.error.stackTrace, + progress = null + ) + } + } + } + } + fun onBack() { installerCoroutineScope.cancel() // tempDir cannot be deleted inside onCleared because it gets called on system-initiated process death. @@ -544,34 +533,66 @@ class PatcherViewModel( LogLevel.ERROR -> Log.e(TAG, msg) } - fun generateSteps(context: Context, selectedApp: SelectedApp): List { - val needsDownload = - selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search + fun generateSteps( + context: Context, + selectedApp: SelectedApp, + selectedPatches: PatchSelection + ): List = buildList { + if (selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search) + add( + Step( + StepId.DownloadAPK, + context.getString(R.string.download_apk), + StepCategory.PREPARING + ) + ) - return listOfNotNull( - Step( - context.getString(R.string.download_apk), - StepCategory.PREPARING, - state = State.RUNNING, - progressKey = ProgressKey.DOWNLOAD, - ).takeIf { needsDownload }, + add( Step( + StepId.LoadPatches, context.getString(R.string.patcher_step_load_patches), - StepCategory.PREPARING, - state = if (needsDownload) State.WAITING else State.RUNNING, - ), + StepCategory.PREPARING + ) + ) + add( Step( + StepId.ReadAPK, context.getString(R.string.patcher_step_unpack), StepCategory.PREPARING - ), - + ) + ) + add( Step( + StepId.ExecutePatches, context.getString(R.string.execute_patches), - StepCategory.PATCHING - ), + StepCategory.PATCHING, + hide = true + ) + ) - Step(context.getString(R.string.patcher_step_write_patched), StepCategory.SAVING), - Step(context.getString(R.string.patcher_step_sign_apk), StepCategory.SAVING) + selectedPatches.values.asSequence().flatten().sorted().forEachIndexed { index, name -> + add( + Step( + StepId.ExecutePatch(index), + name, + StepCategory.PATCHING + ) + ) + } + + add( + Step( + StepId.WriteAPK, + context.getString(R.string.patcher_step_write_patched), + StepCategory.SAVING + ) + ) + add( + Step( + StepId.SignAPK, + context.getString(R.string.patcher_step_sign_apk), + StepCategory.SAVING + ) ) } }