From 956bdda6c05c63a0d2630131935fd7005b8b930f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=9ET=CE=9ERNAL?= Date: Tue, 3 Mar 2026 19:14:37 +0530 Subject: [PATCH] migration from libxposed API to YukiHook --- app/build.gradle.kts | 1 + app/proguard-rules.pro | 4 +- core/build.gradle.kts | 3 + .../core/util/hook/YukiHookBridge.java | 71 +++++++++++++++ .../eternal/purrfectsnap/core/XposedLoader.kt | 16 ++-- .../purrfectsnap/core/bridge/BridgeClient.kt | 13 ++- .../features/impl/ConfigurationOverride.kt | 9 +- .../features/impl/messaging/Notifications.kt | 4 +- .../features/impl/messaging/SendOverride.kt | 17 ++-- .../impl/tweaks/UnsaveableMessages.kt | 7 +- .../purrfectsnap/core/logger/CoreLogger.kt | 9 +- .../purrfectsnap/core/util/CallbackBuilder.kt | 3 +- .../core/util/hook/HookAdapter.kt | 37 ++++---- .../core/util/hook/HookRuntime.kt | 16 ++++ .../purrfectsnap/core/util/hook/Hooker.kt | 90 ++++++++++++------- .../core/util/hook/YukiHookCompat.kt | 17 ++++ .../core/util/ktx/XposedHelperExt.kt | 19 +++- .../core/wrapper/AbstractWrapper.kt | 13 +-- .../core/wrapper/impl/valdi/ValdiContext.kt | 4 +- gradle/libs.versions.toml | 5 ++ settings.gradle.kts | 1 + 21 files changed, 257 insertions(+), 102 deletions(-) create mode 100644 core/src/main/java/me/eternal/purrfectsnap/core/util/hook/YukiHookBridge.java create mode 100644 core/src/main/kotlin/me/eternal/purrfectsnap/core/util/hook/HookRuntime.kt create mode 100644 core/src/main/kotlin/me/eternal/purrfectsnap/core/util/hook/YukiHookCompat.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8db8f222..a256dd3b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -267,6 +267,7 @@ dependencies { } implementation(project(":core")) + compileOnly(files("../core/libs/LSPosed-api-1.0-SNAPSHOT.jar")) implementation(project(":common")) implementation(project(":native")) implementation(libs.androidx.documentfile) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 2d60e596..85161b71 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,6 +1,6 @@ - --dontwarn de.robv.android.xposed.** -dontwarn org.mozilla.javascript.** +-dontwarn android.app.AndroidAppHelper +-dontwarn java.lang.reflect.AnnotatedType -keep class com.tonyodev.fetch2.** { *; } -keep class com.tonyodev.fetch2core.** { *; } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index bf87a4fc..8f1b585d 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -24,6 +24,9 @@ android { } dependencies { + implementation(libs.yukihookapi.api) + implementation(libs.kavaref.core) + implementation(libs.kavaref.extension) compileOnly(files("libs/LSPosed-api-1.0-SNAPSHOT.jar")) implementation(libs.coroutines) implementation(libs.recyclerview) diff --git a/core/src/main/java/me/eternal/purrfectsnap/core/util/hook/YukiHookBridge.java b/core/src/main/java/me/eternal/purrfectsnap/core/util/hook/YukiHookBridge.java new file mode 100644 index 00000000..ab111a1d --- /dev/null +++ b/core/src/main/java/me/eternal/purrfectsnap/core/util/hook/YukiHookBridge.java @@ -0,0 +1,71 @@ +package me.eternal.purrfectsnap.core.util.hook; + +import com.highcapable.yukihookapi.hook.core.api.helper.YukiHookHelper; +import com.highcapable.yukihookapi.hook.core.api.proxy.YukiHookCallback; +import com.highcapable.yukihookapi.hook.core.api.proxy.YukiMemberHook; +import com.highcapable.yukihookapi.hook.core.api.result.YukiHookResult; + +import java.lang.reflect.Member; + +public final class YukiHookBridge { + private YukiHookBridge() {} + + public interface ParamCallback { + void accept(Object param); + } + + public static Object newMemberHook(ParamCallback before, ParamCallback after) { + return new YukiMemberHook() { + @Override + public void beforeHookedMember$yukihookapi_core_release(YukiHookCallback.Param param) { + if (before != null) before.accept(param); + } + + @Override + public void afterHookedMember$yukihookapi_core_release(YukiHookCallback.Param param) { + if (after != null) after.accept(param); + } + }; + } + + public static Object hookMember(Member member, Object callback) { + return YukiHookHelper.INSTANCE.hookMember$yukihookapi_core_release(member, (YukiHookCallback) callback); + } + + public static Object invokeOriginal(Member member, Object instance, Object[] args) { + return YukiHookHelper.INSTANCE.invokeOriginalMember$yukihookapi_core_release(member, instance, args); + } + + public static void unhook(Object hookResult) { + YukiHookResult result = (YukiHookResult) hookResult; + result.getHookedMember().remove$yukihookapi_core_release(); + } + + public static Member member(Object param) { + return ((YukiHookCallback.Param) param).getMember(); + } + + public static Object instance(Object param) { + return ((YukiHookCallback.Param) param).getInstance(); + } + + public static Object[] args(Object param) { + return ((YukiHookCallback.Param) param).getArgs(); + } + + public static Object result(Object param) { + return ((YukiHookCallback.Param) param).getResult(); + } + + public static void setResult(Object param, Object result) { + ((YukiHookCallback.Param) param).setResult(result); + } + + public static Throwable throwable(Object param) { + return ((YukiHookCallback.Param) param).getThrowable(); + } + + public static void setThrowable(Object param, Throwable throwable) { + ((YukiHookCallback.Param) param).setThrowable(throwable); + } +} diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/XposedLoader.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/XposedLoader.kt index fe576a46..2e1d6a27 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/XposedLoader.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/XposedLoader.kt @@ -10,13 +10,15 @@ import me.eternal.purrfectsnap.core.util.hook.HookStage import me.eternal.purrfectsnap.core.util.hook.hook class XposedLoader : IXposedHookLoadPackage { - override fun handleLoadPackage(p0: XC_LoadPackage.LoadPackageParam) { - if (p0.packageName != Constants.SNAPCHAT_PACKAGE_NAME) return + override fun handleLoadPackage(param: XC_LoadPackage.LoadPackageParam) { + if (param.packageName != Constants.SNAPCHAT_PACKAGE_NAME) return // prevent loading in sub-processes - if (p0.processName.contains(":")) return - XposedBridge.log("Loading PurrfectSnap v${BuildConfig.VERSION_NAME}#${BuildConfig.GIT_HASH} (package: ${BuildConfig.APPLICATION_ID})") - Application::class.java.hook("attach", HookStage.BEFORE) { param -> - PurrfectSnap().init(param.arg(0)) + if (param.processName.contains(":")) return + XposedBridge.log( + "Loading PurrfectSnap v${BuildConfig.VERSION_NAME}#${BuildConfig.GIT_HASH} (package: ${BuildConfig.APPLICATION_ID})" + ) + Application::class.java.hook("attach", HookStage.BEFORE) { hookParam -> + PurrfectSnap().init(hookParam.arg(0)) } } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/bridge/BridgeClient.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/bridge/BridgeClient.kt index d41aa076..72491f77 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/bridge/BridgeClient.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/bridge/BridgeClient.kt @@ -7,7 +7,6 @@ import android.content.Intent import android.content.ServiceConnection import android.os.* import android.util.Log -import de.robv.android.xposed.XposedHelpers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.sync.Semaphore @@ -106,17 +105,17 @@ class BridgeClient( this@BridgeClient ) } else { - XposedHelpers.callMethod( + val handler = Handler(HandlerThread("BridgeClient").apply { start() }.looper) + this::class.java.methods.firstOrNull { + it.name == "bindServiceAsUser" && it.parameterTypes.size == 5 + }?.invoke( this, - "bindServiceAsUser", intent, this@BridgeClient, Context.BIND_AUTO_CREATE, - Handler(HandlerThread("BridgeClient").apply { - start() - }.looper), + handler, Process.myUserHandle() - ) + ) ?: throw NoSuchMethodException("bindServiceAsUser") } }.onFailure { onFailure(it) diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ConfigurationOverride.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ConfigurationOverride.kt index cb507aa8..42184603 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ConfigurationOverride.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ConfigurationOverride.kt @@ -1,6 +1,5 @@ package me.eternal.purrfectsnap.core.features.impl -import de.robv.android.xposed.XposedHelpers import me.eternal.purrfectsnap.core.features.Feature import me.eternal.purrfectsnap.core.util.hook.HookStage @@ -93,8 +92,10 @@ class ConfigurationOverride : Feature("Configuration Override") { ) { param -> val enumData = param.arg(0) val key = enumData.toString() - val setValue: (Any?) -> Unit = { value -> - val valueHolder = XposedHelpers.callMethod(enumData, configEnumMapping["getValue"]?.getAsString()) + val setValue: (Any?) -> Unit = setValue@{ value -> + val valueHolder = enumData::class.java.methods.firstOrNull { + it.name == configEnumMapping["getValue"]?.getAsString() && it.parameterCount == 0 + }?.invoke(enumData) ?: return@setValue valueHolder.setObjectField(configEnumMapping["defaultValueField"]?.getAsString()!!, value) } @@ -148,4 +149,4 @@ class ConfigurationOverride : Feature("Configuration Override") { } } } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/Notifications.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/Notifications.kt index d2a19fed..b91d77b6 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/Notifications.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/Notifications.kt @@ -11,7 +11,6 @@ import android.graphics.BitmapFactory import android.os.Build import android.os.Bundle import android.os.UserHandle -import de.robv.android.xposed.XposedBridge import kotlinx.coroutines.* import me.eternal.purrfectsnap.common.data.ContentType import me.eternal.purrfectsnap.common.data.FileType @@ -29,6 +28,7 @@ import me.eternal.purrfectsnap.core.features.impl.downloader.decoder.MessageDeco import me.eternal.purrfectsnap.core.features.impl.experiments.BetterTranscript import me.eternal.purrfectsnap.core.features.impl.spying.StealthMode import me.eternal.purrfectsnap.core.util.hook.HookStage +import me.eternal.purrfectsnap.core.util.hook.YukiHookCompat import me.eternal.purrfectsnap.core.util.hook.findRestrictedConstructor import me.eternal.purrfectsnap.core.util.hook.findRestrictedMethod import me.eternal.purrfectsnap.core.util.hook.hook @@ -48,7 +48,7 @@ class Notifications : Feature("Notifications") { val userHandle: UserHandle ) { fun send() { - XposedBridge.invokeOriginalMethod(notifyAsUserMethod, notificationManager, arrayOf( + YukiHookCompat.invokeOriginal(notifyAsUserMethod, notificationManager, arrayOf( tag, id, notification, userHandle )) } diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/SendOverride.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/SendOverride.kt index d76a19c1..3c26962f 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/SendOverride.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/SendOverride.kt @@ -22,8 +22,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import de.robv.android.xposed.XC_MethodHook -import de.robv.android.xposed.XposedHelpers import kotlinx.coroutines.* import me.eternal.purrfectsnap.bridge.task.TaskListener import me.eternal.purrfectsnap.common.data.ContentType @@ -40,7 +38,9 @@ import me.eternal.purrfectsnap.core.messaging.MessageSender import me.eternal.purrfectsnap.core.ui.PurrfectOverlayPalette import me.eternal.purrfectsnap.core.ui.PurrfectOverlayTheme import me.eternal.purrfectsnap.core.util.ktx.getObjectFieldOrNull +import me.eternal.purrfectsnap.core.util.ktx.setObjectField import me.eternal.purrfectsnap.core.util.hook.HookStage +import me.eternal.purrfectsnap.core.util.hook.Hooker import me.eternal.purrfectsnap.core.util.hook.hook import me.eternal.purrfectsnap.core.util.hook.hookConstructor import java.text.SimpleDateFormat @@ -65,7 +65,7 @@ class SendOverride : Feature("Send Override") { private var notificationIdCounter = 1000 private val backgroundHookLock = Any() private var backgroundHookRefs = 0 - private var backgroundHooks: List? = null + private var backgroundHooks: List? = null private fun acquireScheduledSendBackground(): () -> Unit { if (!context.config.messaging.scheduledSendAllowRunningInBackground.get()) return {} @@ -98,7 +98,7 @@ class SendOverride : Feature("Send Override") { return runCatching { val duplexClass = findClass("com.snapchat.client.duplex.DuplexClient\$CppProxy") val appStateMethod = duplexClass.methods.firstOrNull { it.name == "appStateChanged" } ?: return false - val hooks = mutableListOf() + val hooks = mutableListOf() hooks.addAll( duplexClass.hook("appStateChanged", HookStage.BEFORE) { param -> if (param.arg(0).toString() == "INACTIVE") param.setResult(null) @@ -493,8 +493,11 @@ class SendOverride : Feature("Send Override") { if (shouldPreventSave) { try { val savePolicyEnumClass = runCatching { - XposedHelpers.findClass("com.snapchat.client.messaging.SavePolicy", - localMessageContent.instanceNonNull().javaClass.classLoader) + Class.forName( + "com.snapchat.client.messaging.SavePolicy", + false, + localMessageContent.instanceNonNull().javaClass.classLoader + ) }.getOrNull() if (savePolicyEnumClass != null && savePolicyEnumClass.isEnum) { @@ -510,7 +513,7 @@ class SendOverride : Feature("Send Override") { if (savePolicyField != null) { savePolicyField.isAccessible = true - XposedHelpers.setObjectField(localMessageContent.instanceNonNull(), "mSavePolicy", prohibitedEnum) + localMessageContent.instanceNonNull().setObjectField("mSavePolicy", prohibitedEnum) } } } diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/tweaks/UnsaveableMessages.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/tweaks/UnsaveableMessages.kt index 6b275dd5..a3b87ea1 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/tweaks/UnsaveableMessages.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/tweaks/UnsaveableMessages.kt @@ -1,6 +1,5 @@ package me.eternal.purrfectsnap.core.features.impl.tweaks -import de.robv.android.xposed.XposedHelpers import me.eternal.purrfectsnap.common.data.ContentType import me.eternal.purrfectsnap.common.data.MessagingRuleType import me.eternal.purrfectsnap.common.util.protobuf.ProtoEditor @@ -8,6 +7,7 @@ import me.eternal.purrfectsnap.common.util.protobuf.ProtoReader import me.eternal.purrfectsnap.core.event.events.impl.NativeUnaryCallEvent import me.eternal.purrfectsnap.core.event.events.impl.SendMessageWithContentEvent import me.eternal.purrfectsnap.core.features.MessagingRuleFeature +import me.eternal.purrfectsnap.core.util.ktx.setObjectField class UnsaveableMessages : MessagingRuleFeature( "Unsaveable Messages", @@ -55,8 +55,9 @@ class UnsaveableMessages : MessagingRuleFeature( // Set mSavePolicy on Java object try { - val savePolicyEnumClass = XposedHelpers.findClass( + val savePolicyEnumClass = Class.forName( "com.snapchat.client.messaging.SavePolicy", + false, localMessageContent.instanceNonNull().javaClass.classLoader ) @@ -66,7 +67,7 @@ class UnsaveableMessages : MessagingRuleFeature( savePolicyEnumClass as Class>, "PROHIBITED" ) - XposedHelpers.setObjectField(localMessageContent.instanceNonNull(), "mSavePolicy", prohibitedEnum) + localMessageContent.instanceNonNull().setObjectField("mSavePolicy", prohibitedEnum) } } catch (e: Exception) { context.log.warn("UnsaveableMessages: Failed to set mSavePolicy: ${e.message}") diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/logger/CoreLogger.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/logger/CoreLogger.kt index db1c49e0..d1ddfe37 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/logger/CoreLogger.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/logger/CoreLogger.kt @@ -2,12 +2,12 @@ package me.eternal.purrfectsnap.core.logger import android.annotation.SuppressLint import android.util.Log -import de.robv.android.xposed.XposedBridge import me.eternal.purrfectsnap.common.logger.AbstractLogger import me.eternal.purrfectsnap.common.logger.LogChannel import me.eternal.purrfectsnap.common.logger.LogLevel import me.eternal.purrfectsnap.core.bridge.BridgeClient import me.eternal.purrfectsnap.core.util.hook.HookStage +import me.eternal.purrfectsnap.core.util.hook.YukiHookCompat import me.eternal.purrfectsnap.core.util.hook.hook @SuppressLint("PrivateApi") @@ -20,13 +20,12 @@ class CoreLogger( fun xposedLog(message: Any?, tag: String = TAG) { Log.println(Log.INFO, tag, message.toString()) - XposedBridge.log("$tag: $message") + Log.i(tag, message.toString()) } fun xposedLog(message: Any?, throwable: Throwable, tag: String = TAG) { Log.println(Log.INFO, tag, message.toString()) - XposedBridge.log("$tag: $message") - XposedBridge.log(throwable) + Log.i(tag, message.toString(), throwable) } } @@ -46,7 +45,7 @@ class CoreLogger( internalLog(tag, LogLevel.fromPriority(priority) ?: LogLevel.INFO, message) } invokeOriginalPrintLog = { priority, tag, message -> - XposedBridge.invokeOriginalMethod( + YukiHookCompat.invokeOriginal( printLnMethod, null, arrayOf(priority, tag, message) diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/CallbackBuilder.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/CallbackBuilder.kt index 08aed558..8ce9d4c9 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/CallbackBuilder.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/CallbackBuilder.kt @@ -1,6 +1,5 @@ package me.eternal.purrfectsnap.core.util -import de.robv.android.xposed.XC_MethodHook import me.eternal.purrfectsnap.core.util.hook.HookAdapter import me.eternal.purrfectsnap.core.util.hook.HookStage import me.eternal.purrfectsnap.core.util.hook.Hooker @@ -39,7 +38,7 @@ class CallbackBuilder( val callbackInstance = createEmptyObject(callbackClass.constructors.first())!! val callbackInstanceHashCode: Int = callbackInstance.hashCode() val callbackInstanceClass = callbackInstance.javaClass - val unhooks = mutableListOf() + val unhooks = mutableListOf() callbackInstanceClass.methods.forEach { method -> if (method.declaringClass != callbackInstanceClass) return@forEach if (Modifier.isPrivate(method.modifiers)) return@forEach diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/hook/HookAdapter.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/hook/HookAdapter.kt index 6671a0fc..ef3ca49e 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/hook/HookAdapter.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/hook/HookAdapter.kt @@ -1,69 +1,68 @@ package me.eternal.purrfectsnap.core.util.hook -import de.robv.android.xposed.XC_MethodHook -import de.robv.android.xposed.XposedBridge import java.lang.reflect.Member import java.util.function.Consumer @Suppress("UNCHECKED_CAST") class HookAdapter( - private val methodHookParam: XC_MethodHook.MethodHookParam<*> + private val methodHookParam: Any ) { fun thisObject(): T { - return methodHookParam.thisObject as T + return YukiHookBridge.instance(methodHookParam) as T } fun nullableThisObject(): T? { - return methodHookParam.thisObject as T? + return YukiHookBridge.instance(methodHookParam) as T? } fun method(): Member { - return methodHookParam.method + return YukiHookBridge.member(methodHookParam) } fun arg(index: Int): T { - return methodHookParam.args[index] as T + return YukiHookBridge.args(methodHookParam)[index] as T } fun argNullable(index: Int): T? { - return methodHookParam.args.getOrNull(index) as T? + return YukiHookBridge.args(methodHookParam).getOrNull(index) as T? } fun setArg(index: Int, value: Any?) { - if (index < 0 || index >= methodHookParam.args.size) return - methodHookParam.args[index] = value + val args = YukiHookBridge.args(methodHookParam) + if (index < 0 || index >= args.size) return + args[index] = value } fun args(): Array { - return methodHookParam.args + return YukiHookBridge.args(methodHookParam) } fun getResult(): Any? { - return methodHookParam.result + return YukiHookBridge.result(methodHookParam) } fun setResult(result: Any?) { - methodHookParam.result = result + YukiHookBridge.setResult(methodHookParam, result) } fun setThrowable(throwable: Throwable) { - methodHookParam.throwable = throwable + YukiHookBridge.setThrowable(methodHookParam, throwable) } fun clearThrowable() { - methodHookParam.throwable = null + YukiHookBridge.setThrowable(methodHookParam, null) } fun throwable(): Throwable? { - return methodHookParam.throwable + return YukiHookBridge.throwable(methodHookParam) } fun invokeOriginal(): Any? { - return XposedBridge.invokeOriginalMethod(method(), thisObject(), args()) + return YukiHookCompat.invokeOriginal(method(), nullableThisObject(), args()) } fun invokeOriginal(args: Array): Any? { - return XposedBridge.invokeOriginalMethod(method(), thisObject(), args) + return YukiHookCompat.invokeOriginal(method(), nullableThisObject(), args) } fun invokeOriginalSafe(errorCallback: Consumer) { @@ -72,7 +71,7 @@ class HookAdapter( fun invokeOriginalSafe(args: Array, errorCallback: Consumer) { runCatching { - setResult(XposedBridge.invokeOriginalMethod(method(), thisObject(), args)) + setResult(YukiHookCompat.invokeOriginal(method(), nullableThisObject(), args)) }.onFailure { errorCallback.accept(it) } diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/hook/HookRuntime.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/hook/HookRuntime.kt new file mode 100644 index 00000000..81840eb8 --- /dev/null +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/hook/HookRuntime.kt @@ -0,0 +1,16 @@ +package me.eternal.purrfectsnap.core.util.hook + +import com.highcapable.yukihookapi.hook.param.PackageParam + +object HookRuntime { + @Volatile + private var currentPackageParam: PackageParam? = null + + fun attach(packageParam: PackageParam) { + currentPackageParam = packageParam + } + + fun packageParam(): PackageParam { + return currentPackageParam ?: error("Yuki PackageParam is not initialized") + } +} diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/hook/Hooker.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/hook/Hooker.kt index 578e94b0..bdfc3b7f 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/hook/Hooker.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/hook/Hooker.kt @@ -1,37 +1,43 @@ package me.eternal.purrfectsnap.core.util.hook -import de.robv.android.xposed.XC_MethodHook -import de.robv.android.xposed.XposedBridge import me.eternal.purrfectsnap.common.logger.AbstractLogger import org.lsposed.hiddenapibypass.HiddenApiBypass import java.lang.reflect.Constructor import java.lang.reflect.Member import java.lang.reflect.Method import java.lang.reflect.Modifier +import java.lang.reflect.AccessibleObject object Hooker { + class HookHandle(private val unhooker: () -> Unit) { + fun unhook() = unhooker() + } + inline fun newMethodHook( stage: HookStage, crossinline consumer: (HookAdapter) -> Unit, crossinline filter: ((HookAdapter) -> Boolean) = { true } - ): XC_MethodHook { - return if (stage == HookStage.BEFORE) object : XC_MethodHook() { - override fun beforeHookedMethod(param: MethodHookParam<*>) { - runCatching { - HookAdapter(param).takeIf(filter)?.also(consumer) - }.onFailure { - AbstractLogger.directError("Failed to execute before hook", it) + ): Any { + return YukiHookBridge.newMemberHook( + { param -> + if (stage == HookStage.BEFORE) { + runCatching { + HookAdapter(param).takeIf(filter)?.also(consumer) + }.onFailure { + AbstractLogger.directError("Failed to execute before hook", it) + } + } + }, + { param -> + if (stage == HookStage.AFTER) { + runCatching { + HookAdapter(param).takeIf(filter)?.also(consumer) + }.onFailure { + AbstractLogger.directError("Failed to execute after hook", it) + } } } - } else object : XC_MethodHook() { - override fun afterHookedMethod(param: MethodHookParam<*>) { - runCatching { - HookAdapter(param).takeIf(filter)?.also(consumer) - }.onFailure { - AbstractLogger.directError("Failed to execute after hook", it) - } - } - } + ) } inline fun hook( @@ -40,15 +46,24 @@ object Hooker { stage: HookStage, crossinline filter: (HookAdapter) -> Boolean, noinline consumer: (HookAdapter) -> Unit - ): Set = XposedBridge.hookAllMethods(clazz, methodName, newMethodHook(stage, consumer, filter)) + ): Set = clazz.declaredMethods + .asSequence() + .filter { it.name == methodName } + .map { method -> + method.isAccessible = true + val hookResult = YukiHookCompat.hookMember(method, newMethodHook(stage, consumer, filter)) + HookHandle { YukiHookCompat.unhook(hookResult) } + }.toMutableSet() inline fun hook( member: Member, stage: HookStage, crossinline filter: ((HookAdapter) -> Boolean), crossinline consumer: (HookAdapter) -> Unit - ): XC_MethodHook.Unhook { - return XposedBridge.hookMethod(member, newMethodHook(stage, consumer, filter)) + ): HookHandle { + (member as? AccessibleObject)?.isAccessible = true + val hookResult = YukiHookCompat.hookMember(member, newMethodHook(stage, consumer, filter)) + return HookHandle { YukiHookCompat.unhook(hookResult) } } fun hook( @@ -56,13 +71,13 @@ object Hooker { methodName: String, stage: HookStage, consumer: (HookAdapter) -> Unit - ): Set = hook(clazz, methodName, stage, { true }, consumer) + ): Set = hook(clazz, methodName, stage, { true }, consumer) fun hook( member: Member, stage: HookStage, consumer: (HookAdapter) -> Unit - ): XC_MethodHook.Unhook { + ): HookHandle { return hook(member, stage, { true }, consumer) } @@ -70,7 +85,13 @@ object Hooker { clazz: Class<*>, stage: HookStage, consumer: (HookAdapter) -> Unit - ): Set = XposedBridge.hookAllConstructors(clazz, newMethodHook(stage, consumer)) + ): Set = clazz.declaredConstructors + .asSequence() + .map { constructor -> + constructor.isAccessible = true + val hookResult = YukiHookCompat.hookMember(constructor, newMethodHook(stage, consumer)) + HookHandle { YukiHookCompat.unhook(hookResult) } + }.toMutableSet() fun hookConstructor( clazz: Class<*>, @@ -78,7 +99,10 @@ object Hooker { filter: ((HookAdapter) -> Boolean), consumer: (HookAdapter) -> Unit ) { - XposedBridge.hookAllConstructors(clazz, newMethodHook(stage, consumer, filter)) + clazz.declaredConstructors.forEach { constructor -> + constructor.isAccessible = true + YukiHookCompat.hookMember(constructor, newMethodHook(stage, consumer, filter)) + } } inline fun hookObjectMethod( @@ -88,7 +112,7 @@ object Hooker { stage: HookStage, crossinline hookConsumer: (HookAdapter) -> Unit ): List<() -> Unit> { - val unhooks = mutableSetOf() + val unhooks = mutableSetOf() hook(clazz, methodName, stage) { param-> if (param.nullableThisObject().let { if (it == null) unhooks.forEach { u -> u.unhook() } @@ -107,7 +131,7 @@ object Hooker { stage: HookStage, crossinline hookConsumer: (HookAdapter) -> Unit ) { - val unhooks: MutableSet = HashSet() + val unhooks: MutableSet = HashSet() hook(clazz, methodName, stage) { param-> hookConsumer(param) unhooks.forEach{ it.unhook() } @@ -121,7 +145,7 @@ object Hooker { stage: HookStage, crossinline hookConsumer: (HookAdapter) -> Unit ): Set<() -> Unit> { - val unhooks = mutableSetOf() + val unhooks = mutableSetOf() hook(clazz, methodName, stage) { param-> if (param.nullableThisObject() != instance) return@hook unhooks.forEach { it.unhook() } @@ -137,7 +161,7 @@ object Hooker { stage: HookStage, crossinline hookConsumer: (HookAdapter) -> Unit ) { - val unhooks: MutableSet = HashSet() + val unhooks: MutableSet = HashSet() hookConstructor(clazz, stage) { param-> hookConsumer(param) unhooks.forEach{ it.unhook() } @@ -160,25 +184,25 @@ fun Class<*>.hook( methodName: String, stage: HookStage, consumer: (HookAdapter) -> Unit -): Set = Hooker.hook(this, methodName, stage, consumer) +): Set = Hooker.hook(this, methodName, stage, consumer) fun Class<*>.hook( methodName: String, stage: HookStage, filter: (HookAdapter) -> Boolean, consumer: (HookAdapter) -> Unit -): Set = Hooker.hook(this, methodName, stage, filter, consumer) +): Set = Hooker.hook(this, methodName, stage, filter, consumer) fun Member.hook( stage: HookStage, consumer: (HookAdapter) -> Unit -): XC_MethodHook.Unhook = Hooker.hook(this, stage, consumer) +): Hooker.HookHandle = Hooker.hook(this, stage, consumer) fun Member.hook( stage: HookStage, filter: ((HookAdapter) -> Boolean), consumer: (HookAdapter) -> Unit -): XC_MethodHook.Unhook = Hooker.hook(this, stage, filter, consumer) +): Hooker.HookHandle = Hooker.hook(this, stage, filter, consumer) fun Array.hookAll(stage: HookStage, param: (HookAdapter) -> Unit) { filter { it.declaringClass != Object::class.java && !Modifier.isAbstract(it.modifiers) }.forEach { diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/hook/YukiHookCompat.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/hook/YukiHookCompat.kt new file mode 100644 index 00000000..cd5ad375 --- /dev/null +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/hook/YukiHookCompat.kt @@ -0,0 +1,17 @@ +package me.eternal.purrfectsnap.core.util.hook + +import java.lang.reflect.Member + +object YukiHookCompat { + fun hookMember(member: Member, callback: Any): Any { + return YukiHookBridge.hookMember(member, callback) + } + + fun invokeOriginal(member: Member, instance: Any?, args: Array): Any? { + return YukiHookBridge.invokeOriginal(member, instance, args) + } + + fun unhook(result: Any) { + YukiHookBridge.unhook(result) + } +} diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/ktx/XposedHelperExt.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/ktx/XposedHelperExt.kt index 60323b60..9fc72774 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/ktx/XposedHelperExt.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/ktx/XposedHelperExt.kt @@ -1,9 +1,19 @@ package me.eternal.purrfectsnap.core.util.ktx -import de.robv.android.xposed.XposedHelpers +private fun findDeclaredFieldRecursive(type: Class<*>, fieldName: String): java.lang.reflect.Field? { + var current: Class<*>? = type + while (current != null && current != Any::class.java) { + current.declaredFields.firstOrNull { it.name == fieldName }?.let { return it } + current = current.superclass + } + return null +} fun Any.getObjectField(fieldName: String): Any? { - return XposedHelpers.getObjectField(this, fieldName) + val field = findDeclaredFieldRecursive(this::class.java, fieldName) + ?: throw NoSuchFieldException("${this::class.java.name}#$fieldName") + field.isAccessible = true + return field.get(this) } fun Any.setEnumField(fieldName: String, value: String) { @@ -14,7 +24,10 @@ fun Any.setEnumField(fieldName: String, value: String) { } fun Any.setObjectField(fieldName: String, value: Any?) { - XposedHelpers.setObjectField(this, fieldName, value) + val field = findDeclaredFieldRecursive(this::class.java, fieldName) + ?: throw NoSuchFieldException("${this::class.java.name}#$fieldName") + field.isAccessible = true + field.set(this, value) } fun Any.getObjectFieldOrNull(fieldName: String): Any? { diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/AbstractWrapper.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/AbstractWrapper.kt index b9c7ddbf..e481bb02 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/AbstractWrapper.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/AbstractWrapper.kt @@ -1,7 +1,8 @@ package me.eternal.purrfectsnap.core.wrapper -import de.robv.android.xposed.XposedHelpers import me.eternal.purrfectsnap.core.util.CallbackBuilder +import me.eternal.purrfectsnap.core.util.ktx.getObjectField +import me.eternal.purrfectsnap.core.util.ktx.setObjectField import me.eternal.purrfectsnap.core.wrapper.impl.SnapUUID import kotlin.reflect.KProperty @@ -20,13 +21,13 @@ abstract class AbstractWrapper( @Suppress("UNCHECKED_CAST") operator fun getValue(obj: Any, property: KProperty<*>): T? { return runCatching { - val value = XposedHelpers.getObjectField(instance, fieldName) + val value = instance?.getObjectField(fieldName) mapper?.invoke(value) ?: value as? T }.getOrNull() } operator fun setValue(obj: Any, property: KProperty<*>, value: Any?) { - XposedHelpers.setObjectField(instance, fieldName, when (value) { + instance?.setObjectField(fieldName, when (value) { is AbstractWrapper -> value.instance is ArrayList<*> -> value.map { if (it is AbstractWrapper) it.instance else it }.toMutableList() else -> value @@ -56,13 +57,13 @@ abstract class AbstractWrapper( fun > getEnumValue(fieldName: String, defaultValue: T?): T? { if (defaultValue == null || instance == null) return null - val mContentType = XposedHelpers.getObjectField(instance, fieldName) as? Enum<*> ?: return null + val mContentType = instance?.getObjectField(fieldName) as? Enum<*> ?: return null return java.lang.Enum.valueOf(defaultValue::class.java, mContentType.name) } @Suppress("UNCHECKED_CAST") fun setEnumValue(fieldName: String, value: Enum<*>) { val type = instance!!.javaClass.declaredFields.find { it.name == fieldName }?.type as Class> - XposedHelpers.setObjectField(instance, fieldName, java.lang.Enum.valueOf(type, value.name)) + instance?.setObjectField(fieldName, java.lang.Enum.valueOf(type, value.name)) } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/valdi/ValdiContext.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/valdi/ValdiContext.kt index 436aa858..d1ffdc74 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/valdi/ValdiContext.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/valdi/ValdiContext.kt @@ -1,6 +1,6 @@ package me.eternal.purrfectsnap.core.wrapper.impl.valdi -import de.robv.android.xposed.XposedHelpers +import me.eternal.purrfectsnap.core.util.ktx.getObjectField import me.eternal.purrfectsnap.core.wrapper.AbstractWrapper import java.lang.ref.WeakReference import java.lang.reflect.Proxy @@ -12,7 +12,7 @@ class ValdiContext(obj: Any): AbstractWrapper(obj) { val componentContext by field>("componentContext") val viewModelLegacy: Any? - get() = runCatching { XposedHelpers.getObjectField(instanceNonNull(), "viewModel") }.getOrNull() + get() = runCatching { instanceNonNull().getObjectField("viewModel") }.getOrNull() ?: instanceNonNull()::class.java.methods.firstOrNull { it.name == "getViewModel" && it.parameterTypes.isEmpty() }?.invoke(instanceNonNull()) fun enqueueNextRenderCallback(callback: () -> Unit) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e6313da3..60376430 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,6 +29,8 @@ hiddenapibypass = "6.1" smart-exception-java = "0.2.1" workmanager = "2.10.4" fetch = "3.4.1" +yukihookapi = "1.3.1" +kavaref = "1.0.2" [libraries] accompanist-navigation-animation = { module = "com.google.accompanist:accompanist-navigation-animation", version.ref = "accompanist" } @@ -66,6 +68,9 @@ recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version rhino = { module = "org.mozilla:rhino", version.ref = "rhino" } rhino-android = { group = "com.faendir.rhino", name = "rhino-android", version.ref = "rhino-android" } smart-exception-java = { module = "com.arthenica:smart-exception-java", version.ref = "smart-exception-java" } +yukihookapi-api = { module = "com.highcapable.yukihookapi:api", version.ref = "yukihookapi" } +kavaref-core = { module = "com.highcapable.kavaref:kavaref-core", version.ref = "kavaref" } +kavaref-extension = { module = "com.highcapable.kavaref:kavaref-extension", version.ref = "kavaref" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 63581be5..988894e9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -115,6 +115,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven { url = uri("https://api.xposed.info/") } maven { url = uri("https://jitpack.io") } } }