Update LogBoxModule.kt
This commit is contained in:
parent
69496c1f3c
commit
06dde39cfd
1 changed files with 98 additions and 187 deletions
|
|
@ -1,13 +1,9 @@
|
||||||
package GoonXposed.xposed.modules.LogBox
|
package GoonXposed.xposed.modules.LogBox
|
||||||
|
|
||||||
import GoonXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import GoonXposed.xposed.Utils.Companion.reloadApp
|
|
||||||
import GoonXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
import GoonXposed.xposed.hook
|
import GoonXposed.xposed.Utils.Companion.reloadApp
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import de.robv.android.xposed.XC_MethodHook
|
|
||||||
import de.robv.android.xposed.XC_MethodHook.MethodHookParam
|
|
||||||
import de.robv.android.xposed.XC_MethodReplacement
|
import de.robv.android.xposed.XC_MethodReplacement
|
||||||
import de.robv.android.xposed.XposedBridge
|
import de.robv.android.xposed.XposedBridge
|
||||||
import de.robv.android.xposed.XposedHelpers
|
import de.robv.android.xposed.XposedHelpers
|
||||||
|
|
@ -18,59 +14,26 @@ object LogBoxModule : Module() {
|
||||||
lateinit var packageParam: XC_LoadPackage.LoadPackageParam
|
lateinit var packageParam: XC_LoadPackage.LoadPackageParam
|
||||||
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||||
var contextForMenu: Context? = null
|
var contextForMenu: Context? = null
|
||||||
private val hookedMethods = mutableSetOf<String>()
|
|
||||||
|
|
||||||
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
||||||
this@LogBoxModule.packageParam = packageParam
|
this@LogBoxModule.packageParam = packageParam
|
||||||
|
|
||||||
// Enable React Native developer support so the shake gesture is detected.
|
try {
|
||||||
// MUST happen before the ReactHost is created (i.e. before the first Activity.onCreate),
|
val dcdReactNativeHostClass = classLoader.loadClass("com.discord.bridge.DCDReactNativeHost")
|
||||||
// otherwise useDeveloperSupport is read once and cached.
|
val getUseDeveloperSupportMethod =
|
||||||
enableDeveloperSupport(classLoader)
|
dcdReactNativeHostClass.methods.first { it.name == "getUseDeveloperSupport" }
|
||||||
|
|
||||||
return@with
|
getUseDeveloperSupportMethod.hook {
|
||||||
}
|
|
||||||
|
|
||||||
private fun enableDeveloperSupport(classLoader: ClassLoader) {
|
|
||||||
val hostCandidates = listOf(
|
|
||||||
"com.discord.bridge.DCDReactNativeHost"
|
|
||||||
)
|
|
||||||
|
|
||||||
for (hostName in hostCandidates) {
|
|
||||||
if (hookGetUseDeveloperSupport(classLoader, hostName)) {
|
|
||||||
Log.e("Successfully hooked $hostName for dev support")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.e("Failed to hook any ReactNativeHost for dev support")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hookGetUseDeveloperSupport(classLoader: ClassLoader, hostName: String): Boolean {
|
|
||||||
val hostClass = try {
|
|
||||||
XposedHelpers.findClassIfExists(hostName, classLoader)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
null
|
|
||||||
} ?: return false
|
|
||||||
|
|
||||||
val method = hostClass.declaredMethods.firstOrNull { it.name == "getUseDeveloperSupport" }
|
|
||||||
?: hostClass.methods.firstOrNull { it.name == "getUseDeveloperSupport" }
|
|
||||||
?: return false
|
|
||||||
|
|
||||||
return try {
|
|
||||||
method.isAccessible = true
|
|
||||||
val signature = "${method.declaringClass.name}#${method.name}"
|
|
||||||
if (hookedMethods.add(signature)) {
|
|
||||||
method.hook {
|
|
||||||
before {
|
before {
|
||||||
result = true
|
result = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Log.e("Successfully hooked DCDReactNativeHost")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Failed to hook DCDReactNativeHost: ${e.message}")
|
||||||
}
|
}
|
||||||
true
|
|
||||||
} catch (e: Throwable) {
|
return@with
|
||||||
Log.e("Failed to hook getUseDeveloperSupport on $hostName: ${e.message}")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onContext(context: Context) {
|
override fun onContext(context: Context) {
|
||||||
|
|
@ -83,105 +46,98 @@ object LogBoxModule : Module() {
|
||||||
"com.facebook.react.devsupport.BridgelessDevSupportManager",
|
"com.facebook.react.devsupport.BridgelessDevSupportManager",
|
||||||
"com.facebook.react.devsupport.DevSupportManagerImpl",
|
"com.facebook.react.devsupport.DevSupportManagerImpl",
|
||||||
"com.facebook.react.devsupport.DevSupportManagerBase",
|
"com.facebook.react.devsupport.DevSupportManagerBase",
|
||||||
"com.facebook.react.devsupport.DefaultDevSupportManager",
|
"com.facebook.react.devsupport.DefaultDevSupportManager"
|
||||||
"com.facebook.react.devsupport.DevSupportManager"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var foundAny = false
|
var foundAny = false
|
||||||
possibleClasses.forEach { className ->
|
possibleClasses.forEach { className ->
|
||||||
val clazz = try {
|
try {
|
||||||
XposedHelpers.findClassIfExists(className, packageParam.classLoader)
|
val clazz = packageParam.classLoader.loadClass(className)
|
||||||
} catch (e: Throwable) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
if (clazz == null) {
|
|
||||||
Log.e("Class not found: $className")
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
Log.e("Found class: $className")
|
Log.e("Found class: $className")
|
||||||
hookDevSupportManager(clazz)
|
hookDevSupportManager(clazz, context)
|
||||||
foundAny = true
|
foundAny = true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Class not found: $className - ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!foundAny) {
|
if (!foundAny) {
|
||||||
Log.e("No DevSupport class was found - menu will not show on shake")
|
tryFindDevSupportClasses(context)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Handle exception
|
// Handle exception
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hookDevSupportManager(clazz: Class<*>) {
|
private fun tryFindDevSupportClasses(context: Context) {
|
||||||
|
try {
|
||||||
|
val dexFile = packageParam.classLoader.javaClass.getDeclaredField("pathList")
|
||||||
|
dexFile.isAccessible = true
|
||||||
|
Log.e("Searching for DevSupport classes in classloader...")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Could not search for classes: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hookDevSupportManager(clazz: Class<*>, context: Context) {
|
||||||
Log.e("Attempting to hook ${clazz.name}")
|
Log.e("Attempting to hook ${clazz.name}")
|
||||||
|
|
||||||
hookReloadJS(clazz)
|
Log.e("Available methods in ${clazz.simpleName}:")
|
||||||
|
clazz.methods.forEach { method ->
|
||||||
val showMethod = clazz.declaredMethods.firstOrNull { it.name == "showDevOptionsDialog" }
|
if (method.name.contains("Dev") || method.name.contains("Reload") || method.name.contains("Options")) {
|
||||||
?: clazz.methods.firstOrNull { it.name == "showDevOptionsDialog" }
|
Log.e(" - ${method.name}")
|
||||||
?: run {
|
|
||||||
Log.e("showDevOptionsDialog not found on ${clazz.name}")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val signature = "${showMethod.declaringClass.name}#${showMethod.name}"
|
|
||||||
if (!hookedMethods.add(signature)) {
|
|
||||||
Log.e("showDevOptionsDialog already hooked on ${showMethod.declaringClass.name}, skipping")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
XposedBridge.hookMethod(showMethod, object : XC_MethodHook() {
|
|
||||||
override fun beforeHookedMethod(param: MethodHookParam) {
|
|
||||||
try {
|
try {
|
||||||
val activityContext = getContextFromDevSupport(clazz, param.thisObject)
|
val handleReloadJSMethod = clazz.methods.firstOrNull { it.name == "handleReloadJS" }
|
||||||
?: contextForMenu
|
if (handleReloadJSMethod != null) {
|
||||||
if (activityContext == null) {
|
XposedBridge.hookMethod(handleReloadJSMethod, object : XC_MethodReplacement() {
|
||||||
// No usable context at all - let React's own dev options dialog
|
|
||||||
// run instead of showing nothing.
|
|
||||||
Log.e("No usable context for recovery menu - letting RN dialog show")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.e(
|
|
||||||
"Using context: $activityContext (type: ${activityContext.javaClass.name})"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Only suppress React's dialog once ours is actually being shown.
|
|
||||||
// If showing the menu throws (e.g. bad Context), the exception is
|
|
||||||
// caught below and React's dialog still runs.
|
|
||||||
LogBoxNavigation.showRecoveryMenu(activityContext)
|
|
||||||
param.result = null
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Log.e("Failed to show recovery menu (falling back to RN dialog): ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
Log.e("Hooked showDevOptionsDialog on ${showMethod.declaringClass.name}")
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Log.e("Failed to hook showDevOptionsDialog on ${clazz.name}: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hookReloadJS(clazz: Class<*>) {
|
|
||||||
val method = clazz.declaredMethods.firstOrNull { it.name == "handleReloadJS" }
|
|
||||||
?: clazz.methods.firstOrNull { it.name == "handleReloadJS" }
|
|
||||||
?: return
|
|
||||||
|
|
||||||
val signature = "${method.declaringClass.name}#${method.name}"
|
|
||||||
if (!hookedMethods.add(signature)) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
XposedBridge.hookMethod(method, object : XC_MethodReplacement() {
|
|
||||||
override fun replaceHookedMethod(param: MethodHookParam): Any? {
|
override fun replaceHookedMethod(param: MethodHookParam): Any? {
|
||||||
Log.e("handleReloadJS called - reloading app")
|
Log.e("handleReloadJS called - reloading app")
|
||||||
reloadApp()
|
reloadApp()
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
Log.e("Hooked handleReloadJS on ${method.declaringClass.name}")
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Exception) {
|
||||||
Log.e("Failed to hook handleReloadJS: ${e.message}")
|
Log.e("Failed to hook handleReloadJS: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val showDevOptionsDialogMethod = clazz.methods.firstOrNull { it.name == "showDevOptionsDialog" }
|
||||||
|
if (showDevOptionsDialogMethod != null) {
|
||||||
|
XposedBridge.hookMethod(showDevOptionsDialogMethod, object : XC_MethodReplacement() {
|
||||||
|
override fun replaceHookedMethod(param: MethodHookParam): Any? {
|
||||||
|
try {
|
||||||
|
var activityContext: Context? = null
|
||||||
|
try {
|
||||||
|
activityContext = getContextFromDevSupport(clazz, param.thisObject)
|
||||||
|
if (activityContext != null) {
|
||||||
|
Log.e("Successfully got context from DevSupport")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Failed to get context from DevSupport (non-fatal): ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val finalContext = activityContext ?: contextForMenu ?: context
|
||||||
|
Log.e("Using context: $finalContext (type: ${finalContext.javaClass.name})")
|
||||||
|
|
||||||
|
LogBoxNavigation.showRecoveryMenu(finalContext)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Handle exception
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Handle exception
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getContextFromDevSupport(clazz: Class<*>, instance: Any?): Context? {
|
private fun getContextFromDevSupport(clazz: Class<*>, instance: Any?): Context? {
|
||||||
|
|
@ -190,43 +146,42 @@ object LogBoxModule : Module() {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
return try {
|
||||||
// Helper objects that expose getCurrentActivity(). ReactInstanceDevHelper and
|
|
||||||
// ReactHost are the ones guaranteed to hold the *Activity* (not the app context),
|
|
||||||
// which is what an AlertDialog needs to actually appear on screen.
|
|
||||||
val helpers = listOf(
|
val helpers = listOf(
|
||||||
"mReactInstanceDevHelper",
|
"mReactInstanceDevHelper",
|
||||||
"reactInstanceDevHelper",
|
"reactInstanceDevHelper",
|
||||||
"mReactHost",
|
|
||||||
"reactHost",
|
|
||||||
"mReactInstanceManager",
|
"mReactInstanceManager",
|
||||||
"reactInstanceManager",
|
|
||||||
"mBridge",
|
|
||||||
"bridge",
|
|
||||||
"mApplicationContext"
|
"mApplicationContext"
|
||||||
)
|
)
|
||||||
|
|
||||||
for (helperName in helpers) {
|
for (helperName in helpers) {
|
||||||
try {
|
try {
|
||||||
|
Log.e("Trying field: $helperName")
|
||||||
val helperField = XposedHelpers.findFieldIfExists(clazz, helperName)
|
val helperField = XposedHelpers.findFieldIfExists(clazz, helperName)
|
||||||
?: continue
|
if (helperField == null) {
|
||||||
val helper = helperField.get(instance) ?: continue
|
Log.e("Field $helperName not found, skipping")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val helper = helperField.get(instance)
|
||||||
|
if (helper == null) {
|
||||||
|
Log.e("Field $helperName is null, skipping")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if (helper is Context) {
|
if (helper is Context) {
|
||||||
Log.e("Field $helperName is a Context, returning it")
|
Log.e("Field $helperName is a Context, returning it")
|
||||||
return helper
|
return helper
|
||||||
}
|
}
|
||||||
|
|
||||||
// getCurrentActivity() exists on ReactInstanceDevHelper, ReactHost and
|
|
||||||
// ReactInstanceManager, and returns an actual Activity.
|
|
||||||
val getCurrentActivityMethod = helper.javaClass.methods.firstOrNull {
|
val getCurrentActivityMethod = helper.javaClass.methods.firstOrNull {
|
||||||
it.name == "getCurrentActivity"
|
it.name == "getCurrentActivity"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getCurrentActivityMethod != null) {
|
if (getCurrentActivityMethod != null) {
|
||||||
getCurrentActivityMethod.isAccessible = true
|
|
||||||
val ctx = getCurrentActivityMethod.invoke(helper) as? Context
|
val ctx = getCurrentActivityMethod.invoke(helper) as? Context
|
||||||
if (ctx != null) {
|
if (ctx != null) {
|
||||||
Log.e("Got activity context from $helperName.getCurrentActivity()")
|
Log.e("Got context from $helperName.getCurrentActivity()")
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -235,55 +190,11 @@ object LogBoxModule : Module() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direct Context fields on the DevSupportManager and its superclasses.
|
|
||||||
try {
|
|
||||||
var current = clazz
|
|
||||||
while (current != null && current != Any::class.java) {
|
|
||||||
for (field in current.declaredFields) {
|
|
||||||
try {
|
|
||||||
field.isAccessible = true
|
|
||||||
val value = field.get(instance) ?: continue
|
|
||||||
if (value is Context) {
|
|
||||||
Log.e("Got context from field ${current.simpleName}.${field.name}")
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// skip fields we cannot read
|
|
||||||
}
|
|
||||||
}
|
|
||||||
current = current.superclass
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("Error scanning context fields: ${e.message}")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last resort: the currently resumed Activity from ActivityThread.
|
|
||||||
try {
|
|
||||||
val activityThreadClass = Class.forName("android.app.ActivityThread")
|
|
||||||
val currentActivityThread =
|
|
||||||
activityThreadClass.getMethod("currentActivityThread").invoke(null)
|
|
||||||
val mActivitiesField = activityThreadClass.getDeclaredField("mActivities")
|
|
||||||
mActivitiesField.isAccessible = true
|
|
||||||
val activities = mActivitiesField.get(currentActivityThread)
|
|
||||||
if (activities is Map<*, *>) {
|
|
||||||
for (record in activities.values) {
|
|
||||||
val activityField = record!!.javaClass.getDeclaredField("activity")
|
|
||||||
activityField.isAccessible = true
|
|
||||||
val activity = activityField.get(record) as? Activity
|
|
||||||
if (activity != null && !activity.isFinishing) {
|
|
||||||
Log.e("Got activity context from ActivityThread")
|
|
||||||
return activity
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("ActivityThread lookup failed: ${e.message}")
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.e("Could not get context from DevSupport object using any method")
|
Log.e("Could not get context from DevSupport object using any method")
|
||||||
|
null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("Failed to get context (outer catch): ${e.message}")
|
Log.e("Failed to get context (outer catch): ${e.message}")
|
||||||
|
null
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue