Compare commits

..

2 commits

Author SHA1 Message Date
Rosie Val
8055dbf3ff fix: modern Discord themes, fonts, package hook filter, and bundle update
Some checks failed
Build APK / Build and push (push) Failing after 4m10s
2026-09-19 19:00:04 -04:00
Rosie Val
b9980f5cb9 fix: resolve Discord startup freeze deadlock, 404 download URL, and add native shake detector
Some checks failed
Build APK / Build and push (push) Failing after 7m2s
2026-09-12 09:58:29 -04:00
9 changed files with 267 additions and 1136 deletions

File diff suppressed because one or more lines are too long

View file

@ -16,6 +16,7 @@ import GoonXposed.xposed.modules.bridge.AdditionalBridgeMethodsModule
import GoonXposed.xposed.modules.bridge.BridgeModule import GoonXposed.xposed.modules.bridge.BridgeModule
import GoonXposed.xposed.modules.no_track.BlockCrashReportingModule import GoonXposed.xposed.modules.no_track.BlockCrashReportingModule
import GoonXposed.xposed.modules.no_track.BlockDeepLinksTrackingModule import GoonXposed.xposed.modules.no_track.BlockDeepLinksTrackingModule
import GoonXposed.xposed.modules.LogBox.LogBoxModule
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
object HookStateHolder { object HookStateHolder {
@ -59,11 +60,23 @@ class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) { override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) {
for (module in modules) module.onInit(startupParam) for (module in modules) module.onInit(startupParam)
} }
override fun handleLoadPackage(param: XC_LoadPackage.LoadPackageParam) = with(param) { override fun handleLoadPackage(param: XC_LoadPackage.LoadPackageParam) = with(param) {
val isTargetApp = packageName == Constants.TARGET_PACKAGE
|| packageName.startsWith("com.discord")
|| packageName == "dev.shiggy.cord"
|| packageName.contains("cord")
|| packageName.contains("discord")
if (!isTargetApp) return
if (processName != packageName) return
if (hooked) return if (hooked) return
val reactActivity = classLoader.loadClass(Constants.TARGET_ACTIVITY) val reactActivity = try {
classLoader.loadClass(Constants.TARGET_ACTIVITY)
} catch (e: Throwable) {
Log.e("Failed to load target activity ${Constants.TARGET_ACTIVITY}", e)
null
}
ContextWrapper::class.java.hookMethod("attachBaseContext", Context::class.java) { ContextWrapper::class.java.hookMethod("attachBaseContext", Context::class.java) {
after { after {
@ -74,7 +87,7 @@ class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
} }
} }
reactActivity.hookMethod("onCreate", Bundle::class.java) { reactActivity?.hookMethod("onCreate", Bundle::class.java) {
after { after {
val act = thisObject as Activity val act = thisObject as Activity
Log.i("Received Activity") Log.i("Received Activity")
@ -89,6 +102,20 @@ class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
} }
} }
reactActivity?.hookMethod("onResume") {
after {
val act = thisObject as Activity
this@Main.onResume(act)
}
}
reactActivity?.hookMethod("onPause") {
after {
val act = thisObject as Activity
this@Main.onPause(act)
}
}
this@Main.onLoad(param) this@Main.onLoad(param)
hooked = true hooked = true
@ -105,4 +132,12 @@ class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
override fun onActivity(activity: Activity) { override fun onActivity(activity: Activity) {
for (module in modules) module.onActivity(activity) for (module in modules) module.onActivity(activity)
} }
override fun onResume(activity: Activity) {
for (module in modules) module.onResume(activity)
}
override fun onPause(activity: Activity) {
for (module in modules) module.onPause(activity)
}
} }

View file

@ -27,6 +27,10 @@ open class Module {
open fun onActivity(activity: Activity) {} open fun onActivity(activity: Activity) {}
open fun onResume(activity: Activity) {}
open fun onPause(activity: Activity) {}
@Deprecated("This method is deprecated in the parent class") @Deprecated("This method is deprecated in the parent class")
open fun buildPayload(builder: JsonObjectBuilder) {} open fun buildPayload(builder: JsonObjectBuilder) {}
} }

View file

@ -19,6 +19,7 @@ import GoonXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import java.io.File import java.io.File
import java.lang.reflect.Method import java.lang.reflect.Method
@ -88,16 +89,18 @@ object HookScriptLoaderModule : Module() {
Log.i("Running custom scripts...") Log.i("Running custom scripts...")
runBlocking { runBlocking {
val ready = async { HookStateHolder.readyDeferred.join() }
if (!mainScript.exists()) { if (!mainScript.exists()) {
Log.i("Main script does not exist, downloading before load...") Log.i("Main script does not exist, downloading before load...")
val download = async { UpdaterModule.downloadScript().join() } try {
awaitAll(ready, download) withTimeout(10000L) {
UpdaterModule.downloadScript().join()
}
} catch (e: Throwable) {
Log.w("Bundle download did not complete in time", e)
}
} else { } else {
Log.i("Main script exists, updating in background...") Log.i("Main script exists, updating in background...")
UpdaterModule.downloadScript() UpdaterModule.downloadScript()
ready.await()
} }
} }
@ -122,15 +125,25 @@ object HookScriptLoaderModule : Module() {
if (mainScript.exists()) { if (mainScript.exists()) {
runScriptFile(mainScript) runScriptFile(mainScript)
} else { } 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) if (!::resources.isInitialized) resources = XModuleResources.createInstance(modulePath, null)
XposedBridge.invokeOriginalMethod( var hasFallback = false
loadScriptFromAssets, try {
thisObject, resources.assets.open("Shiggy.bundle").use { hasFallback = true }
arrayOf(resources.assets, "assets://Shiggy.bundle", loadSynchronously) } 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) { } catch (e: Throwable) {

View file

@ -3,6 +3,7 @@ package GoonXposed.xposed.modules.LogBox
import GoonXposed.xposed.Module import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.Utils.Companion.reloadApp 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
import de.robv.android.xposed.XC_MethodReplacement import de.robv.android.xposed.XC_MethodReplacement
@ -10,33 +11,57 @@ import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage import de.robv.android.xposed.callbacks.XC_LoadPackage
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.lang.ref.WeakReference
object LogBoxModule : Module() { 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 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) { override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
this@LogBoxModule.packageParam = packageParam this@LogBoxModule.packageParam = packageParam
try { // Do not force getUseDeveloperSupport = true, as it causes React Native to loop
val dcdReactNativeHostClass = classLoader.loadClass("com.discord.bridge.DCDReactNativeHost") // attempting to connect to a Metro bundler on localhost:8081 and hang the app loading screen.
val getUseDeveloperSupportMethod = // Shake detector and recovery menu work independently without it.
dcdReactNativeHostClass.methods.first { it.name == "getUseDeveloperSupport" }
// NOTE: This used to call a custom `.hook { before { result = true } }` DSL that
// relied on a shared extensions file which broke (unresolved references).
// Rewritten using the standard XposedBridge/XC_MethodHook API, which is already
// used elsewhere in this file, to force this method to always return true.
XposedBridge.hookMethod(getUseDeveloperSupportMethod, object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) {
param.result = true
}
})
Log.e("Successfully hooked DCDReactNativeHost")
} catch (e: Exception) {
Log.e("Failed to hook DCDReactNativeHost: ${e.message}")
}
return@with return@with
} }
@ -126,7 +151,7 @@ object LogBoxModule : Module() {
Log.e("Failed to get context from DevSupport (non-fatal): ${e.message}") 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})") Log.e("Using context: $finalContext (type: ${finalContext.javaClass.name})")
LogBoxNavigation.showRecoveryMenu(finalContext) 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
}
}

View file

@ -61,21 +61,30 @@ object FontsModule : Module() {
} }
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
XposedHelpers.findAndHookMethod( val fontManagerClass = runCatching {
"com.facebook.react.common.assets.ReactFontManager\$Companion", classLoader.loadClass("com.facebook.react.common.assets.ReactFontManager\$Companion")
classLoader, }.getOrNull() ?: runCatching {
"createAssetTypeface", classLoader.loadClass("com.facebook.react.views.text.ReactFontManager\$Companion")
String::class.java, }.getOrNull()
Int::class.java,
"android.content.res.AssetManager", if (fontManagerClass != null) {
object : XC_MethodReplacement() { XposedHelpers.findAndHookMethod(
override fun replaceHookedMethod(param: MethodHookParam): Typeface? { fontManagerClass,
val fontFamilyName: String = param.args[0].toString() "createAssetTypeface",
val style: Int = param.args[1] as Int String::class.java,
val assetManager: AssetManager = param.args[2] as AssetManager Int::class.java,
return createAssetTypeface(fontFamilyName, style, assetManager) "android.content.res.AssetManager",
} object : XC_MethodReplacement() {
}) override fun replaceHookedMethod(param: MethodHookParam): Typeface? {
val fontFamilyName: String = param.args[0].toString()
val style: Int = param.args[1] as Int
val assetManager: AssetManager = param.args[2] as AssetManager
return createAssetTypeface(fontFamilyName, style, assetManager)
}
})
} else {
Log.w("FontsModule: ReactFontManager\$Companion class not found")
}
val fontDefFile = File(appInfo.dataDir, "${Constants.FILES_DIR}/fonts.json").apply { asFile() } val fontDefFile = File(appInfo.dataDir, "${Constants.FILES_DIR}/fonts.json").apply { asFile() }
if (!fontDefFile.exists()) return@with if (!fontDefFile.exists()) return@with
@ -149,12 +158,16 @@ object FontsModule : Module() {
// for use in the CustomFallbackBuilder below. // for use in the CustomFallbackBuilder below.
for (fontFamilyName in fontFamilyNames) { for (fontFamilyName in fontFamilyNames) {
try { try {
for (fileExtension in FILE_EXTENSIONS) { if (fontFamilyName.contains(":")) {
val (customName, refName) = fontFamilyName.split(":") for (fileExtension in FILE_EXTENSIONS) {
val file = File(fontsDownloadsDir, "$customName/$refName.$fileExtension").apply { asFile() } val (customName, refName) = fontFamilyName.split(":")
val font = Font.Builder(file).build() val file = File(fontsDownloadsDir, "$customName/$refName.$fileExtension").apply { asFile() }
val family = FontFamily.Builder(font).build() if (file.exists()) {
fontFamilies.add(family) val font = Font.Builder(file).build()
val family = FontFamily.Builder(font).build()
fontFamilies.add(family)
}
}
} }
} catch (_: Throwable) { } catch (_: Throwable) {
} }
@ -218,11 +231,14 @@ object FontsModule : Module() {
val extension = EXTENSIONS[style] val extension = EXTENSIONS[style]
try { try {
for (fileExtension in FILE_EXTENSIONS) { if (fontFamilyName.contains(":")) {
val (customName, refName) = fontFamilyName.split(":") for (fileExtension in FILE_EXTENSIONS) {
val file = File(fontsDownloadsDir, "$customName/$refName.$fileExtension").apply { asFile() } val (customName, refName) = fontFamilyName.split(":")
if (!file.exists()) throw Exception() val file = File(fontsDownloadsDir, "$customName/$refName.$fileExtension").apply { asFile() }
return Typeface.createFromFile(file.absolutePath) if (file.exists()) {
return Typeface.createFromFile(file.absolutePath)
}
}
} }
} catch (_: Throwable) { } catch (_: Throwable) {
} }

View file

@ -86,18 +86,20 @@ object ThemesModule : Module() {
fun hookTheme() { fun hookTheme() {
try { try {
val themeManager = param.classLoader.loadClass("com.discord.theme.utils.ColorUtilsKt") val themeManager = runCatching { param.classLoader.loadClass("com.discord.theme.utils.ColorUtilsKt") }.getOrNull()
val darkTheme = param.classLoader.loadClass("com.discord.theme.DarkerTheme") val darkTheme = runCatching { param.classLoader.loadClass("com.discord.theme.DarkTheme") }.getOrNull()
val lightTheme = param.classLoader.loadClass("com.discord.theme.LightTheme") ?: runCatching { param.classLoader.loadClass("com.discord.theme.DarkerTheme") }.getOrNull()
val onyxTheme = runCatching { param.classLoader.loadClass("com.discord.theme.OnyxTheme") }.getOrNull()
val lightTheme = runCatching { param.classLoader.loadClass("com.discord.theme.LightTheme") }.getOrNull()
hookThemeInternal(themeManager, darkTheme, lightTheme) hookThemeInternal(themeManager, darkTheme, onyxTheme, lightTheme)
} catch (e: Throwable) { } catch (e: Throwable) {
GoonXposed.xposed.Utils.Log.e("Failed to hook theme: ${e.message}") GoonXposed.xposed.Utils.Log.e("Failed to hook theme: ${e.message}")
} }
} }
private fun hookThemeInternal( private fun hookThemeInternal(
themeManager: Class<*>, darkTheme: Class<*>, lightTheme: Class<*> themeManager: Class<*>?, darkTheme: Class<*>?, onyxTheme: Class<*>?, lightTheme: Class<*>?
) { ) {
val theme = this.theme ?: return val theme = this.theme ?: return
@ -111,26 +113,36 @@ object ThemesModule : Module() {
// TEXT_NORMAL -> getTextNormal // TEXT_NORMAL -> getTextNormal
val methodName = "get${key.fromScreamingSnakeToCamelCase()}" val methodName = "get${key.fromScreamingSnakeToCamelCase()}"
value.forEachIndexed { index, v -> value.forEachIndexed { index, v ->
val colorInt = hexStringToColorInt(v)
when (index) { when (index) {
0 -> hookThemeMethod(darkTheme, methodName, hexStringToColorInt(v)) 0 -> {
1 -> hookThemeMethod(lightTheme, methodName, hexStringToColorInt(v)) if (darkTheme != null) hookThemeMethod(darkTheme, methodName, colorInt)
if (onyxTheme != null) hookThemeMethod(onyxTheme, methodName, colorInt)
}
1 -> {
if (lightTheme != null) hookThemeMethod(lightTheme, methodName, colorInt)
}
} }
} }
} }
// If there's any rawColors value, hook the color getter // If there's any rawColors value, hook the color getter
if (!theme.data.rawColors.isNullOrEmpty()) { if (!theme.data.rawColors.isNullOrEmpty() && themeManager != null) {
try { try {
val getColorCompat = themeManager.getDeclaredMethod( val getColorCompat = runCatching {
"getColorCompat", themeManager.getDeclaredMethod(
Resources::class.java, "getColorCompat",
Int::class.javaPrimitiveType, Resources::class.java,
Resources.Theme::class.java, Int::class.javaPrimitiveType,
) Resources.Theme::class.java,
)
}.getOrNull()
val getColorCompatLegacy = themeManager.getDeclaredMethod( val getColorCompatLegacy = runCatching {
"getColorCompat", Context::class.java, Int::class.javaPrimitiveType themeManager.getDeclaredMethod(
) "getColorCompat", Context::class.java, Int::class.javaPrimitiveType
)
}.getOrNull()
val patch = MethodHookBuilder().run { val patch = MethodHookBuilder().run {
before { before {
@ -147,8 +159,8 @@ object ThemesModule : Module() {
build() build()
} }
getColorCompat.hook(patch) getColorCompat?.hook(patch)
getColorCompatLegacy.hook(patch) getColorCompatLegacy?.hook(patch)
} catch (e: Throwable) { } catch (e: Throwable) {
GoonXposed.xposed.Utils.Log.e("Failed to hook getColorCompat: ${e.message}") GoonXposed.xposed.Utils.Log.e("Failed to hook getColorCompat: ${e.message}")
} }