fix(hook): prevent startup deadlock, fix target package/activity filters, restore bundle asset and shake detector

This commit is contained in:
Rosie Val 2026-09-25 16:34:09 -04:00
parent 43f024969d
commit 38f45a7e0b
5 changed files with 227 additions and 38 deletions

File diff suppressed because one or more lines are too long

View file

@ -61,10 +61,36 @@ class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
dispatch("onInit") { module -> module.onInit(startupParam) }
}
private fun isTargetPackage(pkg: String): Boolean {
return pkg == Constants.TARGET_PACKAGE
|| pkg.startsWith("com.discord")
|| pkg == "dev.shiggy.cord"
|| pkg.contains("discord")
|| pkg.contains("gooncord")
}
override fun handleLoadPackage(param: XC_LoadPackage.LoadPackageParam) = with(param) {
if (!isTargetPackage(packageName)) return
if (processName != packageName) return
if (hooked) return
val reactActivity = classLoader.loadClass(Constants.TARGET_ACTIVITY)
val activityClassNames = listOf(
Constants.TARGET_ACTIVITY,
"$packageName.react_activities.ReactActivity",
"com.discord.react_activities.ReactActivity",
"com.facebook.react.ReactActivity"
)
var reactActivity: Class<*>? = null
for (className in activityClassNames) {
try {
reactActivity = classLoader.loadClass(className)
if (reactActivity != null) break
} catch (_: Throwable) {}
}
if (reactActivity == null) {
Log.w("Target activity not found by name, falling back to Activity base class")
}
ContextWrapper::class.java.hookMethod("attachBaseContext", Context::class.java) {
after {
@ -75,10 +101,17 @@ class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
}
}
reactActivity.hookMethod("onCreate", Bundle::class.java) {
val targetActivityClass = reactActivity ?: Activity::class.java
targetActivityClass.hookMethod("onCreate", Bundle::class.java) {
after {
val act = thisObject as Activity
Log.i("Received Activity")
if (reactActivity == null &&
!act.javaClass.name.contains("ReactActivity", ignoreCase = true) &&
!act.javaClass.name.contains("MainActivity", ignoreCase = true)
) {
return@after
}
Log.i("Received Activity: ${act.javaClass.name}")
if (!HookStateHolder.gotContext) {
Log.w("Activity created before we got Context, process may have been recreated!")
@ -90,6 +123,20 @@ class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
}
}
targetActivityClass.hookMethod("onResume") {
after {
val act = thisObject as Activity
this@Main.onResume(act)
}
}
targetActivityClass.hookMethod("onPause") {
after {
val act = thisObject as Activity
this@Main.onPause(act)
}
}
this@Main.onLoad(param)
hooked = true
@ -107,6 +154,14 @@ class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
dispatch("onActivity") { module -> module.onActivity(activity) }
}
override fun onResume(activity: Activity) {
dispatch("onResume") { module -> module.onResume(activity) }
}
override fun onPause(activity: Activity) {
dispatch("onPause") { module -> module.onPause(activity) }
}
private fun dispatch(stage: String, block: (Module) -> Unit) {
for (module in modules) {
val name = module.javaClass.simpleName.ifEmpty { module.javaClass.name }

View file

@ -19,6 +19,7 @@ import GoonXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import java.io.File
import java.lang.reflect.Method
@ -88,19 +89,21 @@ object HookScriptLoaderModule : Module() {
Log.i("Running custom scripts...")
runBlocking {
val ready = async { HookStateHolder.readyDeferred.join() }
val isCustomUrl = UpdaterModule.isCustomUrlEnabled
if (!mainScript.exists() || isCustomUrl) {
val reason = if (isCustomUrl) "Custom URL enabled" else "Main script does not exist"
Log.i("$reason, downloading before load...")
val download =
async { UpdaterModule.downloadScript(showUpdateDialog = false).join() }
awaitAll(ready, download)
try {
withTimeout(10000L) {
UpdaterModule.downloadScript(showUpdateDialog = false).join()
}
} catch (e: Throwable) {
Log.w("Bundle download did not complete in time", e)
}
} else {
Log.i("Main script exists, updating in background...")
UpdaterModule.downloadScript(showUpdateDialog = true)
ready.await()
}
}
@ -129,15 +132,25 @@ object HookScriptLoaderModule : Module() {
if (mainScript.exists()) {
runScriptFile(mainScript)
} else {
Log.i("Main script does not exist, falling back")
Log.i("Main script does not exist, checking for fallback asset")
if (!::resources.isInitialized) resources = XModuleResources.createInstance(modulePath, null)
XposedBridge.invokeOriginalMethod(
loadScriptFromAssets,
thisObject,
arrayOf(resources.assets, "assets://Shiggy.bundle", loadSynchronously)
)
var hasFallback = false
try {
resources.assets.open("Shiggy.bundle").use { hasFallback = true }
} catch (_: Throwable) {}
if (hasFallback) {
Log.i("Loading fallback assets://Shiggy.bundle")
XposedBridge.invokeOriginalMethod(
loadScriptFromAssets,
thisObject,
arrayOf(resources.assets, "assets://Shiggy.bundle", loadSynchronously)
)
} else {
Log.w("No fallback bundle asset found; letting host app load default JS")
}
}
}
} catch (e: Throwable) {

View file

@ -1,45 +1,67 @@
package GoonXposed.xposed.modules.LogBox
import GoonXposed.xposed.Constants
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.hook
import GoonXposed.xposed.Utils.Companion.reloadApp
import android.app.Activity
import android.content.Context
import de.robv.android.xposed.XC_MethodHook.MethodHookParam
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XC_MethodReplacement
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage
import kotlinx.coroutines.*
import java.io.File
import java.lang.ref.WeakReference
object LogBoxModule : Module() {
lateinit var packageParam: XC_LoadPackage.LoadPackageParam
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
var contextForMenu: Context? = null
private var currentActivity: WeakReference<Activity>? = null
private val shakeDetector = ShakeDetector {
val act = currentActivity?.get() ?: return@ShakeDetector
act.runOnUiThread {
try {
Log.i("Shake detected! Opening Recovery Menu...")
LogBoxNavigation.showRecoveryMenu(act)
} catch (e: Throwable) {
Log.e("Failed to show recovery menu on shake: ${e.message}", e)
}
}
}
override fun onActivity(activity: Activity) {
currentActivity = WeakReference(activity)
contextForMenu = activity
}
override fun onResume(activity: Activity) {
currentActivity = WeakReference(activity)
contextForMenu = activity
try {
shakeDetector.start(activity)
Log.i("ShakeDetector started for activity: ${activity.javaClass.simpleName}")
} catch (e: Throwable) {
Log.e("Failed to start ShakeDetector: ${e.message}", e)
}
}
override fun onPause(activity: Activity) {
try {
shakeDetector.stop()
Log.i("ShakeDetector stopped")
} catch (e: Throwable) {
Log.e("Failed to stop ShakeDetector: ${e.message}", e)
}
}
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
this@LogBoxModule.packageParam = packageParam
try {
val dcdReactNativeHostClass = classLoader.loadClass("com.discord.bridge.DCDReactNativeHost")
val getUseDeveloperSupportMethod =
dcdReactNativeHostClass.methods.first { it.name == "getUseDeveloperSupport" }
getUseDeveloperSupportMethod.hook {
before {
// Only enable dev support when the injected bundle is already present. A fresh
// install (no bundle yet) must load Discord's own bundle; otherwise React Native
// waits for a Metro packager and Discord never opens.
result = File(
appInfo.dataDir, "${Constants.CACHE_DIR}/${Constants.MAIN_SCRIPT_FILE}"
).exists()
}
}
Log.e("Successfully hooked DCDReactNativeHost")
} catch (e: Exception) {
Log.e("Failed to hook DCDReactNativeHost: ${e.message}")
}
// Do not force getUseDeveloperSupport = true, as it causes React Native to loop
// attempting to connect to a Metro bundler on localhost:8081 and hang the app loading screen.
// Shake detector and recovery menu work independently without it.
return@with
}
@ -98,6 +120,21 @@ object LogBoxModule : Module() {
}
try {
try {
val handleReloadJSMethod = clazz.methods.firstOrNull { it.name == "handleReloadJS" }
if (handleReloadJSMethod != null) {
XposedBridge.hookMethod(handleReloadJSMethod, object : XC_MethodReplacement() {
override fun replaceHookedMethod(param: MethodHookParam): Any? {
Log.e("handleReloadJS called - reloading app")
reloadApp()
return null
}
})
}
} catch (e: Exception) {
Log.e("Failed to hook handleReloadJS: ${e.message}")
}
try {
val showDevOptionsDialogMethod = clazz.methods.firstOrNull { it.name == "showDevOptionsDialog" }
if (showDevOptionsDialogMethod != null) {
@ -114,7 +151,7 @@ object LogBoxModule : Module() {
Log.e("Failed to get context from DevSupport (non-fatal): ${e.message}")
}
val finalContext = activityContext ?: contextForMenu ?: context
val finalContext = currentActivity?.get() ?: activityContext ?: contextForMenu ?: context
Log.e("Using context: $finalContext (type: ${finalContext.javaClass.name})")
LogBoxNavigation.showRecoveryMenu(finalContext)

View file

@ -0,0 +1,56 @@
package GoonXposed.xposed.modules.LogBox
import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import kotlin.math.sqrt
class ShakeDetector(private val onShake: () -> Unit) : SensorEventListener {
private var sensorManager: SensorManager? = null
private var accelerometer: Sensor? = null
private var lastShakeTimestamp: Long = 0
fun start(context: Context): Boolean {
if (sensorManager != null) return true
sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager
accelerometer = sensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
return accelerometer?.let {
sensorManager?.registerListener(this, it, SensorManager.SENSOR_DELAY_UI)
} ?: false
}
fun stop() {
sensorManager?.unregisterListener(this)
sensorManager = null
accelerometer = null
}
override fun onSensorChanged(event: SensorEvent) {
if (event.sensor.type != Sensor.TYPE_ACCELEROMETER) return
val x = event.values[0] / SensorManager.GRAVITY_EARTH
val y = event.values[1] / SensorManager.GRAVITY_EARTH
val z = event.values[2] / SensorManager.GRAVITY_EARTH
// gForce will be close to 1 when there is no movement.
val gForce = sqrt((x * x + y * y + z * z).toDouble()).toFloat()
if (gForce > SHAKE_THRESHOLD_GRAVITY) {
val now = System.currentTimeMillis()
// Ignore shake events too close to each other (1 second debounce)
if (now - lastShakeTimestamp > SHAKE_SLOP_TIME_MS) {
lastShakeTimestamp = now
onShake()
}
}
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
companion object {
private const val SHAKE_THRESHOLD_GRAVITY = 2.7f
private const val SHAKE_SLOP_TIME_MS = 1000L
}
}