Engine

Runtime surface

Diagram: the ctx parameter passed to every patch is a PatchRuntime with six named scopes. Bytecode exposes classes, methods, instructions, and structural search helpers. Manifest exposes the AndroidManifest AXML with helpers like addPermission and setVersion. Resources exposes the resource table and string pool with helpers like addString and replaceEntry. Files exposes the raw APK entries with read, write, copy, and delete. Options exposes the values the caller supplied. Log exposes per-patch structured output via info, warn, and debug. Each scope offers a .component(name) variant for split-APK targeting.

ctx (type PatchRuntime) is passed to execute and afterDependents. It exposes five scopes and a logger:

Scope Access
bytecode Classes, methods, instructions, and structural search helpers.
manifest The Android manifest (AXML).
resources Resource table and string pool.
files Read, write, delete, copy files inside the APK.
options Option values set by the caller.
log info(msg), warn(msg), debug(msg).

manifest, resources, and files are split-APK aware: .component(name) narrows the scope to a named split (see Split APKs below).

Bytecode

for (cls in ctx.bytecode.classes) { /* ... */ }

val target = ctx.bytecode.findClass("Lcom/example/app/SomeClass;")
    ?: error("class not found")

for (method in target.methods.filter { it.info.methodName == "doThing" }) {
    // ...
}

Whole-method replacement

  • method.returnEarly(), returnEarly(value: Int|Boolean|Long), returnEarlyNull(). Replace the body with a constant return.
  • method.setInstructions(insns), replaceBody(registersSize, outsSize, insns). Swap the body wholesale.

In-place rewriting

  • method.replaceString(old, new), replaceAllStrings(old, new). Rewrite const-string values.
  • method.replaceLiteral(old, new), replaceAllLiterals(old, new).
  • method.replaceMethodCall(index, newClass, newName, newProto). Retarget an invoke-* at a known index.

Targeted splices

  • method.insertInvokeStatic(index, className, name, proto, registers). Splice a static call.
  • method.insertInvokeStaticWithMoveResult(index, className, name, proto, registers, resultRegister, isObject). Same, capturing the return value.
  • method.addInstructions(index) { ... }. Build and insert a sequence with the instruction builder.

Locating instructions

  • method.indexOfFirst(opcode), indexOfFirstReversed(opcode, start).
  • method.indexOfFirstLiteral(value), indexOfFirstString(s).
  • method.indexOfFirstMethodCall(definingClass, methodName, start).
  • method.indexOfFirstFieldAccess(opcode, fieldType?, definingClass?, start).
  • method.indexOfOpcodeSequence(opcodes, start).

A typical rewrite combines lookup and splice:

val idx = method.indexOfFirstMethodCall(
    "Lcom/example/Api;", "call",
) ?: error("call site not found")

method.replaceMethodCall(
    idx,
    "Lcom/example/PatchedApi;", "patchedCall", "(Ljava/lang/String;)V",
)

For structural lookup across obfuscated app versions, see Queries And Bindings.

Manifest

ctx.manifest.addPermission("android.permission.INTERNET")
ctx.manifest.setVersionName("1.2.3")
ctx.manifest.setActivityConfigChanges(
    "com.example.app.MainActivity",
    "orientation|screenSize",
)
ctx.manifest.addIntentFilter(
    "com.example.app.MainActivity",
    action = "android.intent.action.VIEW",
)

Top-level helpers on ctx.manifest: addPermission, setVersionCode, setVersionName, setMinSdk, setAttributeInt, setAttributeString, setActivityConfigChanges, addIntentFilter, addActivityAlias, copyIntentFilters. Read-only: packageName, versionCode, versionName, minSdkVersion, splitName.

For anything beyond the helpers, open the document:

ctx.manifest.document().use { doc ->
    doc.findByTag("application").forEach { app ->
        val activity = doc.createElement("activity").apply {
            this["android:name"] = "com.example.app.addon.AddonActivity"
            this["android:exported"] = "true"
        }
        app.appendChild(activity)
    }
}

Lookup: doc.findByTag(tag), doc.findByAttribute(name, value). Read and write: el["android:name"]. Numeric and boolean attributes: el.setInt, el.setBool. Resource references: el.setResourceRef("android:theme", resId). Tree editing: appendChild, insertBefore, remove, clone(deep).

Resources

ctx.resources.setString("app_name", "New App Name")
ctx.resources.addString("patch_message", "This app was patched")
ctx.resources.addColor("primary_color", "#FF0000")
ctx.resources.addInteger("max_connections", 10)

val poolIdx = ctx.resources.poolAdd("replacement_label")!!
ctx.resources.replaceEntry(ctx.resources.id("string", "app_name")!!, poolIdx)
  • id(resType, resName): resolve a resource ID. exists(resType, resName): presence check.
  • getString(name), setString(name, value), addString(name, value). Same shape for addBool, addInteger, addColor, addDimen, addId, addRaw.
  • poolAdd(string), poolGet(index), poolSet(index, value), poolFindRefs(index).
  • replaceEntry(resId, poolIndex): redirect a resource ID to a different string-pool entry.
  • owningComponent(resId) / owningComponent(resType, resName): locate which split a resource lives in.

Files

val names = ctx.files.list()
val config = ctx.files.read("assets/config.json")
ctx.files.write("META-INF/patch.txt", "Patched!".toByteArray())
ctx.files.copy("bundle_asset.png", "res/drawable/patch_icon.png")
ctx.files.delete("unneeded_file.xml")
  • list(): every file path in the APK.
  • read(path), write(path, bytes), delete(path), copy(bundlePath, apkPath).
  • xml(path) / useXml(path) { ... }: open a bundled AXML document.

useXml gives you the same XmlDocument API the manifest uses:

ctx.files.useXml("res/xml/config.xml") {
    findByTag("config").forEach { cfg ->
        cfg["enabled"] = "true"
        cfg.setInt("timeout", 30000)

        val setting = createElement("setting").apply {
            this["name"] = "patched"
        }
        cfg.appendChild(setting)
    }

    val added = createElement("config").apply {
        this["key"] = "patch_version"
        this["value"] = "1.0"
    }
    root.appendChild(added)
}

Split APKs

Every component(name) method returns a narrowed scope targeting one split:

ctx.manifest.component("base").addPermission("android.permission.INTERNET")
ctx.manifest.component("config.arm64_v8a").setVersionCode(789)

ctx.resources.component("base").addString("app_name", "Base")
ctx.resources.component("config.xxhdpi").addBool("feature_flag", true)

ctx.files.component("base").write("assets/base_config.json", "{}".toByteArray())
ctx.files.component("feature_dynamic").copy("dynamic_asset.png", "res/drawable/dynamic.png")

ctx.manifest.components(), ctx.resources.components(), ctx.files.components() enumerate what's available. When no component is specified, operations run against the base component.

Reseam Reseam © 2026 Reseam Team