From f8fe4f03a9c16024df92029e1b5a79a44bb1c923 Mon Sep 17 00:00:00 2001 From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com> Date: Thu, 11 Dec 2025 18:45:08 +0100 Subject: [PATCH 1/8] Update README file I don't know, just better documentation --- README.md | 203 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 178 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 49202a2..5dc031e 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,191 @@ # ShiggyXposed -ShiggyXposed is a root Xposed module that injects the ShiggyCord Discord mobile client modifications into the official Discord application. +ShiggyXposed is an Xposed module (Kotlin) that injects the ShiggyCord client modifications into the official Discord Android application. It provides an in-app recovery/dev menu (LogBox) that lets you manage bundle loading, themes, and other developer helpers. -## Info -* Injects ShiggyCord client into the Discord mobile app. -* Built in Kotlin. -* Material 3 like Shake menu with some discord popup accents +This README explains prerequisites, installation, usage, troubleshooting, and credits. If you want help, open an issue on the repository or join the project's support server (links below). + +--- + +## Table of Contents + +- [What is this](#what-is-this) +- [Features](#features) +- [Prerequisites](#prerequisites) +- [Installation](#installation) +- [Using the Module](#using-the-module) + - [Recovery / Options / Themes menu](#recovery--options--themes-menu) + - [Bundle injection / Custom bundle URL](#bundle-injection--custom-bundle-url) +- [Development / Building](#development--building) +- [Troubleshooting & FAQ](#troubleshooting--faq) +- [Contributing](#contributing) +- [Credits](#credits) +- [License](#license) +- [Contact & Links](#contact--links) + +--- + +## What is this + +ShiggyXposed injects a patched ShiggyCord bundle into the official Discord application by using the Xposed framework. It's intended for users who run a rooted Android device and want to apply client-side modifications to Discord. + +--- + +## Features + +- Inject a custom ShiggyCord bundle into Discord. +- Toggle bundle injection on/off (marker-based). +- Load a custom bundle URL and toggle it on/off. +- Recovery/LogBox action sheet for reload, safe-mode, refetch/reset, and options. +- Options to change the LogBox menu appearance (System / Light / Dark) and a set of menu color "flavors". +- In-place UI theming for the recovery/options menus. +- Helpers for refetching or backing up the currently loaded bundle. + +--- ## Prerequisites -To use this module, your device must meet the following requirements: -* **Root Access**: Provided by Magisk, KernelSU, KernelSUNext, or Sukisu Ultra. -* **Xposed Framework**: Use JingMatrix Fork of [LSPosed](https://github.com/JingMatrix/LSPosed). -* **Target Application**: An official Discord application (stable, beta, or alpha version). + +Before installing and using ShiggyXposed: + +- A rooted Android device. Supported root solutions you might use: + - Magisk, KernelSU, KernelSUNext, or Sukisu Ultra. +- Xposed framework installed. The project targets LSPosed or forks because of compatibility with modern Android and Discord: + - Recommended: JingMatrix Fork of [LSPosed](https://github.com/JingMatrix/LSPosed) +- The official Discord Android application (stable/beta/alpha) installed and included in the module scope. + +--- ## Installation -1. Download the latest version of the module from the [Releases](https://github.com/kmmiio99o/ShiggyXposed/releases) page. -2. Install the downloaded APK file on your device. -3. Open the LSPosed manager application. -4. Go to the **Modules** section and enable **ShiggyXposed**. -5. In the module's settings, set the scope to include the **Discord** application. -6. Reboot your device for the changes to take effect. -The project is built with Kotlin and inherits its foundational structure from KettuXposed. +1. Download the latest APK from the repository Releases: + - https://github.com/kmmiio99o/ShiggyXposed/releases +2. Install the APK on your device (standard package install). +3. Open your LSPosed manager (or chosen Xposed manager). +4. Enable the ShiggyXposed module and ensure the module's scope includes the Discord app. +5. Reboot your device (or restart the system process) for changes to take effect. -# Troubleshooting +--- -- Module not working: Ensure the module is enabled in LSPosed and that Discord is correctly selected in its scope. if still nothing, head to our [discord support server](https://discord.gg/nQykFF9Ud6) -- Any other issues: Same server as above, listing them all is not worth it. +## Using the Module -# Thanks & Credits +Once installed and active, you can open the bundled Recovery/LogBox menu (depending on the hooked RN dev dialog) to access all tools. -This project was made possible thanks to the work of: +Recovery → Options → Themes: +- Recovery (Action Sheet) includes: + - Safe Mode toggle (toggle persisted safe-mode setting + optional theme handling) + - Load Custom Bundle + - Reload App + - Options (opens the Options action sheet) +- Options includes: + - Themes (open appearance/flavor controls) + - Toggle bundle injection (rename/add marker file in cache to prevent injection) + - Refetch Bundle (moves or resets current bundle and triggers a reload) + - Clear Cache & Reset (clears cached bundle and resets loader config) + - Back (return to Recovery) -· The LSPosed Team for the framework. -· JingMatrix for their LSPosed fork. -· [cocobo1](https://codeberg.org/cocobo1) for adjusting the Xposed module for Kettu client mod. -· [Revenge Team](https://github.com/revenge-mod) for making a really good Xposed Module to base of +Themes: +- Pick appearance: System / Light / Dark +- Choose Menu Color Flavor: blue, green, mocha, vanilla, purple, amber, teal +- Theme & flavor settings persist under the app's files directory (`files/logbox/LOGBOX_SETTINGS` and `files/logbox/LOGBOX_THEMES`) + +Bundle injection: +- When you disable injection, the module will create/rename a marker file in the cache directory so other components (loader) skip injection. +- Toggling injection is handled by `toggleBundleInjection(context)` and is persisted to `LOGBOX_SETTINGS`. + +Custom bundle URL: +- The custom bundle loader configuration is stored in `files/pyoncord/loader.json`. +- You can enable/disable the custom URL and provide a remote URL (the module exposes a dialog for this). + +--- + +## Development / Building + +If you want to build the project locally: + +- Prerequisites: JDK, Android SDK, and Gradle (the project uses the Gradle wrapper). +- From the repository root: + + - Build debug APK: + ``` + ./gradlew :app:assembleDebug + ``` + + - Install to connected device (adb required): + ``` + adb install -r app/build/outputs/apk/debug/app-debug.apk + ``` + +- Run and test on a rooted device/emulator with Xposed/LSPosed installed and Discord in scope. + +Notes: +- The project is written in Kotlin and uses Android UI primitives to construct the recovery/LogBox UI. The dev menu is created in-place to avoid modifying Discord views. + +--- + +## Troubleshooting & FAQ + +1. Module not working or not showing in Discord? + - Ensure ShiggyXposed is enabled in LSPosed and Discord is included in the module's scope. + - Reboot after enabling the module. + - Confirm you have the compatible LSPosed fork installed (JingMatrix recommended). + +2. Recovery menu shows visual glitches when changing themes? + - The menu attempts to apply colors in-place. If you see transient visual artifacts, try closing and reopening the menu — the settings are persisted and will apply on reopen. + - If an area does not update, please report which dialog/control (and the device/Android version) so it can be targeted. + +3. Bundle injection not toggling? + - Toggling injection manipulates files under the app's cache directory. If that fails, check file permissions or whether the cache path changed on your build of Discord. + - You can also manually inspect `data/data//cache/...` to see `.disabled` marker files (depending on your device and build). + +4. I accidentally broke Discord — how do I recover? + - Use the Recovery → Options → Clear Cache & Reset option to remove cached bundles and restore default loader settings, then reload Discord. + - If Discord fails to start, reboot the device to make sure changes are applied cleanly. + +If you still have issues, open an issue on GitHub or join the support server (link below). Please include: +- Device model and Android version +- Discord app version +- A clear description of the problem and steps to reproduce +- Any relevant logs or screenshots (redact personal data) + +--- + +## Contributing + +Contributions are welcome. If you want to help: + +- Open an issue to discuss a bug or feature. +- Send pull requests with focused, well-documented changes. +- When working on UI or theme changes, test on multiple screen sizes and Android versions if possible. + +Guidelines: +- Keep commits small and descriptive. +- Include code comments for complex logic (hooks, file-system operations). +- Respect user privacy — do not add telemetry or data exfiltration. + +--- + +## Credits + +This project stands on the shoulders of many open-source projects and contributors: + +- [LSPosed Team](https://github.com/LSPosed) — the Xposed framework implementation used to run modules. +- [JingMatrix](https://github.com/JingMatrix) — author of the LSPosed fork referenced by many users. +- [cocobo1](https://codeberg.org/cocobo1) — for adapting some features to Xposed which [Revenge Team](https://github.com/revenge-mod) didn't. +- [Revenge Team](https://github.com/revenge-mod) — provided earlier Xposed module work that informed parts of this project. + +--- + +## License + +Please refer to the `LICENSE` file in the repository root for the project license (if present). If no license file exists, the repository's default (copyright) status applies — check the repo settings or ask the maintainer for clarification. + +--- + +## Contact & Links + +- Releases: https://github.com/kmmiio99o/ShiggyXposed/releases +- Repository: https://github.com/kmmiio99o/ShiggyXposed +- Support server: https://discord.gg/nQykFF9Ud6 + +--- + +Thank you for using ShiggyXposed. From de57766e216cf74255100bef4d34aa07d35d444e Mon Sep 17 00:00:00 2001 From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com> Date: Thu, 11 Dec 2025 18:46:05 +0100 Subject: [PATCH 2/8] rename forgotten things --- .../xposed/xposed/modules/CacheModule.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 01b4971..e064d9b 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/CacheModule.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/CacheModule.kt @@ -35,7 +35,7 @@ import java.io.* */ object CacheModule : Module() { - private const val CACHE_DIR = "revenge" + private const val CACHE_DIR = "Shiggy" private const val MODULES_CACHE_PREFIX = "modules" private const val ASSETS_CACHE_PREFIX = "assets" @@ -45,18 +45,18 @@ object CacheModule : Module() { private lateinit var assetsCache: AssetsCache override fun onContext(context: Context) = with(context) { - val revengeCacheDir = File(cacheDir, CACHE_DIR).apply { asDir() } + val ShiggyCacheDir = File(cacheDir, CACHE_DIR).apply { asDir() } val (_, _, _, versionCode) = getAppInfo() val modulesCacheFile = File( - revengeCacheDir, "$MODULES_CACHE_PREFIX.$versionCode" + ShiggyCacheDir, "$MODULES_CACHE_PREFIX.$versionCode" ).apply { asFile() } val assetsCacheFile = File( - revengeCacheDir, "$ASSETS_CACHE_PREFIX.$versionCode" + ShiggyCacheDir, "$ASSETS_CACHE_PREFIX.$versionCode" ).apply { asFile() } - BridgeModule.registerMethod("revenge.caches.modules.read") { + BridgeModule.registerMethod("Shiggy.caches.modules.read") { if (::modulesCache.isInitialized) modulesCache.toMap() else ModulesCache.loadFromFileOrNull(modulesCacheFile) ?.let { modulesCache = it @@ -64,7 +64,7 @@ object CacheModule : Module() { } } - BridgeModule.registerMethod("revenge.caches.modules.write") { + BridgeModule.registerMethod("Shiggy.caches.modules.write") { val (blacklist, finds) = it @Suppress("UNCHECKED_CAST") modulesCache = ModulesCache( @@ -73,7 +73,7 @@ object CacheModule : Module() { Log.i("Modules cache saved: ${modulesCacheFile.absolutePath} (blacklisted: ${blacklist.size}, finds: ${finds.size})") } - BridgeModule.registerMethod("revenge.caches.assets.read") { + BridgeModule.registerMethod("Shiggy.caches.assets.read") { if (::assetsCache.isInitialized) assetsCache.toMap() else AssetsCache.loadFromFileOrNull(assetsCacheFile) ?.let { assetsCache = it @@ -81,7 +81,7 @@ object CacheModule : Module() { } } - BridgeModule.registerMethod("revenge.caches.assets.write") { + BridgeModule.registerMethod("Shiggy.caches.assets.write") { val (data) = it @Suppress("UNCHECKED_CAST") assetsCache = From 85795c69b9f24c57849809d1eabbab401c82fdd4 Mon Sep 17 00:00:00 2001 From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com> Date: Fri, 19 Dec 2025 22:51:44 +0100 Subject: [PATCH 3/8] Make Xposed Recovery look better :P --- .../revenge/xposed/modules/LogBoxModule.kt | 722 ++++++++++++++++-- 1 file changed, 658 insertions(+), 64 deletions(-) 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 b657987..c8fa021 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,8 +1,25 @@ package io.github.revenge.xposed.modules +import android.animation.AnimatorSet +import android.animation.ArgbEvaluator +import android.animation.ValueAnimator import android.app.AlertDialog import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.drawable.GradientDrawable +import android.os.Build +import android.util.TypedValue +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.view.animation.DecelerateInterpolator +import android.widget.EditText +import android.widget.LinearLayout +import android.widget.TextView import android.widget.Toast +import androidx.core.view.setPadding import de.robv.android.xposed.XC_MethodReplacement import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.XposedHelpers @@ -10,11 +27,12 @@ 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 io.github.revenge.xposed.Constants import kotlinx.coroutines.* +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString import org.json.JSONObject import java.io.File -import java.net.HttpURLConnection -import java.net.URL object LogBoxModule : Module() { lateinit var packageParam: XC_LoadPackage.LoadPackageParam @@ -45,7 +63,7 @@ object LogBoxModule : Module() { try { Log.e("onContext called with context: $context") contextForMenu = context - + val possibleClasses = listOf( "com.facebook.react.devsupport.BridgeDevSupportManager", "com.facebook.react.devsupport.BridgelessDevSupportManager", @@ -53,7 +71,7 @@ object LogBoxModule : Module() { "com.facebook.react.devsupport.DevSupportManagerBase", "com.facebook.react.devsupport.DefaultDevSupportManager" ) - + var foundAny = false possibleClasses.forEach { className -> try { @@ -65,14 +83,14 @@ object LogBoxModule : Module() { Log.e("Class not found: $className - ${e.message}") } } - + if (!foundAny) { tryFindDevSupportClasses(context) } } catch (e: Exception) { } } - + private fun tryFindDevSupportClasses(context: Context) { try { val dexFile = packageParam.classLoader.javaClass.getDeclaredField("pathList") @@ -85,15 +103,14 @@ object LogBoxModule : Module() { private fun hookDevSupportManager(clazz: Class<*>, context: Context) { Log.e("Attempting to hook ${clazz.name}") - - // List all methods to see what's available + Log.e("Available methods in ${clazz.simpleName}:") clazz.methods.forEach { method -> if (method.name.contains("Dev") || method.name.contains("Reload") || method.name.contains("Options")) { Log.e(" - ${method.name}") } } - + try { try { val handleReloadJSMethod = clazz.methods.firstOrNull { it.name == "handleReloadJS" } @@ -105,7 +122,6 @@ object LogBoxModule : Module() { return null } }) - } else { } } catch (e: Exception) { Log.e("Failed to hook handleReloadJS: ${e.message}") @@ -126,10 +142,10 @@ object LogBoxModule : Module() { } catch (e: Exception) { Log.e("Failed to get context from DevSupport (non-fatal): ${e.message}") } - + val finalContext = activityContext ?: contextForMenu ?: context Log.e("Using context: $finalContext (type: ${finalContext.javaClass.name})") - + showRecoveryMenu(finalContext) } catch (e: Exception) { e.printStackTrace() @@ -137,7 +153,6 @@ object LogBoxModule : Module() { return null } }) - } else { } } catch (e: Exception) { } @@ -150,9 +165,9 @@ object LogBoxModule : Module() { Log.e("getContextFromDevSupport: instance is null") return null } - + return try { - + // what if we just did this and searched for literally everything val helpers = listOf( "mReactInstanceDevHelper", @@ -160,7 +175,7 @@ object LogBoxModule : Module() { "mReactInstanceManager", "mApplicationContext" ) - + for (helperName in helpers) { try { Log.e("Trying field: $helperName") @@ -169,22 +184,22 @@ object LogBoxModule : Module() { Log.e("Field $helperName not found, skipping") continue } - + val helper = helperField.get(instance) if (helper == null) { Log.e("Field $helperName is null, skipping") continue } - + if (helper is Context) { Log.e("Field $helperName is a Context, returning it") return helper } - - val getCurrentActivityMethod = helper.javaClass.methods.firstOrNull { - it.name == "getCurrentActivity" + + val getCurrentActivityMethod = helper.javaClass.methods.firstOrNull { + it.name == "getCurrentActivity" } - + if (getCurrentActivityMethod != null) { val ctx = getCurrentActivityMethod.invoke(helper) as? Context if (ctx != null) { @@ -196,7 +211,7 @@ object LogBoxModule : Module() { Log.e("Error trying $helperName: ${e.message}") } } - + Log.e("Could not get context from DevSupport object using any method") null } catch (e: Exception) { @@ -205,22 +220,157 @@ object LogBoxModule : Module() { } } + // material design helper functions + private fun dpToPx(context: Context, dp: Int): Int { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dp.toFloat(), + context.resources.displayMetrics + ).toInt() + } + + private fun getM3Colors(): M3Colors { + // color scheme + return M3Colors( + surface = Color.parseColor("#000000"), + surfaceVariant = Color.parseColor("#1A1A1A"), + onSurface = Color.parseColor("#FFFFFF"), + onSurfaceVariant = Color.parseColor("#CCCCCC"), + primary = Color.parseColor("#FFFFFF"), + onPrimary = Color.parseColor("#000000"), + primaryContainer = Color.parseColor("#2A2A2A"), + onPrimaryContainer = Color.parseColor("#FFFFFF"), + error = Color.parseColor("#FF6B6B") + ) + } + + private data class M3Colors( + val surface: Int, + val surfaceVariant: Int, + val onSurface: Int, + val onSurfaceVariant: Int, + val primary: Int, + val onPrimary: Int, + val primaryContainer: Int, + val onPrimaryContainer: Int, + val error: Int + ) + + private fun createM3Background(context: Context, color: Int, cornerRadius: Float): GradientDrawable { + return GradientDrawable().apply { + setColor(color) + setCornerRadius(dpToPx(context, cornerRadius.toInt()).toFloat()) + } + } + + private fun createButton(context: Context, text: String, colors: M3Colors, onClick: () -> Unit): View { + return LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + gravity = Gravity.CENTER + background = createM3Background(context, colors.primaryContainer, 12f) + setPadding( + dpToPx(context, 32), + dpToPx(context, 12), + dpToPx(context, 32), + dpToPx(context, 12) + ) + isClickable = true + isFocusable = true + + val params = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + dpToPx(context, 48) + ).apply { + setMargins(0, dpToPx(context, 8), 0, 0) + } + layoutParams = params + + addView(TextView(context).apply { + this.text = text + setTextColor(colors.onPrimaryContainer) + textSize = 15f + typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL) + gravity = Gravity.CENTER + }) + + // Ripple effect + val rippleDrawable = android.graphics.drawable.RippleDrawable( + ColorStateList.valueOf(Color.argb(40, 255, 255, 255)), + createM3Background(context, colors.primaryContainer, 12f), + null + ) + background = rippleDrawable + + setOnClickListener { onClick() } + } + } + private fun showRecoveryMenu(context: Context) { Log.e("showRecoveryMenu called with context: $context") try { - val options = arrayOf( - if (isSafeModeEnabled(context)) "Disable Safe Mode" else "Enable Safe Mode", - "Reset Bundle", - "Reload App" + val colors = getM3Colors() + lateinit var dialog: AlertDialog + + val container = LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + setPadding( + dpToPx(context, 24), + dpToPx(context, 24), + dpToPx(context, 24), + dpToPx(context, 24) + ) + background = createM3Background(context, colors.surface, 24f) + } + + val titleView = TextView(context).apply { + text = "KettuXposed Recovery" + textSize = 20f + setTextColor(colors.onSurface) + typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD) + setPadding(0, 0, 0, dpToPx(context, 24)) + gravity = Gravity.CENTER + } + container.addView(titleView) + + // safe Mode Button + val safeModeText = if (isSafeModeEnabled(context)) "Disable Safe Mode" else "Enable Safe Mode" + container.addView(createButton(context, safeModeText, colors) { + dialog.dismiss() + toggleSafeMode(context) + }) + + // load Custom Bundle Button + container.addView(createButton(context, "Load Custom Bundle", colors) { + dialog.dismiss() + showCustomBundleDialog(context) + }) + + // refetch Bundle Button + container.addView(createButton(context, "Refetch Bundle", colors) { + dialog.dismiss() + showConfirmAction( + context, "Refetch Bundle", + "This will download the latest bundle from Github." + ) { refetchBundle(context) } + }) + + // reload App Button + container.addView(createButton(context, "Reload App", colors) { + dialog.dismiss() + reloadApp() + }) + + dialog = AlertDialog.Builder(context) + .setView(container) + .create() + + dialog.window?.setBackgroundDrawable( + createM3Background(context, Color.TRANSPARENT, 24f) ) - AlertDialog.Builder(context) - .setTitle("KettuXposed Recovery Menu") - .setItems(options) { _, which -> - handleMenuSelection(context, which) - } - .setNegativeButton("Cancel", null) - .show() + constrainContainerWidth(context, container) + dialog.show() + setDialogWindowWidth(dialog, context) Log.e("Recovery menu shown successfully") } catch (e: Exception) { Log.e("Error showing recovery menu: ${e.message}", e) @@ -228,21 +378,390 @@ object LogBoxModule : Module() { } } - private fun handleMenuSelection(context: Context, index: Int) { - when (index) { - 0 -> toggleSafeMode(context) - 1 -> confirmAction(context, "reset bundle") { resetBundle(context) } - 2 -> reloadApp() + private fun showConfirmAction(context: Context, title: String, message: String, action: () -> Unit) { + val colors = getM3Colors() + + val container = LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + setPadding( + dpToPx(context, 24), + dpToPx(context, 24), + dpToPx(context, 24), + dpToPx(context, 24) + ) + background = createM3Background(context, colors.surface, 24f) } + + val titleView = TextView(context).apply { + text = title + textSize = 18f + setTextColor(colors.onSurface) + typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD) + setPadding(0, 0, 0, dpToPx(context, 16)) + } + container.addView(titleView) + + val messageView = TextView(context).apply { + text = message + textSize = 14f + setTextColor(colors.onSurfaceVariant) + setPadding(0, 0, 0, dpToPx(context, 24)) + } + container.addView(messageView) + + val buttonContainer = LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.END + } + + val cancelButton = TextView(context).apply { + text = "Cancel" + setTextColor(colors.primary) + textSize = 14f + typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL) + setPadding( + dpToPx(context, 20), + dpToPx(context, 12), + dpToPx(context, 20), + dpToPx(context, 12) + ) + isClickable = true + } + + val confirmButton = TextView(context).apply { + text = "Confirm" + setTextColor(colors.primary) + textSize = 14f + typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL) + setPadding( + dpToPx(context, 20), + dpToPx(context, 12), + dpToPx(context, 20), + dpToPx(context, 12) + ) + isClickable = true + } + + val dialog = AlertDialog.Builder(context) + .setView(container) + .create() + + cancelButton.setOnClickListener { dialog.dismiss() } + confirmButton.setOnClickListener { + dialog.dismiss() + action() + } + + buttonContainer.addView(cancelButton) + buttonContainer.addView(confirmButton) + container.addView(buttonContainer) + + dialog.window?.setBackgroundDrawable( + createM3Background(context, Color.TRANSPARENT, 24f) + ) + + constrainContainerWidth(context, container) + dialog.show() + setDialogWindowWidth(dialog, context) } - private fun confirmAction(context: Context, actionText: String, action: () -> Unit) { - AlertDialog.Builder(context) - .setTitle("Confirm Action") - .setMessage("Are you sure you want to $actionText?") - .setPositiveButton("Confirm") { _, _ -> action() } - .setNegativeButton("Cancel", null) - .show() + private fun showCustomBundleDialog(context: Context) { + val colors = getM3Colors() + val filesDir = File(context.filesDir, "pyoncord") + val configFile = File(filesDir, "loader_config.json") + var currentUrl: String? = null + var isEnabled = false + + if (configFile.exists()) { + try { + val json = JSONObject(configFile.readText()) + val custom = json.optJSONObject("customLoadUrl") + if (custom != null) { + isEnabled = custom.optBoolean("enabled", false) + currentUrl = custom.optString("url", "") + } else { + isEnabled = false + currentUrl = "" + } + } catch (_: Exception) { + } + } + + val container = LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + setPadding( + dpToPx(context, 24), + dpToPx(context, 24), + dpToPx(context, 24), + dpToPx(context, 24) + ) + background = createM3Background(context, colors.surface, 24f) + } + + val titleView = TextView(context).apply { + text = "Custom Bundle URL" + textSize = 18f + setTextColor(colors.onSurface) + typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD) + setPadding(0, 0, 0, dpToPx(context, 16)) + } + container.addView(titleView) + + val toggleContainer = LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + val params = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ).apply { + setMargins(0, 0, 0, dpToPx(context, 16)) + } + layoutParams = params + } + + val toggleLabel = TextView(context).apply { + text = "Enable Custom URL" + textSize = 14f + setTextColor(colors.onSurface) + typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL) + layoutParams = LinearLayout.LayoutParams( + 0, + LinearLayout.LayoutParams.WRAP_CONTENT, + 1f + ) + } + + // switch container + val trackPadding = dpToPx(context, 4) + val switchTrackWidth = dpToPx(context, 52) + val switchTrackHeight = dpToPx(context, 32) + val trackCornerRadius = switchTrackHeight / 2f + + val switchContainer = FrameLayout(context).apply { + background = createM3Background(context, colors.surfaceVariant, trackCornerRadius) + layoutParams = LinearLayout.LayoutParams( + switchTrackWidth, + switchTrackHeight + ) + tag = isEnabled + isClickable = true + } + + // create the thumb with proper sizing + val thumbSize = switchTrackHeight - (trackPadding * 2) + val thumbCornerRadius = thumbSize / 2f + + val switchThumb = View(context).apply { + background = createM3Background(context, if (isEnabled) colors.onPrimary else colors.onSurfaceVariant, thumbCornerRadius) + val params = FrameLayout.LayoutParams(thumbSize, thumbSize) + + val thumbLeft = if (isEnabled) { + switchTrackWidth - thumbSize - trackPadding + } else { + trackPadding + } + + params.leftMargin = thumbLeft + params.topMargin = trackPadding + + layoutParams = params + } + + if (isEnabled) { + switchContainer.background = createM3Background(context, colors.primary, trackCornerRadius) + } + + switchContainer.addView(switchThumb) + + toggleContainer.addView(toggleLabel) + toggleContainer.addView(switchContainer) + container.addView(toggleContainer) + + val urlInput = EditText(context).apply { + hint = "http://localhost:4040/bundle.js" + setTextColor(colors.onSurface) + setHintTextColor(colors.onSurfaceVariant) + background = createM3Background(context, colors.surfaceVariant, 12f) + inputType = android.text.InputType.TYPE_TEXT_VARIATION_URI + setText(currentUrl ?: "") + textSize = 14f + setPadding( + dpToPx(context, 16), + dpToPx(context, 12), + dpToPx(context, 16), + dpToPx(context, 12) + ) + this.isEnabled = isEnabled + alpha = if (isEnabled) 1f else 0.5f + val params = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ).apply { + setMargins(0, 0, 0, dpToPx(context, 24)) + } + layoutParams = params + } + container.addView(urlInput) + + switchContainer.setOnClickListener { + val currentState = switchContainer.tag as Boolean + val newState = !currentState + switchContainer.tag = newState + + val thumbParams = switchThumb.layoutParams as FrameLayout.LayoutParams + val targetLeftMargin = if (newState) { + switchTrackWidth - thumbSize - trackPadding + } else { + trackPadding + } + + // Animate the thumb movement + val animator = ValueAnimator.ofInt(thumbParams.leftMargin, targetLeftMargin).apply { + duration = 200 + interpolator = DecelerateInterpolator() + addUpdateListener { valueAnimator -> + val animatedValue = valueAnimator.animatedValue as Int + thumbParams.leftMargin = animatedValue + switchThumb.layoutParams = thumbParams + } + } + animator.start() + + // animate track color change + val trackAnimator = ValueAnimator.ofObject( + ArgbEvaluator(), + if (currentState) colors.primary else colors.surfaceVariant, + if (newState) colors.primary else colors.surfaceVariant + ).apply { + duration = 200 + addUpdateListener { animator -> + val color = animator.animatedValue as Int + switchContainer.background = createM3Background(context, color, trackCornerRadius) + } + } + trackAnimator.start() + + // animate thumb color change + val thumbAnimator = ValueAnimator.ofObject( + ArgbEvaluator(), + if (currentState) colors.onPrimary else colors.onSurfaceVariant, + if (newState) colors.onPrimary else colors.onSurfaceVariant + ).apply { + duration = 200 + addUpdateListener { animator -> + val color = animator.animatedValue as Int + switchThumb.background = createM3Background(context, color, thumbCornerRadius) + } + } + thumbAnimator.start() + + urlInput.animate() + .alpha(if (newState) 1f else 0.5f) + .setDuration(200) + .withEndAction { + urlInput.isEnabled = newState + } + .start() + } + + val buttonContainer = LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.END + } + + val cancelButton = TextView(context).apply { + text = "Cancel" + setTextColor(colors.primary) + textSize = 14f + typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL) + setPadding( + dpToPx(context, 20), + dpToPx(context, 12), + dpToPx(context, 20), + dpToPx(context, 12) + ) + isClickable = true + } + + val saveButton = TextView(context).apply { + text = "Save" + setTextColor(colors.primary) + textSize = 14f + typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL) + setPadding( + dpToPx(context, 20), + dpToPx(context, 12), + dpToPx(context, 20), + dpToPx(context, 12) + ) + isClickable = true + } + + val dialog = AlertDialog.Builder(context) + .setView(container) + .create() + + cancelButton.setOnClickListener { dialog.dismiss() } + saveButton.setOnClickListener { + try { + val url = urlInput.text?.toString()?.trim() ?: "" + val enabled = switchContainer.tag as Boolean + + if (enabled && url.isNotEmpty()) { + setCustomBundleURL(context, url, true) + dialog.dismiss() + } else if (!enabled) { + setCustomBundleURL(context, url.ifEmpty { "http://localhost:4040/bundle.js" }, false) + dialog.dismiss() + } else { + Toast.makeText(context, "Please enter a valid URL", Toast.LENGTH_SHORT).show() + } + } catch (e: Exception) { + val err = e.message ?: "unknown error" + Toast.makeText(context, "Failed to set custom bundle: $err", Toast.LENGTH_LONG).show() + } + } + + buttonContainer.addView(cancelButton) + buttonContainer.addView(saveButton) + container.addView(buttonContainer) + + dialog.window?.setBackgroundDrawable( + createM3Background(context, Color.TRANSPARENT, 24f) + ) + + constrainContainerWidth(context, container) + dialog.show() + setDialogWindowWidth(dialog, context) + } + + private fun setCustomBundleURL(context: Context, url: String, enabled: Boolean) { + try { + val filesDir = File(context.dataDir, Constants.FILES_DIR) + filesDir.mkdirs() + val configFile = File(filesDir, "loader.json") + + val cfg = io.github.revenge.xposed.modules.LoaderConfig( + io.github.revenge.xposed.modules.CustomLoadUrl(enabled, url) + ) + + val jsonText = io.github.revenge.xposed.Utils.JSON.encodeToString(cfg) + configFile.writeText(jsonText) + + // remove cached bundle so UpdaterModule will fetch the custom one + val cacheBundle = File(context.dataDir, "${Constants.CACHE_DIR}/${Constants.MAIN_SCRIPT_FILE}") + if (enabled) { + if (cacheBundle.exists()) cacheBundle.delete() + Toast.makeText(context, "Custom bundle enabled", Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(context, "Custom bundle disabled", 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 isSafeModeEnabled(context: Context): Boolean { @@ -298,26 +817,23 @@ object LogBoxModule : Module() { } } - private fun resetBundle(context: Context) { + private fun refetchBundle(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() - backupFile.delete() - - if (configFile.exists()) { - val config = JSONObject(configFile.readText()) - config.put("customLoadUrlEnabled", false) - configFile.writeText(config.toString()) + if (bundleFile.exists()) { + backupFile.delete() + bundleFile.renameTo(backupFile) + Log.e("Bundle moved to backup") } reloadApp() + } catch (e: Exception) { - Log.e("Error resetting bundle: ${e.message}") - showError(context, "Failed to reset bundle", e.message) + Log.e("Error refetching bundle: ${e.message}") + showError(context, "Failed to refetch bundle", e.message) } } @@ -329,11 +845,89 @@ object LogBoxModule : Module() { return dir } - private fun showError(context: Context, title: String, message: String?) { - AlertDialog.Builder(context) - .setTitle(title) - .setMessage(message ?: "An unknown error occurred") - .setPositiveButton("OK", null) - .show() + 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) + container.layoutParams = lp + try { + container.minimumWidth = maxW + } catch (_: Exception) {} + } catch (_: Exception) { + } } -} \ No newline at end of file + + private fun setDialogWindowWidth(dialog: AlertDialog, context: Context) { + try { + val screenW = context.resources.displayMetrics.widthPixels + val maxByPercent = (screenW * 0.88).toInt() + val maxByDp = dpToPx(context, 380) + val targetW = if (maxByPercent < maxByDp) maxByPercent else maxByDp + + dialog.window?.setLayout(targetW, LinearLayout.LayoutParams.WRAP_CONTENT) + } catch (_: Exception) { + } + } + + private fun showError(context: Context, title: String, message: String?) { + val colors = getM3Colors() + + val container = LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + setPadding( + dpToPx(context, 24), + dpToPx(context, 24), + dpToPx(context, 24), + dpToPx(context, 24) + ) + background = createM3Background(context, colors.surface, 24f) + } + + val titleView = TextView(context).apply { + text = title + textSize = 18f + setTextColor(colors.error) + typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD) + setPadding(0, 0, 0, dpToPx(context, 16)) + } + container.addView(titleView) + + val messageView = TextView(context).apply { + text = message ?: "An unknown error occurred" + textSize = 14f + setTextColor(colors.onSurfaceVariant) + setPadding(0, 0, 0, dpToPx(context, 24)) + } + container.addView(messageView) + + val okButton = TextView(context).apply { + text = "OK" + setTextColor(colors.primary) + textSize = 14f + typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL) + gravity = Gravity.END + setPadding( + dpToPx(context, 20), + dpToPx(context, 12), + dpToPx(context, 20), + dpToPx(context, 12) + ) + isClickable = true + } + + val dialog = AlertDialog.Builder(context) + .setView(container) + .create() + + okButton.setOnClickListener { dialog.dismiss() } + container.addView(okButton) + + dialog.window?.setBackgroundDrawable( + createM3Background(context, Color.TRANSPARENT, 24f) + ) + + constrainContainerWidth(context, container) + dialog.show() + setDialogWindowWidth(dialog, context) + } +} From 59b5d75f6f8bc880679b4054fa10c6198cc0eedd Mon Sep 17 00:00:00 2001 From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com> Date: Sat, 20 Dec 2025 20:41:59 +0100 Subject: [PATCH 4/8] Patch some things on discord startup to make the app even faster --- .../kotlin/ShiggyXposed/xposed/xposed/Main.kt | 1 + .../xposed/modules/PerfPatchesModule.kt | 194 ++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PerfPatchesModule.kt diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Main.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Main.kt index 9aba2dd..127533f 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Main.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Main.kt @@ -46,6 +46,7 @@ class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit { BlockCrashReportingModule, LogBoxModule, CacheModule, + PerfPatchesModule, FontsModule, ThemesModule, SysColorsModule diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PerfPatchesModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PerfPatchesModule.kt new file mode 100644 index 0000000..bf383a2 --- /dev/null +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PerfPatchesModule.kt @@ -0,0 +1,194 @@ +package ShiggyXposed.xposed.modules + +import de.robv.android.xposed.callbacks.XC_LoadPackage +import kotlinx.serialization.json.JsonObjectBuilder +import kotlinx.serialization.json.put +import ShiggyXposed.xposed.Constants +import ShiggyXposed.xposed.Module +import ShiggyXposed.xposed.Utils.Log +import java.io.File + +/** + * PerfPatchesModule + * + * Writes a small JavaScript preload into the module's `preloads` directory. + * The preload performs guarded, best-effort runtime monkeypatches to: + * - add memoization to expensive pure functions (if present) + * - cache RowGenerator.generate outputs on instances when inputs look stable + * - batch analytics/log events to avoid high-frequency synchronous work + * + * The implementation is intentionally defensive: it never throws and only applies + * patches if the targeted symbols exist. + */ +object PerfPatchesModule : Module() { + private const val FILE_NAME = "perf_patches.js" + + override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) { + try { + // Build preloads path inside the app data files directory + val filesDir = File(packageParam.appInfo.dataDir, Constants.FILES_DIR).apply { mkdirs() } + val preloadsDir = File(filesDir, HookScriptLoaderModule.PRELOADS_DIR).apply { mkdirs() } + val out = File(preloadsDir, FILE_NAME) + + // Defensive JS runtime patch. Avoid template literals with ${} to prevent Kotlin interpolation. + val js = """ + (function(){ + try { + if (!globalThis.__SHIGGY_PERF_PATCHES__) { + globalThis.__SHIGGY_PERF_PATCHES__ = { installedAt: Date.now(), enabled: true }; + } + + function safe(fn) { + return function() { + try { return fn.apply(this, arguments); } + catch (e) { try { console && console.warn && console.warn('shiggy patch error', e); } catch(_){} } + }; + } + + function memoize(fn, maxEntries) { + if (typeof fn !== 'function') return fn; + var cache = new Map(); + maxEntries = Number(maxEntries) || 128; + return function() { + var key; + try { key = JSON.stringify(Array.prototype.slice.call(arguments)); } + catch (e) { key = String(arguments.length) + ':' + String(arguments[0]); } + if (cache.has(key)) return cache.get(key); + var res = fn.apply(this, arguments); + cache.set(key, res); + if (cache.size > maxEntries) { + var it = cache.keys().next(); + if (!it.done) cache.delete(it.value); + } + return res; + }; + } + + function createBatched(fn, intervalMs) { + if (typeof fn !== 'function') return fn; + var buf = []; + var timer = null; + intervalMs = Number(intervalMs) || 2000; + function flush() { + var batch = buf.splice(0, buf.length); + if (!batch.length) return; + try { fn.call(null, batch); } + catch (e) { + // Fallback: call per event asynchronously + try { + batch.forEach(function(ev){ + setTimeout(function(){ try { fn(ev); } catch (_) {} }, 0); + }); + } catch (_) {} + } + } + return function(event) { + try { + buf.push(event); + if (!timer) timer = setTimeout(function(){ timer = null; flush(); }, intervalMs); + } catch (e) {} + }; + } + + function patchIfExists(path, wrapper) { + try { + var parts = path.split('.'); + var obj = globalThis; + for (var i = 0; i < parts.length - 1; i++) { + obj = obj && obj[parts[i]]; + } + if (!obj) return false; + var key = parts[parts.length - 1]; + if (typeof obj[key] !== 'function') return false; + obj[key] = wrapper(obj[key]); + return true; + } catch (e) { return false; } + } + + // Try memoizing a few well-known functions + ['computeHappeningNowState', 'computeNowState'].forEach(function(name){ + try { patchIfExists(name, function(orig){ return memoize(orig, 128); }); } catch(_) {} + }); + + // Try caching RowGenerator.prototype.generate by instance signature + try { + var RG = globalThis && globalThis.RowGenerator; + if (RG && RG.prototype && typeof RG.prototype.generate === 'function') { + (function(){ + var orig = RG.prototype.generate; + RG.prototype.generate = function() { + try { + var sig; + if (arguments && arguments.length) { + var a0 = arguments[0]; + if (Array.isArray(a0)) { + sig = 'arr:' + a0.length + ':' + a0.slice(0,8).map(function(x){ return x && x.id ? x.id : ''; }).join(','); + } else if (a0 && a0.id) { + sig = 'id:' + a0.id; + } else { + try { sig = JSON.stringify(a0).slice(0,200); } catch(e){ sig = String(a0); } + } + } else sig = 'noargs'; + + if (this.__shiggy_sig === sig && typeof this.__shiggy_cache !== 'undefined') { + return this.__shiggy_cache; + } + var res = orig.apply(this, arguments); + this.__shiggy_sig = sig; + this.__shiggy_cache = res; + return res; + } catch (e) { return orig.apply(this, arguments); } + }; + })(); + } + } catch (e) {} + + // Batch analytics-like methods (best-effort) + ['app_ui_viewed', 'logEvent', 'Analytics.log', 'Analytics.track', 'analytics.trackEvent'].forEach(function(name){ + try { + var parts = name.split('.'); + var obj = globalThis; + for (var i = 0; i < parts.length - 1; i++) obj = obj && obj[parts[i]]; + if (!obj) return; + var key = parts[parts.length - 1]; + if (typeof obj[key] === 'function') obj[key] = createBatched(obj[key], 2000); + } catch (e) {} + }); + + // Expose lightweight API + try { + if (!globalThis.__SHIGGY_PERF_API__) { + globalThis.__SHIGGY_PERF_API__ = { + installedAt: Date.now(), + enabled: true, + uninstall: function() { try { globalThis.__SHIGGY_PERF_API__.enabled = false; } catch(e) {} } + }; + } + } catch (e) {} + + } catch (e) { + try { console && console.warn && console.warn('PerfPatches: error', e); } catch(_) {} + } + })(); + """.trimIndent() + + // Write file if absent or changed + try { + if (!out.exists() || out.readText() != js) { + out.writeText(js) + Log.i("PerfPatchesModule: wrote patch to " + out.absolutePath) + } else { + Log.i("PerfPatchesModule: perf patch already up-to-date") + } + } catch (e: Throwable) { + Log.e("PerfPatchesModule: failed writing patch", e) + } + } catch (e: Throwable) { + Log.e("PerfPatchesModule:onLoad failed", e) + } + } + + override fun buildPayload(builder: JsonObjectBuilder) { + builder.put("perfPatches", true) + } +} From 86994eefb95c27c5fb8cbdc3815d2aeecf62fbde Mon Sep 17 00:00:00 2001 From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com> Date: Sat, 20 Dec 2025 21:28:30 +0100 Subject: [PATCH 5/8] bump to 1.2.4 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ab5e7cc..7cd36ed 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "ShiggyXposed.xposed" minSdk = 24 targetSdk = 36 - versionCode = 1230 - versionName = "1.2.3" + versionCode = 1240 + versionName = "1.2.4" } buildTypes { From fdca98a994c683b1b96ae31ffb725866273a9d20 Mon Sep 17 00:00:00 2001 From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:04:10 +0100 Subject: [PATCH 6/8] bump to 1.2.4 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ab5e7cc..7cd36ed 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "ShiggyXposed.xposed" minSdk = 24 targetSdk = 36 - versionCode = 1230 - versionName = "1.2.3" + versionCode = 1240 + versionName = "1.2.4" } buildTypes { From 0805a7314dec70eae861d0b2217b5c653473bde5 Mon Sep 17 00:00:00 2001 From: kmmiio99o Date: Fri, 2 Jan 2026 15:08:43 +0100 Subject: [PATCH 7/8] add some more patches for faster start up --- .../xposed/modules/PerfPatchesModule.kt | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PerfPatchesModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PerfPatchesModule.kt index bf383a2..d16d981 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PerfPatchesModule.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PerfPatchesModule.kt @@ -155,6 +155,128 @@ object PerfPatchesModule : Module() { } catch (e) {} }); + // Make console methods async to avoid blocking hot-paths during startup + (function(){ + try { + if (typeof console !== 'undefined') { + ['log','info','warn','error','debug','trace'].forEach(function(m){ + try { + if (typeof console[m] === 'function') { + var orig = console[m]; + console[m] = function() { + var args = Array.prototype.slice.call(arguments); + setTimeout(function(){ try { orig.apply(console, args); } catch(_){} }, 0); + }; + } + } catch(e){} + }); + } + } catch(e){} + })(); + + // Deduplicate and slightly delay repeated kv calls to reduce IO contention on startup + (function(){ + try { + var pending = new Map(); + // Lightweight stats for dedupe wrapper to help telemetry/debugging in-case we need to tune delays. + try { + globalThis.__SHIGGY_PERF_DEDUPE_STATS__ = globalThis.__SHIGGY_PERF_DEDUPE_STATS__ || { + methods: {}, + totalRequests: 0, + totalHits: 0, + totalMisses: 0, + pending: 0, + lastUpdated: Date.now() + }; + } catch(e) {} + + function _incStats(name, key) { + try { + var s = globalThis.__SHIGGY_PERF_DEDUPE_STATS__; + if (!s) return; + s.lastUpdated = Date.now(); + var m = s.methods[name] || (s.methods[name] = { requests: 0, hits: 0, misses: 0, pending: 0 }); + if (key === 'requests') { m.requests++; s.totalRequests++; } + else if (key === 'hits') { m.hits++; s.totalHits++; } + else if (key === 'misses') { m.misses++; s.totalMisses++; } + else if (key === 'pending_inc') { m.pending++; s.pending++; } + else if (key === 'pending_dec') { m.pending = Math.max(0, m.pending - 1); s.pending = Math.max(0, s.pending - 1); } + } catch(e) {} + } + + function wrapAsyncDedupe(kvObj, methodName, delayMs){ + try { + var orig = kvObj[methodName]; + if (typeof orig !== 'function') return; + kvObj[methodName] = function() { + var argsArr = Array.prototype.slice.call(arguments); + var key; + try { key = JSON.stringify([methodName, argsArr]); } catch(e){ key = methodName + ':' + String(argsArr[0]); } + _incStats(methodName, 'requests'); + if (pending.has(key)) { _incStats(methodName, 'hits'); return pending.get(key); } + _incStats(methodName, 'misses'); + var self = this; + var p = new Promise(function(resolve, reject){ + setTimeout(function(){ + try { + var res = orig.apply(self, argsArr); + if (res && typeof res.then === 'function') res.then(resolve, reject); + else resolve(res); + } catch (e) { reject(e); } + }, Number(delayMs) || 20); + }); + pending.set(key, p); + _incStats(methodName, 'pending_inc'); + var cleanup = function(){ pending.delete(key); _incStats(methodName, 'pending_dec'); }; + p.then(cleanup, cleanup); + return p; + }; + } catch(e){} + } + if (globalThis.kv) { + if (globalThis.kv.get_many) wrapAsyncDedupe(globalThis.kv, 'get_many', 20); + if (globalThis.kv.get_kv_entries) wrapAsyncDedupe(globalThis.kv, 'get_kv_entries', 20); + } + } catch(e){} + })(); + + // Defer heavy filesystem/storage calls slightly to prioritize UI rendering + (function(){ + try { + function makeDeferredIfAsync(obj, name, delayMs) { + try { + var orig = obj[name]; + if (typeof orig !== 'function') return; + var isAsync = orig.constructor && orig.constructor.name === 'AsyncFunction'; + var src = String(orig); + if (!isAsync && src.indexOf('return new Promise') === -1 && src.indexOf('.then(') === -1) { + // Likely synchronous - skip deferral to avoid changing semantics + return; + } + obj[name] = function() { + var args = Array.prototype.slice.call(arguments); + var self = this; + return new Promise(function(resolve, reject){ + setTimeout(function(){ + try { + var res = orig.apply(self, args); + if (res && typeof res.then === 'function') res.then(resolve, reject); + else resolve(res); + } catch (e) { reject(e); } + }, Number(delayMs) || 100); + }); + }; + } catch(e){} + } + if (globalThis.db && typeof globalThis.db.fs_info === 'function') { + makeDeferredIfAsync(globalThis.db, 'fs_info', 100); + } + if (globalThis.Storage && typeof globalThis.Storage.refresh === 'function') { + makeDeferredIfAsync(globalThis.Storage, 'refresh', 50); + } + } catch(e){} + })(); + // Expose lightweight API try { if (!globalThis.__SHIGGY_PERF_API__) { From 04a68d623d2e8e24f5e822050d52431e98dfb594 Mon Sep 17 00:00:00 2001 From: kmmiio99o Date: Fri, 2 Jan 2026 15:16:18 +0100 Subject: [PATCH 8/8] suppress deprecated java version --- app/build.gradle.kts | 8 ++++++-- .../xposed/xposed/modules/PerfPatchesModule.kt | 1 + gradle.properties | 4 +++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7cd36ed..020a31e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "ShiggyXposed.xposed" minSdk = 24 targetSdk = 36 - versionCode = 1240 - versionName = "1.2.4" + versionCode = 1250 + versionName = "1.2.5" } buildTypes { @@ -39,6 +39,10 @@ android { } } +tasks.withType().configureEach { + options.compilerArgs.add("-Xlint:-options") +} + dependencies { compileOnly("de.robv.android.xposed:api:82") diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PerfPatchesModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PerfPatchesModule.kt index d16d981..914b6a0 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PerfPatchesModule.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PerfPatchesModule.kt @@ -310,6 +310,7 @@ object PerfPatchesModule : Module() { } } + @Deprecated("Overrides deprecated Module.buildPayload") override fun buildPayload(builder: JsonObjectBuilder) { builder.put("perfPatches", true) } diff --git a/gradle.properties b/gradle.properties index 3c5031e..374b4b2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,6 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true +# Suppress deprecation warning when building with a newer JDK (e.g., Java 21) +android.javaCompile.suppressSourceTargetDeprecationWarning=true