Created
June 30, 2025 13:21
-
-
Save forsyth47/04e49e91563147c2a6c9512045febd2f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
To make any app (including apps that donโt request device admin privileges by default) into a Device Admin App, you can follow these detailed steps. | |
1. Decompile the App | |
Decompile the APK using APKTool: | |
apktool d app-name.apk | |
2. Modify the App | |
Add Device Admin Receiver: | |
In the decompiled APK folder,in AndroidManifest.xml file the following code is to be added inside the <application> tag: | |
<receiver android:name=".MyDeviceAdminReceiver" | |
android:label="@string/app_name" | |
android:permission="android.permission.BIND_DEVICE_ADMIN"> | |
<meta-data android:name="android.app.device_admin" | |
android:resource="@xml/device_admin" /> | |
<intent-filter> | |
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" /> | |
</intent-filter> | |
</receiver> | |
Create a Device Admin XML: | |
Create a new file device_admin.xml in res/xml folder (create, if doesnโt exist) and Add the following content: | |
<device-admin xmlns:android="http://schemas.android.com/apk/res/android"> | |
<uses-policies> | |
<limit-password /> | |
<watch-login /> | |
</uses-policies> | |
</device-admin> | |
Adding the DeviceAdminReceiver Class: | |
To create a class that extends DeviceAdminReceiver. Since using smali, to locate the appropriate package directory, create the smali representation of the DeviceAdminReceiver class. | |
Directory for DeviceAdminReceiver | |
- Your package is com.kyant.vanilla. | |
- Therefore, the smali file for the DeviceAdminReceiver should go into:./app-folder/smali/com/kyant/vanilla | |
With the File Naming: MyDeviceAdminReceiver.smali (to match the class defined in the manifest. | |
Basic example Content of MyDeviceAdminReceiver.smali: | |
.class public Lcom/kyant/vanilla/MyDeviceAdminReceiver; | |
.super Landroid/app/admin/DeviceAdminReceiver; | |
.method public constructor <init>()V | |
.locals 0 | |
.prologue | |
invoke-direct {p0}, Landroid/app/admin/DeviceAdminReceiver;-><init>()V | |
return-void | |
.end method | |
3. Recompile the APK using APKTool: | |
apktool b app-folder | |
This will generate the modified APK in the app-folder/dist folder. | |
4. Sign the APK: | |
Create Keystore (Optional): keytool -genkey -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-key-alias | |
Use the zipalign tool to optimizes the APK for faster loading: | |
zipalign -v -p 4 ./app-folder/dist/app-folder.apk ./app-folder/dist/aligned-app-folder.apk | |
zipalign -c -v 4 ./app-folder/dist/aligned-app-folder.apk #Verify if align success | |
If alignment is sucessful, Sign the apk: | |
apksigner sign --ks my-release-key.jks --out ./app-folder/dist/signed-app-folder.apk ./app-folder/dist/aligned-app-folder.apk | |
apksigner verify your-app-signed.apk #Verify app signature. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
I want to create a simple universal patch that will make any app request for admin prevlilages, and that app can be made into an admin app. I thought it would be nice to add this as a my custom (universal) patch to my own patches repository (or the custom_patches.rvp), with just this single patch. I have no/less prior experience with creating patches for revanced. I started by reading the ReVanced Patcher Guide but understood very little from it; looked at template; and then some examples. | |
So ahow I should do it? | |
I did it manually by following the below guide: | |
```guide.txt | |
To make any app (including apps that donโt request device admin privileges by default) into a Device Admin App, you can follow these detailed steps. | |
1. Decompile the App | |
Decompile the APK using APKTool: | |
apktool d app-name.apk | |
2. Modify the App | |
Add Device Admin Receiver: | |
In the decompiled APK folder,in AndroidManifest.xml file the following code is to be added inside the <application> tag: | |
<receiver android:name=".MyDeviceAdminReceiver" | |
android:label="@string/app_name" | |
android:permission="android.permission.BIND_DEVICE_ADMIN"> | |
<meta-data android:name="android.app.device_admin" | |
android:resource="@xml/device_admin" /> | |
<intent-filter> | |
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" /> | |
</intent-filter> | |
</receiver> | |
Create a Device Admin XML: | |
Create a new file device_admin.xml in res/xml folder (create, if doesnโt exist) and Add the following content: | |
<device-admin xmlns:android="http://schemas.android.com/apk/res/android"> | |
<uses-policies> | |
<limit-password /> | |
<watch-login /> | |
</uses-policies> | |
</device-admin> | |
Adding the DeviceAdminReceiver Class: | |
To create a class that extends DeviceAdminReceiver. Since using smali, to locate the appropriate package directory, create the smali representation of the DeviceAdminReceiver class. | |
Directory for DeviceAdminReceiver | |
- Your package is com.kyant.vanilla. | |
- Therefore, the smali file for the DeviceAdminReceiver should go into:./app-folder/smali/com/kyant/vanilla | |
With the File Naming: MyDeviceAdminReceiver.smali (to match the class defined in the manifest. | |
Basic example Content of MyDeviceAdminReceiver.smali: | |
.class public Lcom/kyant/vanilla/MyDeviceAdminReceiver; | |
.super Landroid/app/admin/DeviceAdminReceiver; | |
.method public constructor <init>()V | |
.locals 0 | |
.prologue | |
invoke-direct {p0}, Landroid/app/admin/DeviceAdminReceiver;-><init>()V | |
return-void | |
.end method | |
3. Recompile the APK using APKTool: | |
apktool b app-folder | |
This will generate the modified APK in the app-folder/dist folder. | |
4. Sign the APK: | |
Create Keystore (Optional): keytool -genkey -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-key-alias | |
Use the zipalign tool to optimizes the APK for faster loading: | |
zipalign -v -p 4 ./app-folder/dist/app-folder.apk ./app-folder/dist/aligned-app-folder.apk | |
zipalign -c -v 4 ./app-folder/dist/aligned-app-folder.apk #Verify if align success | |
If alignment is sucessful, Sign the apk: | |
apksigner sign --ks my-release-key.jks --out ./app-folder/dist/signed-app-folder.apk ./app-folder/dist/aligned-app-folder.apk | |
apksigner verify your-app-signed.apk #Verify app signature``` | |
and it worked, the app requested for admin previlages, and worked just usual. | |
Here are the some other resources that i read: | |
```Revanced-Patcher-Guide-Readme.md | |
There are multiple types of patches. Each type can modify a different part of the APK, such as the Dalvik VM bytecode, the APK resources, or arbitrary files in the APK: | |
A BytecodePatch modifies the Dalvik VM bytecode | |
A ResourcePatch modifies (decoded) resources | |
A RawResourcePatch modifies arbitrary files | |
Each patch can declare a set of dependencies on other patches. ReVanced Patcher will first execute dependencies before executing the patch itself. This way, multiple patches can work together for abstract purposes in a modular way. | |
The execute function is the entry point for a patch. It is called by ReVanced Patcher when the patch is executed. The execute function receives an instance of a context object that provides access to the APK. The patch can use this context to modify the APK. | |
Each type of context provides different APIs to modify the APK. For example, the BytecodePatchContext provides APIs to modify the Dalvik VM bytecode, while the ResourcePatchContext provides APIs to modify resources. | |
The difference between ResourcePatch and RawResourcePatch is that ReVanced Patcher will decode the resources if it is supplied a ResourcePatch for execution or if any patch depends on a ResourcePatch and will not decode the resources before executing RawResourcePatch. Both, ResourcePatch and RawResourcePatch can modify arbitrary files in the APK, whereas only ResourcePatch can modify decoded resources. The choice of which type to use depends on the use case. Decoding and building resources is a time- and resource-consuming, so if the patch does not need to modify decoded resources, it is better to use RawResourcePatch or BytecodePatch. | |
๐ Prepare the environment | |
Throughout the documentation, ReVanced Patches will be used as an example project. | |
Note | |
To start a fresh project, you can use the ReVanced Patches template (https://github.com/revanced/revanced-patches-template). | |
Clone the repository | |
git clone https://github.com/revanced/revanced-patches && cd revanced-patches | |
Build the project | |
./gradlew build | |
โณ๏ธ Example patch | |
The following example patch disables ads in an app. | |
In the following sections, each part of the patch will be explained in detail. | |
package app.revanced.patches.ads | |
val disableAdsPatch = bytecodePatch( | |
name = "Disable ads", | |
description = "Disable ads in the app.", | |
) { | |
compatibleWith("com.some.app"("1.0.0")) | |
// Patches can depend on other patches, executing them first. | |
dependsOn(disableAdsResourcePatch) | |
// Merge precompiled DEX files into the patched app, before the patch is executed. | |
extendWith("disable-ads.rve") | |
// Business logic of the patch to disable ads in the app. | |
execute { | |
// Fingerprint to find the method to patch. | |
val showAdsMatch by showAdsFingerprint { | |
// More about fingerprints on the next page of the documentation. | |
} | |
// In the method that shows ads, | |
// call DisableAdsPatch.shouldDisableAds() from the extension (precompiled DEX file) | |
// to enable or disable ads. | |
showAdsMatch.method.addInstructions( | |
0, | |
""" | |
invoke-static {}, LDisableAdsPatch;->shouldDisableAds()Z | |
move-result v0 | |
return v0 | |
""" | |
) | |
} | |
} | |
โ๏ธ Patch options | |
Patches can have options to get and set before a patch is executed. Options are useful for making patches configurable. After loading the patches using PatchLoader, options can be set for a patch. Multiple types are already built into ReVanced Patcher and are supported by any application that uses ReVanced Patcher. | |
To define an option, use the available option functions: | |
val patch = bytecodePatch(name = "Patch") { | |
// Add an inbuilt option and delegate it to a property. | |
val value by stringOption(key = "option") | |
// Add an option with a custom type and delegate it to a property. | |
val string by option<String>(key = "string") | |
execute { | |
println(value) | |
println(string) | |
} | |
} | |
Options of a patch can be set after loading the patches with PatchLoader by obtaining the instance for the patch: | |
loadPatchesJar(patches).apply { | |
// Type is checked at runtime. | |
first { it.name == "Patch" }.options["option"] = "Value" | |
} | |
The type of an option can be obtained from the type property of the option: | |
option.type // The KType of the option. Captures the full type information of the option. | |
Options can be declared outside a patch and added to a patch manually: | |
val option = stringOption(key = "option") | |
bytecodePatch(name = "Patch") { | |
val value by option() | |
} | |
This is useful when the same option is referenced in multiple patches. | |
๐งฉ Extensions | |
An extension is a precompiled DEX file merged into the patched app before a patch is executed. While patches are compile-time constructs, extensions are runtime constructs that extend the patched app with additional classes. | |
Assume you want to add a complex feature to an app that would need multiple classes and methods: | |
public class ComplexPatch { | |
public static void doSomething() { | |
// ... | |
} | |
} | |
After compiling the above code as a DEX file, you can add the DEX file as a resource in the patches file and use it in a patch: | |
val patch = bytecodePatch(name = "Complex patch") { | |
extendWith("complex-patch.rve") | |
execute { | |
fingerprint.match!!.mutableMethod.addInstructions(0, "invoke-static { }, LComplexPatch;->doSomething()V") | |
} | |
} | |
ReVanced Patcher merges the classes from the extension into context.classes before executing the patch. When the patch is executed, it can reference the classes and methods from the extension. | |
โป๏ธ Finalization | |
Patches can have a finalization block called after all patches have been executed, in reverse order of patch execution. The finalization block is called after all patches that depend on the patch have been executed. This is useful for doing post-processing tasks. A simple real-world example would be a patch that opens a resource file of the app for writing. Other patches that depend on this patch can write to the file, and the finalization block can close the file. | |
val patch = bytecodePatch(name = "Patch") { | |
dependsOn( | |
bytecodePatch(name = "Dependency") { | |
execute { | |
print("1") | |
} | |
finalize { | |
print("4") | |
} | |
} | |
) | |
execute { | |
print("2") | |
} | |
finalize { | |
print("3") | |
} | |
} | |
Because Patch depends on Dependency, first Dependency is executed, then Patch. Finalization blocks are called in reverse order of patch execution, which means, first, the finalization block of Patch, then the finalization block of Dependency is called. The output after executing the patch above would be 1234. The same order is followed for multiple patches depending on the patch. | |
๐ก Additional tips | |
When using PatchLoader to load patches, only patches with a name are loaded. Refer to the inline documentation of PatchLoader for detailed information. | |
Patches can depend on others. Dependencies are executed first. The dependent patch will not be executed if a dependency raises an exception while executing. | |
A patch can declare compatibility with specific packages and versions, but patches can still be executed on any package or version. It is recommended that compatibility is specified to present known compatible packages and versions. | |
If compatibleWith is not used, the patch is treated as compatible with any package | |
If a package is specified with no versions, the patch is compatible with any version of the package | |
If an empty array of versions is specified, the patch is not compatible with any version of the package. This is useful for declaring incompatibility with a specific package. | |
A patch can raise a PatchException at any time of execution to indicate that the patch failed to execute. | |
๐ Fingerprinting | |
In the context of ReVanced, a fingerprint is a partial description of a method. It is used to uniquely match a method by its characteristics. Fingerprinting is used to match methods with a limited amount of known information. Methods with obfuscated names that change with each update are primary candidates for fingerprinting. The goal of fingerprinting is to uniquely identify a method by capturing various attributes, such as the return type, access flags, an opcode pattern, strings, and more. | |
โณ๏ธ Example fingerprint | |
An example fingerprint is shown below: | |
package app.revanced.patches.ads.fingerprints | |
fingerprint { | |
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) | |
returns("Z") | |
parameters("Z") | |
opcodes(Opcode.RETURN) | |
strings("pro") | |
custom { (method, classDef) -> classDef == "Lcom/some/app/ads/AdsLoader;" } | |
} | |
๐ Reconstructing the original code from the example fingerprint from above | |
The following code is reconstructed from the fingerprint to understand how a fingerprint is created. | |
The fingerprint contains the following information: | |
Method signature: | |
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) | |
returns("Z") | |
parameters("Z") | |
Method implementation: | |
opcodes(Opcode.RETURN) | |
strings("pro") | |
Package and class name: | |
custom { (method, classDef) -> classDef == "Lcom/some/app/ads/AdsLoader;" } | |
With this information, the original code can be reconstructed: | |
package com.some.app.ads; | |
<accessFlags> | |
class AdsLoader { | |
public final boolean <methodName>(boolean <parameter>) | |
{ | |
// ... | |
var userStatus = "pro"; | |
// ... | |
return <returnValue >; | |
} | |
} | |
Using that fingerprint, this method can be matched uniquely from all other methods. | |
Tip | |
A fingerprint should contain information about a method likely to remain the same across updates. A method's name is not included in the fingerprint because it will likely change with each update in an obfuscated app. In contrast, the return type, access flags, parameters, patterns of opcodes, and strings are likely to remain the same. | |
๐จ How to use fingerprints | |
After declaring a fingerprint, it can be used in a patch to find the method it matches to: | |
val fingerprint = fingerprint { | |
// ... | |
} | |
val patch = bytecodePatch { | |
execute { | |
fingerprint.method | |
} | |
} | |
The fingerprint won't be matched again, if it has already been matched once, for performance reasons. This makes it useful, to share fingerprints between multiple patches, and let the first executing patch match the fingerprint: | |
// Either of these two patches will match the fingerprint first and the other patch can reuse the match: | |
val mainActivityPatch1 = bytecodePatch { | |
execute { | |
mainActivityOnCreateFingerprint.method | |
} | |
} | |
val mainActivityPatch2 = bytecodePatch { | |
execute { | |
mainActivityOnCreateFingerprint.method | |
} | |
} | |
Warning | |
If the fingerprint can not be matched to any method, accessing certain properties of the fingerprint will raise an exception. Instead, the orNull properties can be used to return null if no match is found. | |
Tip | |
If a fingerprint has an opcode pattern, you can use the fuzzyPatternScanThreshhold parameter of the opcode function to fuzzy match the pattern. | |
null can be used as a wildcard to match any opcode: | |
fingerprint(fuzzyPatternScanThreshhold = 2) { | |
opcodes( | |
Opcode.ICONST_0, | |
null, | |
Opcode.ICONST_1, | |
Opcode.IRETURN, | |
) | |
} | |
The following properties can be accessed in a fingerprint: | |
originalClassDef: The original class definition the fingerprint matches to. | |
originalClassDefOrNull: The original class definition the fingerprint matches to. | |
originalMethod: The original method the fingerprint matches to. | |
originalMethodOrNull: The original method the fingerprint matches to. | |
classDef: The class the fingerprint matches to. | |
classDefOrNull: The class the fingerprint matches to. | |
method: The method the fingerprint matches to. If no match is found, an exception is raised. | |
methodOrNull: The method the fingerprint matches to. | |
The difference between the original and non-original properties is that the original properties return the original class or method definition, while the non-original properties return a mutable copy of the class or method. The mutable copies can be modified. They are lazy properties, so they are only computed and only then will effectively replace the original method or class definition when accessed. | |
Tip | |
If only read-only access to the class or method is needed, the originalClassDef and originalMethod properties should be used, to avoid making a mutable copy of the class or method. | |
๐น Manually matching fingerprints | |
By default, a fingerprint is matched automatically against all classes when one of the fingerprint's properties is accessed. | |
Instead, the fingerprint can be matched manually using various overloads of a fingerprint's match function: | |
In a list of classes, if the fingerprint can match in a known subset of classes | |
If you have a known list of classes you know the fingerprint can match in, you can match the fingerprint on the list of classes: | |
execute { | |
val match = showAdsFingerprint(classes) | |
} | |
In a single class, if the fingerprint can match in a single known class | |
If you know the fingerprint can match a method in a specific class, you can match the fingerprint in the class: | |
execute { | |
val adsLoaderClass = classes.single { it.name == "Lcom/some/app/ads/Loader;" } | |
val match = showAdsFingerprint.match(adsLoaderClass) | |
} | |
Another common usecase is to use a fingerprint to reduce the search space of a method to a single class. | |
execute { | |
// Match showAdsFingerprint in the class of the ads loader found by adsLoaderClassFingerprint. | |
val match = showAdsFingerprint.match(adsLoaderClassFingerprint.classDef) | |
} | |
Match a single method, to extract certain information about it | |
The match of a fingerprint contains useful information about the method, such as the start and end index of an opcode pattern or the indices of the instructions with certain string references. A fingerprint can be leveraged to extract such information from a method instead of manually figuring it out: | |
execute { | |
val currentPlanFingerprint = fingerprint { | |
strings("free", "trial") | |
} | |
currentPlanFingerprint.match(adsFingerprint.method).let { match -> | |
match.stringMatches.forEach { match -> | |
println("The index of the string '${match.string}' is ${match.index}") | |
} | |
} | |
} | |
Warning | |
If the fingerprint can not be matched to any method, calling match will raise an exception. Instead, the orNull overloads can be used to return null if no match is found. | |
๐ File structure | |
Patches are organized in a specific way. The file structure looks as follows: | |
๐ฆyour.patches.app.category | |
โ ๐Fingerprints.kt | |
โ ๐งฉSomePatch.kt | |
Note | |
Moving fingerprints to a separate file isn't strictly necessary, but it helps the organization when a patch uses multiple fingerprints. | |
๐ Conventions | |
๐ฅ Name a patch after what it does. For example, if a patch removes ads, name it Remove ads. If a patch changes the color of a button, name it Change button color | |
๐ฅ Write the patch description in the third person, present tense, and end it with a period. If a patch removes ads, the description can be omitted because of redundancy, but if a patch changes the color of a button, the description can be Changes the color of the resume button to red. | |
๐ฅ Write patches with modularity and reusability in mind. Patches can depend on each other, so it is important to write patches in a way that can be used in different contexts. | |
๐ฅ๐ฅ Keep patches as minimal as possible. This reduces the risk of failing patches. Instead of involving many abstract changes in one patch or writing entire methods or classes in a patch, you can write code in extensions. An extension is a precompiled DEX file that is merged into the patched app before this patch is executed. Patches can then reference methods and classes from extensions. A real-world example of extensions can be found in the ReVanced Patches repository | |
๐ฅ๐ฅ๐ฅ Do not overload a fingerprint with information about a method that's likely to change. In the example of an obfuscated method, it's better to fingerprint the method by its return type and parameters rather than its name because the name is likely to change. An intelligent selection of an opcode pattern or strings in a method can result in a strong fingerprint dynamic to app updates. | |
๐ฅ๐ฅ๐ฅ Document your patches. Patches are abstract, so it is important to document parts of the code that are not self-explanatory. For example, explain why and how a certain method is patched or large blocks of instructions that are modified or added to a method | |
๐ช Advanced APIs | |
A handful of APIs are available to make patch development easier and more efficient. | |
๐ Overview | |
๐น Create mutable replacements of classes with proxy(ClassDef) | |
๐ Find and create mutable replaces with classBy(Predicate) | |
๐โ Navigate method calls recursively by index with navigate(Method) | |
๐พ Read and write resource files with get(String, Boolean) and delete(String) | |
๐ Read and write DOM files using document(String) and document(InputStream) | |
๐งฐ APIs | |
๐น proxy(ClassDef) | |
By default, the classes are immutable, meaning they cannot be modified. To make a class mutable, use the proxy(ClassDef) function. This function creates a lazy mutable copy of the class definition. Accessing the property will replace the original class definition with the mutable copy, thus allowing you to make changes to the class. Subsequent accesses will return the same mutable copy. | |
execute { | |
val mutableClass = proxy(classDef) | |
mutableClass.methods.add(Method()) | |
} | |
๐ classBy(Predicate) | |
The classBy(Predicate) function is an alternative to finding and creating mutable classes by a predicate. It automatically proxies the class definition, making it mutable. | |
execute { | |
// Alternative to proxy(classes.find { it.name == "Lcom/example/MyClass;" })?.classDef | |
val classDef = classBy { it.name == "Lcom/example/MyClass;" }?.classDef | |
} | |
๐โ navigate(Method).at(index) | |
The navigate(Method) function allows you to navigate method calls recursively by index. | |
execute { | |
// Sequentially navigate to the instructions at index 1 within 'someMethod'. | |
val method = navigate(someMethod).to(1).original() // original() returns the original immutable method. | |
// Further navigate to the second occurrence where the instruction's opcode is 'INVOKEVIRTUAL'. | |
// stop() returns the mutable copy of the method. | |
val method = navigate(someMethod).to(2) { instruction -> instruction.opcode == Opcode.INVOKEVIRTUAL }.stop() | |
// Alternatively, to stop(), you can delegate the method to a variable. | |
val method by navigate(someMethod).to(1) | |
// You can chain multiple calls to at() to navigate deeper into the method. | |
val method by navigate(someMethod).to(1).to(2, 3, 4).to(5) | |
} | |
๐พ get(String, Boolean) and delete(String) | |
The get(String, Boolean) function returns a File object that can be used to read and write resource files. | |
execute { | |
val file = get("res/values/strings.xml") | |
val content = file.readText() | |
file.writeText(content) | |
} | |
The delete function can mark files for deletion when the APK is rebuilt. | |
execute { | |
delete("res/values/strings.xml") | |
} | |
๐ document(String) and document(InputStream) | |
The document function is used to read and write DOM files. | |
execute { | |
document("res/values/strings.xml").use { document -> | |
val element = doc.createElement("string").apply { | |
textContent = "Hello, World!" | |
} | |
document.documentElement.appendChild(element) | |
} | |
} | |
You can also read documents from an InputStream: | |
execute { | |
val inputStream = classLoader.getResourceAsStream("some.xml") | |
document(inputStream).use { document -> | |
// ... | |
} | |
} | |
๐ Afterword | |
ReVanced Patcher is a powerful library to patch Android applications, offering a rich set of APIs to develop patches that outlive app updates. Patches make up ReVanced; without you, the community of patch developers, ReVanced would not be what it is today. We hope that this documentation has been helpful to you and are excited to see what you will create with ReVanced Patcher. If you have any questions or need help, talk to us on one of our platforms linked on revanced.app or open an issue in case of a bug or feature request, | |
ReVanced``` | |
This is the template code named ExamplePatch.kt | |
```ExamplePatch.kt | |
package app.revanced.patches.example | |
import app.revanced.patcher.patch.bytecodePatch | |
@Suppress("unused") | |
val examplePatch = bytecodePatch( | |
name = "Example Patch", | |
description = "This is an example patch to start with.", | |
) { | |
compatibleWith("com.example.app"("1.0.0")) | |
extendWith("extensions/extension.rve") | |
execute { | |
// TODO("Not yet implemented") | |
} | |
}``` | |
This is an actual example patch called "OverrideCertificatePinningPatch.kt" which can be used For adding resources to the AndroidManifest.xml: | |
```OverrideCertificatePinningPatch.kt | |
package app.revanced.patches.all.misc.network | |
import app.revanced.patcher.patch.resourcePatch | |
import app.revanced.patches.all.misc.debugging.enableAndroidDebuggingPatch | |
import app.revanced.util.Utils.trimIndentMultiline | |
import org.w3c.dom.Element | |
import java.io.File | |
@Suppress("unused") | |
val overrideCertificatePinningPatch = resourcePatch( | |
name = "Override certificate pinning", | |
description = "Overrides certificate pinning, allowing to inspect traffic via a proxy.", | |
use = false, | |
) { | |
dependsOn(enableAndroidDebuggingPatch) | |
execute { | |
val resXmlDirectory = get("res/xml") | |
// Add android:networkSecurityConfig="@xml/network_security_config" and the "networkSecurityConfig" attribute if it does not exist. | |
document("AndroidManifest.xml").use { document -> | |
val applicationNode = document.getElementsByTagName("application").item(0) as Element | |
if (!applicationNode.hasAttribute("networkSecurityConfig")) { | |
document.createAttribute("android:networkSecurityConfig") | |
.apply { value = "@xml/network_security_config" }.let(applicationNode.attributes::setNamedItem) | |
} | |
} | |
// In case the file does not exist create the "network_security_config.xml" file. | |
File(resXmlDirectory, "network_security_config.xml").apply { | |
writeText( | |
""" | |
<?xml version="1.0" encoding="utf-8"?> | |
<network-security-config> | |
<base-config cleartextTrafficPermitted="true"> | |
<trust-anchors> | |
<certificates src="system" /> | |
<certificates | |
src="user" | |
overridePins="true" /> | |
</trust-anchors> | |
</base-config> | |
<debug-overrides> | |
<trust-anchors> | |
<certificates src="system" /> | |
<certificates | |
src="user" | |
overridePins="true" /> | |
</trust-anchors> | |
</debug-overrides> | |
</network-security-config> | |
""".trimIndentMultiline(), | |
) | |
} | |
} | |
}``` | |
and btw someone recommended me that For adding a new class I should use revanced extensions and call them from patch. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Disk /dev/sda: 465.76 GiB, 500107862016 bytes, 976773168 sectors | |
Disk model: WDC WD5000LPCX-2 | |
Units: sectors of 1 * 512 = 512 bytes | |
Sector size (logical/physical): 512 bytes / 4096 bytes | |
I/O size (minimum/optimal): 4096 bytes / 4096 bytes | |
Disklabel type: dos | |
Disk identifier: 0x31f26811 | |
Device Boot Start End Sectors Size Id Type | |
/dev/sda1 * 2048 208449605 208447558 99.4G 7 HPFS/NTFS/exFAT | |
/dev/sda2 208451584 209725439 1273856 622M 27 Hidden NTFS WinRE | |
/dev/sda3 209725440 441112575 231387136 110.3G 7 HPFS/NTFS/exFAT | |
/dev/sda4 441114622 976769023 535654402 255.4G f W95 Ext'd (LBA) | |
/dev/sda5 441114624 662364159 221249536 105.5G 83 Linux | |
/dev/sda6 871909418 968710185 96800768 46.2G 83 Linux | |
/dev/sda7 968714240 976769023 8054784 3.8G 82 Linux swap / Solaris | |
/dev/sda8 662366208 767137791 104771584 50G 83 Linux | |
/dev/sda9 767139840 871895039 104755200 50G 83 Linux | |
Partition table entries are not in disk order. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment