feat!: Add patch annotation processor

This commit introduces an annotation processor for patches. Patches can use the `@Patch` instead of super constructor parameters.

BREAKING CHANGE: The manifest for patches has been removed, and the properties have been added to patches. Patches are now `OptionsContainer`. The `@Patch` annotation has been removed in favour of the `@Patch` annotation from the annotation processor.
This commit is contained in:
oSumAtrIX 2023-09-04 05:38:35 +02:00
parent 4dd04975d9
commit 3fc6a139ee
No known key found for this signature in database
GPG key ID: A9B3094ACDB604B4
29 changed files with 707 additions and 161 deletions

View file

@ -6,8 +6,12 @@ kotlin-test = "1.8.20-RC"
kotlinx-coroutines-core = "1.7.1" kotlinx-coroutines-core = "1.7.1"
multidexlib2 = "3.0.3.r2" multidexlib2 = "3.0.3.r2"
smali = "3.0.3" smali = "3.0.3"
symbol-processing-api = "1.9.0-1.0.11"
xpp3 = "1.1.4c" xpp3 = "1.1.4c"
binary-compatibility-validator = "0.13.2" binary-compatibility-validator = "0.13.2"
kotlin-compile-testing-ksp = "1.5.0"
kotlinpoet-ksp = "1.14.2"
ksp = "1.9.0-1.0.11"
[libraries] [libraries]
android = { module = "com.google.android:android", version.ref = "android" } android = { module = "com.google.android:android", version.ref = "android" }
@ -17,7 +21,11 @@ kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotl
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" }
multidexlib2 = { module = "app.revanced:multidexlib2", version.ref = "multidexlib2" } multidexlib2 = { module = "app.revanced:multidexlib2", version.ref = "multidexlib2" }
smali = { module = "com.android.tools.smali:smali", version.ref = "smali" } smali = { module = "com.android.tools.smali:smali", version.ref = "smali" }
symbol-processing-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "symbol-processing-api" }
xpp3 = { module = "xpp3:xpp3", version.ref = "xpp3" } xpp3 = { module = "xpp3:xpp3", version.ref = "xpp3" }
kotlin-compile-testing = { module = "com.github.tschuchortdev:kotlin-compile-testing-ksp", version.ref = "kotlin-compile-testing-ksp" }
kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlinpoet-ksp" }
[plugins] [plugins]
binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" } binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }

View file

@ -0,0 +1,25 @@
public abstract interface annotation class app/revanced/patcher/patch/annotations/CompatiblePackage : java/lang/annotation/Annotation {
public abstract fun name ()Ljava/lang/String;
public abstract fun versions ()[Ljava/lang/String;
}
public abstract interface annotation class app/revanced/patcher/patch/annotations/Patch : java/lang/annotation/Annotation {
public abstract fun compatiblePackages ()[Lapp/revanced/patcher/patch/annotations/CompatiblePackage;
public abstract fun dependencies ()[Ljava/lang/Class;
public abstract fun description ()Ljava/lang/String;
public abstract fun name ()Ljava/lang/String;
public abstract fun requiresIntegrations ()Z
public abstract fun use ()Z
}
public final class app/revanced/patcher/patch/annotations/processor/PatchProcessor : com/google/devtools/ksp/processing/SymbolProcessor {
public fun <init> (Lcom/google/devtools/ksp/processing/CodeGenerator;Lcom/google/devtools/ksp/processing/KSPLogger;)V
public fun process (Lcom/google/devtools/ksp/processing/Resolver;)Ljava/util/List;
}
public final class app/revanced/patcher/patch/annotations/processor/PatchProcessorProvider : com/google/devtools/ksp/processing/SymbolProcessorProvider {
public fun <init> ()V
public fun create (Lcom/google/devtools/ksp/processing/SymbolProcessorEnvironment;)Lapp/revanced/patcher/patch/annotations/processor/PatchProcessor;
public synthetic fun create (Lcom/google/devtools/ksp/processing/SymbolProcessorEnvironment;)Lcom/google/devtools/ksp/processing/SymbolProcessor;
}

View file

@ -0,0 +1,50 @@
plugins {
kotlin("jvm") version "1.9.0"
`maven-publish`
alias(libs.plugins.ksp)
}
group = "app.revanced"
dependencies {
implementation(libs.symbol.processing.api)
implementation(libs.kotlinpoet.ksp)
implementation(project(":revanced-patcher"))
testImplementation(libs.kotlin.test)
testImplementation(libs.kotlin.compile.testing)
}
tasks {
test {
useJUnitPlatform()
testLogging {
events("PASSED", "SKIPPED", "FAILED")
}
}
}
kotlin { jvmToolchain(11) }
java {
withSourcesJar()
}
publishing {
repositories {
mavenLocal()
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/revanced/revanced-patch-annotations-processor")
credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN")
}
}
}
publications {
create<MavenPublication>("gpr") {
from(components["java"])
}
}
}

View file

@ -0,0 +1,2 @@
rootProject.name = "revanced-patch-annotations-processor"

View file

@ -0,0 +1,38 @@
package app.revanced.patcher.patch.annotations
import java.lang.annotation.Inherited
import kotlin.reflect.KClass
/**
* Annotation for [app.revanced.patcher.patch.Patch] classes.
*
* @param name The name of the patch. If empty, the patch will be unnamed.
* @param description The description of the patch. If empty, no description will be used.
* @param dependencies The patches this patch depends on.
* @param compatiblePackages The packages this patch is compatible with.
* @param use Whether this patch should be used.
* @param requiresIntegrations Whether this patch requires integrations.
*/
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.CLASS)
@Inherited
annotation class Patch(
val name: String = "",
val description: String = "",
val dependencies: Array<KClass<out app.revanced.patcher.patch.Patch<*>>> = [],
val compatiblePackages: Array<CompatiblePackage> = [],
val use: Boolean = true,
// TODO: Remove this property, once integrations are coupled with patches.
val requiresIntegrations: Boolean = false,
)
/**
* A package that a [app.revanced.patcher.patch.Patch] is compatible with.
*
* @param name The name of the package.
* @param versions The versions of the package.
*/
annotation class CompatiblePackage(
val name: String,
val versions: Array<String> = [],
)

View file

@ -0,0 +1,198 @@
package app.revanced.patcher.patch.annotations.processor
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.BytecodePatch
import app.revanced.patcher.patch.PatchOptions
import app.revanced.patcher.patch.ResourcePatch
import app.revanced.patcher.patch.annotations.Patch
import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSAnnotation
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.validate
import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.ksp.toClassName
import com.squareup.kotlinpoet.ksp.writeTo
import kotlin.reflect.KClass
class PatchProcessor(
private val codeGenerator: CodeGenerator,
private val logger: KSPLogger
) : SymbolProcessor {
private fun KSAnnotated.isSubclassOf(cls: KClass<*>): Boolean {
if (this !is KSClassDeclaration) return false
if (qualifiedName?.asString() == cls.qualifiedName) return true
return superTypes.any { it.resolve().declaration.isSubclassOf(cls) }
}
@Suppress("UNCHECKED_CAST")
override fun process(resolver: Resolver): List<KSAnnotated> {
val executablePatches = buildMap {
resolver.getSymbolsWithAnnotation(Patch::class.qualifiedName!!).filter {
// Do not check here if Patch is super of the class, because it is expensive.
// Check it later when processing.
it.validate() && it.isSubclassOf(app.revanced.patcher.patch.Patch::class)
}.map {
it as KSClassDeclaration
}.forEach { patchDeclaration ->
patchDeclaration.annotations.find {
it.annotationType.resolve().declaration.qualifiedName!!.asString() == Patch::class.qualifiedName!!
}?.let { annotation ->
fun KSAnnotation.property(name: String) =
arguments.find { it.name!!.asString() == name }?.value!!
val name =
annotation.property("name").toString().ifEmpty { null }
val description =
annotation.property("description").toString().ifEmpty { null }
val dependencies =
(annotation.property("dependencies") as List<KSType>).ifEmpty { null }
val compatiblePackages =
(annotation.property("compatiblePackages") as List<KSAnnotation>).ifEmpty { null }
val use =
annotation.property("use") as Boolean
val requiresIntegrations =
annotation.property("requiresIntegrations") as Boolean
// Data class for KotlinPoet
data class PatchData(
val name: String?,
val description: String?,
val dependencies: List<ClassName>?,
val compatiblePackages: List<CodeBlock>?,
val use: Boolean,
val requiresIntegrations: Boolean
)
this[patchDeclaration] = PatchData(
name,
description,
dependencies?.map { dependency -> dependency.toClassName() },
compatiblePackages?.map {
val packageName = it.property("name")
val packageVersions = (it.property("versions") as List<String>)
.joinToString(", ") { version -> "\"$version\"" }
CodeBlock.of(
"%T(%S, setOf(%L))",
app.revanced.patcher.patch.Patch.CompatiblePackage::class,
packageName,
packageVersions
)
},
use,
requiresIntegrations
)
}
}
}
// If a patch depends on another, that is annotated, the dependency should be replaced with the generated patch,
// because the generated patch has all the necessary properties to invoke the super constructor,
// unlike the annotated patch.
val dependencyResolutionMap = buildMap {
executablePatches.values.filter { it.dependencies != null }.flatMap {
it.dependencies!!
}.distinct().forEach { dependency ->
executablePatches.keys.find { it.qualifiedName?.asString() == dependency.toString() }
?.let { patch ->
this[dependency] = ClassName(
patch.packageName.asString(),
patch.simpleName.asString() + "Generated"
)
}
}
}
// kotlin poet generate a class for each patch
executablePatches.forEach { (patchDeclaration, patchAnnotation) ->
val isBytecodePatch = patchDeclaration.isSubclassOf(BytecodePatch::class)
val superClass = if (isBytecodePatch) {
BytecodePatch::class
} else {
ResourcePatch::class
}
val contextClass = if (isBytecodePatch) {
BytecodeContext::class
} else {
ResourceContext::class
}
val generatedPatchClassName = ClassName(
patchDeclaration.packageName.asString(),
patchDeclaration.simpleName.asString() + "Generated"
)
FileSpec.builder(generatedPatchClassName)
.addType(
TypeSpec.objectBuilder(generatedPatchClassName)
.superclass(superClass).apply {
patchAnnotation.name?.let { name ->
addSuperclassConstructorParameter("name = %S", name)
}
patchAnnotation.description?.let { description ->
addSuperclassConstructorParameter("description = %S", description)
}
patchAnnotation.compatiblePackages?.let { compatiblePackages ->
addSuperclassConstructorParameter(
"compatiblePackages = setOf(%L)",
compatiblePackages.joinToString(", ")
)
}
patchAnnotation.dependencies?.let { dependencies ->
addSuperclassConstructorParameter(
"dependencies = setOf(%L)",
buildList {
addAll(dependencies)
// Also add the source class of the generated class so that it is also executed
add(patchDeclaration.toClassName())
}.joinToString(", ") { dependency ->
"${(dependencyResolutionMap[dependency] ?: dependency)}::class"
}
)
}
addSuperclassConstructorParameter(
"use = %L", patchAnnotation.use
)
addSuperclassConstructorParameter(
"requiresIntegrations = %L",
patchAnnotation.requiresIntegrations
)
}
.addFunction(
FunSpec.builder("execute")
.addModifiers(KModifier.OVERRIDE)
.addParameter("context", contextClass)
.build()
)
.addProperty(
PropertySpec.builder("options", PatchOptions::class, KModifier.OVERRIDE)
.initializer("%T.options", patchDeclaration.toClassName())
.build()
)
.build()
).build().writeTo(
codeGenerator,
Dependencies(false, patchDeclaration.containingFile!!)
)
}
return emptyList()
}
}

View file

@ -0,0 +1,9 @@
package app.revanced.patcher.patch.annotations.processor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider
class PatchProcessorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment) =
PatchProcessor(environment.codeGenerator, environment.logger)
}

View file

@ -0,0 +1 @@
app.revanced.patcher.patch.annotations.processor.PatchProcessorProvider

View file

@ -0,0 +1,131 @@
package app.revanced.patcher.patch.annotations.processor
import app.revanced.patcher.patch.Patch
import com.tschuchort.compiletesting.KotlinCompilation
import com.tschuchort.compiletesting.SourceFile
import com.tschuchort.compiletesting.kspWithCompilation
import com.tschuchort.compiletesting.symbolProcessorProviders
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
class TestPatchAnnotationProcessor {
// region Processing
@Test
fun testProcessing() = assertEquals(
"Processable patch", compile(
getSourceFile(
"processing", "ProcessablePatch"
)
).loadPatch("$SAMPLE_PACKAGE.processing.ProcessablePatchGenerated").name
)
// endregion
// region Dependencies
@Test
fun testDependencies() {
compile(
getSourceFile(
"dependencies", "DependentPatch"
), getSourceFile(
"dependencies", "DependencyPatch"
)
).let { result ->
result.loadPatch("$SAMPLE_PACKAGE.dependencies.DependentPatchGenerated").let {
// Dependency as well as the source class of the generated class.
assertEquals(
2,
it.dependencies!!.size
)
// The last dependency is always the source class of the generated class to respect
// order of dependencies.
assertEquals(
result.loadPatch("$SAMPLE_PACKAGE.dependencies.DependentPatch")::class,
it.dependencies!!.last()
)
}
}
}
// endregion
// region Options
@Test
fun testOptions() {
val patch = compile(
getSourceFile(
"options", "OptionsPatch"
)
).loadPatch("$SAMPLE_PACKAGE.options.OptionsPatchGenerated")
assertNotNull(patch.options)
assertEquals(patch.options["print"].title, "Print message")
}
// endregion
// region Limitations
@Test
fun failingManualDependency() = assertNull(
compile(
getSourceFile(
"limitations/manualdependency", "DependentPatch"
), getSourceFile(
"limitations/manualdependency", "DependencyPatch"
)
).loadPatch("$SAMPLE_PACKAGE.limitations.manualdependency.DependentPatchGenerated").dependencies
)
// endregion
private companion object Utils {
const val SAMPLE_PACKAGE = "app.revanced.patcher.patch.annotations.processor.samples"
/**
* Get a source file from the given sample and class name.
*
* @param sample The sample to get the source file from.
* @param className The name of the class to get the source file from.
* @return The source file.
*/
fun getSourceFile(sample: String, className: String) = SourceFile.kotlin(
"$className.kt", TestPatchAnnotationProcessor::class.java.classLoader.getResourceAsStream(
"app/revanced/patcher/patch/annotations/processor/samples/$sample/$className.kt"
)?.readAllBytes()?.toString(Charsets.UTF_8) ?: error("Could not find resource $className")
)
/**
* Compile the given source files and return the result.
*
* @param sourceFiles The source files to compile.
* @return The result of the compilation.
*/
fun compile(vararg sourceFiles: SourceFile) = KotlinCompilation().apply {
sources = sourceFiles.asList()
symbolProcessorProviders = listOf(PatchProcessorProvider())
// Required until https://github.com/tschuchortdev/kotlin-compile-testing/issues/312 closed.
kspWithCompilation = true
inheritClassPath = true
messageOutputStream = System.out
}.compile().also { result ->
assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode)
}
// region Class loading
fun KotlinCompilation.Result.loadPatch(name: String) = classLoader.loadClass(name).loadPatch()
fun Class<*>.loadPatch() = this.getField("INSTANCE").get(null) as Patch<*>
// endregion
}
}

View file

@ -0,0 +1,10 @@
package app.revanced.patcher.patch.annotations.processor.samples.dependencies
import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.ResourcePatch
import app.revanced.patcher.patch.annotations.Patch
@Patch(name = "Dependency patch")
object DependencyPatch : ResourcePatch() {
override fun execute(context: ResourceContext) {}
}

View file

@ -0,0 +1,12 @@
package app.revanced.patcher.patch.annotations.processor.samples.dependencies
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.patch.BytecodePatch
import app.revanced.patcher.patch.annotations.Patch
@Patch(
name = "Dependent patch",
dependencies = [DependencyPatch::class],
)
object DependentPatch : BytecodePatch() {
override fun execute(context: BytecodeContext) {}
}

View file

@ -0,0 +1,10 @@
package app.revanced.patcher.patch.annotations.processor.samples.limitations.manualdependency
import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.ResourcePatch
import app.revanced.patcher.patch.annotations.Patch
@Patch(name = "Dependency patch")
object DependencyPatch : ResourcePatch() {
override fun execute(context: ResourceContext) { }
}

View file

@ -0,0 +1,17 @@
package app.revanced.patcher.patch.annotations.processor.samples.limitations.manualdependency
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.patch.BytecodePatch
import app.revanced.patcher.patch.annotations.Patch
@Patch(name = "Dependent patch")
object DependentPatch : BytecodePatch(
// Dependency will not be executed correctly if it is manually specified.
// The reason for this is that the dependency patch is annotated too,
// so the processor will generate a new patch class for it embedding the annotated information.
// Because the dependency is manually specified,
// the processor will not be able to change this dependency to the generated class,
// which means that the dependency will lose the annotated information.
dependencies = setOf(DependencyPatch::class)
) {
override fun execute(context: BytecodeContext) {}
}

View file

@ -0,0 +1,21 @@
package app.revanced.patcher.patch.annotations.processor.samples.options
import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.PatchOption
import app.revanced.patcher.patch.ResourcePatch
import app.revanced.patcher.patch.annotations.Patch
@Patch(name = "Options patch")
object OptionsPatch : ResourcePatch() {
override fun execute(context: ResourceContext) {}
@Suppress("unused")
private val printOption by option(
PatchOption.StringOption(
"print",
null,
"Print message",
"The message to print."
)
)
}

View file

@ -0,0 +1,10 @@
package app.revanced.patcher.patch.annotations.processor.samples.processing
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.patch.BytecodePatch
import app.revanced.patcher.patch.annotations.Patch
@Patch("Processable patch")
object ProcessablePatch : BytecodePatch() {
override fun execute(context: BytecodeContext) {}
}

View file

@ -327,7 +327,9 @@ public final class app/revanced/patcher/logging/impl/NopLogger : app/revanced/pa
} }
public abstract class app/revanced/patcher/patch/BytecodePatch : app/revanced/patcher/patch/Patch { public abstract class app/revanced/patcher/patch/BytecodePatch : app/revanced/patcher/patch/Patch {
public fun <init> (Lapp/revanced/patcher/patch/Patch$Manifest;[Lapp/revanced/patcher/fingerprint/method/impl/MethodFingerprint;)V public fun <init> ()V
public fun <init> (Ljava/util/Set;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZ)V
public synthetic fun <init> (Ljava/util/Set;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
} }
public final class app/revanced/patcher/patch/IllegalValueException : java/lang/Exception { public final class app/revanced/patcher/patch/IllegalValueException : java/lang/Exception {
@ -348,38 +350,29 @@ public final class app/revanced/patcher/patch/NoSuchOptionException : java/lang/
public abstract class app/revanced/patcher/patch/OptionsContainer { public abstract class app/revanced/patcher/patch/OptionsContainer {
public fun <init> ()V public fun <init> ()V
public final fun getOptions ()Lapp/revanced/patcher/patch/PatchOptions; public fun getOptions ()Lapp/revanced/patcher/patch/PatchOptions;
protected final fun option (Lapp/revanced/patcher/patch/PatchOption;)Lapp/revanced/patcher/patch/PatchOption; protected final fun option (Lapp/revanced/patcher/patch/PatchOption;)Lapp/revanced/patcher/patch/PatchOption;
} }
public abstract class app/revanced/patcher/patch/Patch { public abstract class app/revanced/patcher/patch/Patch : app/revanced/patcher/patch/OptionsContainer {
public synthetic fun <init> (Lapp/revanced/patcher/patch/Patch$Manifest;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZLkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun equals (Ljava/lang/Object;)Z public fun equals (Ljava/lang/Object;)Z
public abstract fun execute (Lapp/revanced/patcher/data/Context;)V public abstract fun execute (Lapp/revanced/patcher/data/Context;)V
public final fun getManifest ()Lapp/revanced/patcher/patch/Patch$Manifest;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class app/revanced/patcher/patch/Patch$Manifest {
public fun <init> (Ljava/lang/String;Ljava/lang/String;ZLjava/util/Set;Ljava/util/Set;ZLapp/revanced/patcher/patch/PatchOptions;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZLjava/util/Set;Ljava/util/Set;ZLapp/revanced/patcher/patch/PatchOptions;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun equals (Ljava/lang/Object;)Z
public final fun getCompatiblePackages ()Ljava/util/Set; public final fun getCompatiblePackages ()Ljava/util/Set;
public final fun getDependencies ()Ljava/util/Set; public final fun getDependencies ()Ljava/util/Set;
public final fun getDescription ()Ljava/lang/String; public final fun getDescription ()Ljava/lang/String;
public final fun getName ()Ljava/lang/String; public final fun getName ()Ljava/lang/String;
public final fun getOptions ()Lapp/revanced/patcher/patch/PatchOptions;
public final fun getRequiresIntegrations ()Z public final fun getRequiresIntegrations ()Z
public final fun getUse ()Z public final fun getUse ()Z
public fun hashCode ()I public fun hashCode ()I
public fun toString ()Ljava/lang/String;
} }
public final class app/revanced/patcher/patch/Patch$Manifest$CompatiblePackage { public final class app/revanced/patcher/patch/Patch$CompatiblePackage {
public fun <init> (Ljava/lang/String;Ljava/util/Set;)V public fun <init> (Ljava/lang/String;Ljava/util/Set;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun <init> (Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getName ()Ljava/lang/String; public final fun getName ()Ljava/lang/String;
public final fun getVersions ()Ljava/util/Set;
} }
public final class app/revanced/patcher/patch/PatchException : java/lang/Exception { public final class app/revanced/patcher/patch/PatchException : java/lang/Exception {
@ -446,10 +439,9 @@ public final class app/revanced/patcher/patch/RequirementNotMetException : java/
} }
public abstract class app/revanced/patcher/patch/ResourcePatch : app/revanced/patcher/patch/Patch { public abstract class app/revanced/patcher/patch/ResourcePatch : app/revanced/patcher/patch/Patch {
public fun <init> (Lapp/revanced/patcher/patch/Patch$Manifest;)V public fun <init> ()V
} public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZ)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public abstract interface annotation class app/revanced/patcher/patch/annotations/Patch : java/lang/annotation/Annotation {
} }
public final class app/revanced/patcher/util/DomFileEditor : java/io/Closeable { public final class app/revanced/patcher/util/DomFileEditor : java/io/Closeable {

View file

@ -15,6 +15,7 @@ dependencies {
compileOnly(libs.android) compileOnly(libs.android)
testImplementation(project(":revanced-patch-annotations-processor"))
testImplementation(libs.kotlin.test) testImplementation(libs.kotlin.test)
} }

View file

@ -2,7 +2,6 @@
package app.revanced.patcher package app.revanced.patcher
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
import dalvik.system.DexClassLoader import dalvik.system.DexClassLoader
import lanchon.multidexlib2.BasicDexFileNamer import lanchon.multidexlib2.BasicDexFileNamer
@ -25,7 +24,7 @@ typealias PatchClass = KClass<out Patch<*>>
/** /**
* A loader of [Patch]es from patch bundles. * A loader of [Patch]es from patch bundles.
* This will load all [Patch]es from the given patch bundles. * This will load all [Patch]es from the given patch bundles that have a name.
* *
* @param getBinaryClassNames A function that returns the binary names of all classes in a patch bundle. * @param getBinaryClassNames A function that returns the binary names of all classes in a patch bundle.
* @param classLoader The [ClassLoader] to use for loading the classes. * @param classLoader The [ClassLoader] to use for loading the classes.
@ -38,22 +37,22 @@ sealed class PatchBundleLoader private constructor(
private val logger = Logger.getLogger(PatchBundleLoader::class.java.name) private val logger = Logger.getLogger(PatchBundleLoader::class.java.name)
init { init {
patchBundles.flatMap(getBinaryClassNames).map { patchBundles.flatMap(getBinaryClassNames).asSequence().map {
classLoader.loadClass(it) classLoader.loadClass(it)
}.filter { }.filter {
if (it.isAnnotation) return@filter false it.isInstance(Patch::class.java)
it.findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class) != null
}.mapNotNull { patchClass -> }.mapNotNull { patchClass ->
patchClass.getInstance(logger) patchClass.getInstance(logger)
}.associateBy { it.manifest.name } }.filter {
let { patches -> it.name != null
}.associateBy {
it.name!!
}.let { patches ->
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
(this as MutableMap<String, Patch<*>>).putAll(patches) (this as MutableMap<String, Patch<*>>).putAll(patches)
} }
} }
internal companion object Utils { internal companion object Utils {
/** /**
* Instantiates a [Patch]. If the class is a singleton, the INSTANCE field will be used. * Instantiates a [Patch]. If the class is a singleton, the INSTANCE field will be used.

View file

@ -2,7 +2,6 @@ package app.revanced.patcher
import app.revanced.patcher.PatchBundleLoader.Utils.getInstance import app.revanced.patcher.PatchBundleLoader.Utils.getInstance
import app.revanced.patcher.data.ResourceContext import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolveUsingLookupMap import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolveUsingLookupMap
import app.revanced.patcher.patch.* import app.revanced.patcher.patch.*
@ -46,13 +45,19 @@ class Patcher(
context.resourceContext.decodeResources(ResourceContext.ResourceDecodingMode.MANIFEST_ONLY) context.resourceContext.decodeResources(ResourceContext.ResourceDecodingMode.MANIFEST_ONLY)
} }
// TODO: Fix circular dependency detection.
// /**
// * Add [Patch]es to ReVanced [Patcher].
// * It is not guaranteed that all supplied [Patch]es will be accepted, if an exception is thrown.
// *
// * @param patches The [Patch]es to add.
// * @throws PatcherException.CircularDependencyException If a circular dependency is detected.
// */
/** /**
* Add [Patch]es to ReVanced [Patcher]. * Add [Patch]es to ReVanced [Patcher].
* It is not guaranteed that all supplied [Patch]es will be accepted, if an exception is thrown.
* *
* @param patches The [Patch]es to add. * @param patches The [Patch]es to add.
* @throws PatcherException.CircularDependencyException If a circular dependency is detected. */
*/
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
override fun acceptPatches(patches: List<Patch<*>>) { override fun acceptPatches(patches: List<Patch<*>>) {
/** /**
@ -65,14 +70,14 @@ class Patcher(
val dependency = this.java.getInstance(logger)!! val dependency = this.java.getInstance(logger)!!
context.allPatches[this] = dependency context.allPatches[this] = dependency
dependency.manifest.dependencies?.forEach { it.putDependenciesRecursively() } dependency.dependencies?.forEach { it.putDependenciesRecursively() }
} }
// Add all patches and their dependencies to the context. // Add all patches and their dependencies to the context.
for (patch in patches) context.executablePatches.putIfAbsent(patch::class, patch) ?: { for (patch in patches) context.executablePatches.putIfAbsent(patch::class, patch) ?: {
context.allPatches[patch::class] = patch context.allPatches[patch::class] = patch
patch.manifest.dependencies?.forEach { it.putDependenciesRecursively() } patch.dependencies?.forEach { it.putDependenciesRecursively() }
} }
/* TODO: Fix circular dependency detection. /* TODO: Fix circular dependency detection.
@ -99,7 +104,7 @@ class Patcher(
* @param predicate The predicate to match. * @param predicate The predicate to match.
*/ */
fun Patch<*>.anyRecursively(predicate: (Patch<*>) -> Boolean): Boolean = fun Patch<*>.anyRecursively(predicate: (Patch<*>) -> Boolean): Boolean =
predicate(this) || manifest.dependencies?.any { dependency -> predicate(this) || dependencies?.any { dependency ->
context.allPatches[dependency]!!.anyRecursively(predicate) context.allPatches[dependency]!!.anyRecursively(predicate)
} ?: false } ?: false
@ -113,7 +118,7 @@ class Patcher(
// Determine, if merging integrations is required. // Determine, if merging integrations is required.
for (patch in patches) for (patch in patches)
if (!patch.anyRecursively { it.manifest.requiresIntegrations }) { if (!patch.anyRecursively { it.requiresIntegrations }) {
context.bytecodeContext.integrations.merge = true context.bytecodeContext.integrations.merge = true
break break
} }
@ -148,7 +153,7 @@ class Patcher(
patch: Patch<*>, patch: Patch<*>,
executedPatches: LinkedHashMap<Patch<*>, PatchResult> executedPatches: LinkedHashMap<Patch<*>, PatchResult>
): PatchResult { ): PatchResult {
val patchName = patch.manifest.name val patchName = patch.name
executedPatches[patch]?.let { patchResult -> executedPatches[patch]?.let { patchResult ->
patchResult.exception ?: return patchResult patchResult.exception ?: return patchResult
@ -159,7 +164,7 @@ class Patcher(
} }
// Recursively execute all dependency patches. // Recursively execute all dependency patches.
patch.manifest.dependencies?.forEach { dependencyName -> patch.dependencies?.forEach { dependencyName ->
val dependency = context.executablePatches[dependencyName]!! val dependency = context.executablePatches[dependencyName]!!
val result = executePatch(dependency, executedPatches) val result = executePatch(dependency, executedPatches)
@ -171,17 +176,17 @@ class Patcher(
} }
} }
// TODO: Implement this in a more polymorphic way.
val patchContext = if (patch is BytecodePatch) {
patch.fingerprints.asList().resolveUsingLookupMap(context.bytecodeContext)
context.bytecodeContext
} else {
context.resourceContext
}
return try { return try {
patch.execute(patchContext) // TODO: Implement this in a more polymorphic way.
when (patch) {
is BytecodePatch -> {
patch.fingerprints.toList().resolveUsingLookupMap(context.bytecodeContext)
patch.execute(context.bytecodeContext)
}
is ResourcePatch -> {
patch.execute(context.resourceContext)
}
}
PatchResult(patch) PatchResult(patch)
} catch (exception: PatchException) { } catch (exception: PatchException) {
@ -203,11 +208,11 @@ class Patcher(
val executedPatches = LinkedHashMap<Patch<*>, PatchResult>() // Key is name. val executedPatches = LinkedHashMap<Patch<*>, PatchResult>() // Key is name.
context.executablePatches.map { it.value }.sortedBy { it.manifest.name }.forEach { patch -> context.executablePatches.map { it.value }.sortedBy { it.name }.forEach { patch ->
val patchResult = executePatch(patch, executedPatches) val patchResult = executePatch(patch, executedPatches)
// If the patch failed, emit the result, even if it is closeable. // If the patch failed, emit the result, even if it is closeable.
// Results of successfully executed patches that are closeable will be emitted later. // Results of executed patches that are closeable will be emitted later.
patchResult.exception?.let { patchResult.exception?.let {
// Propagate exception to caller instead of wrapping it in a new exception. // Propagate exception to caller instead of wrapping it in a new exception.
emit(patchResult) emit(patchResult)
@ -240,7 +245,7 @@ class Patcher(
PatchResult( PatchResult(
patch, patch,
PatchException( PatchException(
"'${patch.manifest.name}' raised an exception while being closed: $it", "'${patch.name}' raised an exception while being closed: $it",
result.exception result.exception
) )
) )
@ -248,10 +253,7 @@ class Patcher(
if (returnOnError) return@flow if (returnOnError) return@flow
} ?: run { } ?: run {
patch::class patch.name ?: return@run
.java
.findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class)
?: return@run
emit(result) emit(result)
} }

View file

@ -9,8 +9,13 @@ abstract class OptionsContainer {
* @see PatchOptions * @see PatchOptions
*/ */
@Suppress("MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate")
val options = PatchOptions() open val options = PatchOptions()
/**
* Registers a [PatchOption].
* @param opt The [PatchOption] to register.
* @return The registered [PatchOption].
*/
protected fun <T> option(opt: PatchOption<T>): PatchOption<T> { protected fun <T> option(opt: PatchOption<T>): PatchOption<T> {
options.register(opt) options.register(opt)
return opt return opt

View file

@ -1,3 +1,5 @@
@file:Suppress("MemberVisibilityCanBePrivate", "UNUSED_PARAMETER")
package app.revanced.patcher.patch package app.revanced.patcher.patch
import app.revanced.patcher.PatchClass import app.revanced.patcher.PatchClass
@ -14,10 +16,23 @@ import java.io.Closeable
* If an implementation of [Patch] also implements [Closeable] * If an implementation of [Patch] also implements [Closeable]
* it will be closed in reverse execution order of patches executed by ReVanced [Patcher]. * it will be closed in reverse execution order of patches executed by ReVanced [Patcher].
* *
* @param manifest The manifest of the [Patch]. * @param name The name of the patch.
* @param description The description of the patch.
* @param compatiblePackages The packages the patch is compatible with.
* @param dependencies The names of patches this patch depends on.
* @param use Weather or not the patch should be used.
* @param requiresIntegrations Weather or not the patch requires integrations.
* @param T The [Context] type this patch will work on. * @param T The [Context] type this patch will work on.
*/ */
sealed class Patch<out T : Context<*>>(val manifest: Manifest) { sealed class Patch<out T : Context<*>>(
val name: String? = null,
val description: String? = null,
val compatiblePackages: Set<CompatiblePackage>? = null,
val dependencies: Set<PatchClass>? = null,
val use: Boolean = true,
// TODO: Remove this property, once integrations are coupled with patches.
val requiresIntegrations: Boolean = false,
) : OptionsContainer() {
/** /**
* The execution function of the patch. * The execution function of the patch.
* *
@ -26,72 +41,69 @@ sealed class Patch<out T : Context<*>>(val manifest: Manifest) {
*/ */
abstract fun execute(context: @UnsafeVariance T) abstract fun execute(context: @UnsafeVariance T)
override fun hashCode() = manifest.hashCode() override fun hashCode() = name.hashCode()
override fun equals(other: Any?) = other is Patch<*> && manifest == other.manifest override fun toString() = name ?: this::class.simpleName ?: "Unnamed patch"
override fun toString() = manifest.name override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Patch<*>
return name == other.name
}
/** /**
* The manifest of a [Patch]. * A package a [Patch] is compatible with.
* *
* @param name The name of the patch. * @param name The name of the package.
* @param description The description of the patch. * @param versions The versions of the package.
* @param use Weather or not the patch should be used.
* @param dependencies The names of patches this patch depends on.
* @param compatiblePackages The packages the patch is compatible with.
* @param requiresIntegrations Weather or not the patch requires integrations.
* @param options The options of the patch.
*/ */
class Manifest( class CompatiblePackage(
val name: String, val name: String,
val description: String, versions: Set<String>? = null,
val use: Boolean = true, )
val dependencies: Set<PatchClass>? = null,
val compatiblePackages: Set<CompatiblePackage>? = null,
// TODO: Remove this property, once integrations are coupled with patches.
val requiresIntegrations: Boolean = false,
val options: PatchOptions? = null,
) {
override fun hashCode() = name.hashCode()
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Manifest
return name == other.name
}
/**
* A package a [Patch] is compatible with.
*
* @param name The name of the package.
* @param versions The versions of the package.
*/
class CompatiblePackage(
val name: String,
val versions: Set<String>? = null,
)
}
} }
/** /**
* A ReVanced [Patch] that works on [ResourceContext]. * A ReVanced [Patch] that works on [ResourceContext].
* *
* @param metadata The manifest of the [ResourcePatch]. * @param name The name of the patch.
* @param description The description of the patch.
* @param compatiblePackages The packages the patch is compatible with.
* @param dependencies The names of patches this patch depends on.
* @param use Weather or not the patch should be used.
* @param requiresIntegrations Weather or not the patch requires integrations.
*/ */
abstract class ResourcePatch( abstract class ResourcePatch(
metadata: Manifest, name: String? = null,
) : Patch<ResourceContext>(metadata) description: String? = null,
compatiblePackages: Set<CompatiblePackage>? = null,
dependencies: Set<PatchClass>? = null,
use: Boolean = true,
// TODO: Remove this property, once integrations are coupled with patches.
requiresIntegrations: Boolean = false,
) : Patch<ResourceContext>(name, description, compatiblePackages, dependencies, use, requiresIntegrations)
/** /**
* A ReVanced [Patch] that works on [BytecodeContext]. * A ReVanced [Patch] that works on [BytecodeContext].
* *
* @param manifest The manifest of the [BytecodePatch].
* @param fingerprints A list of [MethodFingerprint]s which will be resolved before the patch is executed. * @param fingerprints A list of [MethodFingerprint]s which will be resolved before the patch is executed.
* @param name The name of the patch.
* @param description The description of the patch.
* @param compatiblePackages The packages the patch is compatible with.
* @param dependencies The names of patches this patch depends on.
* @param use Weather or not the patch should be used.
* @param requiresIntegrations Weather or not the patch requires integrations.
*/ */
abstract class BytecodePatch( abstract class BytecodePatch(
manifest: Manifest, internal val fingerprints: Set<MethodFingerprint> = emptySet(),
internal vararg val fingerprints: MethodFingerprint, name: String? = null,
) : Patch<BytecodeContext>(manifest) description: String? = null,
compatiblePackages: Set<CompatiblePackage>? = null,
dependencies: Set<PatchClass>? = null,
use: Boolean = true,
// TODO: Remove this property, once integrations are coupled with patches.
requiresIntegrations: Boolean = false,
) : Patch<BytecodeContext>(name, description, compatiblePackages, dependencies, use, requiresIntegrations)

View file

@ -2,8 +2,6 @@
package app.revanced.patcher.patch package app.revanced.patcher.patch
import java.nio.file.Path
import kotlin.io.path.pathString
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
class NoSuchOptionException(val option: String) : Exception("No such option: $option") class NoSuchOptionException(val option: String) : Exception("No such option: $option")

View file

@ -1,7 +0,0 @@
package app.revanced.patcher.patch.annotations
/**
* Annotation to mark a class as a patch.
*/
@Target(AnnotationTarget.CLASS)
annotation class Patch

View file

@ -19,7 +19,7 @@ import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21s
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
private object InstructionExtensionsTest { private object InstructionExtensionsTest {

View file

@ -1,8 +1,8 @@
package app.revanced.patcher.patch package app.revanced.patcher.patch
import app.revanced.patcher.usage.bytecode.ExampleBytecodePatch import app.revanced.patcher.usage.ExampleBytecodePatch
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNotEquals import kotlin.test.assertNotEquals
import kotlin.test.assertNotNull import kotlin.test.assertNotNull

View file

@ -1,14 +1,13 @@
package app.revanced.patcher.usage.bytecode package app.revanced.patcher.usage
import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.extensions.or import app.revanced.patcher.extensions.or
import app.revanced.patcher.patch.BytecodePatch import app.revanced.patcher.patch.BytecodePatch
import app.revanced.patcher.patch.OptionsContainer
import app.revanced.patcher.patch.PatchOption import app.revanced.patcher.patch.PatchOption
import app.revanced.patcher.patch.annotations.CompatiblePackage
import app.revanced.patcher.patch.annotations.Patch import app.revanced.patcher.patch.annotations.Patch
import app.revanced.patcher.usage.resource.patch.ExampleResourcePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
@ -26,19 +25,19 @@ import com.android.tools.smali.dexlib2.immutable.reference.ImmutableStringRefere
import com.android.tools.smali.dexlib2.immutable.value.ImmutableFieldEncodedValue import com.android.tools.smali.dexlib2.immutable.value.ImmutableFieldEncodedValue
import com.android.tools.smali.dexlib2.util.Preconditions import com.android.tools.smali.dexlib2.util.Preconditions
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import kotlin.test.assertNotNull
@Patch @Suppress("unused")
class ExampleBytecodePatch : BytecodePatch( @Patch(
Manifest( name = "Example bytecode patch",
"Example patch", description = "Example demonstration of a bytecode patch.",
"Example demonstration of a bytecode patch.", dependencies = [ExampleResourcePatch::class],
dependencies = setOf(ExampleResourcePatch::class), compatiblePackages = [CompatiblePackage("com.example.examplePackage", arrayOf("0.0.1", "0.0.2"))]
compatiblePackages = setOf( )
Manifest.CompatiblePackage("com.example.examplePackage", setOf("0.0.1", "0.0.2")) object ExampleBytecodePatch : BytecodePatch(
) setOf(ExampleFingerprint)
),
ExampleFingerprint
) { ) {
// This function will be executed by the patcher. // This function will be executed by the patcher.
// You can treat it as a constructor // You can treat it as a constructor
override fun execute(context: BytecodeContext) { override fun execute(context: BytecodeContext) {
@ -46,7 +45,7 @@ class ExampleBytecodePatch : BytecodePatch(
val result = ExampleFingerprint.result!! val result = ExampleFingerprint.result!!
// Patch options // Patch options
println(key1) assertNotNull(key1)
key2 = false key2 = false
// Get the implementation for the resolved method // Get the implementation for the resolved method
@ -161,32 +160,34 @@ class ExampleBytecodePatch : BytecodePatch(
) )
} }
@Suppress("unused") private var key1 by option(
companion object : OptionsContainer() { PatchOption.StringOption(
private var key1 by option( "key1", "default", "title", "description", true
PatchOption.StringOption(
"key1", "default", "title", "description", true
)
) )
private var key2 by option( )
PatchOption.BooleanOption(
"key2", true, "title", "description" // required defaults to false private var key2 by option(
) PatchOption.BooleanOption(
"key2", true, "title", "description" // required defaults to false
) )
private var key3 by option( )
PatchOption.StringListOption(
"key3", "TEST", listOf("TEST", "TEST1", "TEST2"), "title", "description" private var key3 by option(
) PatchOption.StringListOption(
"key3", "TEST", listOf("TEST", "TEST1", "TEST2"), "title", "description"
) )
private var key4 by option( )
PatchOption.IntListOption(
"key4", 1, listOf(1, 2, 3), "title", "description" private var key4 by option(
) PatchOption.IntListOption(
"key4", 1, listOf(1, 2, 3), "title", "description"
) )
private var key5 by option( )
PatchOption.StringOption(
"key5", null, "title", "description", true private var key5 by option(
) PatchOption.StringOption(
"key5", null, "title", "description", true
) )
} )
} }

View file

@ -1,4 +1,4 @@
package app.revanced.patcher.usage.bytecode package app.revanced.patcher.usage
import app.revanced.patcher.extensions.or import app.revanced.patcher.extensions.or
import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint

View file

@ -1,10 +1,11 @@
package app.revanced.patcher.usage.resource.patch package app.revanced.patcher.usage
import app.revanced.patcher.data.ResourceContext import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.ResourcePatch import app.revanced.patcher.patch.ResourcePatch
import org.w3c.dom.Element import org.w3c.dom.Element
class ExampleResourcePatch : ResourcePatch(Manifest("Example name", "Example description")) {
class ExampleResourcePatch : ResourcePatch() {
override fun execute(context: ResourceContext) { override fun execute(context: ResourceContext) {
context.xmlEditor["AndroidManifest.xml"].use { editor -> context.xmlEditor["AndroidManifest.xml"].use { editor ->
val element = editor // regular DomFileEditor val element = editor // regular DomFileEditor

View file

@ -19,4 +19,4 @@ dependencyResolutionManagement {
} }
} }
include("revanced-patcher") include("revanced-patch-annotations-processor", "revanced-patcher")