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"))
compileOnly(files("../core/libs/LSPosed-api-1.0-SNAPSHOT.jar"))
implementation(project(":common"))
implementation(project(":native"))
implementation(libs.androidx.documentfile)

View File

@@ -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.** { *; }

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -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<XC_MethodHook.Unhook>? = null
private var backgroundHooks: List<Hooker.HookHandle>? = 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<XC_MethodHook.Unhook>()
val hooks = mutableListOf<Hooker.HookHandle>()
hooks.addAll(
duplexClass.hook("appStateChanged", HookStage.BEFORE) { param ->
if (param.arg<Any>(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)
}
}
}

View File

@@ -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<out Enum<*>>,
"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}")

View File

@@ -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<Any?>(priority, tag, message)

View File

@@ -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<XC_MethodHook.Unhook>()
val unhooks = mutableListOf<Hooker.HookHandle>()
callbackInstanceClass.methods.forEach { method ->
if (method.declaringClass != callbackInstanceClass) return@forEach
if (Modifier.isPrivate(method.modifiers)) return@forEach

View File

@@ -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 <T : Any> thisObject(): T {
return methodHookParam.thisObject as T
return YukiHookBridge.instance(methodHookParam) as T
}
fun <T : Any> nullableThisObject(): T? {
return methodHookParam.thisObject as T?
return YukiHookBridge.instance(methodHookParam) as T?
}
fun method(): Member {
return methodHookParam.method
return YukiHookBridge.member(methodHookParam)
}
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? {
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<Any?> {
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<Any>(), args())
}
fun invokeOriginal(args: Array<Any?>): Any? {
return XposedBridge.invokeOriginalMethod(method(), thisObject(), args)
return YukiHookCompat.invokeOriginal(method(), nullableThisObject<Any>(), args)
}
fun invokeOriginalSafe(errorCallback: Consumer<Throwable>) {
@@ -72,7 +71,7 @@ class HookAdapter(
fun invokeOriginalSafe(args: Array<Any?>, errorCallback: Consumer<Throwable>) {
runCatching {
setResult(XposedBridge.invokeOriginalMethod(method(), thisObject(), args))
setResult(YukiHookCompat.invokeOriginal(method(), nullableThisObject<Any>(), args))
}.onFailure {
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
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<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(
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<XC_MethodHook.Unhook> = hook(clazz, methodName, stage, { true }, consumer)
): Set<HookHandle> = 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<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(
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<XC_MethodHook.Unhook>()
val unhooks = mutableSetOf<HookHandle>()
hook(clazz, methodName, stage) { param->
if (param.nullableThisObject<Any>().let {
if (it == null) unhooks.forEach { u -> u.unhook() }
@@ -107,7 +131,7 @@ object Hooker {
stage: HookStage,
crossinline hookConsumer: (HookAdapter) -> Unit
) {
val unhooks: MutableSet<XC_MethodHook.Unhook> = HashSet()
val unhooks: MutableSet<HookHandle> = 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<XC_MethodHook.Unhook>()
val unhooks = mutableSetOf<HookHandle>()
hook(clazz, methodName, stage) { param->
if (param.nullableThisObject<Any>() != instance) return@hook
unhooks.forEach { it.unhook() }
@@ -137,7 +161,7 @@ object Hooker {
stage: HookStage,
crossinline hookConsumer: (HookAdapter) -> Unit
) {
val unhooks: MutableSet<XC_MethodHook.Unhook> = HashSet()
val unhooks: MutableSet<HookHandle> = 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<XC_MethodHook.Unhook> = Hooker.hook(this, methodName, stage, consumer)
): Set<Hooker.HookHandle> = Hooker.hook(this, methodName, stage, consumer)
fun Class<*>.hook(
methodName: String,
stage: HookStage,
filter: (HookAdapter) -> Boolean,
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(
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<Method>.hookAll(stage: HookStage, param: (HookAdapter) -> Unit) {
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
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? {

View File

@@ -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 <T : Enum<*>> 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<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
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<WeakReference<Any?>>("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) {

View File

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

View File

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