migration from libxposed API to YukiHook

This commit is contained in:
ΞTΞRNAL
2026-03-03 19:14:37 +05:30
parent bc252ea9a3
commit 956bdda6c0
21 changed files with 257 additions and 102 deletions

View File

@@ -267,6 +267,7 @@ dependencies {
} }
implementation(project(":core")) implementation(project(":core"))
compileOnly(files("../core/libs/LSPosed-api-1.0-SNAPSHOT.jar"))
implementation(project(":common")) implementation(project(":common"))
implementation(project(":native")) implementation(project(":native"))
implementation(libs.androidx.documentfile) implementation(libs.androidx.documentfile)

View File

@@ -1,6 +1,6 @@
-dontwarn de.robv.android.xposed.**
-dontwarn org.mozilla.javascript.** -dontwarn org.mozilla.javascript.**
-dontwarn android.app.AndroidAppHelper
-dontwarn java.lang.reflect.AnnotatedType
-keep class com.tonyodev.fetch2.** { *; } -keep class com.tonyodev.fetch2.** { *; }
-keep class com.tonyodev.fetch2core.** { *; } -keep class com.tonyodev.fetch2core.** { *; }

View File

@@ -24,6 +24,9 @@ android {
} }
dependencies { dependencies {
implementation(libs.yukihookapi.api)
implementation(libs.kavaref.core)
implementation(libs.kavaref.extension)
compileOnly(files("libs/LSPosed-api-1.0-SNAPSHOT.jar")) compileOnly(files("libs/LSPosed-api-1.0-SNAPSHOT.jar"))
implementation(libs.coroutines) implementation(libs.coroutines)
implementation(libs.recyclerview) implementation(libs.recyclerview)

View File

@@ -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);
}
}

View File

@@ -10,13 +10,15 @@ import me.eternal.purrfectsnap.core.util.hook.HookStage
import me.eternal.purrfectsnap.core.util.hook.hook import me.eternal.purrfectsnap.core.util.hook.hook
class XposedLoader : IXposedHookLoadPackage { class XposedLoader : IXposedHookLoadPackage {
override fun handleLoadPackage(p0: XC_LoadPackage.LoadPackageParam) { override fun handleLoadPackage(param: XC_LoadPackage.LoadPackageParam) {
if (p0.packageName != Constants.SNAPCHAT_PACKAGE_NAME) return if (param.packageName != Constants.SNAPCHAT_PACKAGE_NAME) return
// prevent loading in sub-processes // prevent loading in sub-processes
if (p0.processName.contains(":")) return if (param.processName.contains(":")) return
XposedBridge.log("Loading PurrfectSnap v${BuildConfig.VERSION_NAME}#${BuildConfig.GIT_HASH} (package: ${BuildConfig.APPLICATION_ID})") XposedBridge.log(
Application::class.java.hook("attach", HookStage.BEFORE) { param -> "Loading PurrfectSnap v${BuildConfig.VERSION_NAME}#${BuildConfig.GIT_HASH} (package: ${BuildConfig.APPLICATION_ID})"
PurrfectSnap().init(param.arg(0)) )
Application::class.java.hook("attach", HookStage.BEFORE) { hookParam ->
PurrfectSnap().init(hookParam.arg(0))
} }
} }
} }

View File

@@ -7,7 +7,6 @@ import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.os.* import android.os.*
import android.util.Log import android.util.Log
import de.robv.android.xposed.XposedHelpers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
@@ -106,17 +105,17 @@ class BridgeClient(
this@BridgeClient this@BridgeClient
) )
} else { } 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, this,
"bindServiceAsUser",
intent, intent,
this@BridgeClient, this@BridgeClient,
Context.BIND_AUTO_CREATE, Context.BIND_AUTO_CREATE,
Handler(HandlerThread("BridgeClient").apply { handler,
start()
}.looper),
Process.myUserHandle() Process.myUserHandle()
) ) ?: throw NoSuchMethodException("bindServiceAsUser")
} }
}.onFailure { }.onFailure {
onFailure(it) onFailure(it)

View File

@@ -1,6 +1,5 @@
package me.eternal.purrfectsnap.core.features.impl 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.features.Feature
import me.eternal.purrfectsnap.core.util.hook.HookStage import me.eternal.purrfectsnap.core.util.hook.HookStage
@@ -93,8 +92,10 @@ class ConfigurationOverride : Feature("Configuration Override") {
) { param -> ) { param ->
val enumData = param.arg<Any>(0) val enumData = param.arg<Any>(0)
val key = enumData.toString() val key = enumData.toString()
val setValue: (Any?) -> Unit = { value -> val setValue: (Any?) -> Unit = setValue@{ value ->
val valueHolder = XposedHelpers.callMethod(enumData, configEnumMapping["getValue"]?.getAsString()) 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) valueHolder.setObjectField(configEnumMapping["defaultValueField"]?.getAsString()!!, value)
} }
@@ -148,4 +149,4 @@ class ConfigurationOverride : Feature("Configuration Override") {
} }
} }
} }
} }

View File

@@ -11,7 +11,6 @@ import android.graphics.BitmapFactory
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.UserHandle import android.os.UserHandle
import de.robv.android.xposed.XposedBridge
import kotlinx.coroutines.* import kotlinx.coroutines.*
import me.eternal.purrfectsnap.common.data.ContentType import me.eternal.purrfectsnap.common.data.ContentType
import me.eternal.purrfectsnap.common.data.FileType 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.experiments.BetterTranscript
import me.eternal.purrfectsnap.core.features.impl.spying.StealthMode import me.eternal.purrfectsnap.core.features.impl.spying.StealthMode
import me.eternal.purrfectsnap.core.util.hook.HookStage 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.findRestrictedConstructor
import me.eternal.purrfectsnap.core.util.hook.findRestrictedMethod import me.eternal.purrfectsnap.core.util.hook.findRestrictedMethod
import me.eternal.purrfectsnap.core.util.hook.hook import me.eternal.purrfectsnap.core.util.hook.hook
@@ -48,7 +48,7 @@ class Notifications : Feature("Notifications") {
val userHandle: UserHandle val userHandle: UserHandle
) { ) {
fun send() { fun send() {
XposedBridge.invokeOriginalMethod(notifyAsUserMethod, notificationManager, arrayOf( YukiHookCompat.invokeOriginal(notifyAsUserMethod, notificationManager, arrayOf(
tag, id, notification, userHandle tag, id, notification, userHandle
)) ))
} }

View File

@@ -22,8 +22,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedHelpers
import kotlinx.coroutines.* import kotlinx.coroutines.*
import me.eternal.purrfectsnap.bridge.task.TaskListener import me.eternal.purrfectsnap.bridge.task.TaskListener
import me.eternal.purrfectsnap.common.data.ContentType 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.PurrfectOverlayPalette
import me.eternal.purrfectsnap.core.ui.PurrfectOverlayTheme import me.eternal.purrfectsnap.core.ui.PurrfectOverlayTheme
import me.eternal.purrfectsnap.core.util.ktx.getObjectFieldOrNull 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.HookStage
import me.eternal.purrfectsnap.core.util.hook.Hooker
import me.eternal.purrfectsnap.core.util.hook.hook import me.eternal.purrfectsnap.core.util.hook.hook
import me.eternal.purrfectsnap.core.util.hook.hookConstructor import me.eternal.purrfectsnap.core.util.hook.hookConstructor
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@@ -65,7 +65,7 @@ class SendOverride : Feature("Send Override") {
private var notificationIdCounter = 1000 private var notificationIdCounter = 1000
private val backgroundHookLock = Any() private val backgroundHookLock = Any()
private var backgroundHookRefs = 0 private var backgroundHookRefs = 0
private var backgroundHooks: List<XC_MethodHook.Unhook>? = null private var backgroundHooks: List<Hooker.HookHandle>? = null
private fun acquireScheduledSendBackground(): () -> Unit { private fun acquireScheduledSendBackground(): () -> Unit {
if (!context.config.messaging.scheduledSendAllowRunningInBackground.get()) return {} if (!context.config.messaging.scheduledSendAllowRunningInBackground.get()) return {}
@@ -98,7 +98,7 @@ class SendOverride : Feature("Send Override") {
return runCatching { return runCatching {
val duplexClass = findClass("com.snapchat.client.duplex.DuplexClient\$CppProxy") val duplexClass = findClass("com.snapchat.client.duplex.DuplexClient\$CppProxy")
val appStateMethod = duplexClass.methods.firstOrNull { it.name == "appStateChanged" } ?: return false val appStateMethod = duplexClass.methods.firstOrNull { it.name == "appStateChanged" } ?: return false
val hooks = mutableListOf<XC_MethodHook.Unhook>() val hooks = mutableListOf<Hooker.HookHandle>()
hooks.addAll( hooks.addAll(
duplexClass.hook("appStateChanged", HookStage.BEFORE) { param -> duplexClass.hook("appStateChanged", HookStage.BEFORE) { param ->
if (param.arg<Any>(0).toString() == "INACTIVE") param.setResult(null) if (param.arg<Any>(0).toString() == "INACTIVE") param.setResult(null)
@@ -493,8 +493,11 @@ class SendOverride : Feature("Send Override") {
if (shouldPreventSave) { if (shouldPreventSave) {
try { try {
val savePolicyEnumClass = runCatching { val savePolicyEnumClass = runCatching {
XposedHelpers.findClass("com.snapchat.client.messaging.SavePolicy", Class.forName(
localMessageContent.instanceNonNull().javaClass.classLoader) "com.snapchat.client.messaging.SavePolicy",
false,
localMessageContent.instanceNonNull().javaClass.classLoader
)
}.getOrNull() }.getOrNull()
if (savePolicyEnumClass != null && savePolicyEnumClass.isEnum) { if (savePolicyEnumClass != null && savePolicyEnumClass.isEnum) {
@@ -510,7 +513,7 @@ class SendOverride : Feature("Send Override") {
if (savePolicyField != null) { if (savePolicyField != null) {
savePolicyField.isAccessible = true savePolicyField.isAccessible = true
XposedHelpers.setObjectField(localMessageContent.instanceNonNull(), "mSavePolicy", prohibitedEnum) localMessageContent.instanceNonNull().setObjectField("mSavePolicy", prohibitedEnum)
} }
} }
} }

View File

@@ -1,6 +1,5 @@
package me.eternal.purrfectsnap.core.features.impl.tweaks 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.ContentType
import me.eternal.purrfectsnap.common.data.MessagingRuleType import me.eternal.purrfectsnap.common.data.MessagingRuleType
import me.eternal.purrfectsnap.common.util.protobuf.ProtoEditor 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.NativeUnaryCallEvent
import me.eternal.purrfectsnap.core.event.events.impl.SendMessageWithContentEvent import me.eternal.purrfectsnap.core.event.events.impl.SendMessageWithContentEvent
import me.eternal.purrfectsnap.core.features.MessagingRuleFeature import me.eternal.purrfectsnap.core.features.MessagingRuleFeature
import me.eternal.purrfectsnap.core.util.ktx.setObjectField
class UnsaveableMessages : MessagingRuleFeature( class UnsaveableMessages : MessagingRuleFeature(
"Unsaveable Messages", "Unsaveable Messages",
@@ -55,8 +55,9 @@ class UnsaveableMessages : MessagingRuleFeature(
// Set mSavePolicy on Java object // Set mSavePolicy on Java object
try { try {
val savePolicyEnumClass = XposedHelpers.findClass( val savePolicyEnumClass = Class.forName(
"com.snapchat.client.messaging.SavePolicy", "com.snapchat.client.messaging.SavePolicy",
false,
localMessageContent.instanceNonNull().javaClass.classLoader localMessageContent.instanceNonNull().javaClass.classLoader
) )
@@ -66,7 +67,7 @@ class UnsaveableMessages : MessagingRuleFeature(
savePolicyEnumClass as Class<out Enum<*>>, savePolicyEnumClass as Class<out Enum<*>>,
"PROHIBITED" "PROHIBITED"
) )
XposedHelpers.setObjectField(localMessageContent.instanceNonNull(), "mSavePolicy", prohibitedEnum) localMessageContent.instanceNonNull().setObjectField("mSavePolicy", prohibitedEnum)
} }
} catch (e: Exception) { } catch (e: Exception) {
context.log.warn("UnsaveableMessages: Failed to set mSavePolicy: ${e.message}") context.log.warn("UnsaveableMessages: Failed to set mSavePolicy: ${e.message}")

View File

@@ -2,12 +2,12 @@ package me.eternal.purrfectsnap.core.logger
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.util.Log import android.util.Log
import de.robv.android.xposed.XposedBridge
import me.eternal.purrfectsnap.common.logger.AbstractLogger import me.eternal.purrfectsnap.common.logger.AbstractLogger
import me.eternal.purrfectsnap.common.logger.LogChannel import me.eternal.purrfectsnap.common.logger.LogChannel
import me.eternal.purrfectsnap.common.logger.LogLevel import me.eternal.purrfectsnap.common.logger.LogLevel
import me.eternal.purrfectsnap.core.bridge.BridgeClient import me.eternal.purrfectsnap.core.bridge.BridgeClient
import me.eternal.purrfectsnap.core.util.hook.HookStage import me.eternal.purrfectsnap.core.util.hook.HookStage
import me.eternal.purrfectsnap.core.util.hook.YukiHookCompat
import me.eternal.purrfectsnap.core.util.hook.hook import me.eternal.purrfectsnap.core.util.hook.hook
@SuppressLint("PrivateApi") @SuppressLint("PrivateApi")
@@ -20,13 +20,12 @@ class CoreLogger(
fun xposedLog(message: Any?, tag: String = TAG) { fun xposedLog(message: Any?, tag: String = TAG) {
Log.println(Log.INFO, tag, message.toString()) 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) { fun xposedLog(message: Any?, throwable: Throwable, tag: String = TAG) {
Log.println(Log.INFO, tag, message.toString()) Log.println(Log.INFO, tag, message.toString())
XposedBridge.log("$tag: $message") Log.i(tag, message.toString(), throwable)
XposedBridge.log(throwable)
} }
} }
@@ -46,7 +45,7 @@ class CoreLogger(
internalLog(tag, LogLevel.fromPriority(priority) ?: LogLevel.INFO, message) internalLog(tag, LogLevel.fromPriority(priority) ?: LogLevel.INFO, message)
} }
invokeOriginalPrintLog = { priority, tag, message -> invokeOriginalPrintLog = { priority, tag, message ->
XposedBridge.invokeOriginalMethod( YukiHookCompat.invokeOriginal(
printLnMethod, printLnMethod,
null, null,
arrayOf<Any?>(priority, tag, message) arrayOf<Any?>(priority, tag, message)

View File

@@ -1,6 +1,5 @@
package me.eternal.purrfectsnap.core.util 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.HookAdapter
import me.eternal.purrfectsnap.core.util.hook.HookStage import me.eternal.purrfectsnap.core.util.hook.HookStage
import me.eternal.purrfectsnap.core.util.hook.Hooker import me.eternal.purrfectsnap.core.util.hook.Hooker
@@ -39,7 +38,7 @@ class CallbackBuilder(
val callbackInstance = createEmptyObject(callbackClass.constructors.first())!! val callbackInstance = createEmptyObject(callbackClass.constructors.first())!!
val callbackInstanceHashCode: Int = callbackInstance.hashCode() val callbackInstanceHashCode: Int = callbackInstance.hashCode()
val callbackInstanceClass = callbackInstance.javaClass val callbackInstanceClass = callbackInstance.javaClass
val unhooks = mutableListOf<XC_MethodHook.Unhook>() val unhooks = mutableListOf<Hooker.HookHandle>()
callbackInstanceClass.methods.forEach { method -> callbackInstanceClass.methods.forEach { method ->
if (method.declaringClass != callbackInstanceClass) return@forEach if (method.declaringClass != callbackInstanceClass) return@forEach
if (Modifier.isPrivate(method.modifiers)) return@forEach if (Modifier.isPrivate(method.modifiers)) return@forEach

View File

@@ -1,69 +1,68 @@
package me.eternal.purrfectsnap.core.util.hook 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.lang.reflect.Member
import java.util.function.Consumer import java.util.function.Consumer
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
class HookAdapter( class HookAdapter(
private val methodHookParam: XC_MethodHook.MethodHookParam<*> private val methodHookParam: Any
) { ) {
fun <T : Any> thisObject(): T { fun <T : Any> thisObject(): T {
return methodHookParam.thisObject as T return YukiHookBridge.instance(methodHookParam) as T
} }
fun <T : Any> nullableThisObject(): T? { fun <T : Any> nullableThisObject(): T? {
return methodHookParam.thisObject as T? return YukiHookBridge.instance(methodHookParam) as T?
} }
fun method(): Member { fun method(): Member {
return methodHookParam.method return YukiHookBridge.member(methodHookParam)
} }
fun <T : Any> arg(index: Int): T { fun <T : Any> arg(index: Int): T {
return methodHookParam.args[index] as T return YukiHookBridge.args(methodHookParam)[index] as T
} }
fun <T : Any> argNullable(index: Int): T? { fun <T : Any> 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?) { fun setArg(index: Int, value: Any?) {
if (index < 0 || index >= methodHookParam.args.size) return val args = YukiHookBridge.args(methodHookParam)
methodHookParam.args[index] = value if (index < 0 || index >= args.size) return
args[index] = value
} }
fun args(): Array<Any?> { fun args(): Array<Any?> {
return methodHookParam.args return YukiHookBridge.args(methodHookParam)
} }
fun getResult(): Any? { fun getResult(): Any? {
return methodHookParam.result return YukiHookBridge.result(methodHookParam)
} }
fun setResult(result: Any?) { fun setResult(result: Any?) {
methodHookParam.result = result YukiHookBridge.setResult(methodHookParam, result)
} }
fun setThrowable(throwable: Throwable) { fun setThrowable(throwable: Throwable) {
methodHookParam.throwable = throwable YukiHookBridge.setThrowable(methodHookParam, throwable)
} }
fun clearThrowable() { fun clearThrowable() {
methodHookParam.throwable = null YukiHookBridge.setThrowable(methodHookParam, null)
} }
fun throwable(): Throwable? { fun throwable(): Throwable? {
return methodHookParam.throwable return YukiHookBridge.throwable(methodHookParam)
} }
fun invokeOriginal(): Any? { fun invokeOriginal(): Any? {
return XposedBridge.invokeOriginalMethod(method(), thisObject(), args()) return YukiHookCompat.invokeOriginal(method(), nullableThisObject<Any>(), args())
} }
fun invokeOriginal(args: Array<Any?>): Any? { fun invokeOriginal(args: Array<Any?>): Any? {
return XposedBridge.invokeOriginalMethod(method(), thisObject(), args) return YukiHookCompat.invokeOriginal(method(), nullableThisObject<Any>(), args)
} }
fun invokeOriginalSafe(errorCallback: Consumer<Throwable>) { fun invokeOriginalSafe(errorCallback: Consumer<Throwable>) {
@@ -72,7 +71,7 @@ class HookAdapter(
fun invokeOriginalSafe(args: Array<Any?>, errorCallback: Consumer<Throwable>) { fun invokeOriginalSafe(args: Array<Any?>, errorCallback: Consumer<Throwable>) {
runCatching { runCatching {
setResult(XposedBridge.invokeOriginalMethod(method(), thisObject(), args)) setResult(YukiHookCompat.invokeOriginal(method(), nullableThisObject<Any>(), args))
}.onFailure { }.onFailure {
errorCallback.accept(it) errorCallback.accept(it)
} }

View File

@@ -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")
}
}

View File

@@ -1,37 +1,43 @@
package me.eternal.purrfectsnap.core.util.hook 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 me.eternal.purrfectsnap.common.logger.AbstractLogger
import org.lsposed.hiddenapibypass.HiddenApiBypass import org.lsposed.hiddenapibypass.HiddenApiBypass
import java.lang.reflect.Constructor import java.lang.reflect.Constructor
import java.lang.reflect.Member import java.lang.reflect.Member
import java.lang.reflect.Method import java.lang.reflect.Method
import java.lang.reflect.Modifier import java.lang.reflect.Modifier
import java.lang.reflect.AccessibleObject
object Hooker { object Hooker {
class HookHandle(private val unhooker: () -> Unit) {
fun unhook() = unhooker()
}
inline fun newMethodHook( inline fun newMethodHook(
stage: HookStage, stage: HookStage,
crossinline consumer: (HookAdapter) -> Unit, crossinline consumer: (HookAdapter) -> Unit,
crossinline filter: ((HookAdapter) -> Boolean) = { true } crossinline filter: ((HookAdapter) -> Boolean) = { true }
): XC_MethodHook { ): Any {
return if (stage == HookStage.BEFORE) object : XC_MethodHook() { return YukiHookBridge.newMemberHook(
override fun beforeHookedMethod(param: MethodHookParam<*>) { { param ->
runCatching { if (stage == HookStage.BEFORE) {
HookAdapter(param).takeIf(filter)?.also(consumer) runCatching {
}.onFailure { HookAdapter(param).takeIf(filter)?.also(consumer)
AbstractLogger.directError("Failed to execute before hook", it) }.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( inline fun hook(
@@ -40,15 +46,24 @@ object Hooker {
stage: HookStage, stage: HookStage,
crossinline filter: (HookAdapter) -> Boolean, crossinline filter: (HookAdapter) -> Boolean,
noinline consumer: (HookAdapter) -> Unit noinline consumer: (HookAdapter) -> Unit
): Set<XC_MethodHook.Unhook> = XposedBridge.hookAllMethods(clazz, methodName, newMethodHook(stage, consumer, filter)) ): Set<HookHandle> = 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( inline fun hook(
member: Member, member: Member,
stage: HookStage, stage: HookStage,
crossinline filter: ((HookAdapter) -> Boolean), crossinline filter: ((HookAdapter) -> Boolean),
crossinline consumer: (HookAdapter) -> Unit crossinline consumer: (HookAdapter) -> Unit
): XC_MethodHook.Unhook { ): HookHandle {
return XposedBridge.hookMethod(member, newMethodHook(stage, consumer, filter)) (member as? AccessibleObject)?.isAccessible = true
val hookResult = YukiHookCompat.hookMember(member, newMethodHook(stage, consumer, filter))
return HookHandle { YukiHookCompat.unhook(hookResult) }
} }
fun hook( fun hook(
@@ -56,13 +71,13 @@ object Hooker {
methodName: String, methodName: String,
stage: HookStage, stage: HookStage,
consumer: (HookAdapter) -> Unit consumer: (HookAdapter) -> Unit
): Set<XC_MethodHook.Unhook> = hook(clazz, methodName, stage, { true }, consumer) ): Set<HookHandle> = hook(clazz, methodName, stage, { true }, consumer)
fun hook( fun hook(
member: Member, member: Member,
stage: HookStage, stage: HookStage,
consumer: (HookAdapter) -> Unit consumer: (HookAdapter) -> Unit
): XC_MethodHook.Unhook { ): HookHandle {
return hook(member, stage, { true }, consumer) return hook(member, stage, { true }, consumer)
} }
@@ -70,7 +85,13 @@ object Hooker {
clazz: Class<*>, clazz: Class<*>,
stage: HookStage, stage: HookStage,
consumer: (HookAdapter) -> Unit consumer: (HookAdapter) -> Unit
): Set<XC_MethodHook.Unhook> = XposedBridge.hookAllConstructors(clazz, newMethodHook(stage, consumer)) ): Set<HookHandle> = clazz.declaredConstructors
.asSequence()
.map { constructor ->
constructor.isAccessible = true
val hookResult = YukiHookCompat.hookMember(constructor, newMethodHook(stage, consumer))
HookHandle { YukiHookCompat.unhook(hookResult) }
}.toMutableSet()
fun hookConstructor( fun hookConstructor(
clazz: Class<*>, clazz: Class<*>,
@@ -78,7 +99,10 @@ object Hooker {
filter: ((HookAdapter) -> Boolean), filter: ((HookAdapter) -> Boolean),
consumer: (HookAdapter) -> Unit 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( inline fun hookObjectMethod(
@@ -88,7 +112,7 @@ object Hooker {
stage: HookStage, stage: HookStage,
crossinline hookConsumer: (HookAdapter) -> Unit crossinline hookConsumer: (HookAdapter) -> Unit
): List<() -> Unit> { ): List<() -> Unit> {
val unhooks = mutableSetOf<XC_MethodHook.Unhook>() val unhooks = mutableSetOf<HookHandle>()
hook(clazz, methodName, stage) { param-> hook(clazz, methodName, stage) { param->
if (param.nullableThisObject<Any>().let { if (param.nullableThisObject<Any>().let {
if (it == null) unhooks.forEach { u -> u.unhook() } if (it == null) unhooks.forEach { u -> u.unhook() }
@@ -107,7 +131,7 @@ object Hooker {
stage: HookStage, stage: HookStage,
crossinline hookConsumer: (HookAdapter) -> Unit crossinline hookConsumer: (HookAdapter) -> Unit
) { ) {
val unhooks: MutableSet<XC_MethodHook.Unhook> = HashSet() val unhooks: MutableSet<HookHandle> = HashSet()
hook(clazz, methodName, stage) { param-> hook(clazz, methodName, stage) { param->
hookConsumer(param) hookConsumer(param)
unhooks.forEach{ it.unhook() } unhooks.forEach{ it.unhook() }
@@ -121,7 +145,7 @@ object Hooker {
stage: HookStage, stage: HookStage,
crossinline hookConsumer: (HookAdapter) -> Unit crossinline hookConsumer: (HookAdapter) -> Unit
): Set<() -> Unit> { ): Set<() -> Unit> {
val unhooks = mutableSetOf<XC_MethodHook.Unhook>() val unhooks = mutableSetOf<HookHandle>()
hook(clazz, methodName, stage) { param-> hook(clazz, methodName, stage) { param->
if (param.nullableThisObject<Any>() != instance) return@hook if (param.nullableThisObject<Any>() != instance) return@hook
unhooks.forEach { it.unhook() } unhooks.forEach { it.unhook() }
@@ -137,7 +161,7 @@ object Hooker {
stage: HookStage, stage: HookStage,
crossinline hookConsumer: (HookAdapter) -> Unit crossinline hookConsumer: (HookAdapter) -> Unit
) { ) {
val unhooks: MutableSet<XC_MethodHook.Unhook> = HashSet() val unhooks: MutableSet<HookHandle> = HashSet()
hookConstructor(clazz, stage) { param-> hookConstructor(clazz, stage) { param->
hookConsumer(param) hookConsumer(param)
unhooks.forEach{ it.unhook() } unhooks.forEach{ it.unhook() }
@@ -160,25 +184,25 @@ fun Class<*>.hook(
methodName: String, methodName: String,
stage: HookStage, stage: HookStage,
consumer: (HookAdapter) -> Unit consumer: (HookAdapter) -> Unit
): Set<XC_MethodHook.Unhook> = Hooker.hook(this, methodName, stage, consumer) ): Set<Hooker.HookHandle> = Hooker.hook(this, methodName, stage, consumer)
fun Class<*>.hook( fun Class<*>.hook(
methodName: String, methodName: String,
stage: HookStage, stage: HookStage,
filter: (HookAdapter) -> Boolean, filter: (HookAdapter) -> Boolean,
consumer: (HookAdapter) -> Unit consumer: (HookAdapter) -> Unit
): Set<XC_MethodHook.Unhook> = Hooker.hook(this, methodName, stage, filter, consumer) ): Set<Hooker.HookHandle> = Hooker.hook(this, methodName, stage, filter, consumer)
fun Member.hook( fun Member.hook(
stage: HookStage, stage: HookStage,
consumer: (HookAdapter) -> Unit consumer: (HookAdapter) -> Unit
): XC_MethodHook.Unhook = Hooker.hook(this, stage, consumer) ): Hooker.HookHandle = Hooker.hook(this, stage, consumer)
fun Member.hook( fun Member.hook(
stage: HookStage, stage: HookStage,
filter: ((HookAdapter) -> Boolean), filter: ((HookAdapter) -> Boolean),
consumer: (HookAdapter) -> Unit consumer: (HookAdapter) -> Unit
): XC_MethodHook.Unhook = Hooker.hook(this, stage, filter, consumer) ): Hooker.HookHandle = Hooker.hook(this, stage, filter, consumer)
fun Array<Method>.hookAll(stage: HookStage, param: (HookAdapter) -> Unit) { fun Array<Method>.hookAll(stage: HookStage, param: (HookAdapter) -> Unit) {
filter { it.declaringClass != Object::class.java && !Modifier.isAbstract(it.modifiers) }.forEach { filter { it.declaringClass != Object::class.java && !Modifier.isAbstract(it.modifiers) }.forEach {

View File

@@ -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?>): Any? {
return YukiHookBridge.invokeOriginal(member, instance, args)
}
fun unhook(result: Any) {
YukiHookBridge.unhook(result)
}
}

View File

@@ -1,9 +1,19 @@
package me.eternal.purrfectsnap.core.util.ktx 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? { 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) { 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?) { 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? { fun Any.getObjectFieldOrNull(fieldName: String): Any? {

View File

@@ -1,7 +1,8 @@
package me.eternal.purrfectsnap.core.wrapper 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.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 me.eternal.purrfectsnap.core.wrapper.impl.SnapUUID
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
@@ -20,13 +21,13 @@ abstract class AbstractWrapper(
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
operator fun getValue(obj: Any, property: KProperty<*>): T? { operator fun getValue(obj: Any, property: KProperty<*>): T? {
return runCatching { return runCatching {
val value = XposedHelpers.getObjectField(instance, fieldName) val value = instance?.getObjectField(fieldName)
mapper?.invoke(value) ?: value as? T mapper?.invoke(value) ?: value as? T
}.getOrNull() }.getOrNull()
} }
operator fun setValue(obj: Any, property: KProperty<*>, value: Any?) { 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 AbstractWrapper -> value.instance
is ArrayList<*> -> value.map { if (it is AbstractWrapper) it.instance else it }.toMutableList() is ArrayList<*> -> value.map { if (it is AbstractWrapper) it.instance else it }.toMutableList()
else -> value else -> value
@@ -56,13 +57,13 @@ abstract class AbstractWrapper(
fun <T : Enum<*>> getEnumValue(fieldName: String, defaultValue: T?): T? { fun <T : Enum<*>> getEnumValue(fieldName: String, defaultValue: T?): T? {
if (defaultValue == null || instance == null) return null 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) return java.lang.Enum.valueOf(defaultValue::class.java, mContentType.name)
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun setEnumValue(fieldName: String, value: Enum<*>) { fun setEnumValue(fieldName: String, value: Enum<*>) {
val type = instance!!.javaClass.declaredFields.find { it.name == fieldName }?.type as Class<out Enum<*>> val type = instance!!.javaClass.declaredFields.find { it.name == fieldName }?.type as Class<out Enum<*>>
XposedHelpers.setObjectField(instance, fieldName, java.lang.Enum.valueOf(type, value.name)) instance?.setObjectField(fieldName, java.lang.Enum.valueOf(type, value.name))
} }
} }

View File

@@ -1,6 +1,6 @@
package me.eternal.purrfectsnap.core.wrapper.impl.valdi 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 me.eternal.purrfectsnap.core.wrapper.AbstractWrapper
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.lang.reflect.Proxy import java.lang.reflect.Proxy
@@ -12,7 +12,7 @@ class ValdiContext(obj: Any): AbstractWrapper(obj) {
val componentContext by field<WeakReference<Any?>>("componentContext") val componentContext by field<WeakReference<Any?>>("componentContext")
val viewModelLegacy: Any? 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()) ?: instanceNonNull()::class.java.methods.firstOrNull { it.name == "getViewModel" && it.parameterTypes.isEmpty() }?.invoke(instanceNonNull())
fun enqueueNextRenderCallback(callback: () -> Unit) { fun enqueueNextRenderCallback(callback: () -> Unit) {

View File

@@ -29,6 +29,8 @@ hiddenapibypass = "6.1"
smart-exception-java = "0.2.1" smart-exception-java = "0.2.1"
workmanager = "2.10.4" workmanager = "2.10.4"
fetch = "3.4.1" fetch = "3.4.1"
yukihookapi = "1.3.1"
kavaref = "1.0.2"
[libraries] [libraries]
accompanist-navigation-animation = { module = "com.google.accompanist:accompanist-navigation-animation", version.ref = "accompanist" } 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 = { module = "org.mozilla:rhino", version.ref = "rhino" }
rhino-android = { group = "com.faendir.rhino", name = "rhino-android", version.ref = "rhino-android" } 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" } 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] [plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" } androidApplication = { id = "com.android.application", version.ref = "agp" }

View File

@@ -115,6 +115,7 @@ dependencyResolutionManagement {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven { url = uri("https://api.xposed.info/") }
maven { url = uri("https://jitpack.io") } maven { url = uri("https://jitpack.io") }
} }
} }