From 43f024969d582b92a942c39ee8ed4cd55cd21c36 Mon Sep 17 00:00:00 2001 From: Rosie Val Date: Fri, 25 Sep 2026 16:14:38 -0400 Subject: [PATCH] fix(ci): fix compilation errors from package rename, restore missing imports and helpers, fix 404 bundle URL --- .../ShiggyXposed/xposed/xposed/Module.kt | 251 ++++++++++-------- .../xposed/xposed/modules/CacheModule.kt | 3 + .../xposed/modules/FixResourcesModule.kt | 1 + .../xposed/modules/HookScriptLoaderModule.kt | 6 + .../xposed/modules/PayloadGlobalModule.kt | 4 + .../xposed/xposed/modules/PluginsModule.kt | 1 + .../xposed/xposed/modules/UpdaterModule.kt | 24 +- .../xposed/modules/appearance/FontsModule.kt | 2 + .../xposed/modules/appearance/ThemesModule.kt | 101 ++++--- 9 files changed, 247 insertions(+), 146 deletions(-) diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Module.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Module.kt index 41f878f..ae894f3 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Module.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Module.kt @@ -2,10 +2,12 @@ package GoonXposed.xposed import android.app.Activity import android.content.Context +import android.content.pm.PackageManager import android.os.Build import de.robv.android.xposed.IXposedHookZygoteInit import de.robv.android.xposed.XC_MethodHook import de.robv.android.xposed.XposedBridge +import de.robv.android.xposed.XposedHelpers import de.robv.android.xposed.callbacks.XC_LoadPackage import kotlinx.serialization.json.JsonObjectBuilder import java.io.File @@ -18,13 +20,18 @@ data class AppInfo( 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. */ @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. @@ -63,121 +70,151 @@ abstract class Module { */ open fun onActivity(activity: Activity) {} - protected fun Context.getAppInfo(): AppInfo { - 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() + open fun onResume(activity: Activity) {} - return AppInfo( - 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()) + open fun onPause(activity: Activity) {} + 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( - name: String, vararg params: Class<*>?, block: MethodHookBuilder.() -> Unit - ): XC_MethodHook.Unhook = method(name, *params).hook(MethodHookBuilder().apply(block).build()) + name: String, + 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 - private var afterBlock: (HookScope.() -> Unit)? = null +/** + * Runtime wrapper around an [XC_MethodHook.MethodHookParam] used as the receiver of every + * `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) { - beforeBlock = block + val thisObject: Any? + get() = param.thisObject + + val args: Array + get() = param.args + + var result: Any? + get() = param.result + set(value) { + param.result = value } - fun after(block: HookScope.() -> Unit) { - afterBlock = block + var throwable: Throwable? + 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 Unit>() + private val afterCallbacks = mutableListOf 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 beforeHookedMethod(param: MethodHookParam) { - val b = beforeBlock - 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) - } - } + override fun afterHookedMethod(param: MethodHookParam) { + val scope = HookScope(param = param, proceed = { p -> super.afterHookedMethod(p) }) + afterCallbacks.forEach { scope.run(it) } } } - /** - * Scope object passed to before/after hook blocks. - * - * Provides: - * - 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 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 - } + companion object { + fun from(block: MethodHookBuilder.() -> Unit): XC_MethodHook = MethodHookBuilder().run { + block() + build() + } } } + +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 + ) +} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/CacheModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/CacheModule.kt index 329d925..4684ca1 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/CacheModule.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/CacheModule.kt @@ -4,6 +4,9 @@ import android.content.Context import android.util.AtomicFile import GoonXposed.xposed.Module import GoonXposed.xposed.Utils.Log +import GoonXposed.xposed.asDir +import GoonXposed.xposed.asFile +import GoonXposed.xposed.getAppInfo import GoonXposed.xposed.modules.bridge.BridgeModule import java.io.* diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/FixResourcesModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/FixResourcesModule.kt index c6f39fe..2501a12 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/FixResourcesModule.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/FixResourcesModule.kt @@ -4,6 +4,7 @@ import android.content.res.Resources import de.robv.android.xposed.callbacks.XC_LoadPackage import GoonXposed.xposed.Constants.Companion.TARGET_PACKAGE import GoonXposed.xposed.Module +import GoonXposed.xposed.hookMethod /** * Hooks [Resources.getIdentifier] to fix resource package name mismatch. diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/HookScriptLoaderModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/HookScriptLoaderModule.kt index 427eb59..9017233 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/HookScriptLoaderModule.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/HookScriptLoaderModule.kt @@ -6,9 +6,15 @@ import de.robv.android.xposed.IXposedHookZygoteInit import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.callbacks.XC_LoadPackage import GoonXposed.xposed.Constants +import GoonXposed.xposed.HookScope import GoonXposed.xposed.HookStateHolder import GoonXposed.xposed.Module 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 kotlinx.coroutines.async import kotlinx.coroutines.awaitAll diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PayloadGlobalModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PayloadGlobalModule.kt index 9fe4604..1e27102 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PayloadGlobalModule.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PayloadGlobalModule.kt @@ -6,9 +6,13 @@ import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.callbacks.XC_LoadPackage import GoonXposed.xposed.BuildConfig import GoonXposed.xposed.Constants +import GoonXposed.xposed.MethodHookBuilder import GoonXposed.xposed.Module import GoonXposed.xposed.Utils.Companion.JSON 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 kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PluginsModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PluginsModule.kt index 8aae94b..283284c 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PluginsModule.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PluginsModule.kt @@ -4,6 +4,7 @@ import android.content.Context import android.util.AtomicFile import GoonXposed.xposed.Module import GoonXposed.xposed.Utils.Log +import GoonXposed.xposed.asFile import GoonXposed.xposed.modules.bridge.BridgeModule import java.io.* diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/UpdaterModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/UpdaterModule.kt index a38666e..02f79f0 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/UpdaterModule.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/UpdaterModule.kt @@ -56,8 +56,11 @@ object UpdaterModule : Module() { private const val ETAG_FILE = "etag.txt" 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 = - "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) { // store app data dir for later checks (LogBox settings live under files/logbox) @@ -77,7 +80,7 @@ object UpdaterModule : Module() { }.getOrDefault(LoaderConfig()) } - fun downloadScript(activity: Activity? = null): Job = scope.launch { + fun downloadScript(activity: Activity? = null, showUpdateDialog: Boolean = true): Job = scope.launch { try { // Respect LogBox setting to disable bundle injections. // 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)") - // This is a retry, so we show a dialog if (activity != null) { withContext(Dispatchers.Main) { AlertDialog.Builder(activity).setTitle("GoonCord Update Successful") @@ -139,6 +141,22 @@ object UpdaterModule : Module() { dialog.dismiss() }.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() + } } } diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/appearance/FontsModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/appearance/FontsModule.kt index 7a42ecc..a44b444 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/appearance/FontsModule.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/appearance/FontsModule.kt @@ -13,6 +13,8 @@ import GoonXposed.xposed.Constants import GoonXposed.xposed.Module import GoonXposed.xposed.Utils.Companion.JSON import GoonXposed.xposed.Utils.Log +import GoonXposed.xposed.asDir +import GoonXposed.xposed.asFile import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.engine.cio.* diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/appearance/ThemesModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/appearance/ThemesModule.kt index a983ff9..d39d411 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/appearance/ThemesModule.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/appearance/ThemesModule.kt @@ -5,9 +5,12 @@ import android.content.res.Resources import androidx.core.graphics.toColorInt import de.robv.android.xposed.callbacks.XC_LoadPackage import GoonXposed.xposed.Constants +import GoonXposed.xposed.MethodHookBuilder import GoonXposed.xposed.Module 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.Serializable import kotlinx.serialization.json.JsonObjectBuilder @@ -43,7 +46,7 @@ object ThemesModule : Module() { private const val THEME_FILE = "current-theme.json" - + @Deprecated("This method is deprecated in the parent class") @ExperimentalSerializationApi override fun buildPayload(builder: JsonObjectBuilder) { builder.apply { @@ -82,16 +85,23 @@ object ThemesModule : Module() { } 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") - val darkTheme = param.classLoader.safeLoadClass("com.discord.theme.DarkerTheme") - val lightTheme = param.classLoader.safeLoadClass("com.discord.theme.LightTheme") - - if (themeManager == null || darkTheme == null || lightTheme == null) { - Log.e("Failed to load Discord theme classes, skipping theme hooks") - return + hookThemeInternal(themeManager, darkTheme, onyxTheme, lightTheme) + } catch (e: Throwable) { + GoonXposed.xposed.Utils.Log.e("Failed to hook theme: ${e.message}") } + } + + private fun hookThemeInternal( + themeManager: Class<*>?, darkTheme: Class<*>?, onyxTheme: Class<*>?, lightTheme: Class<*>? + ) { + val theme = this.theme ?: return // Apply rawColors theme.data.rawColors?.forEach { (key, value) -> @@ -103,53 +113,72 @@ object ThemesModule : Module() { // TEXT_NORMAL -> getTextNormal val methodName = "get${key.fromScreamingSnakeToCamelCase()}" value.forEachIndexed { index, v -> + val colorInt = hexStringToColorInt(v) when (index) { - 0 -> hookThemeMethod(darkTheme, methodName, hexStringToColorInt(v)) - 1 -> hookThemeMethod(lightTheme, methodName, hexStringToColorInt(v)) + 0 -> { + 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 (!theme.data.rawColors.isNullOrEmpty()) { - val patch = MethodHookBuilder().run { - before { - 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 { + if (!theme.data.rawColors.isNullOrEmpty() && themeManager != null) { + try { + val getColorCompat = runCatching { themeManager.getDeclaredMethod( "getColorCompat", Resources::class.java, Int::class.javaPrimitiveType, Resources.Theme::class.java, ) - }.getOrNull(), - runCatching { + }.getOrNull() + + val getColorCompatLegacy = runCatching { themeManager.getDeclaredMethod( "getColorCompat", Context::class.java, Int::class.javaPrimitiveType ) }.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" private fun hexStringToColorInt(hexString: String): Int { - return if (hexString.length == 9) { - // Rearrange RRGGBBAA -> AARRGGBB so parseColor() is happy - val alpha = hexString.substring(7, 9) - val rrggbb = hexString.substring(1, 7) - "#$alpha$rrggbb".toColorInt() - } else hexString.toColorInt() + return try { + if (hexString.length == 9) { + // Rearrange RRGGBBAA -> AARRGGBB so parseColor() is happy + val alpha = hexString.substring(7, 9) + val rrggbb = hexString.substring(1, 7) + "#$alpha$rrggbb".toColorInt() + } else hexString.toColorInt() + } catch (_: Throwable) { + 0 + } } private fun hookThemeMethod(themeClass: Class<*>, methodName: String, themeValue: Int) {