From c5cff14c2f863c8aa046ed63a20f5bfc53543d31 Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Sun, 17 Aug 2025 00:01:10 +0700 Subject: [PATCH] feat: ability to call custom native callbacks --- .../kotlin/io/github/revenge/xposed/Main.kt | 6 +- .../xposed/modules/NativeBridgeModule.kt | 106 ++++++++++++++++++ 2 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 app/src/main/kotlin/io/github/revenge/xposed/modules/NativeBridgeModule.kt diff --git a/app/src/main/kotlin/io/github/revenge/xposed/Main.kt b/app/src/main/kotlin/io/github/revenge/xposed/Main.kt index 52e62d4..9363f85 100644 --- a/app/src/main/kotlin/io/github/revenge/xposed/Main.kt +++ b/app/src/main/kotlin/io/github/revenge/xposed/Main.kt @@ -1,11 +1,9 @@ package io.github.revenge.xposed import android.app.Activity -import android.content.pm.ApplicationInfo import android.content.res.AssetManager import android.content.res.Resources import android.os.Bundle -import android.util.Log import android.widget.Toast import de.robv.android.xposed.IXposedHookLoadPackage import de.robv.android.xposed.XC_MethodHook @@ -13,6 +11,7 @@ import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.callbacks.XC_LoadPackage import io.github.revenge.xposed.modules.FontsModule import io.github.revenge.xposed.modules.LogBoxModule +import io.github.revenge.xposed.modules.NativeBridgeModule import io.github.revenge.xposed.modules.SysColorsModule import io.github.revenge.xposed.modules.ThemeModule import io.github.revenge.xposed.Utils.Companion.JSON @@ -62,7 +61,8 @@ class Main : Module(), IXposedHookLoadPackage { ThemeModule(), SysColorsModule(), FontsModule(), - LogBoxModule() + LogBoxModule(), + NativeBridgeModule() ) private fun getPayloadString(): String = JSON.encodeToString( diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/NativeBridgeModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/NativeBridgeModule.kt new file mode 100644 index 0000000..069448d --- /dev/null +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/NativeBridgeModule.kt @@ -0,0 +1,106 @@ +package io.github.revenge.xposed.modules + +import de.robv.android.xposed.callbacks.XC_LoadPackage +import io.github.revenge.xposed.Module +import io.github.revenge.xposed.Utils.Log +import java.lang.reflect.Method + +typealias BridgeMethodCallback = (HashMap) -> Any? + +class NativeBridgeModule : Module() { + + private lateinit var packageParam: XC_LoadPackage.LoadPackageParam + + private lateinit var readableMapGetStringMethod: Method + private lateinit var readableMapToHashMapMethod: Method + private lateinit var argumentsMakeNativeObject: Method + + companion object { + private val BRIDGE_CALL_KEY = "revenge" + private val methods = mutableMapOf( + "revenge.test" to { + mapOf( + "string" to "string", + "number" to 7256, + "array" to listOf("testing", 527737, listOf(true)), + "object" to mapOf("nested" to true), + "boolean" to false + ) + } + ) + + fun registerMethod(name: String, callback: BridgeMethodCallback) { + require(!methods.containsKey(name)) { + "Bridge callback already exists: $name" + } + + methods[name] = callback + } + } + + + private fun Any?.toNativeObject(): Any? = + argumentsMakeNativeObject.invoke(null, this) + + override fun onInit(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { + this@NativeBridgeModule.packageParam = packageParam + + val argumentsClass = classLoader.loadClass("com.facebook.react.bridge.Arguments") + val readableMapClass = classLoader.loadClass("com.facebook.react.bridge.ReadableMap") + val promiseClass = classLoader.loadClass("com.facebook.react.bridge.Promise") + + val promiseResolveMethod = promiseClass.method("resolve", Object::class.java) + argumentsMakeNativeObject = + argumentsClass.method("makeNativeObject", Object::class.java) + readableMapGetStringMethod = + readableMapClass.method("getString", String::class.java) + readableMapToHashMapMethod = readableMapClass.method("toHashMap") + + classLoader.loadClass("com.horcrux.svg.RNSVGRenderableManager") + .hookMethod("getBBox", Double::class.javaObjectType, readableMapClass) { + before { + val map = args[1]!! + callBridgeMethod(map)?.let { result = it.toNativeObject() } + } + } + + classLoader.loadClass("com.facebook.react.modules.blob.FileReaderModule") + .hookMethod("readAsDataURL", readableMapClass, promiseClass) { + before { + val map = args[0]!! + callBridgeMethod(map)?.let { ret -> + promiseResolveMethod.invoke(args[1], ret.toNativeObject()) + result = null + } + } + } + + return@with + } + + private fun callBridgeMethod(map: Any): Map? = try { + val method = getBridgeCallMethod(map) ?: return null + val ret = method(getBridgeCallArgs(map)).toNativeObject() + mapOf("result" to ret) + } catch (e: Error) { + mapOf("error" to e.toString()) + } + + private fun getBridgeCallArgs(map: Any): HashMap { + @Suppress("UNCHECKED_CAST") + val args = readableMapToHashMapMethod.invoke(map) as HashMap + args.remove(BRIDGE_CALL_KEY) + return args + } + + private fun getBridgeCallMethod(map: Any): BridgeMethodCallback? = try { + val name = readableMapGetStringMethod.invoke(map, BRIDGE_CALL_KEY) as? String + ?: return null + + methods[name] ?: throw Error("Method $name not registered") + } catch (e: RuntimeException) { + // UnexpectedNativeTypeException : RuntimeException + Log.e(e.toString()) + throw Error("Bad method name") + } +}