feat: Update to Patcher v22 (#2939)

This commit is contained in:
Ax333l
2026-02-22 00:52:09 +01:00
committed by GitHub
parent e0ce6b4ef3
commit 8667051283
17 changed files with 167 additions and 137 deletions

View File

@@ -23,6 +23,10 @@ kotlin {
jvmToolchain(17)
compilerOptions {
jvmTarget = JvmTarget.JVM_17
freeCompilerArgs.addAll(
"-Xexplicit-backing-fields",
"-Xcontext-parameters",
)
}
}

View File

@@ -253,6 +253,7 @@ android {
excludes += "/META-INF/INDEX.LIST"
excludes += "/META-INF/**/*.txt"
excludes += "/META-INF/**/*.properties"
excludes += "/META-INF/DEPENDENCIES"
// Desktop AAPT binaries
excludes += "/prebuilt/**"
@@ -288,6 +289,17 @@ kotlin {
jvmToolchain(17)
compilerOptions {
jvmTarget = JvmTarget.JVM_17
freeCompilerArgs.addAll(
"-Xexplicit-backing-fields",
"-Xcontext-parameters",
)
}
}
configurations {
all {
// ReVanced Library has a dependency which conflicts with whatever this is. We don't use protobuf, so it should be fine.
exclude(group = "com.google.api.grpc", module = "proto-google-common-protos")
}
}

View File

@@ -5,8 +5,6 @@ import androidx.room.Entity
import androidx.room.ForeignKey
import app.revanced.manager.patcher.patch.Option
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
@@ -36,7 +34,7 @@ import kotlin.reflect.typeOf
data class Option(
@ColumnInfo(name = "group") val group: Int,
@ColumnInfo(name = "patch_name") val patchName: String,
@ColumnInfo(name = "key") val key: String,
@ColumnInfo(name = "key") val name: String,
// Encoded as Json.
@ColumnInfo(name = "value") val value: SerializedValue,
) {

View File

@@ -38,6 +38,7 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import java.io.InputStream
import kotlin.collections.joinToString
@@ -115,32 +116,18 @@ class PatchBundleRepository(
Log.d(tag, "Bundle: $it")
}
val sources = entities.associate { it.uid to it.load() }.toPersistentMap()
val sources = entities
.associateTo(mutableMapOf()) { it.uid to it.load() }
sources.forEach syncName@{ (uid, src) ->
val newName = src.patchBundle?.manifestAttributes?.name?.takeIf { it != src.name }
?: return@syncName
val hasOutOfDateNames = sources.values.any { it.isNameOutOfDate }
if (hasOutOfDateNames) dispatchAction(
"Sync names"
) { state ->
val nameChanges = state.sources.mapNotNull { (_, src) ->
if (!src.isNameOutOfDate) return@mapNotNull null
val newName = src.patchBundle?.manifestAttributes?.name?.takeIf { it != src.name }
?: return@mapNotNull null
src.uid to newName
}
val sources = state.sources.toMutableMap()
val info = state.info.toMutableMap()
nameChanges.forEach { (uid, name) ->
updateDb(uid) { it.copy(name = name) }
sources[uid] = sources[uid]!!.copy(name = name)
info[uid] = info[uid]?.copy(name = name) ?: return@forEach
}
State(sources.toPersistentMap(), info.toPersistentMap())
updateDb(uid) { it.copy(name = newName) }
sources[uid] = src.copy(name = newName)
}
val info = loadMetadata(sources).toPersistentMap()
return State(sources, info)
val info = loadMetadata(sources).toPersistentMap()
return State(sources.toPersistentMap(), info)
}
suspend fun reload() = dispatchAction("Full reload") {
@@ -157,38 +144,46 @@ class PatchBundleRepository(
return all
}
private suspend fun loadMetadata(sources: Map<Int, PatchBundleSource>): Map<Int, PatchBundleInfo.Global> {
private suspend fun loadMetadata(sources: MutableMap<Int, PatchBundleSource>): Map<Int, PatchBundleInfo.Global> {
// Map bundles -> sources
val map = sources.mapNotNull { (_, src) ->
(src.patchBundle ?: return@mapNotNull null) to src
}.toMap()
val metadata = try {
PatchBundle.Loader.metadata(map.keys)
runInterruptible(Dispatchers.Default) {
PatchBundle.Loader.metadata(map.keys)
}
} catch (e: CancellationException) {
throw e
} catch (error: Throwable) {
val uids = map.values.map { it.uid }
dispatchAction("Mark bundles as failed") { state ->
state.copy(sources = state.sources.mutate {
uids.forEach { uid ->
it[uid] = it[uid]?.copy(error = error) ?: return@forEach
}
})
sources.entries.forEach { entry ->
entry.setValue(entry.value.copy(error = error))
}
Log.e(tag, "Failed to load bundles", error)
emptyMap()
}
return metadata.entries.associate { (bundle, patches) ->
val src = map[bundle]!!
src.uid to PatchBundleInfo.Global(
src.name,
bundle.manifestAttributes?.version,
src.uid,
patches
)
val output = buildMap {
metadata.forEach { (bundle, result) ->
val src = map[bundle]!!
val error = result.exceptionOrNull()
if (error != null) {
sources[src.uid] = src.copy(error = error)
return@forEach
}
this[src.uid] = PatchBundleInfo.Global(
src.name,
bundle.manifestAttributes?.version,
src.uid,
result.getOrThrow().toList()
)
}
}
return output
}
suspend fun isVersionAllowed(packageName: String, version: String) =
@@ -313,9 +308,9 @@ class PatchBundleRepository(
}
suspend fun reloadApiBundles() = dispatchAction("Reload API bundles") {
this@PatchBundleRepository.sources.first().filterIsInstance<APIPatchBundle>().forEach {
with(it) { deleteLocalFile() }
updateDb(it.uid) { it.copy(versionHash = null) }
this@PatchBundleRepository.sources.first().filterIsInstance<APIPatchBundle>().forEach { src ->
with(src) { deleteLocalFile() }
updateDb(src.uid) { it.copy(versionHash = null) }
}
doReload()

View File

@@ -35,15 +35,15 @@ class PatchOptionsRepository(db: AppDatabase) {
bundlePatchOptions.getOrPut(dbOption.patchName, ::mutableMapOf)
val option =
bundlePatches[sourceUid]?.get(dbOption.patchName)?.options?.find { it.key == dbOption.key }
bundlePatches[sourceUid]?.get(dbOption.patchName)?.options?.find { it.name == dbOption.name }
if (option != null) {
try {
deserializedPatchOptions[option.key] =
deserializedPatchOptions[option.name] =
dbOption.value.deserializeFor(option)
} catch (e: Option.SerializationException) {
Log.w(
tag,
"Option ${dbOption.patchName}:${option.key} could not be deserialized",
"Option ${dbOption.patchName}:${option.name} could not be deserialized",
e
)
}

View File

@@ -4,66 +4,71 @@ import app.revanced.library.ApkUtils.applyTo
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.patcher.Patcher
import app.revanced.patcher.PatcherConfig
import app.revanced.patcher.PatchesResult
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.PatchResult
import app.revanced.patcher.patcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext
import java.io.Closeable
import java.io.File
import java.nio.file.Files
import java.nio.file.StandardCopyOption
internal typealias PatchList = List<Patch<*>>
internal typealias PatchList = List<Patch>
private typealias Patcher = (emit: (PatchResult) -> Unit) -> PatchesResult
class Session(
cacheDir: String,
frameworkDir: String,
aaptPath: String,
private val frameworkDir: String,
private val aaptPath: String,
private val logger: Logger,
private val input: File,
private val onEvent: (ProgressEvent) -> Unit,
) : Closeable {
private val tempDir = File(cacheDir).resolve("patcher").also { it.mkdirs() }
private val patcher = Patcher(
PatcherConfig(
apkFile = input,
temporaryFilesPath = tempDir,
frameworkFileDirectory = frameworkDir,
aaptBinaryPath = aaptPath
)
)
private suspend fun Patcher.applyPatchesVerbose(selectedPatches: PatchList) {
this().collect { (patch, exception) ->
val index = selectedPatches.indexOf(patch)
if (index == -1) return@collect
if (exception != null) {
private suspend fun applyPatchesVerbose(patcher: Patcher, indices: Map<Patch, Int>) =
withContext(
Dispatchers.Default
) {
val context = currentCoroutineContext()
patcher { (patch, exception) ->
// Make the patching process cancelable.
context.ensureActive()
val index = indices[patch] ?: return@patcher
if (exception != null) {
onEvent(
ProgressEvent.Failed(
StepId.ExecutePatch(index),
exception.toRemoteError(),
)
)
logger.error("${patch.name} failed:")
logger.error(exception.stackTraceToString())
throw exception
}
onEvent(
ProgressEvent.Failed(
ProgressEvent.Completed(
StepId.ExecutePatch(index),
exception.toRemoteError(),
)
)
logger.error("${patch.name} failed:")
logger.error(exception.stackTraceToString())
throw exception
logger.info("${patch.name} succeeded")
}
onEvent(
ProgressEvent.Completed(
StepId.ExecutePatch(index),
)
)
logger.info("${patch.name} succeeded")
}
}
suspend fun run(output: File, selectedPatches: PatchList) {
runStep(StepId.ExecutePatches, onEvent) {
val indices = HashMap<Patch, Int>(selectedPatches.size)
selectedPatches.forEachIndexed { idx, patch -> indices[patch] = idx }
val result = runStep(StepId.ExecutePatches, onEvent) {
java.util.logging.Logger.getLogger("").apply {
handlers.forEach {
it.close()
@@ -73,18 +78,21 @@ class Session(
addHandler(logger.handler)
}
with(patcher) {
logger.info("Merging integrations")
this += selectedPatches.toSet()
logger.info("Applying patches...")
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
val patcher = patcher(
apkFile = input,
temporaryFilesPath = tempDir,
frameworkFileDirectory = frameworkDir,
aaptBinaryPath = File(aaptPath)
) { _packageName, _version ->
selectedPatches.toSet()
}
logger.info("Applying patches...")
applyPatchesVerbose(patcher, indices)
}
runStep(StepId.WriteAPK, onEvent) {
logger.info("Writing patched files...")
val result = patcher.get()
val patched = tempDir.resolve("result.apk")
withContext(Dispatchers.IO) {
@@ -102,7 +110,6 @@ class Session(
override fun close() {
tempDir.deleteRecursively()
patcher.close()
}
companion object {

View File

@@ -2,7 +2,8 @@ package app.revanced.manager.patcher.patch
import kotlinx.parcelize.IgnoredOnParcel
import android.os.Parcelable
import app.revanced.patcher.patch.loadPatchesFromDex
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.loadPatches
import kotlinx.parcelize.Parcelize
import java.io.File
import java.io.IOException
@@ -54,31 +55,45 @@ data class PatchBundle(val patchesJar: String) : Parcelable {
)
object Loader {
private fun patches(bundles: Iterable<PatchBundle>) =
loadPatchesFromDex(
bundles.map { File(it.patchesJar) }.toSet()
).byPatchesFile.mapKeys { (file, _) ->
val absPath = file.absolutePath
bundles.single { absPath == it.patchesJar }
private fun patches(bundles: Iterable<PatchBundle>) = buildMap {
val bundleMap = bundles.associateBy { it.patchesJar }
loadPatches(
*bundleMap.keys.map(::File).toTypedArray(),
onFailedToLoad = { file, throwable ->
this[bundleMap[file.absolutePath]!!] = Result.failure(throwable)
}
).patchesByFile.forEach { (file, patches) ->
putIfAbsent(bundleMap[file.absolutePath]!!, Result.success(patches))
}
}
fun metadata(bundles: Iterable<PatchBundle>): Map<PatchBundle, Result<Set<PatchInfo>>> =
patches(bundles).mapValues { (_, result) ->
result.map { patches ->
patches.mapTo(
HashSet(patches.size),
::PatchInfo
)
}
}
fun metadata(bundles: Iterable<PatchBundle>) =
patches(bundles).mapValues { (_, patches) -> patches.map(::PatchInfo) }
fun patches(bundles: Iterable<PatchBundle>, packageName: String): Map<PatchBundle, Set<Patch>> =
patches(bundles).mapValues { (_, result) ->
val patches = result.getOrDefault(emptySet())
fun patches(bundles: Iterable<PatchBundle>, packageName: String) =
patches(bundles).mapValues { (_, patches) ->
patches.filter { patch ->
patches.filterTo(HashSet(patches.size)) { patch ->
val compatiblePackages = patch.compatiblePackages
?: // The patch has no compatibility constraints, which means it is universal.
return@filter true
return@filterTo true
if (!compatiblePackages.any { (name, _) -> name == packageName }) {
// Patch is not compatible with this package.
return@filter false
return@filterTo false
}
true
}.toSet()
}
}
}
}
}

View File

@@ -137,7 +137,7 @@ sealed class PatchBundleInfo {
it.options.all option@{ option ->
if (!option.required || option.default != null) return@option true
option.key in opts
option.name in opts
}
}
}

View File

@@ -17,7 +17,7 @@ data class PatchInfo(
val compatiblePackages: ImmutableList<CompatiblePackage>?,
val options: ImmutableList<Option<*>>?
) {
constructor(patch: Patch<*>) : this(
constructor(patch: Patch) : this(
patch.name.orEmpty(),
patch.description,
patch.use,
@@ -49,7 +49,7 @@ data class PatchInfo(
* The resulting patch cannot be executed.
* This is necessary because some functions in ReVanced Library only accept full [Patch] objects.
*/
fun toPatcherPatch(): Patch<*> =
fun toPatcherPatch(): Patch =
resourcePatch(name = name, description = description, use = include) {
compatiblePackages?.let { pkgs ->
compatibleWith(*pkgs.map { it.packageName to it.versions }.toTypedArray())
@@ -65,8 +65,7 @@ data class CompatiblePackage(
@Immutable
data class Option<T>(
val title: String,
val key: String,
val name: String,
val description: String,
val required: Boolean,
val type: KType,
@@ -75,8 +74,7 @@ data class Option<T>(
val validator: (T?) -> Boolean,
) {
constructor(option: PatchOption<T>) : this(
option.title ?: option.key,
option.key,
option.name,
option.description.orEmpty(),
option.required,
option.type,

View File

@@ -212,7 +212,7 @@ fun PatchItem(
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = option.title,
text = option.name,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary
)

View File

@@ -205,7 +205,7 @@ fun <T : Any> OptionItem(
WithOptionEditor(editor, option, value, setValue, selectionWarningEnabled) {
ListItem(
modifier = Modifier.clickable(onClick = ::clickAction),
headlineContent = { Text(option.title) },
headlineContent = { Text(option.name) },
supportingContent = {
Column {
Text(option.description)
@@ -250,7 +250,7 @@ private object StringOptionEditor : OptionEditor<String> {
AlertDialog(
onDismissRequest = scope.dismissDialog,
title = { Text(scope.option.title) },
title = { Text(scope.option.name) },
text = {
OutlinedTextField(
value = fieldValue,
@@ -330,7 +330,7 @@ private abstract class NumberOptionEditor<T : Number> : OptionEditor<T> {
@Composable
override fun Dialog(scope: OptionEditorScope<T>) {
NumberDialog(scope.option.title, scope.value, scope.option.validator) {
NumberDialog(scope.option.name, scope.value, scope.option.validator) {
if (it == null) return@NumberDialog scope.dismissDialog()
scope.submitDialog(it)
@@ -455,7 +455,7 @@ private class PresetOptionEditor<T : Any>(private val innerEditor: OptionEditor<
Text(stringResource(R.string.cancel))
}
},
title = { Text(scope.option.title) },
title = { Text(scope.option.name) },
textHorizontalPadding = PaddingValues(horizontal = 0.dp),
text = {
val presets = remember(scope.option.presets) {
@@ -496,8 +496,7 @@ private class PresetOptionEditor<T : Any>(private val innerEditor: OptionEditor<
private class ListOptionEditor<T : Serializable>(private val elementEditor: OptionEditor<T>) :
OptionEditor<List<T>> {
private fun createElementOption(option: Option<List<T>>) = Option<T>(
option.title,
option.key,
option.name,
option.description,
option.required,
option.type.arguments.first().type!!,
@@ -566,7 +565,7 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
R.plurals.selected_count,
deletionTargets.size,
deletionTargets.size
) else scope.option.title,
) else scope.option.name,
onBackClick = back,
backIcon = {
if (deleteMode) {

View File

@@ -640,17 +640,17 @@ private fun OptionsDialog(
) {
if (patch.options == null) return@LazyColumnWithScrollbar
items(patch.options, key = { it.key }) { option ->
val key = option.key
items(patch.options, key = { it.name }) { option ->
val name = option.name
val value =
if (values == null || !values.contains(key)) option.default else values[key]
if (values == null || !values.contains(name)) option.default else values[name]
@Suppress("UNCHECKED_CAST")
OptionItem(
option = option as Option<Any>,
value = value,
setValue = {
set(key, it)
set(name, it)
},
selectionWarningEnabled = selectionWarningEnabled
)

View File

@@ -144,16 +144,16 @@ fun RequiredOptionsScreen(
val values = vm.getOptions(bundle.uid, it)
it.options?.forEach { option ->
val key = option.key
val name = option.name
val value =
if (values == null || key !in values) option.default else values[key]
if (values == null || name !in values) option.default else values[name]
@Suppress("UNCHECKED_CAST")
OptionItem(
option = option as Option<Any>,
value = value,
setValue = { new ->
vm.setOption(bundle.uid, it, key, new)
vm.setOption(bundle.uid, it, name, new)
},
selectionWarningEnabled = vm.selectionWarningEnabled
)

View File

@@ -129,7 +129,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
isSelected(
bundle.uid,
patch
) && patch.options?.any { it.required && it.default == null && it.key !in opts } ?: false
) && patch.options?.any { it.required && it.default == null && it.name !in opts } ?: false
}.toList()
}.filter { (_, patches) -> patches.isNotEmpty() }
}
@@ -180,13 +180,13 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
fun getOptions(bundle: Int, patch: PatchInfo) = patchOptions[bundle]?.get(patch.name)
fun setOption(bundle: Int, patch: PatchInfo, key: String, value: Any?) {
fun setOption(bundle: Int, patch: PatchInfo, name: String, value: Any?) {
// All patches
val patchesToOpts = patchOptions.getOrElse(bundle, ::persistentMapOf)
// The key-value options of an individual patch
val patchToOpts = patchesToOpts
.getOrElse(patch.name, ::persistentMapOf)
.put(key, value)
.put(name, value)
patchOptions[bundle] = patchesToOpts.put(patch.name, patchToOpts)
}

View File

@@ -325,7 +325,7 @@ class SelectedAppInfoViewModel(
bundleOptions.forEach patch@{ (patchName, values) ->
// Get all valid option keys for the patch.
val validOptionKeys =
patches[patchName]?.options?.map { it.key }?.toSet() ?: return@patch
patches[patchName]?.options?.map { it.name }?.toSet() ?: return@patch
this@bundleOptions[patchName] = values.filterKeys { key ->
key in validOptionKeys

View File

@@ -17,8 +17,8 @@ serialization = "1.9.0"
collection = "0.4.0"
datetime = "0.7.1"
room-version = "2.8.4"
revanced-patcher = "21.0.0"
revanced-library = "3.0.2"
revanced-patcher = "22.0.0"
revanced-library = "4.0.0"
koin = "4.1.1"
ktor = "3.3.3"
markdown-renderer = "0.39.0"
@@ -81,8 +81,8 @@ room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room-ver
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room-version" }
# Patcher
revanced-patcher = { group = "app.revanced", name = "revanced-patcher", version.ref = "revanced-patcher" }
revanced-library = { group = "app.revanced", name = "revanced-library", version.ref = "revanced-library" }
revanced-patcher = { group = "app.revanced", name = "patcher-android", version.ref = "revanced-patcher" }
revanced-library = { group = "app.revanced", name = "library-android", version.ref = "revanced-library" }
# Koin
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" }

View File

@@ -1,5 +1,6 @@
pluginManagement {
repositories {
mavenLocal()
mavenCentral()
google()
gradlePluginPortal()
@@ -8,6 +9,7 @@ pluginManagement {
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenLocal()
mavenCentral()
google()
maven("https://jitpack.io")