feat: Add option to disable bundle injection (#2)
This commit is contained in:
commit
3ba04c366c
3 changed files with 118 additions and 14 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue