fix(ci): fix compilation errors from package rename, restore missing imports and helpers, fix 404 bundle URL

This commit is contained in:
Rosie Val 2026-09-25 16:14:38 -04:00
parent 109f7d5775
commit 43f024969d
9 changed files with 247 additions and 146 deletions

View file

@ -2,10 +2,12 @@ package GoonXposed.xposed
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import de.robv.android.xposed.IXposedHookZygoteInit import de.robv.android.xposed.IXposedHookZygoteInit
import de.robv.android.xposed.XC_MethodHook import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage import de.robv.android.xposed.callbacks.XC_LoadPackage
import kotlinx.serialization.json.JsonObjectBuilder import kotlinx.serialization.json.JsonObjectBuilder
import java.io.File import java.io.File
@ -18,13 +20,18 @@ data class AppInfo(
val versionCode: Long, val versionCode: Long,
) )
abstract class Module { /**
* Base class for every Xposed side module.
*
* The [Main] loader forwards the various [XC_LoadPackage] lifecycle callbacks to every
* registered [Module] instance. Modules may override any subset of the hooks below.
*/
open class Module {
/** /**
* Builds a JSON payload to be injected into the JavaScript context. * Builds a JSON payload to be injected into the JavaScript context.
*/ */
@Deprecated("This will be removed in future versions. Payloads can be replaced via synchronous bridge methods.") @Deprecated("This will be removed in future versions. Payloads can be replaced via synchronous bridge methods.")
open fun buildPayload(builder: JsonObjectBuilder) { open fun buildPayload(builder: JsonObjectBuilder) {}
}
/** /**
* Called during Zygote initialization. * Called during Zygote initialization.
@ -63,121 +70,151 @@ abstract class Module {
*/ */
open fun onActivity(activity: Activity) {} open fun onActivity(activity: Activity) {}
protected fun Context.getAppInfo(): AppInfo { open fun onResume(activity: Activity) {}
val pInfo = packageManager.getPackageInfo(packageName, 0)
val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) pInfo.longVersionCode
else @Suppress("DEPRECATION") pInfo.versionCode.toLong()
return AppInfo( open fun onPause(activity: Activity) {}
packageManager.getApplicationLabel(applicationInfo).toString(),
packageName,
pInfo.versionName ?: versionCode.toString(),
versionCode,
)
}
protected fun File.asDir() {
if (!this.isDirectory) this.delete()
this.mkdirs()
}
protected fun File.asFile() {
if (!this.isFile) this.deleteRecursively()
}
protected fun Class<*>.method(
name: String, vararg params: Class<*>?
): Method = getDeclaredMethod(name, *params).apply {
isAccessible = true
}
protected fun ClassLoader.safeLoadClass(name: String): Class<*>? = runCatching { loadClass(name) }.getOrNull()
protected fun Method.hook(hook: XC_MethodHook): XC_MethodHook.Unhook = XposedBridge.hookMethod(this, hook)
protected fun Method.hook(block: MethodHookBuilder.() -> Unit): XC_MethodHook.Unhook =
hook(MethodHookBuilder().apply(block).build())
protected fun Context.getAppInfo(): AppInfo = (this as Context).getAppInfo()
protected fun File.asDir(): File = (this as File).asDir()
protected fun File.asFile(): File = (this as File).asFile()
protected fun ClassLoader.safeLoadClass(name: String): Class<*>? = (this as ClassLoader).safeLoadClass(name)
protected fun Class<*>.method(name: String, vararg parameterTypes: Class<*>?): Method = (this as Class<*>).method(name, *parameterTypes)
protected fun Method.hook(hook: XC_MethodHook): XC_MethodHook.Unhook = (this as Method).hook(hook)
protected fun Method.hook(block: MethodHookBuilder.() -> Unit): XC_MethodHook.Unhook = (this as Method).hook(block)
protected fun Class<*>.hookMethod( protected fun Class<*>.hookMethod(
name: String, vararg params: Class<*>?, block: MethodHookBuilder.() -> Unit name: String,
): XC_MethodHook.Unhook = method(name, *params).hook(MethodHookBuilder().apply(block).build()) vararg parameterTypes: Class<*>?,
block: MethodHookBuilder.() -> Unit
): XC_MethodHook.Unhook = (this as Class<*>).hookMethod(name, *parameterTypes, block = block)
}
protected class MethodHookBuilder { /**
private var beforeBlock: (HookScope.() -> Unit)? = null * Runtime wrapper around an [XC_MethodHook.MethodHookParam] used as the receiver of every
private var afterBlock: (HookScope.() -> Unit)? = null * `before`/`after` hook block, exposing [thisObject], [args], [result] and [param] directly.
*/
class HookScope(
val param: XC_MethodHook.MethodHookParam,
private val proceed: ((XC_MethodHook.MethodHookParam) -> Unit)? = null
) {
fun proceed() {
proceed?.invoke(param)
}
fun before(block: HookScope.() -> Unit) { val thisObject: Any?
beforeBlock = block get() = param.thisObject
val args: Array<Any?>
get() = param.args
var result: Any?
get() = param.result
set(value) {
param.result = value
} }
fun after(block: HookScope.() -> Unit) { var throwable: Throwable?
afterBlock = block get() = param.throwable
set(value) {
param.throwable = value
}
}
/**
* DSL builder for lazily constructed [XC_MethodHook] instances.
*
* ```kotlin
* val hook = MethodHookBuilder().run {
* before { result = null }
* after { Log.i("done") }
* build()
* }
* ```
*/
class MethodHookBuilder {
private val beforeCallbacks = mutableListOf<HookScope.() -> Unit>()
private val afterCallbacks = mutableListOf<HookScope.() -> Unit>()
fun before(callback: HookScope.() -> Unit) {
beforeCallbacks += callback
}
fun after(callback: HookScope.() -> Unit) {
afterCallbacks += callback
}
fun build(): XC_MethodHook = object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) {
val scope = HookScope(param = param, proceed = { p -> super.beforeHookedMethod(p) })
beforeCallbacks.forEach { scope.run(it) }
} }
fun build(): XC_MethodHook = object : XC_MethodHook() { override fun afterHookedMethod(param: MethodHookParam) {
override fun beforeHookedMethod(param: MethodHookParam) { val scope = HookScope(param = param, proceed = { p -> super.afterHookedMethod(p) })
val b = beforeBlock afterCallbacks.forEach { scope.run(it) }
if (b != null) {
val scope = HookScope(
param = param, proceed = { p ->
super.beforeHookedMethod(p)
})
scope.b()
} else {
super.beforeHookedMethod(param)
}
}
override fun afterHookedMethod(param: MethodHookParam) {
val a = afterBlock
if (a != null) {
val scope = HookScope(
param = param, proceed = { p ->
super.afterHookedMethod(p)
})
scope.a()
} else {
super.afterHookedMethod(param)
}
}
} }
} }
/** companion object {
* Scope object passed to before/after hook blocks. fun from(block: MethodHookBuilder.() -> Unit): XC_MethodHook = MethodHookBuilder().run {
* block()
* Provides: build()
* - Access to the [param] object }
* - [proceed] to call the original XC_MethodHook super method
* - Accessors for `thisObject`, `args`, `result`, and `throwable`
*
* @property param The [XC_MethodHook.MethodHookParam] for the current hook.
* @property proceed Function that calls the super method.
*/
protected class HookScope internal constructor(
val param: XC_MethodHook.MethodHookParam, private val proceed: (XC_MethodHook.MethodHookParam) -> Unit
) {
/**
* Continues with the default XC_MethodHook super behavior.
* Equivalent to calling `super.beforeHookedMethod(param)` or
* `super.afterHookedMethod(param)` depending on the phase.
*/
fun proceed() = proceed(param)
val thisObject: Any? get() = param.thisObject
val args: Array<Any?> get() = param.args
var result: Any?
get() = param.result
set(value) {
param.result = value
}
var throwable: Throwable?
get() = param.throwable
set(value) {
param.throwable = value
}
} }
} }
fun ClassLoader.safeLoadClass(name: String): Class<*>? = try {
loadClass(name)
} catch (e: Throwable) {
null
}
fun Class<*>.method(name: String, vararg parameterTypes: Class<*>?): Method =
XposedHelpers.findMethodExact(this, name, *parameterTypes)
fun Method.hook(hook: XC_MethodHook): XC_MethodHook.Unhook = XposedBridge.hookMethod(this, hook)
fun Method.hook(block: MethodHookBuilder.() -> Unit): XC_MethodHook.Unhook {
val builder = MethodHookBuilder()
builder.block()
return XposedBridge.hookMethod(this, builder.build())
}
fun Class<*>.hookMethod(
name: String,
vararg parameterTypes: Class<*>?,
block: MethodHookBuilder.() -> Unit
): XC_MethodHook.Unhook {
val method = method(name, *parameterTypes)
return method.hook(block)
}
fun File.asDir(): File {
if (!exists() || !isDirectory) mkdirs()
return this
}
fun File.asFile(): File {
parentFile?.mkdirs()
return this
}
fun Context.getAppInfo(): AppInfo {
val label = applicationInfo.loadLabel(packageManager).toString()
val packageInfo = try {
packageManager.getPackageInfo(packageName, 0)
} catch (e: PackageManager.NameNotFoundException) {
null
}
val versionCode = if (packageInfo != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) packageInfo.longVersionCode
else @Suppress("DEPRECATION") packageInfo.versionCode.toLong()
} else 0L
return AppInfo(
name = label,
packageName = packageName,
version = packageInfo?.versionName ?: "",
versionCode = versionCode
)
}

View file

@ -4,6 +4,9 @@ import android.content.Context
import android.util.AtomicFile import android.util.AtomicFile
import GoonXposed.xposed.Module import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.asDir
import GoonXposed.xposed.asFile
import GoonXposed.xposed.getAppInfo
import GoonXposed.xposed.modules.bridge.BridgeModule import GoonXposed.xposed.modules.bridge.BridgeModule
import java.io.* import java.io.*

View file

@ -4,6 +4,7 @@ import android.content.res.Resources
import de.robv.android.xposed.callbacks.XC_LoadPackage import de.robv.android.xposed.callbacks.XC_LoadPackage
import GoonXposed.xposed.Constants.Companion.TARGET_PACKAGE import GoonXposed.xposed.Constants.Companion.TARGET_PACKAGE
import GoonXposed.xposed.Module import GoonXposed.xposed.Module
import GoonXposed.xposed.hookMethod
/** /**
* Hooks [Resources.getIdentifier] to fix resource package name mismatch. * Hooks [Resources.getIdentifier] to fix resource package name mismatch.

View file

@ -6,9 +6,15 @@ import de.robv.android.xposed.IXposedHookZygoteInit
import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.callbacks.XC_LoadPackage import de.robv.android.xposed.callbacks.XC_LoadPackage
import GoonXposed.xposed.Constants import GoonXposed.xposed.Constants
import GoonXposed.xposed.HookScope
import GoonXposed.xposed.HookStateHolder import GoonXposed.xposed.HookStateHolder
import GoonXposed.xposed.Module import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.asDir
import GoonXposed.xposed.asFile
import GoonXposed.xposed.hook
import GoonXposed.xposed.method
import GoonXposed.xposed.safeLoadClass
import GoonXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR import GoonXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll

View file

@ -6,9 +6,13 @@ import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.callbacks.XC_LoadPackage import de.robv.android.xposed.callbacks.XC_LoadPackage
import GoonXposed.xposed.BuildConfig import GoonXposed.xposed.BuildConfig
import GoonXposed.xposed.Constants import GoonXposed.xposed.Constants
import GoonXposed.xposed.MethodHookBuilder
import GoonXposed.xposed.Module import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Companion.JSON import GoonXposed.xposed.Utils.Companion.JSON
import GoonXposed.xposed.Utils.Log import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.hook
import GoonXposed.xposed.method
import GoonXposed.xposed.safeLoadClass
import GoonXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR import GoonXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put import kotlinx.serialization.json.put

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.util.AtomicFile import android.util.AtomicFile
import GoonXposed.xposed.Module import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.asFile
import GoonXposed.xposed.modules.bridge.BridgeModule import GoonXposed.xposed.modules.bridge.BridgeModule
import java.io.* import java.io.*

View file

@ -56,8 +56,11 @@ object UpdaterModule : Module() {
private const val ETAG_FILE = "etag.txt" private const val ETAG_FILE = "etag.txt"
private const val CONFIG_FILE = "loader.json" private const val CONFIG_FILE = "loader.json"
val isCustomUrlEnabled: Boolean
get() = if (::config.isInitialized) config.customLoadUrl.enabled else false
private const val DEFAULT_BUNDLE_URL = private const val DEFAULT_BUNDLE_URL =
"https://github.com/OTKUSteyler/GoonCord/releases/download/latest/gooncord.js" "https://github.com/OTKUSteyler/GoonCord/releases/latest/download/gooncord.js"
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
// store app data dir for later checks (LogBox settings live under files/logbox) // store app data dir for later checks (LogBox settings live under files/logbox)
@ -77,7 +80,7 @@ object UpdaterModule : Module() {
}.getOrDefault(LoaderConfig()) }.getOrDefault(LoaderConfig())
} }
fun downloadScript(activity: Activity? = null): Job = scope.launch { fun downloadScript(activity: Activity? = null, showUpdateDialog: Boolean = true): Job = scope.launch {
try { try {
// Respect LogBox setting to disable bundle injections. // Respect LogBox setting to disable bundle injections.
// If the file "files/logbox/LOGBOX_SETTINGS" contains the flag bundleInjectionDisabled=true, // If the file "files/logbox/LOGBOX_SETTINGS" contains the flag bundleInjectionDisabled=true,
@ -129,7 +132,6 @@ object UpdaterModule : Module() {
Log.i("Bundle updated (${bytes.size} bytes)") Log.i("Bundle updated (${bytes.size} bytes)")
// This is a retry, so we show a dialog
if (activity != null) { if (activity != null) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
AlertDialog.Builder(activity).setTitle("GoonCord Update Successful") AlertDialog.Builder(activity).setTitle("GoonCord Update Successful")
@ -139,6 +141,22 @@ object UpdaterModule : Module() {
dialog.dismiss() dialog.dismiss()
}.setCancelable(false).show() }.setCancelable(false).show()
} }
} else if (showUpdateDialog) {
val act = lastActivity?.get()
act?.runOnUiThread {
AlertDialog.Builder(act)
.setTitle("GoonCord Update Downloaded")
.setMessage("A reload is required for changes to take effect.")
.setPositiveButton("Reload") { dialog, _ ->
reloadApp()
dialog.dismiss()
}
.setNegativeButton("Later") { dialog, _ ->
dialog.dismiss()
}
.setCancelable(false)
.show()
}
} }
} }

View file

@ -13,6 +13,8 @@ import GoonXposed.xposed.Constants
import GoonXposed.xposed.Module import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Companion.JSON import GoonXposed.xposed.Utils.Companion.JSON
import GoonXposed.xposed.Utils.Log import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.asDir
import GoonXposed.xposed.asFile
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.call.* import io.ktor.client.call.*
import io.ktor.client.engine.cio.* import io.ktor.client.engine.cio.*

View file

@ -5,9 +5,12 @@ import android.content.res.Resources
import androidx.core.graphics.toColorInt import androidx.core.graphics.toColorInt
import de.robv.android.xposed.callbacks.XC_LoadPackage import de.robv.android.xposed.callbacks.XC_LoadPackage
import GoonXposed.xposed.Constants import GoonXposed.xposed.Constants
import GoonXposed.xposed.MethodHookBuilder
import GoonXposed.xposed.Module import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Companion.JSON import GoonXposed.xposed.Utils.Companion.JSON
import GoonXposed.xposed.Utils.Log import GoonXposed.xposed.asFile
import GoonXposed.xposed.hook
import GoonXposed.xposed.method
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObjectBuilder import kotlinx.serialization.json.JsonObjectBuilder
@ -43,7 +46,7 @@ object ThemesModule : Module() {
private const val THEME_FILE = "current-theme.json" private const val THEME_FILE = "current-theme.json"
@Deprecated("This method is deprecated in the parent class")
@ExperimentalSerializationApi @ExperimentalSerializationApi
override fun buildPayload(builder: JsonObjectBuilder) { override fun buildPayload(builder: JsonObjectBuilder) {
builder.apply { builder.apply {
@ -82,16 +85,23 @@ object ThemesModule : Module() {
} }
fun hookTheme() { fun hookTheme() {
val theme = this.theme ?: return try {
val themeManager = runCatching { param.classLoader.loadClass("com.discord.theme.utils.ColorUtilsKt") }.getOrNull()
val darkTheme = runCatching { param.classLoader.loadClass("com.discord.theme.DarkTheme") }.getOrNull()
?: 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()
val themeManager = param.classLoader.safeLoadClass("com.discord.theme.utils.ColorUtilsKt") hookThemeInternal(themeManager, darkTheme, onyxTheme, lightTheme)
val darkTheme = param.classLoader.safeLoadClass("com.discord.theme.DarkerTheme") } catch (e: Throwable) {
val lightTheme = param.classLoader.safeLoadClass("com.discord.theme.LightTheme") GoonXposed.xposed.Utils.Log.e("Failed to hook theme: ${e.message}")
if (themeManager == null || darkTheme == null || lightTheme == null) {
Log.e("Failed to load Discord theme classes, skipping theme hooks")
return
} }
}
private fun hookThemeInternal(
themeManager: Class<*>?, darkTheme: Class<*>?, onyxTheme: Class<*>?, lightTheme: Class<*>?
) {
val theme = this.theme ?: return
// Apply rawColors // Apply rawColors
theme.data.rawColors?.forEach { (key, value) -> theme.data.rawColors?.forEach { (key, value) ->
@ -103,53 +113,72 @@ 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) {
val patch = MethodHookBuilder().run { try {
before { val getColorCompat = runCatching {
val arg1 = args[0]
val resources = if (arg1 is Context) arg1.resources else (arg1 as Resources)
val name = resources.getResourceEntryName(args[1] as Int)
if (rawColorMap[name] != null) result = rawColorMap[name]
}
build()
}
listOf(
runCatching {
themeManager.getDeclaredMethod( themeManager.getDeclaredMethod(
"getColorCompat", "getColorCompat",
Resources::class.java, Resources::class.java,
Int::class.javaPrimitiveType, Int::class.javaPrimitiveType,
Resources.Theme::class.java, Resources.Theme::class.java,
) )
}.getOrNull(), }.getOrNull()
runCatching {
val getColorCompatLegacy = runCatching {
themeManager.getDeclaredMethod( themeManager.getDeclaredMethod(
"getColorCompat", Context::class.java, Int::class.javaPrimitiveType "getColorCompat", Context::class.java, Int::class.javaPrimitiveType
) )
}.getOrNull() }.getOrNull()
).filterNotNull().forEach { it.hook(patch) }
val patch = MethodHookBuilder().run {
before {
try {
val arg1 = args[0]
val resources = if (arg1 is Context) arg1.resources else (arg1 as Resources)
val name = resources.getResourceEntryName(args[1] as Int)
if (rawColorMap[name] != null) result = rawColorMap[name]
} catch (_: Throwable) {
}
}
build()
}
getColorCompat?.hook(patch)
getColorCompatLegacy?.hook(patch)
} catch (e: Throwable) {
GoonXposed.xposed.Utils.Log.e("Failed to hook getColorCompat: ${e.message}")
}
} }
} }
// Parse HEX colour string to INT. Takes "#RRGGBBAA" or "#RRGGBB" // Parse HEX colour string to INT. Takes "#RRGGBBAA" or "#RRGGBB"
private fun hexStringToColorInt(hexString: String): Int { private fun hexStringToColorInt(hexString: String): Int {
return if (hexString.length == 9) { return try {
// Rearrange RRGGBBAA -> AARRGGBB so parseColor() is happy if (hexString.length == 9) {
val alpha = hexString.substring(7, 9) // Rearrange RRGGBBAA -> AARRGGBB so parseColor() is happy
val rrggbb = hexString.substring(1, 7) val alpha = hexString.substring(7, 9)
"#$alpha$rrggbb".toColorInt() val rrggbb = hexString.substring(1, 7)
} else hexString.toColorInt() "#$alpha$rrggbb".toColorInt()
} else hexString.toColorInt()
} catch (_: Throwable) {
0
}
} }
private fun hookThemeMethod(themeClass: Class<*>, methodName: String, themeValue: Int) { private fun hookThemeMethod(themeClass: Class<*>, methodName: String, themeValue: Int) {