Skip to content

Commit 11dd6e4

Browse files
feat: Show patches as individual steps in patcher screen (#2889)
Co-authored-by: Ax333l <[email protected]>
1 parent 35fb59b commit 11dd6e4

File tree

13 files changed

+394
-300
lines changed

13 files changed

+394
-300
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// ProgressEventParcel.aidl
2+
package app.revanced.manager.patcher;
3+
4+
parcelable ProgressEventParcel;
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
// IPatcherEvents.aidl
22
package app.revanced.manager.patcher.runtime.process;
33

4+
import app.revanced.manager.patcher.ProgressEventParcel;
5+
46
// Interface for sending events back to the main app process.
57
oneway interface IPatcherEvents {
68
void log(String level, String msg);
7-
void patchSucceeded();
8-
void progress(String name, String state, String msg);
9+
void event(in ProgressEventParcel event);
910
// The patching process has ended. The exceptionStackTrace is null if it finished successfully.
1011
void finished(String exceptionStackTrace);
1112
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package app.revanced.manager.patcher
2+
3+
import android.os.Parcelable
4+
import kotlinx.parcelize.Parcelize
5+
6+
7+
@Parcelize
8+
sealed class ProgressEvent : Parcelable {
9+
abstract val stepId: StepId?
10+
11+
data class Started(override val stepId: StepId) : ProgressEvent()
12+
13+
data class Progress(
14+
override val stepId: StepId,
15+
val current: Long? = null,
16+
val total: Long? = null,
17+
val message: String? = null,
18+
) : ProgressEvent()
19+
20+
data class Completed(
21+
override val stepId: StepId,
22+
) : ProgressEvent()
23+
24+
data class Failed(
25+
override val stepId: StepId?,
26+
val error: RemoteError,
27+
) : ProgressEvent()
28+
}
29+
30+
/**
31+
* Parcelable wrapper for [ProgressEvent].
32+
*
33+
* Required because AIDL does not support sealed classes.
34+
*/
35+
@Parcelize
36+
data class ProgressEventParcel(val event: ProgressEvent) : Parcelable
37+
38+
fun ProgressEventParcel.toEvent(): ProgressEvent = event
39+
fun ProgressEvent.toParcel(): ProgressEventParcel = ProgressEventParcel(this)
40+
41+
@Parcelize
42+
sealed class StepId : Parcelable {
43+
data object DownloadAPK : StepId()
44+
data object LoadPatches : StepId()
45+
data object ReadAPK : StepId()
46+
data object ExecutePatches : StepId()
47+
data class ExecutePatch(val index: Int) : StepId()
48+
data object WriteAPK : StepId()
49+
data object SignAPK : StepId()
50+
}
51+
52+
@Parcelize
53+
data class RemoteError(
54+
val type: String,
55+
val message: String?,
56+
val stackTrace: String,
57+
) : Parcelable
58+
59+
fun Exception.toRemoteError() = RemoteError(
60+
type = this::class.java.name,
61+
message = this.message,
62+
stackTrace = this.stackTraceToString(),
63+
)
64+
65+
66+
inline fun <T> runStep(
67+
stepId: StepId,
68+
onEvent: (ProgressEvent) -> Unit,
69+
block: () -> T,
70+
): T = try {
71+
onEvent(ProgressEvent.Started(stepId))
72+
val value = block()
73+
onEvent(ProgressEvent.Completed(stepId))
74+
value
75+
} catch (error: Exception) {
76+
onEvent(ProgressEvent.Failed(stepId, error.toRemoteError()))
77+
throw error
78+
}

app/src/main/java/app/revanced/manager/patcher/Session.kt

Lines changed: 40 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
package app.revanced.manager.patcher
22

3-
import android.content.Context
43
import app.revanced.library.ApkUtils.applyTo
5-
import app.revanced.manager.R
4+
import app.revanced.manager.patcher.Session.Companion.component1
5+
import app.revanced.manager.patcher.Session.Companion.component2
66
import app.revanced.manager.patcher.logger.Logger
7-
import app.revanced.manager.ui.model.State
87
import app.revanced.patcher.Patcher
98
import app.revanced.patcher.PatcherConfig
109
import app.revanced.patcher.patch.Patch
@@ -22,15 +21,10 @@ class Session(
2221
cacheDir: String,
2322
frameworkDir: String,
2423
aaptPath: String,
25-
private val androidContext: Context,
2624
private val logger: Logger,
2725
private val input: File,
28-
private val onPatchCompleted: suspend () -> Unit,
29-
private val onProgress: (name: String?, state: State?, message: String?) -> Unit
26+
private val onEvent: (ProgressEvent) -> Unit,
3027
) : Closeable {
31-
private fun updateProgress(name: String? = null, state: State? = null, message: String? = null) =
32-
onProgress(name, state, message)
33-
3428
private val tempDir = File(cacheDir).resolve("patcher").also { it.mkdirs() }
3529
private val patcher = Patcher(
3630
PatcherConfig(
@@ -42,86 +36,68 @@ class Session(
4236
)
4337

4438
private suspend fun Patcher.applyPatchesVerbose(selectedPatches: PatchList) {
45-
var nextPatchIndex = 0
46-
47-
updateProgress(
48-
name = androidContext.getString(R.string.executing_patch, selectedPatches[nextPatchIndex]),
49-
state = State.RUNNING
50-
)
51-
5239
this().collect { (patch, exception) ->
53-
if (patch !in selectedPatches) return@collect
40+
val index = selectedPatches.indexOf(patch)
41+
if (index == -1) return@collect
5442

5543
if (exception != null) {
56-
updateProgress(
57-
name = androidContext.getString(R.string.failed_to_execute_patch, patch.name),
58-
state = State.FAILED,
59-
message = exception.stackTraceToString()
44+
onEvent(
45+
ProgressEvent.Failed(
46+
StepId.ExecutePatch(index),
47+
exception.toRemoteError(),
48+
)
6049
)
61-
6250
logger.error("${patch.name} failed:")
6351
logger.error(exception.stackTraceToString())
6452
throw exception
6553
}
6654

67-
nextPatchIndex++
68-
69-
onPatchCompleted()
70-
71-
selectedPatches.getOrNull(nextPatchIndex)?.let { nextPatch ->
72-
updateProgress(
73-
name = androidContext.getString(R.string.executing_patch, nextPatch.name)
55+
onEvent(
56+
ProgressEvent.Completed(
57+
StepId.ExecutePatch(index),
7458
)
75-
}
59+
)
7660

7761
logger.info("${patch.name} succeeded")
7862
}
79-
80-
updateProgress(
81-
state = State.COMPLETED,
82-
name = androidContext.resources.getQuantityString(
83-
R.plurals.patches_executed,
84-
selectedPatches.size,
85-
selectedPatches.size
86-
)
87-
)
8863
}
8964

9065
suspend fun run(output: File, selectedPatches: PatchList) {
91-
updateProgress(state = State.COMPLETED) // Unpacking
92-
93-
java.util.logging.Logger.getLogger("").apply {
94-
handlers.forEach {
95-
it.close()
96-
removeHandler(it)
66+
runStep(StepId.ExecutePatches, onEvent) {
67+
java.util.logging.Logger.getLogger("").apply {
68+
handlers.forEach {
69+
it.close()
70+
removeHandler(it)
71+
}
72+
73+
addHandler(logger.handler)
9774
}
9875

99-
addHandler(logger.handler)
100-
}
101-
102-
with(patcher) {
103-
logger.info("Merging integrations")
104-
this += selectedPatches.toSet()
76+
with(patcher) {
77+
logger.info("Merging integrations")
78+
this += selectedPatches.toSet()
10579

106-
logger.info("Applying patches...")
107-
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
80+
logger.info("Applying patches...")
81+
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
82+
}
10883
}
10984

110-
logger.info("Writing patched files...")
111-
val result = patcher.get()
85+
runStep(StepId.WriteAPK, onEvent) {
86+
logger.info("Writing patched files...")
87+
val result = patcher.get()
11288

113-
val patched = tempDir.resolve("result.apk")
114-
withContext(Dispatchers.IO) {
115-
Files.copy(input.toPath(), patched.toPath(), StandardCopyOption.REPLACE_EXISTING)
116-
}
117-
result.applyTo(patched)
89+
val patched = tempDir.resolve("result.apk")
90+
withContext(Dispatchers.IO) {
91+
Files.copy(input.toPath(), patched.toPath(), StandardCopyOption.REPLACE_EXISTING)
92+
}
93+
result.applyTo(patched)
11894

119-
logger.info("Patched apk saved to $patched")
95+
logger.info("Patched apk saved to $patched")
12096

121-
withContext(Dispatchers.IO) {
122-
Files.move(patched.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING)
97+
withContext(Dispatchers.IO) {
98+
Files.move(patched.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING)
99+
}
123100
}
124-
updateProgress(state = State.COMPLETED) // Saving
125101
}
126102

127103
override fun close() {

app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,71 @@
11
package app.revanced.manager.patcher.runtime
22

33
import android.content.Context
4+
import app.revanced.manager.patcher.ProgressEvent
45
import app.revanced.manager.patcher.Session
6+
import app.revanced.manager.patcher.StepId
57
import app.revanced.manager.patcher.logger.Logger
68
import app.revanced.manager.patcher.patch.PatchBundle
7-
import app.revanced.manager.patcher.worker.ProgressEventHandler
8-
import app.revanced.manager.ui.model.State
9+
import app.revanced.manager.patcher.runStep
910
import app.revanced.manager.util.Options
1011
import app.revanced.manager.util.PatchSelection
1112
import java.io.File
1213

1314
/**
1415
* Simple [Runtime] implementation that runs the patcher using coroutines.
1516
*/
16-
class CoroutineRuntime(private val context: Context) : Runtime(context) {
17+
class CoroutineRuntime(context: Context) : Runtime(context) {
1718
override suspend fun execute(
1819
inputFile: String,
1920
outputFile: String,
2021
packageName: String,
2122
selectedPatches: PatchSelection,
2223
options: Options,
2324
logger: Logger,
24-
onPatchCompleted: suspend () -> Unit,
25-
onProgress: ProgressEventHandler,
25+
onEvent: (ProgressEvent) -> Unit,
2626
) {
27-
val selectedBundles = selectedPatches.keys
28-
val bundles = bundles()
29-
val uids = bundles.entries.associate { (key, value) -> value to key }
27+
val patchList = runStep(StepId.LoadPatches, onEvent) {
28+
val selectedBundles = selectedPatches.keys
29+
val bundles = bundles()
30+
val uids = bundles.entries.associate { (key, value) -> value to key }
3031

31-
val allPatches =
32-
PatchBundle.Loader.patches(bundles.values, packageName)
33-
.mapKeys { (b, _) -> uids[b]!! }
34-
.filterKeys { it in selectedBundles }
32+
val allPatches =
33+
PatchBundle.Loader.patches(bundles.values, packageName)
34+
.mapKeys { (b, _) -> uids[b]!! }
35+
.filterKeys { it in selectedBundles }
3536

36-
val patchList = selectedPatches.flatMap { (bundle, selected) ->
37-
allPatches[bundle]?.filter { it.name in selected }
38-
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
39-
}
37+
val patchList = selectedPatches.flatMap { (bundle, selected) ->
38+
allPatches[bundle]?.filter { it.name in selected }
39+
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
40+
}
4041

41-
// Set all patch options.
42-
options.forEach { (bundle, bundlePatchOptions) ->
43-
val patches = allPatches[bundle] ?: return@forEach
44-
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
45-
val patchOptions = patches.single { it.name == patchName }.options
46-
configuredPatchOptions.forEach { (key, value) ->
47-
patchOptions[key] = value
42+
// Set all patch options.
43+
options.forEach { (bundle, bundlePatchOptions) ->
44+
val patches = allPatches[bundle] ?: return@forEach
45+
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
46+
val patchOptions = patches.single { it.name == patchName }.options
47+
configuredPatchOptions.forEach { (key, value) ->
48+
patchOptions[key] = value
49+
}
4850
}
4951
}
52+
53+
patchList
5054
}
5155

52-
onProgress(null, State.COMPLETED, null) // Loading patches
56+
val session = runStep(StepId.ReadAPK, onEvent) {
57+
Session(
58+
cacheDir,
59+
frameworkPath,
60+
aaptPath,
61+
logger,
62+
File(inputFile),
63+
onEvent,
64+
)
65+
}
5366

54-
Session(
55-
cacheDir,
56-
frameworkPath,
57-
aaptPath,
58-
context,
59-
logger,
60-
File(inputFile),
61-
onPatchCompleted = onPatchCompleted,
62-
onProgress
63-
).use { session ->
64-
session.run(
67+
session.use { s ->
68+
s.run(
6569
File(outputFile),
6670
patchList
6771
)

0 commit comments

Comments
 (0)