diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index ccd5bd0..b268ef3 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -5,9 +5,6 @@ - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index e0ef6f7..2557637 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/app/src/main/kotlin/io/github/revenge/xposed/Main.kt b/app/src/main/kotlin/io/github/revenge/xposed/Main.kt index 1ccea51..43a5363 100644 --- a/app/src/main/kotlin/io/github/revenge/xposed/Main.kt +++ b/app/src/main/kotlin/io/github/revenge/xposed/Main.kt @@ -44,7 +44,7 @@ class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit { FixResourcesModule, BlockDeepLinksTrackingModule, BlockCrashReportingModule, - LogBoxModule(), + LogBoxModule, CacheModule, FontsModule, ThemesModule, 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 6a1aa0e..2516414 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 @@ -1,70 +1,82 @@ package io.github.revenge.xposed.modules -import android.content.Context -import de.robv.android.xposed.callbacks.XC_LoadPackage -import cocobo1.pupu.xposed.BuildConfig -import io.github.revenge.xposed.Module -import io.github.revenge.xposed.Utils -import io.github.revenge.xposed.Utils.Companion.reloadApp -import io.github.revenge.xposed.Utils.Log import android.app.AlertDialog -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.widget.EditText +import android.content.Context import android.widget.Toast import de.robv.android.xposed.XposedHelpers +import de.robv.android.xposed.callbacks.XC_LoadPackage +import io.github.revenge.xposed.Module +import io.github.revenge.xposed.Utils.Companion.reloadApp +import io.github.revenge.xposed.Utils.Log import kotlinx.coroutines.* -import org.json.JSONArray import org.json.JSONObject import java.io.File import java.net.HttpURLConnection import java.net.URL -class LogBoxModule : Module() { +object LogBoxModule : Module() { lateinit var packageParam: XC_LoadPackage.LoadPackageParam private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) - + override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { this@LogBoxModule.packageParam = packageParam - - val dcdReactNativeHostClass = classLoader.loadClass("com.discord.bridge.DCDReactNativeHost") - val getUseDeveloperSupportMethod = dcdReactNativeHostClass.methods.first { it.name == "getUseDeveloperSupport" } - - getUseDeveloperSupportMethod.hook { - before { - result = true + + try { + val dcdReactNativeHostClass = classLoader.loadClass("com.discord.bridge.DCDReactNativeHost") + val getUseDeveloperSupportMethod = dcdReactNativeHostClass.methods.first { it.name == "getUseDeveloperSupport" } + + getUseDeveloperSupportMethod.hook { + before { + result = true + } } + } catch (e: Exception) { + Log.e("Failed to hook DCDReactNativeHost: ${e.message}") } - + + listOf( "com.facebook.react.devsupport.BridgeDevSupportManager", "com.facebook.react.devsupport.BridgelessDevSupportManager" - ).mapNotNull { packageParam.classLoader.safeLoadClass(it) }.forEach { - hookDevSupportManager(it) + ).mapNotNull { packageParam.classLoader.safeLoadClass(it) }.forEach { + hookDevSupportManager(it) } - + return@with } - + + override fun onContext(context: Context) { + } + private fun hookDevSupportManager(clazz: Class<*>) { - val showDevOptionsDialogMethod = clazz.methods.first { it.name == "showDevOptionsDialog" } - - showDevOptionsDialogMethod.hook { - before { - try { - val context = getContextFromDevSupport(clazz, thisObject) - if (context != null) { - showRecoveryMenu(context) - } + try { + val handleReloadJSMethod = clazz.methods.first { it.name == "handleReloadJS" } + handleReloadJSMethod.hook { + before { + reloadApp() result = null - } catch (e: Exception) { - Log.e("Failed to show recovery menu: ${e.message}") } } + + val showDevOptionsDialogMethod = clazz.methods.first { it.name == "showDevOptionsDialog" } + showDevOptionsDialogMethod.hook { + before { + try { + val context = getContextFromDevSupport(clazz, thisObject) + if (context != null) { + showRecoveryMenu(context) + } + result = null // Ignore the original dev menu + } catch (e: Exception) { + Log.e("Failed to show recovery menu: ${e.message}") + } + } + } + } catch (e: Exception) { + Log.e("Failed to hook DevSupportManager methods on class ${clazz.name}: ${e.message}") } } - + private fun getContextFromDevSupport(clazz: Class<*>, instance: Any?): Context? { return try { val mReactInstanceDevHelperField = XposedHelpers.findField(clazz, "mReactInstanceDevHelper") @@ -76,44 +88,14 @@ class LogBoxModule : Module() { null } } - - private fun reloadReactNative(context: Context) { - try { - val reactInstanceManagerClass = packageParam.classLoader.loadClass("com.facebook.react.ReactInstanceManager") - val reactApplicationClass = packageParam.classLoader.loadClass("com.facebook.react.ReactApplication") - - val app = context.applicationContext - - if (reactApplicationClass.isInstance(app)) { - val getReactNativeHostMethod = reactApplicationClass.methods.first { it.name == "getReactNativeHost" } - val reactNativeHost = getReactNativeHostMethod.invoke(app) - - val getReactInstanceManagerMethod = reactNativeHost.javaClass.methods.first { it.name == "getReactInstanceManager" } - val reactInstanceManager = getReactInstanceManagerMethod.invoke(reactNativeHost) - - if (reactInstanceManager != null) { - val reloadMethod = reactInstanceManagerClass.methods.first { it.name == "recreateReactContextInBackground" } - reloadMethod.invoke(reactInstanceManager) - } else { - showError(context, "Error", "Could not get React Instance Manager") - } - } else { - showError(context, "Error", "Application is not a React Application") - } - } catch (e: Exception) { - Log.e("Failed to Reload App: ${e.message}") - showError(context, "Failed to reload", "Error: ${e.message}") - } - } - + private fun showRecoveryMenu(context: Context) { val options = arrayOf( if (isSafeModeEnabled(context)) "Disable Safe Mode" else "Enable Safe Mode", - "Refetch Bundle", - "Load Custom Bundle", - "Reload App" + "Reset Bundle", + "Reload App (Full Relaunch)" ) - + AlertDialog.Builder(context) .setTitle("KettuXposed Recovery Menu") .setItems(options) { _, which -> @@ -122,16 +104,15 @@ class LogBoxModule : Module() { .setNegativeButton("Cancel", null) .show() } - + private fun handleMenuSelection(context: Context, index: Int) { when (index) { 0 -> toggleSafeMode(context) - 1 -> confirmAction(context, "refetch the bundle") { refetchBundle(context) } - 2 -> loadCustomBundle(context) - 3 -> reloadApp() + 1 -> confirmAction(context, "reset bundle") { resetBundle(context) } + 2 -> reloadApp() } } - + private fun confirmAction(context: Context, actionText: String, action: () -> Unit) { AlertDialog.Builder(context) .setTitle("Confirm Action") @@ -140,12 +121,12 @@ class LogBoxModule : Module() { .setNegativeButton("Cancel", null) .show() } - + private fun isSafeModeEnabled(context: Context): Boolean { return try { val settingsFile = File(context.filesDir, "vd_mmkv/VENDETTA_SETTINGS") if (!settingsFile.exists()) return false - + val json = JSONObject(settingsFile.readText()) json.optJSONObject("safeMode")?.optBoolean("enabled", false) ?: false } catch (e: Exception) { @@ -153,26 +134,26 @@ class LogBoxModule : Module() { false } } - + private fun toggleSafeMode(context: Context) { try { val settingsFile = File(context.filesDir, "vd_mmkv/VENDETTA_SETTINGS") val themeFile = File(context.filesDir, "vd_mmkv/VENDETTA_THEMES") - + settingsFile.parentFile?.mkdirs() - + val settings = if (settingsFile.exists()) { JSONObject(settingsFile.readText()) } else { JSONObject() } - + val safeMode = settings.optJSONObject("safeMode") ?: JSONObject() val currentState = safeMode.optBoolean("enabled", false) val newState = !currentState - + safeMode.put("enabled", newState) - + if (newState && themeFile.exists()) { val theme = JSONObject(themeFile.readText()) val themeId = theme.optString("id") @@ -181,12 +162,11 @@ class LogBoxModule : Module() { themeFile.delete() } } - + settings.put("safeMode", safeMode) settingsFile.writeText(settings.toString()) - + Toast.makeText(context, "Safe Mode ${if (newState) "Enabled" else "Disabled"}", Toast.LENGTH_SHORT).show() - reloadApp() } catch (e: Exception) { @@ -194,40 +174,20 @@ class LogBoxModule : Module() { showError(context, "Failed to toggle safe mode", e.message) } } - - private fun refetchBundle(context: Context) { - try { - val pyoncordDir = getPyoncordDirectory(context) - val bundleFile = File(pyoncordDir, "bundle.js") - val backupFile = File(pyoncordDir, "bundle.js.backup") - - if (bundleFile.exists()) { - backupFile.delete() - bundleFile.renameTo(backupFile) - Log.e("Bundle moved to backup") - } - reloadApp() - - } catch (e: Exception) { - Log.e("Error refetching bundle: ${e.message}") - showError(context, "Failed to refetch bundle", e.message) - } - } - private fun resetBundle(context: Context) { try { val pyoncordDir = getPyoncordDirectory(context) val bundleFile = File(pyoncordDir, "bundle.js") + val backupFile = File(pyoncordDir, "bundle.js.backup") val configFile = File(pyoncordDir, "loader_config.json") - + bundleFile.delete() - - // Reset config + backupFile.delete() + if (configFile.exists()) { val config = JSONObject(configFile.readText()) config.put("customLoadUrlEnabled", false) - config.put("customLoadUrl", "http://localhost:4040/kettu.js") configFile.writeText(config.toString()) } @@ -237,100 +197,7 @@ class LogBoxModule : Module() { showError(context, "Failed to reset bundle", e.message) } } - - private fun loadCustomBundle(context: Context) { - val input = EditText(context).apply { - hint = "http://localhost:4040/kettu.js" - inputType = android.text.InputType.TYPE_TEXT_VARIATION_URI - } - - AlertDialog.Builder(context) - .setTitle("Load Custom Bundle") - .setMessage("Enter the URL for your custom bundle:") - .setView(input) - .setPositiveButton("Load") { _, _ -> - val urlString = input.text.toString().trim() - if (urlString.isEmpty()) { - showError(context, "Invalid URL", "Please enter a URL") - return@setPositiveButton - } - - validateAndLoadBundle(context, urlString) - } - .setNegativeButton("Cancel", null) - .show() - } - - private fun validateAndLoadBundle(context: Context, urlString: String) { - scope.launch(Dispatchers.IO) { - try { - val url = URL(urlString) - if (url.protocol != "http" && url.protocol != "https") { - withContext(Dispatchers.Main) { - showError(context, "Invalid URL", "URL must start with http:// or https://") - } - return@launch - } - - val connection = url.openConnection() as HttpURLConnection - connection.requestMethod = "GET" - connection.connectTimeout = 10000 - connection.readTimeout = 10000 - - val responseCode = connection.responseCode - if (responseCode != 200) { - withContext(Dispatchers.Main) { - showError(context, "Error", "Server returned error $responseCode") - } - return@launch - } - - val contentType = connection.contentType - if (contentType != null && !contentType.contains("javascript") && !contentType.contains("text")) { - withContext(Dispatchers.Main) { - showError(context, "Invalid Content", "URL must point to a JavaScript file") - } - return@launch - } - - // Save custom bundle URL - withContext(Dispatchers.Main) { - setCustomBundleURL(context, urlString) - } - } catch (e: Exception) { - withContext(Dispatchers.Main) { - showError(context, "Connection Error", "Could not reach URL: ${e.message}") - } - } - } - } - - private fun setCustomBundleURL(context: Context, url: String) { - try { - val pyoncordDir = getPyoncordDirectory(context) - val configFile = File(pyoncordDir, "loader_config.json") - - val config = if (configFile.exists()) { - JSONObject(configFile.readText()) - } else { - JSONObject() - } - - config.put("customLoadUrlEnabled", true) - config.put("customLoadUrl", url) - configFile.writeText(config.toString()) - - File(pyoncordDir, "bundle.js").delete() - - Toast.makeText(context, "Custom bundle set", Toast.LENGTH_SHORT).show() - - reloadApp() - } catch (e: Exception) { - Log.e("Error setting custom bundle URL: ${e.message}") - showError(context, "Failed to save configuration", e.message) - } - } - + private fun getPyoncordDirectory(context: Context): File { val dir = File(context.filesDir, "pyoncord") if (!dir.exists()) { @@ -338,7 +205,7 @@ class LogBoxModule : Module() { } return dir } - + private fun showError(context: Context, title: String, message: String?) { AlertDialog.Builder(context) .setTitle(title) @@ -346,7 +213,4 @@ class LogBoxModule : Module() { .setPositiveButton("OK", null) .show() } - - override fun onContext(context: Context) { - } } \ No newline at end of file