feat: Add option to disable bundle injection

why? People can disable bundle injection when Kettu gets stuck
This commit is contained in:
kmmiio99o 2026-01-06 13:33:41 +01:00
parent 27e4917c34
commit 52bcb9912b
No known key found for this signature in database
GPG key ID: 163FE39F7772D2EC
3 changed files with 118 additions and 14 deletions

View file

@ -2,6 +2,7 @@ package io.github.revenge.xposed.modules
import android.content.res.AssetManager import android.content.res.AssetManager
import android.content.res.XModuleResources import android.content.res.XModuleResources
import android.content.Context
import de.robv.android.xposed.IXposedHookZygoteInit 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
@ -28,6 +29,9 @@ object HookScriptLoaderModule : Module() {
private lateinit var preloadsDir: File private lateinit var preloadsDir: File
private lateinit var mainScript: 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 modulePath: String
private lateinit var resources: XModuleResources private lateinit var resources: XModuleResources
@ -37,6 +41,16 @@ object HookScriptLoaderModule : Module() {
this@HookScriptLoaderModule.modulePath = startupParam.modulePath 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) { override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
val cacheDir = File(appInfo.dataDir, Constants.CACHE_DIR).apply { asDir() } val cacheDir = File(appInfo.dataDir, Constants.CACHE_DIR).apply { asDir() }
val filesDir = File(appInfo.dataDir, Constants.FILES_DIR).apply { asDir() } val filesDir = File(appInfo.dataDir, Constants.FILES_DIR).apply { asDir() }
@ -102,17 +116,32 @@ object HookScriptLoaderModule : Module() {
try { try {
preloadsDir.walk().filter { it.isFile }.forEach(runScriptFile) preloadsDir.walk().filter { it.isFile }.forEach(runScriptFile)
if (mainScript.exists()) runScriptFile(mainScript) // Check whether bundle injection execution is disabled via loader config.
else { // UpdaterModule.isInjectionDisabled reads the in-memory config (loaded in its onLoad),
Log.i("Main script does not exist, falling back") // 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( if (!::resources.isInitialized) resources = XModuleResources.createInstance(modulePath, null)
loadScriptFromAssets,
thisObject, XposedBridge.invokeOriginalMethod(
arrayOf(resources.assets, "assets://revenge.bundle", loadSynchronously) loadScriptFromAssets,
) thisObject,
arrayOf(resources.assets, "assets://revenge.bundle", loadSynchronously)
)
}
} }
} catch (e: Throwable) { } catch (e: Throwable) {
Log.e("Unable to run scripts:", e) Log.e("Unable to run scripts:", e)

View file

@ -44,7 +44,8 @@ object LogBoxModule : Module() {
try { try {
val dcdReactNativeHostClass = classLoader.loadClass("com.discord.bridge.DCDReactNativeHost") 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 { getUseDeveloperSupportMethod.hook {
before { before {
@ -354,6 +355,25 @@ object LogBoxModule : Module() {
) { refetchBundle(context) } ) { 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 // reload App Button
container.addView(createButton(context, "Reload App", colors) { container.addView(createButton(context, "Reload App", colors) {
dialog.dismiss() dialog.dismiss()
@ -552,7 +572,11 @@ object LogBoxModule : Module() {
val thumbCornerRadius = thumbSize / 2f val thumbCornerRadius = thumbSize / 2f
val switchThumb = View(context).apply { 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 params = FrameLayout.LayoutParams(thumbSize, thumbSize)
val thumbLeft = if (isEnabled) { val thumbLeft = if (isEnabled) {
@ -848,11 +872,15 @@ object LogBoxModule : Module() {
private fun constrainContainerWidth(context: Context, container: LinearLayout) { private fun constrainContainerWidth(context: Context, container: LinearLayout) {
try { try {
val maxW = (context.resources.displayMetrics.widthPixels * 0.88).toInt() 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 container.layoutParams = lp
try { try {
container.minimumWidth = maxW container.minimumWidth = maxW
} catch (_: Exception) {} } catch (_: Exception) {
}
} catch (_: Exception) { } catch (_: Exception) {
} }
} }

View file

@ -32,7 +32,8 @@ data class CustomLoadUrl(
@Serializable @Serializable
data class LoaderConfig( 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) val config = File(filesDir, CONFIG_FILE)
if (config.exists()) config.delete() 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<LoaderConfig>(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<LoaderConfig>(configFile.readText())
return disk.disableInjection
}
}
} catch (e: Exception) {
Log.e("Error checking disableInjection flag: ${e.message}", e)
}
return false
}
} }