Writing patches
Manifest, resources, and files
Besides code, a patch can change the rest of the APK. Inside execute { }:
manifest |
AndroidManifest.xml: permissions, activities, package, version |
resources |
the app's resources: strings, colors, styles, layouts, images |
files |
any file in the APK |
bytecode |
every class, for lookups and app-wide replacements |
hermes |
the app's Hermes JavaScript bundle |
options |
the patch's options |
log |
info, warn, and debug messages in the patch log |
Manifest
manifest.addPermission("android.permission.INTERNET")
manifest.addActivity("app.example.SettingsActivity") { this["android:label"] = "Settings" }
manifest.setVersionName("2.14.0-patched")
manifest.edit {
val application = findByTag("application").first()
application["android:debuggable"] = "true"
}
edit { } opens the manifest as an XML document for anything the helpers don't cover.
Mount builds
On a rooted phone, a build can be mounted over the app installed from the store instead of installed as its own app. The app keeps its signature, its data, and Google sign-in. Android keeps reading the manifest of the installed app, so a mount build leaves out every patch that edits the manifest, and the patches that depend on it. Code, resources, and other files apply as usual.
Reach for code and resource changes where they do the job, so the patch works both ways. A patch whose purpose is the manifest, such as a package rename, is left out of mount builds.
Resources
resources.addString("reseam_label", "Reseam")
val appName = resources.getString("app_name")
val id = resources.id("string", "app_name")
resources.editXml("layout", "player_controls") {
findByTag("ImageView").first()["android:layout_marginBottom"] = "6dp"
}
Release builds often rename resource files (res/layout/player_controls.xml becomes res/QYO.xml), so reach them by resource type and name, as editXml does.
Add an image or other file as a resource:
resources.addFile("drawable", "reseam_icon", "res/reseam_icon.png", bytes)
resources.addFile("mipmap", "reseam_launcher", "res/mipmap-xxhdpi/reseam_launcher.png", png, "xxhdpi")
The last argument is the configuration: empty for the default, a density, night or notnight, or vN. XML passed this way is compiled.
Styles and arrays:
resources.style("Theme.App.Light") { this["headerIcon"] = "@drawable/reseam_icon" }
resources.setStringArray("speed_values", resources.getArray("speed_values") + "3.0")
Warning
Release builds often strip resource names too, so resources.id("string", "app_name") can return null for a resource that exists. Take the id from where the app refers to it instead.
XML
manifest.edit, resources.editXml, and files.editXml(path) give you an XmlDocument: find elements by tag or attribute, read and set attributes, add, insert, and remove elements. The document closes when the block ends, so take values out of it, never the elements themselves.
To add your own layout, compile it and copy its elements in:
XmlDocument.compile(layoutXml).use { source ->
resources.editXml("layout", "player_controls") {
root.appendChild(adopt(source.root))
}
}
Attribute names need resource ids: android: names resolve through Android's own table, other prefixes through the app's attributes. A name that resolves to nothing fails the patch.
Files
files.write("assets/reseam/logo.png", bytes)
files.copy("resources/logo.png", "assets/logo.png")
files.delete("assets/tracking.json")
files.copy takes a file from your bundle's resources/ folder; see Bundle projects.
App-wide replacements
bytecode.replaceAllStrings("com.example", newPackage)
bytecode.replaceStringsContaining("example.com") { old -> old.replace("example.com", "example.org") }
replaceAllStrings only matches whole strings. replaceStringsContaining lets you rewrite strings that contain a value; return null to leave one unchanged.
Split APKs
Apps from the Play Store often come as a base APK plus split APKs for languages, screen densities, and CPU types. manifest, resources, and files act on the base APK unless you pick a split with component(name):
for (name in manifest.components()) {
manifest.component(name).edit { root["package"] = newPackage }
}
A package name or version change has to reach every split, or Android won't install the set.
Next: Reading obfuscated objects.