From 52bcb9912bb378c295965d57182e6cf63e5d4edc Mon Sep 17 00:00:00 2001 From: kmmiio99o Date: Tue, 6 Jan 2026 13:33:41 +0100 Subject: [PATCH] feat: Add option to disable bundle injection why? People can disable bundle injection when Kettu gets stuck --- .../xposed/modules/HookScriptLoaderModule.kt | 47 ++++++++++++++---- .../revenge/xposed/modules/LogBoxModule.kt | 36 ++++++++++++-- .../revenge/xposed/modules/UpdaterModule.kt | 49 ++++++++++++++++++- 3 files changed, 118 insertions(+), 14 deletions(-) diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/HookScriptLoaderModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/HookScriptLoaderModule.kt index 1e0d282..0d39d45 100644 --- a/app/src/main/kotlin/io/github/revenge/xposed/modules/HookScriptLoaderModule.kt +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/HookScriptLoaderModule.kt @@ -2,6 +2,7 @@ package io.github.revenge.xposed.modules import android.content.res.AssetManager import android.content.res.XModuleResources +import android.content.Context import de.robv.android.xposed.IXposedHookZygoteInit import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.callbacks.XC_LoadPackage @@ -28,6 +29,9 @@ object HookScriptLoaderModule : Module() { private lateinit var preloadsDir: File private lateinit var mainScript: File + // Directory to read loader config from when checking disable flag + private var moduleFilesDir: File? = null + private lateinit var modulePath: String private lateinit var resources: XModuleResources @@ -37,6 +41,16 @@ object HookScriptLoaderModule : Module() { this@HookScriptLoaderModule.modulePath = startupParam.modulePath } + override fun onContext(context: android.content.Context) { + // Keep a reference to the module's files directory so we can check the loader config + try { + val dir = File(context.dataDir, Constants.FILES_DIR).apply { asDir() } + moduleFilesDir = dir + } catch (_: Exception) { + // best-effort only + } + } + override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { val cacheDir = File(appInfo.dataDir, Constants.CACHE_DIR).apply { asDir() } val filesDir = File(appInfo.dataDir, Constants.FILES_DIR).apply { asDir() } @@ -102,17 +116,32 @@ object HookScriptLoaderModule : Module() { try { preloadsDir.walk().filter { it.isFile }.forEach(runScriptFile) - if (mainScript.exists()) runScriptFile(mainScript) - else { - Log.i("Main script does not exist, falling back") + // Check whether bundle injection execution is disabled via loader config. + // UpdaterModule.isInjectionDisabled reads the in-memory config (loaded in its onLoad), + // and falls back to disk if necessary. We call it without a Context because by the time + // this runs the UpdaterModule config should already be available. + val injectionDisabled = try { + io.github.revenge.xposed.modules.UpdaterModule.isInjectionDisabled() + } catch (e: Throwable) { + Log.e("Failed to check injection disabled flag: ${e.message}") + false + } - if (!::resources.isInitialized) resources = XModuleResources.createInstance(modulePath, null) + if (injectionDisabled) { + Log.i("Bundle injection disabled by loader config - skipping main bundle execution") + } else { + if (mainScript.exists()) runScriptFile(mainScript) + else { + Log.i("Main script does not exist, falling back") - XposedBridge.invokeOriginalMethod( - loadScriptFromAssets, - thisObject, - arrayOf(resources.assets, "assets://revenge.bundle", loadSynchronously) - ) + if (!::resources.isInitialized) resources = XModuleResources.createInstance(modulePath, null) + + XposedBridge.invokeOriginalMethod( + loadScriptFromAssets, + thisObject, + arrayOf(resources.assets, "assets://revenge.bundle", loadSynchronously) + ) + } } } catch (e: Throwable) { Log.e("Unable to run scripts:", e) diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/LogBoxModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/LogBoxModule.kt index c8fa021..6ba79ea 100644 --- a/app/src/main/kotlin/io/github/revenge/xposed/modules/LogBoxModule.kt +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/LogBoxModule.kt @@ -44,7 +44,8 @@ object LogBoxModule : Module() { try { val dcdReactNativeHostClass = classLoader.loadClass("com.discord.bridge.DCDReactNativeHost") - val getUseDeveloperSupportMethod = dcdReactNativeHostClass.methods.first { it.name == "getUseDeveloperSupport" } + val getUseDeveloperSupportMethod = + dcdReactNativeHostClass.methods.first { it.name == "getUseDeveloperSupport" } getUseDeveloperSupportMethod.hook { before { @@ -354,6 +355,25 @@ object LogBoxModule : Module() { ) { refetchBundle(context) } }) + // toggle bundle injection + val bundleInjectionText = if (io.github.revenge.xposed.modules.UpdaterModule.isInjectionDisabled(context)) { + "Enable Bundle Injection" + } else { + "Disable Bundle Injection" + } + container.addView(createButton(context, bundleInjectionText, colors) { + dialog.dismiss() + showConfirmAction( + context, + if (io.github.revenge.xposed.modules.UpdaterModule.isInjectionDisabled(context)) "Enable Bundle Injection" else "Disable Bundle Injection", + "This will ${if (io.github.revenge.xposed.modules.UpdaterModule.isInjectionDisabled(context)) "enable" else "disable"} executing the cached/downloaded bundle. The app will reload to apply the change." + ) { + val currentlyDisabled = io.github.revenge.xposed.modules.UpdaterModule.isInjectionDisabled(context) + io.github.revenge.xposed.modules.UpdaterModule.setDisableInjection(context, !currentlyDisabled) + reloadApp() + } + }) + // reload App Button container.addView(createButton(context, "Reload App", colors) { dialog.dismiss() @@ -552,7 +572,11 @@ object LogBoxModule : Module() { val thumbCornerRadius = thumbSize / 2f val switchThumb = View(context).apply { - background = createM3Background(context, if (isEnabled) colors.onPrimary else colors.onSurfaceVariant, thumbCornerRadius) + background = createM3Background( + context, + if (isEnabled) colors.onPrimary else colors.onSurfaceVariant, + thumbCornerRadius + ) val params = FrameLayout.LayoutParams(thumbSize, thumbSize) val thumbLeft = if (isEnabled) { @@ -848,11 +872,15 @@ object LogBoxModule : Module() { private fun constrainContainerWidth(context: Context, container: LinearLayout) { try { val maxW = (context.resources.displayMetrics.widthPixels * 0.88).toInt() - val lp = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT) + val lp = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ) container.layoutParams = lp try { container.minimumWidth = maxW - } catch (_: Exception) {} + } catch (_: Exception) { + } } catch (_: Exception) { } } diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt index 1769ca0..351ce91 100644 --- a/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt @@ -32,7 +32,8 @@ data class CustomLoadUrl( @Serializable data class LoaderConfig( - val customLoadUrl: CustomLoadUrl = CustomLoadUrl() + val customLoadUrl: CustomLoadUrl = CustomLoadUrl(), + val disableInjection: Boolean = false ) /** @@ -152,4 +153,50 @@ object UpdaterModule : Module() { val config = File(filesDir, CONFIG_FILE) if (config.exists()) config.delete() } + + fun setDisableInjection(context: Context, disabled: Boolean) { + try { + val filesDir = File(context.dataDir, Constants.FILES_DIR) + filesDir.mkdirs() + val configFile = File(filesDir, CONFIG_FILE) + + val current = runCatching { + if (configFile.exists()) JSON.decodeFromString(configFile.readText()) else LoaderConfig() + }.getOrDefault(LoaderConfig()) + + val newCfg = current.copy(disableInjection = disabled) + configFile.writeText(JSON.encodeToString(newCfg)) + Toast.makeText(context, "Bundle injection ${if (disabled) "disabled" else "enabled"}", Toast.LENGTH_SHORT) + .show() + } catch (e: Exception) { + Log.e("Failed to update loader config: ${e.message}", e) + Toast.makeText(context, "Failed to update loader config", Toast.LENGTH_SHORT).show() + } + } + + /** + * Convenience to check whether injection is disabled (reads current in-memory config first, + * falls back to disk if needed). + */ + fun isInjectionDisabled(context: Context? = null): Boolean { + // Prefer in-memory config if loaded + try { + if (::config.isInitialized) { + return config.disableInjection + } + + // If a Context is provided, attempt to read on-disk config + if (context != null) { + val filesDir = File(context.dataDir, Constants.FILES_DIR) + val configFile = File(filesDir, CONFIG_FILE) + if (configFile.exists()) { + val disk = JSON.decodeFromString(configFile.readText()) + return disk.disableInjection + } + } + } catch (e: Exception) { + Log.e("Error checking disableInjection flag: ${e.message}", e) + } + return false + } }