Hook any Java/Kotlin method at runtime on Android. No root needed, no recompilation, just drop the AAR into your project.
Sandwitcher swaps the entry point of any ArtMethod at runtime, letting you run your own code before or after the original method. It works on instance methods, static methods, native methods, final classes, private methods, constructors, framework classes -- anything the runtime can call, you can hook.
val method = URL::class.java.getDeclaredMethod("openConnection")
Sandwitcher.hook(method, object : HookCallback {
override fun beforeMethod(param: MethodHookParam): HookAction {
Log.d("Audit", "Connection to: ${param.thisObject}")
return HookAction.Continue
}
override fun afterMethod(param: MethodHookParam) {
val conn = param.result as HttpURLConnection
conn.setRequestProperty("X-Custom-Header", "injected")
}
})No compile-time dependency on the target classes. Everything is resolved via reflection.
demo.sandwitcher.1.mp4
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
// app/build.gradle.kts
dependencies {
implementation("io.sandwitcher:sandwitcher:0.1.0")
}Initialize once in your Application class:
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
Sandwitcher.init(this)
}
}Hook a method by reflection:
val method = SomeClass::class.java.getDeclaredMethod("doWork", String::class.java)
val handle = Sandwitcher.hook(method, myCallback)Or by class name if you don't have the class at compile time:
val handle = Sandwitcher.hook(
className = "com.example.target.PaymentProcessor",
methodName = "processPayment",
parameterTypes = arrayOf(Double::class.java, String::class.java),
callback = myCallback
)Write your callback:
object myCallback : HookCallback {
override fun beforeMethod(param: MethodHookParam): HookAction {
val args = param.args
val thisObj = param.thisObject
return HookAction.Continue
// or: return HookAction.ReturnEarly(customResult)
}
override fun afterMethod(param: MethodHookParam) {
val result = param.result
param.result = modifiedResult
}
}Remove the hook when you're done:
handle.unhook()For more detailed guides, check the docs/ folder:
- Getting Started -- setup, first hook, basic patterns
- Advanced Usage -- static methods, native methods, threading, performance
- Troubleshooting -- common issues and how to fix them
The main entry point. Call init once, then hook as many methods as you want.
Sandwitcher.init(application) sets up the hooking engine. You can pass a SandwitcherConfig if you want debug logging:
Sandwitcher.init(this, SandwitcherConfig(debugLogging = true))Sandwitcher.hook(method, callback) takes a java.lang.reflect.Method and a HookCallback. Returns a HookHandle you can use to remove the hook later.
Sandwitcher.hook(className, methodName, parameterTypes, callback) does the same thing but resolves the class by name at runtime. Useful when you don't have the target class in your classpath.
Sandwitcher.reset() removes all active hooks at once.
An interface with two methods. Both are optional -- override the ones you need.
beforeMethod(param) runs before the original method. You get access to the arguments and can modify them. Return HookAction.Continue to let the original run, or HookAction.ReturnEarly(value) to skip it and return your own value instead.
afterMethod(param) runs after the original method (or after beforeMethod if you returned early). You can read the return value from param.result and change it if you want.
This is what gets passed to your callback. It has:
method-- thejava.lang.reflect.Methodbeing hookedthisObject-- the instance the method was called on, ornullfor static methodsargs-- the argument array, you can modify these in-place inbeforeMethodresult-- the return value, available inafterMethodthrowable-- if the original method threw an exception, it shows up here
The same MethodHookParam instance is shared between beforeMethod and afterMethod within a single call, so any state you set in beforeMethod is still there in afterMethod.
Returned by Sandwitcher.hook(). Call handle.unhook() to remove the hook and restore the original method. Check handle.isActive to see if the hook is still installed. Thread-safe -- calling unhook() multiple times is fine.
A sealed class with two cases:
HookAction.Continue-- let the original method runHookAction.ReturnEarly(result)-- skip the original and returnresultinstead
Pass this to Sandwitcher.init() to configure the SDK. Currently has one option:
debugLogging-- set totrueto log hook install/uninstall events to logcat under theSandwitchertag
Under the hood, Sandwitcher uses Pine which builds on LSPlant. When you hook a method:
- The target
ArtMethod*is resolved via JNI - A backup copy of the method is created (allocated through DexBuilder so it's GC-safe)
- The original's
entry_point_from_quick_compiled_code_is replaced with a trampoline - JIT inlining is disabled for hooked methods so the compiler can't optimize around the hook
- Calls go through: your
beforeMethod-> original (via backup) -> yourafterMethod
This happens at the native level, below Java. Works on interpreted, JIT-compiled, and AOT-compiled methods.
- Android 5.0 through 15 (API 21-35)
- ARM, ARM64, x86, x86_64
- No root required
- Works in release builds
./gradlew :sandwitcher:assembleRelease
# run the demo app
./gradlew :app:installDebugYou'll need JDK 17 and Android SDK 35.
sandwitcher/ SDK module (ships as AAR)
src/main/kotlin/io/sandwitcher/
Sandwitcher.kt entry point
HookCallback.kt before/after interface
HookAction.kt Continue or ReturnEarly
HookHandle.kt unhook handle
MethodHookParam.kt call context
SandwitcherConfig.kt config
internal/
HookEngine.kt Pine/LSPlant bridge
app/ demo app
src/main/kotlin/.../demo/
SandwitcherDemoApp.kt
MainActivity.kt
Contributions are welcome. See CONTRIBUTING.md for details on how to get started, submit PRs, and what to work on.
Built on Pine by canyie and LSPlant by LSPosed.
Apache 2.0. See LICENSE for details.
