mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2026-01-23 19:21:03 +00:00
Compare commits
1 Commits
v1.26.0-de
...
fix/instal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05ace8180e |
@@ -1,24 +1,3 @@
|
|||||||
# 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)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* install dialog getting stuck ([#2900](https://github.com/ReVanced/revanced-manager/issues/2900)) ([18a4df9](https://github.com/ReVanced/revanced-manager/commit/18a4df9af9cac120fdb8e4ff7aadd2e2a8d5c1a6))
|
|
||||||
|
|
||||||
# app [1.26.0-dev.14](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.13...v1.26.0-dev.14) (2025-12-28)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* Update selected patch count when SelectionState changes ([#2896](https://github.com/ReVanced/revanced-manager/issues/2896)) ([0d26df0](https://github.com/ReVanced/revanced-manager/commit/0d26df03f463195dae550240c7f652680763079c))
|
|
||||||
|
|
||||||
# app [1.26.0-dev.13](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.12...v1.26.0-dev.13) (2025-12-17)
|
# app [1.26.0-dev.13](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.12...v1.26.0-dev.13) (2025-12-17)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
version = 1.26.0-dev.16
|
version = 1.26.0-dev.13
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
// ProgressEventParcel.aidl
|
|
||||||
package app.revanced.manager.patcher;
|
|
||||||
|
|
||||||
parcelable ProgressEventParcel;
|
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
// 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 event(in ProgressEventParcel event);
|
void patchSucceeded();
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
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.patcher.Session.Companion.component1
|
import app.revanced.manager.R
|
||||||
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
|
||||||
@@ -21,10 +22,15 @@ 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 onEvent: (ProgressEvent) -> Unit,
|
private val onPatchCompleted: suspend () -> 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(
|
||||||
@@ -36,68 +42,86 @@ 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) ->
|
||||||
val index = selectedPatches.indexOf(patch)
|
if (patch !in selectedPatches) return@collect
|
||||||
if (index == -1) return@collect
|
|
||||||
|
|
||||||
if (exception != null) {
|
if (exception != null) {
|
||||||
onEvent(
|
updateProgress(
|
||||||
ProgressEvent.Failed(
|
name = androidContext.getString(R.string.failed_to_execute_patch, patch.name),
|
||||||
StepId.ExecutePatch(index),
|
state = State.FAILED,
|
||||||
exception.toRemoteError(),
|
message = exception.stackTraceToString()
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.error("${patch.name} failed:")
|
logger.error("${patch.name} failed:")
|
||||||
logger.error(exception.stackTraceToString())
|
logger.error(exception.stackTraceToString())
|
||||||
throw exception
|
throw exception
|
||||||
}
|
}
|
||||||
|
|
||||||
onEvent(
|
nextPatchIndex++
|
||||||
ProgressEvent.Completed(
|
|
||||||
StepId.ExecutePatch(index),
|
onPatchCompleted()
|
||||||
|
|
||||||
|
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) {
|
||||||
runStep(StepId.ExecutePatches, onEvent) {
|
updateProgress(state = State.COMPLETED) // Unpacking
|
||||||
java.util.logging.Logger.getLogger("").apply {
|
|
||||||
handlers.forEach {
|
|
||||||
it.close()
|
|
||||||
removeHandler(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
addHandler(logger.handler)
|
java.util.logging.Logger.getLogger("").apply {
|
||||||
|
handlers.forEach {
|
||||||
|
it.close()
|
||||||
|
removeHandler(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
with(patcher) {
|
addHandler(logger.handler)
|
||||||
logger.info("Merging integrations")
|
|
||||||
this += selectedPatches.toSet()
|
|
||||||
|
|
||||||
logger.info("Applying patches...")
|
|
||||||
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
runStep(StepId.WriteAPK, onEvent) {
|
with(patcher) {
|
||||||
logger.info("Writing patched files...")
|
logger.info("Merging integrations")
|
||||||
val result = patcher.get()
|
this += selectedPatches.toSet()
|
||||||
|
|
||||||
val patched = tempDir.resolve("result.apk")
|
logger.info("Applying patches...")
|
||||||
withContext(Dispatchers.IO) {
|
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
|
||||||
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() {
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
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.runStep
|
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
||||||
|
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
|
||||||
@@ -14,7 +13,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(context: Context) : Runtime(context) {
|
class CoroutineRuntime(private val context: Context) : Runtime(context) {
|
||||||
override suspend fun execute(
|
override suspend fun execute(
|
||||||
inputFile: String,
|
inputFile: String,
|
||||||
outputFile: String,
|
outputFile: String,
|
||||||
@@ -22,50 +21,47 @@ class CoroutineRuntime(context: Context) : Runtime(context) {
|
|||||||
selectedPatches: PatchSelection,
|
selectedPatches: PatchSelection,
|
||||||
options: Options,
|
options: Options,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
onEvent: (ProgressEvent) -> Unit,
|
onPatchCompleted: suspend () -> Unit,
|
||||||
|
onProgress: ProgressEventHandler,
|
||||||
) {
|
) {
|
||||||
val patchList = runStep(StepId.LoadPatches, onEvent) {
|
val selectedBundles = selectedPatches.keys
|
||||||
val selectedBundles = selectedPatches.keys
|
val bundles = bundles()
|
||||||
val bundles = bundles()
|
val uids = bundles.entries.associate { (key, value) -> value to key }
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val session = runStep(StepId.ReadAPK, onEvent) {
|
onProgress(null, State.COMPLETED, null) // Loading patches
|
||||||
Session(
|
|
||||||
cacheDir,
|
|
||||||
frameworkPath,
|
|
||||||
aaptPath,
|
|
||||||
logger,
|
|
||||||
File(inputFile),
|
|
||||||
onEvent,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
session.use { s ->
|
Session(
|
||||||
s.run(
|
cacheDir,
|
||||||
|
frameworkPath,
|
||||||
|
aaptPath,
|
||||||
|
context,
|
||||||
|
logger,
|
||||||
|
File(inputFile),
|
||||||
|
onPatchCompleted = onPatchCompleted,
|
||||||
|
onProgress
|
||||||
|
).use { session ->
|
||||||
|
session.run(
|
||||||
File(outputFile),
|
File(outputFile),
|
||||||
patchList
|
patchList
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,13 +10,12 @@ 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.toEvent
|
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
||||||
|
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
|
||||||
@@ -67,7 +66,8 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
|||||||
selectedPatches: PatchSelection,
|
selectedPatches: PatchSelection,
|
||||||
options: Options,
|
options: Options,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
onEvent: (ProgressEvent) -> Unit,
|
onPatchCompleted: suspend () -> 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,6 +111,7 @@ 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()
|
||||||
@@ -123,10 +124,13 @@ 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 event(event: ProgressEventParcel?) {
|
override fun patchSucceeded() {
|
||||||
event?.let { onEvent(it.toEvent()) }
|
scope.launch { onPatchCompleted() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
|||||||
@@ -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,6 +34,7 @@ sealed class Runtime(context: Context) : KoinComponent {
|
|||||||
selectedPatches: PatchSelection,
|
selectedPatches: PatchSelection,
|
||||||
options: Options,
|
options: Options,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
onEvent: (ProgressEvent) -> Unit,
|
onPatchCompleted: suspend () -> Unit,
|
||||||
|
onProgress: ProgressEventHandler,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -8,15 +8,12 @@ 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.patcher.toParcel
|
import app.revanced.manager.ui.model.State
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -27,7 +24,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() : IPatcherProcess.Stub() {
|
class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
||||||
private var eventBinder: IPatcherEvents? = null
|
private var eventBinder: IPatcherEvents? = null
|
||||||
|
|
||||||
private val scope =
|
private val scope =
|
||||||
@@ -49,8 +46,6 @@ class PatcherProcess() : 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 {
|
||||||
@@ -61,42 +56,38 @@ class PatcherProcess() : IPatcherProcess.Stub() {
|
|||||||
|
|
||||||
logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
|
logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
|
||||||
|
|
||||||
val patchList = runStep(StepId.LoadPatches, ::onEvent) {
|
val allPatches = PatchBundle.Loader.patches(parameters.configurations.map { it.bundle }, parameters.packageName)
|
||||||
val allPatches = PatchBundle.Loader.patches(
|
val patchList = parameters.configurations.flatMap { config ->
|
||||||
parameters.configurations.map { it.bundle },
|
val patches = (allPatches[config.bundle] ?: return@flatMap emptyList())
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
val session = runStep(StepId.ReadAPK, ::onEvent) {
|
events.progress(null, State.COMPLETED.name, null) // Loading patches
|
||||||
Session(
|
|
||||||
cacheDir = parameters.cacheDir,
|
|
||||||
aaptPath = parameters.aaptPath,
|
|
||||||
frameworkDir = parameters.frameworkDir,
|
|
||||||
logger = logger,
|
|
||||||
input = File(parameters.inputFile),
|
|
||||||
onEvent = ::onEvent,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
session.use {
|
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 {
|
||||||
it.run(File(parameters.outputFile), patchList)
|
it.run(File(parameters.outputFile), patchList)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +119,7 @@ class PatcherProcess() : IPatcherProcess.Stub() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val ipcInterface = PatcherProcess()
|
val ipcInterface = PatcherProcess(appContext)
|
||||||
|
|
||||||
appContext.sendBroadcast(Intent().apply {
|
appContext.sendBroadcast(Intent().apply {
|
||||||
action = ProcessRuntime.CONNECT_TO_APP_ACTION
|
action = ProcessRuntime.CONNECT_TO_APP_ACTION
|
||||||
|
|||||||
@@ -29,17 +29,14 @@ 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
|
||||||
@@ -51,6 +48,8 @@ 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,
|
||||||
@@ -72,9 +71,11 @@ 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 onEvent: (ProgressEvent) -> Unit,
|
val onProgress: ProgressEventHandler
|
||||||
) {
|
) {
|
||||||
val packageName get() = input.packageName
|
val packageName get() = input.packageName
|
||||||
}
|
}
|
||||||
@@ -139,6 +140,10 @@ 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 {
|
||||||
@@ -158,65 +163,51 @@ class PatcherWorker(
|
|||||||
args.input.version,
|
args.input.version,
|
||||||
prefs.suggestedVersionSafeguard.get(),
|
prefs.suggestedVersionSafeguard.get(),
|
||||||
!prefs.disablePatchVersionCompatCheck.get(),
|
!prefs.disablePatchVersionCompatCheck.get(),
|
||||||
onDownload = { progress ->
|
onDownload = args.onDownloadProgress
|
||||||
args.onEvent(
|
).also {
|
||||||
ProgressEvent.Progress(
|
args.setInputFile(it)
|
||||||
stepId = StepId.DownloadAPK,
|
updateProgress(state = State.COMPLETED) // Download APK
|
||||||
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 -> {
|
||||||
runStep(StepId.DownloadAPK, args.onEvent) {
|
val (plugin, data) = downloaderPluginRepository.unwrapParceledData(selectedApp.data)
|
||||||
val (plugin, data) = downloaderPluginRepository.unwrapParceledData(
|
|
||||||
selectedApp.data
|
|
||||||
)
|
|
||||||
|
|
||||||
download(plugin, data)
|
download(plugin, data)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is SelectedApp.Search -> {
|
is SelectedApp.Search -> {
|
||||||
runStep(StepId.DownloadAPK, args.onEvent) {
|
downloaderPluginRepository.loadedPluginsFlow.first()
|
||||||
downloaderPluginRepository.loadedPluginsFlow.first()
|
.firstNotNullOfOrNull { plugin ->
|
||||||
.firstNotNullOfOrNull { plugin ->
|
try {
|
||||||
try {
|
val getScope = object : GetScope {
|
||||||
val getScope = object : GetScope {
|
override val pluginPackageName = plugin.packageName
|
||||||
override val pluginPackageName = plugin.packageName
|
override val hostPackageName = applicationContext.packageName
|
||||||
override val hostPackageName =
|
override suspend fun requestStartActivity(intent: Intent): Intent? {
|
||||||
applicationContext.packageName
|
val result = args.handleStartActivityRequest(plugin, intent)
|
||||||
|
return when (result.resultCode) {
|
||||||
override suspend fun requestStartActivity(intent: Intent): Intent? {
|
Activity.RESULT_OK -> result.data
|
||||||
val result =
|
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
|
||||||
args.handleStartActivityRequest(plugin, intent)
|
else -> throw UserInteractionException.Activity.NotCompleted(
|
||||||
return when (result.resultCode) {
|
result.resultCode,
|
||||||
Activity.RESULT_OK -> result.data
|
result.data
|
||||||
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
|
)
|
||||||
else -> throw UserInteractionException.Activity.NotCompleted(
|
|
||||||
result.resultCode,
|
|
||||||
result.data
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.IO) {
|
}
|
||||||
plugin.get(
|
withContext(Dispatchers.IO) {
|
||||||
getScope,
|
plugin.get(
|
||||||
selectedApp.packageName,
|
getScope,
|
||||||
selectedApp.version
|
selectedApp.packageName,
|
||||||
)
|
selectedApp.version
|
||||||
}?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
|
)
|
||||||
} catch (e: UserInteractionException.Activity.NotCompleted) {
|
}?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
|
||||||
throw e
|
} catch (e: UserInteractionException.Activity.NotCompleted) {
|
||||||
} catch (_: UserInteractionException) {
|
throw e
|
||||||
null
|
} catch (_: UserInteractionException) {
|
||||||
}?.let { (data, _) -> download(plugin, data) }
|
null
|
||||||
} ?: throw Exception("App is not available.")
|
}?.let { (data, _) -> download(plugin, data) }
|
||||||
}
|
} ?: throw Exception("App is not available.")
|
||||||
}
|
}
|
||||||
|
|
||||||
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
|
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
|
||||||
@@ -236,12 +227,12 @@ class PatcherWorker(
|
|||||||
args.selectedPatches,
|
args.selectedPatches,
|
||||||
args.options,
|
args.options,
|
||||||
args.logger,
|
args.logger,
|
||||||
args.onEvent,
|
args.onPatchCompleted,
|
||||||
|
args.onProgress
|
||||||
)
|
)
|
||||||
|
|
||||||
runStep(StepId.SignAPK, args.onEvent) {
|
keystoreManager.sign(patchedApk, File(args.output))
|
||||||
keystoreManager.sign(patchedApk, File(args.output))
|
updateProgress(state = State.COMPLETED) // Signing
|
||||||
}
|
|
||||||
|
|
||||||
Log.i(tag, "Patching succeeded".logFmt())
|
Log.i(tag, "Patching succeeded".logFmt())
|
||||||
Result.success()
|
Result.success()
|
||||||
@@ -250,11 +241,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()
|
||||||
)
|
)
|
||||||
args.onEvent(ProgressEvent.Failed(null, e.toRemoteError())) // Fallback if exception doesn't occur within step
|
updateProgress(state = State.FAILED, message = e.originalStackTrace)
|
||||||
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)
|
||||||
args.onEvent(ProgressEvent.Failed(null, e.toRemoteError())) // Fallback if exception doesn't occur within step
|
updateProgress(state = State.FAILED, message = e.stackTraceToString())
|
||||||
Result.failure()
|
Result.failure()
|
||||||
} finally {
|
} finally {
|
||||||
patchedApk.delete()
|
patchedApk.delete()
|
||||||
|
|||||||
@@ -39,9 +39,11 @@ 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.StepCategory
|
|
||||||
import app.revanced.manager.ui.model.Step
|
import app.revanced.manager.ui.model.Step
|
||||||
|
import app.revanced.manager.ui.model.StepCategory
|
||||||
|
import app.revanced.manager.ui.model.StepProgressProvider
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
|
|
||||||
@@ -50,6 +52,8 @@ 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
|
||||||
@@ -63,17 +67,8 @@ 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 || state == State.FAILED)
|
if (state == State.RUNNING)
|
||||||
onExpand()
|
onExpand()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,8 +92,13 @@ 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 = "${filteredSteps.count { it.state == State.COMPLETED }}/${filteredSteps.size}",
|
text = stepProgress,
|
||||||
style = MaterialTheme.typography.labelSmall
|
style = MaterialTheme.typography.labelSmall
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -112,20 +112,23 @@ fun Steps(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = 10.dp)
|
.padding(top = 10.dp)
|
||||||
) {
|
) {
|
||||||
filteredSteps.forEachIndexed { index, step ->
|
steps.forEachIndexed { index, step ->
|
||||||
val (progress, progressText) = step.progress?.let { (current, total) ->
|
val (progress, progressText) = when (step.progressKey) {
|
||||||
if (total != null) current.toFloat() / total.toFloat() to "${current.megaBytes}/${total.megaBytes} MB"
|
null -> null
|
||||||
else null to "${current.megaBytes} MB"
|
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"
|
||||||
|
}
|
||||||
} ?: (null to null)
|
} ?: (null to null)
|
||||||
|
|
||||||
SubStep(
|
SubStep(
|
||||||
name = step.title,
|
name = step.name,
|
||||||
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 == filteredSteps.lastIndex,
|
isLast = index == steps.lastIndex,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ 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) {
|
||||||
@@ -16,20 +15,19 @@ 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 id: StepId,
|
val name: String,
|
||||||
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 progress: Pair<Long, Long?>? = null,
|
val progressKey: ProgressKey? = null
|
||||||
val hide: Boolean = false,
|
) : Parcelable
|
||||||
) : 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)
|
|
||||||
@@ -87,7 +87,7 @@ fun PatcherScreen(
|
|||||||
|
|
||||||
val steps by remember {
|
val steps by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
viewModel.steps.groupBy { it.category }.toList()
|
viewModel.steps.groupBy { it.category }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,12 +230,14 @@ fun PatcherScreen(
|
|||||||
contentPadding = PaddingValues(16.dp)
|
contentPadding = PaddingValues(16.dp)
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
items = steps,
|
items = steps.toList(),
|
||||||
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 = {
|
||||||
|
|||||||
@@ -29,22 +29,20 @@ 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.StepCategory
|
|
||||||
import app.revanced.manager.ui.model.Step
|
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.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
|
||||||
@@ -82,7 +80,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, InstallerModel {
|
) : ViewModel(), KoinComponent, StepProgressProvider, 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()
|
||||||
@@ -159,15 +157,35 @@ class PatcherViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) {
|
private val patchCount = input.selectedPatches.values.sumOf { it.size }
|
||||||
generateSteps(app, input.selectedApp, input.selectedPatches).toMutableStateList()
|
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()) {
|
||||||
|
generateSteps(
|
||||||
|
app,
|
||||||
|
input.selectedApp
|
||||||
|
).toMutableStateList()
|
||||||
|
}
|
||||||
|
private var currentStepIndex = 0
|
||||||
|
|
||||||
val progress by derivedStateOf {
|
val progress by derivedStateOf {
|
||||||
val steps = steps.filter { it.id != StepId.ExecutePatches }
|
val current = steps.count {
|
||||||
|
it.state == State.COMPLETED && it.category != StepCategory.PATCHING
|
||||||
|
} + completedPatchCount
|
||||||
|
|
||||||
val current = steps.count { it.state == State.COMPLETED }
|
val total = steps.size - 1 + patchCount
|
||||||
val total = steps.size
|
|
||||||
|
|
||||||
current.toFloat() / total.toFloat()
|
current.toFloat() / total.toFloat()
|
||||||
}
|
}
|
||||||
@@ -183,6 +201,12 @@ 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) {
|
||||||
@@ -211,10 +235,26 @@ class PatcherViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onEvent = ::handleProgressEvent,
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
)
|
))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val patcherSucceeded =
|
val patcherSucceeded =
|
||||||
@@ -268,35 +308,6 @@ 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.
|
||||||
@@ -533,66 +544,34 @@ class PatcherViewModel(
|
|||||||
LogLevel.ERROR -> Log.e(TAG, msg)
|
LogLevel.ERROR -> Log.e(TAG, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun generateSteps(
|
fun generateSteps(context: Context, selectedApp: SelectedApp): List<Step> {
|
||||||
context: Context,
|
val needsDownload =
|
||||||
selectedApp: SelectedApp,
|
selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search
|
||||||
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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
add(
|
return listOfNotNull(
|
||||||
|
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
|
),
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
selectedPatches.values.asSequence().flatten().sorted().forEachIndexed { index, name ->
|
Step(context.getString(R.string.patcher_step_write_patched), StepCategory.SAVING),
|
||||||
add(
|
Step(context.getString(R.string.patcher_step_sign_apk), StepCategory.SAVING)
|
||||||
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
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user