Compare commits

...

4 Commits

Author SHA1 Message Date
semantic-release-bot
25d82e869c chore: Release v1.26.0-dev.17 [skip ci]
# app [1.26.0-dev.17](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.16...v1.26.0-dev.17) (2026-01-06)

### Bug Fixes

* allow updating patches on metered networks ([9d9a0e8](9d9a0e81db))
2026-01-06 21:45:09 +00:00
Ax333l
9d9a0e81db fix: allow updating patches on metered networks 2026-01-06 22:37:25 +01:00
semantic-release-bot
ffa42099e3 chore: Release v1.26.0-dev.16 [skip ci]
# app [1.26.0-dev.16](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.15...v1.26.0-dev.16) (2025-12-30)

### Features

* Show patches as individual steps in patcher screen ([#2889](https://github.com/ReVanced/revanced-manager/issues/2889)) ([11dd6e4](11dd6e4064))
2025-12-30 00:16:11 +00:00
Robert
11dd6e4064 feat: Show patches as individual steps in patcher screen (#2889)
Co-authored-by: Ax333l <main@axelen.xyz>
2025-12-30 01:08:54 +01:00
22 changed files with 458 additions and 328 deletions

View File

@@ -1,3 +1,17 @@
# app [1.26.0-dev.17](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.16...v1.26.0-dev.17) (2026-01-06)
### Bug Fixes
* allow updating patches on metered networks ([9d9a0e8](https://github.com/ReVanced/revanced-manager/commit/9d9a0e81dbc9e73e6e3181f6bea9cabb69e49ea8))
# app [1.26.0-dev.16](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.15...v1.26.0-dev.16) (2025-12-30)
### Features
* Show patches as individual steps in patcher screen ([#2889](https://github.com/ReVanced/revanced-manager/issues/2889)) ([11dd6e4](https://github.com/ReVanced/revanced-manager/commit/11dd6e4064099427a8c9bc6f225a19412e5c70e2))
# app [1.26.0-dev.15](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.14...v1.26.0-dev.15) (2025-12-29) # app [1.26.0-dev.15](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.14...v1.26.0-dev.15) (2025-12-29)

View File

@@ -1 +1 @@
version = 1.26.0-dev.15 version = 1.26.0-dev.17

View File

@@ -0,0 +1,4 @@
// ProgressEventParcel.aidl
package app.revanced.manager.patcher;
parcelable ProgressEventParcel;

View File

@@ -1,11 +1,12 @@
// IPatcherEvents.aidl // IPatcherEvents.aidl
package app.revanced.manager.patcher.runtime.process; package app.revanced.manager.patcher.runtime.process;
import app.revanced.manager.patcher.ProgressEventParcel;
// Interface for sending events back to the main app process. // Interface for sending events back to the main app process.
oneway interface IPatcherEvents { oneway interface IPatcherEvents {
void log(String level, String msg); void log(String level, String msg);
void patchSucceeded(); void event(in ProgressEventParcel event);
void progress(String name, String state, String msg);
// The patching process has ended. The exceptionStackTrace is null if it finished successfully. // The patching process has ended. The exceptionStackTrace is null if it finished successfully.
void finished(String exceptionStackTrace); void finished(String exceptionStackTrace);
} }

View File

@@ -15,5 +15,5 @@ class NetworkInfo(app: Application) {
/** /**
* Returns true if it is safe to download large files. * Returns true if it is safe to download large files.
*/ */
fun isSafe() = isConnected() && isUnmetered() fun isSafe(ignoreMetered: Boolean) = isConnected() && (ignoreMetered || isUnmetered())
} }

View File

@@ -34,4 +34,6 @@ class PreferencesManager(
val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet()) val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet())
val showDeveloperSettings = booleanPreference("show_developer_settings", context.isDebuggable) val showDeveloperSettings = booleanPreference("show_developer_settings", context.isDebuggable)
val allowMeteredNetworks = booleanPreference("allow_metered_networks", false)
} }

View File

@@ -286,28 +286,29 @@ class PatchBundleRepository(
State(sources.toPersistentMap(), info.toPersistentMap()) State(sources.toPersistentMap(), info.toPersistentMap())
} }
suspend fun createLocal(createStream: suspend () -> InputStream) = dispatchAction("Add bundle") { suspend fun createLocal(createStream: suspend () -> InputStream) =
with(createEntity("", SourceInfo.Local).load() as LocalPatchBundle) { dispatchAction("Add bundle") {
try { with(createEntity("", SourceInfo.Local).load() as LocalPatchBundle) {
createStream().use { patches -> replace(patches) } try {
} catch (e: Exception) { createStream().use { patches -> replace(patches) }
if (e is CancellationException) throw e } catch (e: Exception) {
Log.e(tag, "Got exception while importing bundle", e) if (e is CancellationException) throw e
withContext(Dispatchers.Main) { Log.e(tag, "Got exception while importing bundle", e)
app.toast(app.getString(R.string.patches_replace_fail, e.simpleMessage())) withContext(Dispatchers.Main) {
app.toast(app.getString(R.string.patches_replace_fail, e.simpleMessage()))
}
deleteLocalFile()
} }
deleteLocalFile()
} }
}
doReload() doReload()
} }
suspend fun createRemote(url: String, autoUpdate: Boolean) = suspend fun createRemote(url: String, autoUpdate: Boolean) =
dispatchAction("Add bundle ($url)") { state -> dispatchAction("Add bundle ($url)") { state ->
val src = createEntity("", SourceInfo.from(url), autoUpdate).load() as RemotePatchBundle val src = createEntity("", SourceInfo.from(url), autoUpdate).load() as RemotePatchBundle
update(src) update(src, force = true)
state.copy(sources = state.sources.put(src.uid, src)) state.copy(sources = state.sources.put(src.uid, src))
} }
@@ -329,32 +330,38 @@ class PatchBundleRepository(
state.copy(sources = state.sources.put(uid, newSrc)) state.copy(sources = state.sources.put(uid, newSrc))
} }
suspend fun update(vararg sources: RemotePatchBundle, showToast: Boolean = false) { suspend fun update(
vararg sources: RemotePatchBundle,
showToast: Boolean = false,
force: Boolean = false
) {
val uids = sources.map { it.uid }.toSet() val uids = sources.map { it.uid }.toSet()
store.dispatch(Update(showToast = showToast) { it.uid in uids }) store.dispatch(Update(showToast = showToast, force = force) { it.uid in uids })
} }
suspend fun redownloadRemoteBundles() = store.dispatch(Update(force = true)) suspend fun redownloadRemoteBundles() = store.dispatch(Update(force = true, redownload = true))
/** /**
* Updates all bundles that should be automatically updated. * Updates all bundles that should be automatically updated.
*/ */
suspend fun updateCheck() = store.dispatch(Update { it.autoUpdate }) suspend fun updateCheck() =
store.dispatch(Update(force = prefs.allowMeteredNetworks.get()) { it.autoUpdate })
private inner class Update( private inner class Update(
private val force: Boolean = false, private val force: Boolean = false,
private val redownload: Boolean = false,
private val showToast: Boolean = false, private val showToast: Boolean = false,
private val predicate: (bundle: RemotePatchBundle) -> Boolean = { true }, private val predicate: (bundle: RemotePatchBundle) -> Boolean = { true },
) : Action<State> { ) : Action<State> {
private suspend fun toast(@StringRes id: Int, vararg args: Any?) = private suspend fun toast(@StringRes id: Int, vararg args: Any?) =
withContext(Dispatchers.Main) { app.toast(app.getString(id, *args)) } withContext(Dispatchers.Main) { app.toast(app.getString(id, *args)) }
override fun toString() = if (force) "Redownload remote bundles" else "Update check" override fun toString() = if (redownload) "Redownload remote bundles" else "Update check"
override suspend fun ActionContext.execute( override suspend fun ActionContext.execute(
current: State current: State
) = coroutineScope { ) = coroutineScope {
if (!networkInfo.isSafe()) { if (!networkInfo.isSafe(force)) {
Log.d(tag, "Skipping update check because the network is down or metered.") Log.d(tag, "Skipping update check because the network is down or metered.")
return@coroutineScope current return@coroutineScope current
} }
@@ -367,7 +374,7 @@ class PatchBundleRepository(
Log.d(tag, "Updating patch bundle: ${it.name}") Log.d(tag, "Updating patch bundle: ${it.name}")
val newVersion = with(it) { val newVersion = with(it) {
if (force) downloadLatest() else update() if (redownload) downloadLatest() else update()
} ?: return@async null } ?: return@async null
it to newVersion it to newVersion

View File

@@ -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 <T> 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
}

View File

@@ -1,10 +1,9 @@
package app.revanced.manager.patcher package app.revanced.manager.patcher
import android.content.Context
import app.revanced.library.ApkUtils.applyTo 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.patcher.logger.Logger
import app.revanced.manager.ui.model.State
import app.revanced.patcher.Patcher import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherConfig import app.revanced.patcher.PatcherConfig
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
@@ -22,15 +21,10 @@ class Session(
cacheDir: String, cacheDir: String,
frameworkDir: String, frameworkDir: String,
aaptPath: String, aaptPath: String,
private val androidContext: Context,
private val logger: Logger, private val logger: Logger,
private val input: File, private val input: File,
private val onPatchCompleted: suspend () -> Unit, private val onEvent: (ProgressEvent) -> Unit,
private val onProgress: (name: String?, state: State?, message: String?) -> Unit
) : Closeable { ) : 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 tempDir = File(cacheDir).resolve("patcher").also { it.mkdirs() }
private val patcher = Patcher( private val patcher = Patcher(
PatcherConfig( PatcherConfig(
@@ -42,86 +36,68 @@ class Session(
) )
private suspend fun Patcher.applyPatchesVerbose(selectedPatches: PatchList) { 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) -> this().collect { (patch, exception) ->
if (patch !in selectedPatches) return@collect val index = selectedPatches.indexOf(patch)
if (index == -1) return@collect
if (exception != null) { if (exception != null) {
updateProgress( onEvent(
name = androidContext.getString(R.string.failed_to_execute_patch, patch.name), ProgressEvent.Failed(
state = State.FAILED, StepId.ExecutePatch(index),
message = exception.stackTraceToString() exception.toRemoteError(),
)
) )
logger.error("${patch.name} failed:") logger.error("${patch.name} failed:")
logger.error(exception.stackTraceToString()) logger.error(exception.stackTraceToString())
throw exception throw exception
} }
nextPatchIndex++ onEvent(
ProgressEvent.Completed(
onPatchCompleted() StepId.ExecutePatch(index),
selectedPatches.getOrNull(nextPatchIndex)?.let { nextPatch ->
updateProgress(
name = androidContext.getString(R.string.executing_patch, nextPatch.name)
) )
} )
logger.info("${patch.name} succeeded") 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) { 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 { addHandler(logger.handler)
handlers.forEach {
it.close()
removeHandler(it)
} }
addHandler(logger.handler) with(patcher) {
logger.info("Merging integrations")
this += selectedPatches.toSet()
logger.info("Applying patches...")
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
}
} }
with(patcher) { runStep(StepId.WriteAPK, onEvent) {
logger.info("Merging integrations") logger.info("Writing patched files...")
this += selectedPatches.toSet() val result = patcher.get()
logger.info("Applying patches...") val patched = tempDir.resolve("result.apk")
applyPatchesVerbose(selectedPatches.sortedBy { it.name }) 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() { override fun close() {

View File

@@ -1,11 +1,12 @@
package app.revanced.manager.patcher.runtime package app.revanced.manager.patcher.runtime
import android.content.Context import android.content.Context
import app.revanced.manager.patcher.ProgressEvent
import app.revanced.manager.patcher.Session import app.revanced.manager.patcher.Session
import app.revanced.manager.patcher.StepId
import app.revanced.manager.patcher.logger.Logger import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.patch.PatchBundle import app.revanced.manager.patcher.patch.PatchBundle
import app.revanced.manager.patcher.worker.ProgressEventHandler import app.revanced.manager.patcher.runStep
import app.revanced.manager.ui.model.State
import app.revanced.manager.util.Options import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.PatchSelection
import java.io.File import java.io.File
@@ -13,7 +14,7 @@ import java.io.File
/** /**
* Simple [Runtime] implementation that runs the patcher using coroutines. * 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( override suspend fun execute(
inputFile: String, inputFile: String,
outputFile: String, outputFile: String,
@@ -21,47 +22,50 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) {
selectedPatches: PatchSelection, selectedPatches: PatchSelection,
options: Options, options: Options,
logger: Logger, logger: Logger,
onPatchCompleted: suspend () -> Unit, onEvent: (ProgressEvent) -> Unit,
onProgress: ProgressEventHandler,
) { ) {
val selectedBundles = selectedPatches.keys val patchList = runStep(StepId.LoadPatches, onEvent) {
val bundles = bundles() val selectedBundles = selectedPatches.keys
val uids = bundles.entries.associate { (key, value) -> value to key } val bundles = bundles()
val uids = bundles.entries.associate { (key, value) -> value to key }
val allPatches = val allPatches =
PatchBundle.Loader.patches(bundles.values, packageName) PatchBundle.Loader.patches(bundles.values, packageName)
.mapKeys { (b, _) -> uids[b]!! } .mapKeys { (b, _) -> uids[b]!! }
.filterKeys { it in selectedBundles } .filterKeys { it in selectedBundles }
val patchList = selectedPatches.flatMap { (bundle, selected) -> val patchList = selectedPatches.flatMap { (bundle, selected) ->
allPatches[bundle]?.filter { it.name in selected } allPatches[bundle]?.filter { it.name in selected }
?: throw IllegalArgumentException("Patch bundle $bundle does not exist") ?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
} }
// Set all patch options. // Set all patch options.
options.forEach { (bundle, bundlePatchOptions) -> options.forEach { (bundle, bundlePatchOptions) ->
val patches = allPatches[bundle] ?: return@forEach val patches = allPatches[bundle] ?: return@forEach
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) -> bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
val patchOptions = patches.single { it.name == patchName }.options val patchOptions = patches.single { it.name == patchName }.options
configuredPatchOptions.forEach { (key, value) -> configuredPatchOptions.forEach { (key, value) ->
patchOptions[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( session.use { s ->
cacheDir, s.run(
frameworkPath,
aaptPath,
context,
logger,
File(inputFile),
onPatchCompleted = onPatchCompleted,
onProgress
).use { session ->
session.run(
File(outputFile), File(outputFile),
patchList patchList
) )

View File

@@ -10,12 +10,13 @@ import app.revanced.manager.BuildConfig
import app.revanced.manager.patcher.runtime.process.IPatcherEvents import app.revanced.manager.patcher.runtime.process.IPatcherEvents
import app.revanced.manager.patcher.runtime.process.IPatcherProcess import app.revanced.manager.patcher.runtime.process.IPatcherProcess
import app.revanced.manager.patcher.LibraryResolver 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.logger.Logger
import app.revanced.manager.patcher.runtime.process.Parameters import app.revanced.manager.patcher.runtime.process.Parameters
import app.revanced.manager.patcher.runtime.process.PatchConfiguration import app.revanced.manager.patcher.runtime.process.PatchConfiguration
import app.revanced.manager.patcher.runtime.process.PatcherProcess import app.revanced.manager.patcher.runtime.process.PatcherProcess
import app.revanced.manager.patcher.worker.ProgressEventHandler import app.revanced.manager.patcher.toEvent
import app.revanced.manager.ui.model.State
import app.revanced.manager.util.Options import app.revanced.manager.util.Options
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.PatchSelection
@@ -66,8 +67,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
selectedPatches: PatchSelection, selectedPatches: PatchSelection,
options: Options, options: Options,
logger: Logger, logger: Logger,
onPatchCompleted: suspend () -> Unit, onEvent: (ProgressEvent) -> Unit,
onProgress: ProgressEventHandler,
) = coroutineScope { ) = coroutineScope {
// Get the location of our own Apk. // Get the location of our own Apk.
val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo!!.sourceDir val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo!!.sourceDir
@@ -111,7 +111,6 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
} }
val patching = CompletableDeferred<Unit>() val patching = CompletableDeferred<Unit>()
val scope = this
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
val binder = awaitBinderConnection() val binder = awaitBinderConnection()
@@ -124,13 +123,10 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
val eventHandler = object : IPatcherEvents.Stub() { val eventHandler = object : IPatcherEvents.Stub() {
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg) override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg)
override fun patchSucceeded() { override fun event(event: ProgressEventParcel?) {
scope.launch { onPatchCompleted() } event?.let { onEvent(it.toEvent()) }
} }
override fun progress(name: String?, state: String?, msg: String?) =
onProgress(name, state?.let { enumValueOf<State>(it) }, msg)
override fun finished(exceptionStackTrace: String?) { override fun finished(exceptionStackTrace: String?) {
binder.exit() binder.exit()

View File

@@ -4,9 +4,9 @@ import android.content.Context
import app.revanced.manager.data.platform.Filesystem import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchBundleRepository 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.aapt.Aapt
import app.revanced.manager.patcher.logger.Logger 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.Options
import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.PatchSelection
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -34,7 +34,6 @@ sealed class Runtime(context: Context) : KoinComponent {
selectedPatches: PatchSelection, selectedPatches: PatchSelection,
options: Options, options: Options,
logger: Logger, logger: Logger,
onPatchCompleted: suspend () -> Unit, onEvent: (ProgressEvent) -> Unit,
onProgress: ProgressEventHandler,
) )
} }

View File

@@ -8,12 +8,15 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Looper import android.os.Looper
import app.revanced.manager.BuildConfig import app.revanced.manager.BuildConfig
import app.revanced.manager.patcher.ProgressEvent
import app.revanced.manager.patcher.Session 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.LogLevel
import app.revanced.manager.patcher.logger.Logger import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.patch.PatchBundle import app.revanced.manager.patcher.patch.PatchBundle
import app.revanced.manager.patcher.runStep
import app.revanced.manager.patcher.runtime.ProcessRuntime 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.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -24,7 +27,7 @@ import kotlin.system.exitProcess
/** /**
* The main class that runs inside the runner process launched by [ProcessRuntime]. * 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 var eventBinder: IPatcherEvents? = null
private val scope = private val scope =
@@ -46,6 +49,8 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
override fun exit() = exitProcess(0) override fun exit() = exitProcess(0)
override fun start(parameters: Parameters, events: IPatcherEvents) { override fun start(parameters: Parameters, events: IPatcherEvents) {
fun onEvent(event: ProgressEvent) = events.event(event.toParcel())
eventBinder = events eventBinder = events
scope.launch { scope.launch {
@@ -56,38 +61,42 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB") logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
val allPatches = PatchBundle.Loader.patches(parameters.configurations.map { it.bundle }, parameters.packageName) val patchList = runStep(StepId.LoadPatches, ::onEvent) {
val patchList = parameters.configurations.flatMap { config -> val allPatches = PatchBundle.Loader.patches(
val patches = (allPatches[config.bundle] ?: return@flatMap emptyList()) 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 } .filter { it.name in config.patches }
.associateBy { it.name } .associateBy { it.name }
config.options.forEach { (patchName, opts) -> config.options.forEach { (patchName, opts) ->
val patchOptions = patches[patchName]?.options val patchOptions = patches[patchName]?.options
?: throw Exception("Patch with name $patchName does not exist.") ?: throw Exception("Patch with name $patchName does not exist.")
opts.forEach { (key, value) -> opts.forEach { (key, value) ->
patchOptions[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( session.use {
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 {
it.run(File(parameters.outputFile), patchList) 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 { appContext.sendBroadcast(Intent().apply {
action = ProcessRuntime.CONNECT_TO_APP_ACTION action = ProcessRuntime.CONNECT_TO_APP_ACTION

View File

@@ -29,14 +29,17 @@ import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.worker.Worker import app.revanced.manager.domain.worker.Worker
import app.revanced.manager.domain.worker.WorkerRepository import app.revanced.manager.domain.worker.WorkerRepository
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin 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.logger.Logger
import app.revanced.manager.patcher.runStep
import app.revanced.manager.patcher.runtime.CoroutineRuntime import app.revanced.manager.patcher.runtime.CoroutineRuntime
import app.revanced.manager.patcher.runtime.ProcessRuntime 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.GetScope
import app.revanced.manager.plugin.downloader.PluginHostApi import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.plugin.downloader.UserInteractionException import app.revanced.manager.plugin.downloader.UserInteractionException
import app.revanced.manager.ui.model.SelectedApp 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.Options
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.PatchSelection
@@ -48,8 +51,6 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import java.io.File import java.io.File
typealias ProgressEventHandler = (name: String?, state: State?, message: String?) -> Unit
@OptIn(PluginHostApi::class) @OptIn(PluginHostApi::class)
class PatcherWorker( class PatcherWorker(
context: Context, context: Context,
@@ -71,11 +72,9 @@ class PatcherWorker(
val selectedPatches: PatchSelection, val selectedPatches: PatchSelection,
val options: Options, val options: Options,
val logger: Logger, val logger: Logger,
val onDownloadProgress: suspend (Pair<Long, Long?>?) -> Unit,
val onPatchCompleted: suspend () -> Unit,
val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult, val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult,
val setInputFile: suspend (File) -> Unit, val setInputFile: suspend (File) -> Unit,
val onProgress: ProgressEventHandler val onEvent: (ProgressEvent) -> Unit,
) { ) {
val packageName get() = input.packageName val packageName get() = input.packageName
} }
@@ -140,10 +139,6 @@ class PatcherWorker(
} }
private suspend fun runPatcher(args: Args): Result { 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") val patchedApk = fs.tempDir.resolve("patched.apk")
return try { return try {
@@ -163,51 +158,65 @@ class PatcherWorker(
args.input.version, args.input.version,
prefs.suggestedVersionSafeguard.get(), prefs.suggestedVersionSafeguard.get(),
!prefs.disablePatchVersionCompatCheck.get(), !prefs.disablePatchVersionCompatCheck.get(),
onDownload = args.onDownloadProgress onDownload = { progress ->
).also { args.onEvent(
args.setInputFile(it) ProgressEvent.Progress(
updateProgress(state = State.COMPLETED) // Download APK stepId = StepId.DownloadAPK,
} current = progress.first,
total = progress.second
)
)
}
).also { args.setInputFile(it) }
val inputFile = when (val selectedApp = args.input) { val inputFile = when (val selectedApp = args.input) {
is SelectedApp.Download -> { 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 -> { is SelectedApp.Search -> {
downloaderPluginRepository.loadedPluginsFlow.first() runStep(StepId.DownloadAPK, args.onEvent) {
.firstNotNullOfOrNull { plugin -> downloaderPluginRepository.loadedPluginsFlow.first()
try { .firstNotNullOfOrNull { plugin ->
val getScope = object : GetScope { try {
override val pluginPackageName = plugin.packageName val getScope = object : GetScope {
override val hostPackageName = applicationContext.packageName override val pluginPackageName = plugin.packageName
override suspend fun requestStartActivity(intent: Intent): Intent? { override val hostPackageName =
val result = args.handleStartActivityRequest(plugin, intent) applicationContext.packageName
return when (result.resultCode) {
Activity.RESULT_OK -> result.data override suspend fun requestStartActivity(intent: Intent): Intent? {
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled() val result =
else -> throw UserInteractionException.Activity.NotCompleted( args.handleStartActivityRequest(plugin, intent)
result.resultCode, return when (result.resultCode) {
result.data Activity.RESULT_OK -> result.data
) Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
else -> throw UserInteractionException.Activity.NotCompleted(
result.resultCode,
result.data
)
}
} }
} }
} withContext(Dispatchers.IO) {
withContext(Dispatchers.IO) { plugin.get(
plugin.get( getScope,
getScope, selectedApp.packageName,
selectedApp.packageName, selectedApp.version
selectedApp.version )
) }?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
}?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version } } catch (e: UserInteractionException.Activity.NotCompleted) {
} catch (e: UserInteractionException.Activity.NotCompleted) { throw e
throw e } catch (_: UserInteractionException) {
} catch (_: UserInteractionException) { null
null }?.let { (data, _) -> download(plugin, data) }
}?.let { (data, _) -> download(plugin, data) } } ?: throw Exception("App is not available.")
} ?: throw Exception("App is not available.") }
} }
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) } is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
@@ -227,12 +236,12 @@ class PatcherWorker(
args.selectedPatches, args.selectedPatches,
args.options, args.options,
args.logger, args.logger,
args.onPatchCompleted, args.onEvent,
args.onProgress
) )
keystoreManager.sign(patchedApk, File(args.output)) runStep(StepId.SignAPK, args.onEvent) {
updateProgress(state = State.COMPLETED) // Signing keystoreManager.sign(patchedApk, File(args.output))
}
Log.i(tag, "Patching succeeded".logFmt()) Log.i(tag, "Patching succeeded".logFmt())
Result.success() Result.success()
@@ -241,11 +250,11 @@ class PatcherWorker(
tag, tag,
"An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt() "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() Result.failure()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(tag, "An exception occurred while patching".logFmt(), e) 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() Result.failure()
} finally { } finally {
patchedApk.delete() patchedApk.delete()

View File

@@ -39,11 +39,9 @@ import androidx.compose.ui.unit.dp
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.ui.component.ArrowButton import app.revanced.manager.ui.component.ArrowButton
import app.revanced.manager.ui.component.LoadingIndicator 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.State
import app.revanced.manager.ui.model.Step
import app.revanced.manager.ui.model.StepCategory 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 java.util.Locale
import kotlin.math.floor import kotlin.math.floor
@@ -52,8 +50,6 @@ import kotlin.math.floor
fun Steps( fun Steps(
category: StepCategory, category: StepCategory,
steps: List<Step>, steps: List<Step>,
stepCount: Pair<Int, Int>? = null,
stepProgressProvider: StepProgressProvider,
isExpanded: Boolean = false, isExpanded: Boolean = false,
onExpand: () -> Unit, onExpand: () -> Unit,
onClick: () -> 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) { LaunchedEffect(state) {
if (state == State.RUNNING) if (state == State.RUNNING || state == State.FAILED)
onExpand() onExpand()
} }
@@ -92,13 +97,8 @@ fun Steps(
Spacer(modifier = Modifier.weight(1f)) 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(
text = stepProgress, text = "${filteredSteps.count { it.state == State.COMPLETED }}/${filteredSteps.size}",
style = MaterialTheme.typography.labelSmall style = MaterialTheme.typography.labelSmall
) )
@@ -112,23 +112,20 @@ fun Steps(
.fillMaxWidth() .fillMaxWidth()
.padding(top = 10.dp) .padding(top = 10.dp)
) { ) {
steps.forEachIndexed { index, step -> filteredSteps.forEachIndexed { index, step ->
val (progress, progressText) = when (step.progressKey) { val (progress, progressText) = step.progress?.let { (current, total) ->
null -> null if (total != null) current.toFloat() / total.toFloat() to "${current.megaBytes}/${total.megaBytes} MB"
ProgressKey.DOWNLOAD -> stepProgressProvider.downloadProgress?.let { (downloaded, total) -> else null to "${current.megaBytes} MB"
if (total != null) downloaded.toFloat() / total.toFloat() to "${downloaded.megaBytes}/${total.megaBytes} MB"
else null to "${downloaded.megaBytes} MB"
}
} ?: (null to null) } ?: (null to null)
SubStep( SubStep(
name = step.name, name = step.title,
state = step.state, state = step.state,
message = step.message, message = step.message,
progress = progress, progress = progress,
progressText = progressText, progressText = progressText,
isFirst = index == 0, isFirst = index == 0,
isLast = index == steps.lastIndex, isLast = index == filteredSteps.lastIndex,
) )
} }
} }

View File

@@ -3,6 +3,7 @@ package app.revanced.manager.ui.model
import android.os.Parcelable import android.os.Parcelable
import androidx.annotation.StringRes import androidx.annotation.StringRes
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.patcher.StepId
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
enum class StepCategory(@StringRes val displayName: Int) { enum class StepCategory(@StringRes val displayName: Int) {
@@ -15,19 +16,20 @@ enum class State {
WAITING, RUNNING, FAILED, COMPLETED WAITING, RUNNING, FAILED, COMPLETED
} }
enum class ProgressKey {
DOWNLOAD
}
interface StepProgressProvider {
val downloadProgress: Pair<Long, Long?>?
}
@Parcelize @Parcelize
data class Step( data class Step(
val name: String, val id: StepId,
val title: String,
val category: StepCategory, val category: StepCategory,
val state: State = State.WAITING, val state: State = State.WAITING,
val message: String? = null, val message: String? = null,
val progressKey: ProgressKey? = null val progress: Pair<Long, Long?>? = null,
) : Parcelable val hide: Boolean = false,
) : Parcelable
fun Step.withState(
state: State = this.state,
message: String? = this.message,
progress: Pair<Long, Long?>? = this.progress
) = copy(state = state, message = message, progress = progress)

View File

@@ -87,7 +87,7 @@ fun PatcherScreen(
val steps by remember { val steps by remember {
derivedStateOf { derivedStateOf {
viewModel.steps.groupBy { it.category } viewModel.steps.groupBy { it.category }.toList()
} }
} }
@@ -230,14 +230,12 @@ fun PatcherScreen(
contentPadding = PaddingValues(16.dp) contentPadding = PaddingValues(16.dp)
) { ) {
items( items(
items = steps.toList(), items = steps,
key = { it.first } key = { it.first }
) { (category, steps) -> ) { (category, steps) ->
Steps( Steps(
category = category, category = category,
steps = steps, steps = steps,
stepCount = if (category == StepCategory.PATCHING) viewModel.patchesProgress else null,
stepProgressProvider = viewModel,
isExpanded = expandedCategory == category, isExpanded = expandedCategory == category,
onExpand = { expandCategory(category) }, onExpand = { expandCategory(category) },
onClick = { onClick = {

View File

@@ -105,6 +105,14 @@ fun GeneralSettingsScreen(
description = R.string.pure_black_theme_description description = R.string.pure_black_theme_description
) )
} }
GroupHeader(stringResource(R.string.networking))
BooleanItem(
preference = prefs.allowMeteredNetworks,
coroutineScope = coroutineScope,
headline = R.string.allow_metered_networks,
description = R.string.allow_metered_networks_description
)
} }
} }
} }

View File

@@ -54,6 +54,7 @@ class BundleListViewModel : ViewModel(), KoinComponent {
patchBundleRepository.update( patchBundleRepository.update(
*getSelectedSources().filterIsInstance<RemotePatchBundle>().toTypedArray(), *getSelectedSources().filterIsInstance<RemotePatchBundle>().toTypedArray(),
showToast = true, showToast = true,
force = true
) )
} }
} }
@@ -65,7 +66,7 @@ class BundleListViewModel : ViewModel(), KoinComponent {
fun update(src: PatchBundleSource) = viewModelScope.launch { fun update(src: PatchBundleSource) = viewModelScope.launch {
if (src !is RemotePatchBundle) return@launch if (src !is RemotePatchBundle) return@launch
patchBundleRepository.update(src, showToast = true) patchBundleRepository.update(src, showToast = true, force = true)
} }
enum class Event { enum class Event {

View File

@@ -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.installer.RootInstaller
import app.revanced.manager.domain.repository.InstalledAppRepository import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.worker.WorkerRepository 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.LogLevel
import app.revanced.manager.patcher.logger.Logger import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.worker.PatcherWorker import app.revanced.manager.patcher.worker.PatcherWorker
import app.revanced.manager.plugin.downloader.PluginHostApi import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.plugin.downloader.UserInteractionException import app.revanced.manager.plugin.downloader.UserInteractionException
import app.revanced.manager.ui.model.InstallerModel 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.SelectedApp
import app.revanced.manager.ui.model.State 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.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.navigation.Patcher
import app.revanced.manager.ui.model.withState
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.asCode import app.revanced.manager.util.asCode
import app.revanced.manager.util.saveableVar import app.revanced.manager.util.saveableVar
import app.revanced.manager.util.saver.snapshotStateListSaver import app.revanced.manager.util.saver.snapshotStateListSaver
@@ -80,7 +82,7 @@ import java.time.Duration
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class) @OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
class PatcherViewModel( class PatcherViewModel(
private val input: Patcher.ViewModelParams private val input: Patcher.ViewModelParams
) : ViewModel(), KoinComponent, StepProgressProvider, InstallerModel { ) : ViewModel(), KoinComponent, InstallerModel {
private val app: Application by inject() private val app: Application by inject()
private val fs: Filesystem by inject() private val fs: Filesystem by inject()
private val pm: PM 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<Pair<Long, Long?>?>(null)
}
private set
val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) { val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) {
generateSteps( generateSteps(app, input.selectedApp, input.selectedPatches).toMutableStateList()
app,
input.selectedApp
).toMutableStateList()
} }
private var currentStepIndex = 0
val progress by derivedStateOf { val progress by derivedStateOf {
val current = steps.count { val steps = steps.filter { it.id != StepId.ExecutePatches }
it.state == State.COMPLETED && it.category != StepCategory.PATCHING
} + completedPatchCount
val total = steps.size - 1 + patchCount val current = steps.count { it.state == State.COMPLETED }
val total = steps.size
current.toFloat() / total.toFloat() current.toFloat() / total.toFloat()
} }
@@ -201,12 +183,6 @@ class PatcherViewModel(
input.selectedPatches, input.selectedPatches,
input.options, input.options,
logger, logger,
onDownloadProgress = {
withContext(Dispatchers.Main) {
downloadProgress = it
}
},
onPatchCompleted = { withContext(Dispatchers.Main) { completedPatchCount += 1 } },
setInputFile = { withContext(Dispatchers.Main) { inputFile = it } }, setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
handleStartActivityRequest = { plugin, intent -> handleStartActivityRequest = { plugin, intent ->
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -235,26 +211,10 @@ class PatcherViewModel(
} }
} }
}, },
onProgress = { name, state, message -> onEvent = ::handleProgressEvent,
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)
}
}
}
) )
)) )
)
} }
val patcherSucceeded = 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() { fun onBack() {
installerCoroutineScope.cancel() installerCoroutineScope.cancel()
// tempDir cannot be deleted inside onCleared because it gets called on system-initiated process death. // 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) LogLevel.ERROR -> Log.e(TAG, msg)
} }
fun generateSteps(context: Context, selectedApp: SelectedApp): List<Step> { fun generateSteps(
val needsDownload = context: Context,
selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search selectedApp: SelectedApp,
selectedPatches: PatchSelection
): List<Step> = buildList {
if (selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search)
add(
Step(
StepId.DownloadAPK,
context.getString(R.string.download_apk),
StepCategory.PREPARING
)
)
return listOfNotNull( add(
Step(
context.getString(R.string.download_apk),
StepCategory.PREPARING,
state = State.RUNNING,
progressKey = ProgressKey.DOWNLOAD,
).takeIf { needsDownload },
Step( Step(
StepId.LoadPatches,
context.getString(R.string.patcher_step_load_patches), context.getString(R.string.patcher_step_load_patches),
StepCategory.PREPARING, StepCategory.PREPARING
state = if (needsDownload) State.WAITING else State.RUNNING, )
), )
add(
Step( Step(
StepId.ReadAPK,
context.getString(R.string.patcher_step_unpack), context.getString(R.string.patcher_step_unpack),
StepCategory.PREPARING StepCategory.PREPARING
), )
)
add(
Step( Step(
StepId.ExecutePatches,
context.getString(R.string.execute_patches), context.getString(R.string.execute_patches),
StepCategory.PATCHING StepCategory.PATCHING,
), hide = true
)
)
Step(context.getString(R.string.patcher_step_write_patched), StepCategory.SAVING), selectedPatches.values.asSequence().flatten().sorted().forEachIndexed { index, name ->
Step(context.getString(R.string.patcher_step_sign_apk), StepCategory.SAVING) 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
)
) )
} }
} }

View File

@@ -82,7 +82,7 @@ class UpdateViewModel(
uiSafe(app, R.string.failed_to_download_update, "Failed to download update") { uiSafe(app, R.string.failed_to_download_update, "Failed to download update") {
val release = releaseInfo!! val release = releaseInfo!!
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (!networkInfo.isSafe() && !ignoreInternetCheck) { if (!networkInfo.isSafe(false) && !ignoreInternetCheck) {
showInternetCheckDialog = true showInternetCheckDialog = true
} else { } else {
state = State.DOWNLOADING state = State.DOWNLOADING

View File

@@ -217,6 +217,10 @@ You will not be able to update the previously installed apps from this source."<
<string name="light">Light</string> <string name="light">Light</string>
<string name="dark">Dark</string> <string name="dark">Dark</string>
<string name="appearance">Appearance</string> <string name="appearance">Appearance</string>
<string name="networking">Networking</string>
<string name="allow_metered_networks">Allow metered networks</string>
<string name="allow_metered_networks_description">Permits automatic updates on metered networks.
The application might still warn about metered networks for manual operations.</string>
<string name="downloaded_apps">Downloaded apps</string> <string name="downloaded_apps">Downloaded apps</string>
<string name="process_runtime">Run Patcher in another process (experimental)</string> <string name="process_runtime">Run Patcher in another process (experimental)</string>
<string name="process_runtime_description">This is faster and allows Patcher to use more memory</string> <string name="process_runtime_description">This is faster and allows Patcher to use more memory</string>