migration from libxposed API to YukiHook
This commit is contained in:
@@ -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)
|
||||
|
||||
4
app/proguard-rules.pro
vendored
4
app/proguard-rules.pro
vendored
@@ -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.** { *; }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,35 @@
|
||||
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<*>) {
|
||||
): 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)
|
||||
}
|
||||
}
|
||||
} else object : XC_MethodHook() {
|
||||
override fun afterHookedMethod(param: MethodHookParam<*>) {
|
||||
},
|
||||
{ param ->
|
||||
if (stage == HookStage.AFTER) {
|
||||
runCatching {
|
||||
HookAdapter(param).takeIf(filter)?.also(consumer)
|
||||
}.onFailure {
|
||||
@@ -32,6 +37,7 @@ object Hooker {
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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? {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -115,6 +115,7 @@ dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url = uri("https://api.xposed.info/") }
|
||||
maven { url = uri("https://jitpack.io") }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user