From 5fe672884e8ed295762a656c74681363ab92ab27 Mon Sep 17 00:00:00 2001 From: OTKUSteyler <90369564+OTKUSteyler@users.noreply.github.com> Date: Mon, 24 Nov 2025 17:29:20 +0100 Subject: [PATCH] Delete app/src/main/kotlin/ShiggyXposed/xposed/xposed directory --- .../ShiggyXposed/xposed/xposed/Constants.kt | 19 - .../kotlin/ShiggyXposed/xposed/xposed/Main.kt | 107 -- .../ShiggyXposed/xposed/xposed/Module.kt | 183 --- .../ShiggyXposed/xposed/xposed/Utils.kt | 58 - .../xposed/xposed/modules/CacheModule.kt | 270 ----- .../xposed/modules/FixResourcesModule.kt | 23 - .../xposed/modules/HookScriptLoaderModule.kt | 121 -- .../xposed/xposed/modules/LogBoxModule.kt | 1051 ----------------- .../xposed/modules/PayloadGlobalModule.kt | 80 -- .../xposed/xposed/modules/PluginsModule.kt | 124 -- .../xposed/xposed/modules/UpdaterModule.kt | 155 --- .../xposed/modules/appearance/FontsModule.kt | 228 ---- .../modules/appearance/SysColorsModule.kt | 43 - .../xposed/modules/appearance/ThemesModule.kt | 159 --- .../bridge/AdditionalBridgeMethodsModule.kt | 94 -- .../xposed/modules/bridge/BridgeModule.kt | 157 --- .../no_track/BlockCrashReportingModule.kt | 50 - .../no_track/BlockDeepLinksTrackingModule.kt | 25 - 18 files changed, 2947 deletions(-) delete mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/Constants.kt delete mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/Main.kt delete mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/Module.kt delete mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/Utils.kt delete mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/CacheModule.kt delete mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/FixResourcesModule.kt delete mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/HookScriptLoaderModule.kt delete mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt delete mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PayloadGlobalModule.kt delete mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PluginsModule.kt delete mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/UpdaterModule.kt delete mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/appearance/FontsModule.kt delete mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/appearance/SysColorsModule.kt delete mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/appearance/ThemesModule.kt delete mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/bridge/AdditionalBridgeMethodsModule.kt delete mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/bridge/BridgeModule.kt delete mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/no_track/BlockCrashReportingModule.kt delete mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/no_track/BlockDeepLinksTrackingModule.kt diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Constants.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Constants.kt deleted file mode 100644 index 1c91b08..0000000 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Constants.kt +++ /dev/null @@ -1,19 +0,0 @@ -package GoonXposed.xposed - -class Constants { - companion object { - const val TARGET_PACKAGE = "com.discord" - const val TARGET_ACTIVITY = "$TARGET_PACKAGE.react_activities.ReactActivity" - - const val FILES_DIR = "files/pyoncord" - const val CACHE_DIR = "cache/pyoncord" - const val MAIN_SCRIPT_FILE = "bundle.js" - - - const val LOG_TAG = "Goon" - - const val LOADER_NAME = "GoonXposed" - - const val USER_AGENT = "GoonXposed" - } -} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Main.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Main.kt deleted file mode 100644 index f3b328e..0000000 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Main.kt +++ /dev/null @@ -1,107 +0,0 @@ -package GoonXposed.xposed - -import android.app.Activity -import android.content.Context -import android.content.ContextWrapper -import android.os.Bundle -import de.robv.android.xposed.IXposedHookLoadPackage -import de.robv.android.xposed.IXposedHookZygoteInit -import de.robv.android.xposed.callbacks.XC_LoadPackage -import GoonXposed.xposed.Utils.Log -import GoonXposed.xposed.modules.* -import GoonXposed.xposed.modules.appearance.FontsModule -import GoonXposed.xposed.modules.appearance.SysColorsModule -import GoonXposed.xposed.modules.appearance.ThemesModule -import GoonXposed.xposed.modules.bridge.AdditionalBridgeMethodsModule -import GoonXposed.xposed.modules.bridge.BridgeModule -import GoonXposed.xposed.modules.no_track.BlockCrashReportingModule -import GoonXposed.xposed.modules.no_track.BlockDeepLinksTrackingModule -import kotlinx.coroutines.CompletableDeferred - -object HookStateHolder { - /** - * Whether all hooks are completed, and we are ready to load the JS bundle. - */ - val readyDeferred = CompletableDeferred() - - /** - * Whether we have successfully received a [Context] yet. - * Sometimes the app process is recreated and Xposed hooks way too late for us to get [Context] from [ContextWrapper.attachBaseContext]. - * But since Xposed hooks before [Activity.onCreate], we can still get it from there and still initialize properly. - */ - @Volatile - var gotContext = false -} - -class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit { - private var hooked = false - private val modules = mutableListOf( - HookScriptLoaderModule, - BridgeModule, - AdditionalBridgeMethodsModule, - PluginsModule(), - UpdaterModule, - FixResourcesModule, - BlockDeepLinksTrackingModule, - BlockCrashReportingModule, - LogBoxModule, - CacheModule, - FontsModule, - ThemesModule, - SysColorsModule - ) - - init { - modules += PayloadGlobalModule(modules) - } - - override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) { - for (module in modules) module.onInit(startupParam) - } - - override fun handleLoadPackage(param: XC_LoadPackage.LoadPackageParam) = with(param) { - if (hooked) return - - val reactActivity = classLoader.loadClass(Constants.TARGET_ACTIVITY) - - ContextWrapper::class.java.hookMethod("attachBaseContext", Context::class.java) { - after { - val ctx = args[0] as Context - HookStateHolder.gotContext = true - Log.i("Received Context") - this@Main.onContext(ctx) - } - } - - reactActivity.hookMethod("onCreate", Bundle::class.java) { - after { - val act = thisObject as Activity - Log.i("Received Activity") - - if (!HookStateHolder.gotContext) { - Log.w("Activity created before we got Context, process may have been recreated!") - this@Main.onContext(act.applicationContext) - } - - this@Main.onActivity(act) - HookStateHolder.readyDeferred.complete(Unit) - } - } - - this@Main.onLoad(param) - - hooked = true - } - - override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) { - for (module in modules) module.onLoad(packageParam) - } - - override fun onContext(context: Context) { - for (module in modules) module.onContext(context) - } - - override fun onActivity(activity: Activity) { - for (module in modules) module.onActivity(activity) - } -} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Module.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Module.kt deleted file mode 100644 index 41f878f..0000000 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Module.kt +++ /dev/null @@ -1,183 +0,0 @@ -package GoonXposed.xposed - -import android.app.Activity -import android.content.Context -import android.os.Build -import de.robv.android.xposed.IXposedHookZygoteInit -import de.robv.android.xposed.XC_MethodHook -import de.robv.android.xposed.XposedBridge -import de.robv.android.xposed.callbacks.XC_LoadPackage -import kotlinx.serialization.json.JsonObjectBuilder -import java.io.File -import java.lang.reflect.Method - -data class AppInfo( - val name: String, - val packageName: String, - val version: String, - val versionCode: Long, -) - -abstract class Module { - /** - * Builds a JSON payload to be injected into the JavaScript context. - */ - @Deprecated("This will be removed in future versions. Payloads can be replaced via synchronous bridge methods.") - open fun buildPayload(builder: JsonObjectBuilder) { - } - - /** - * Called during Zygote initialization. - */ - open fun onInit(startupParam: IXposedHookZygoteInit.StartupParam) {} - - /** - * Called when a package is loaded. - * - * Hooks should typically be set up here. - * - * Bridge methods that do not require [Context] can also be registered here. - */ - open fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {} - - /** - * Called after [Context] has been attached to a [android.content.ContextWrapper] - * via [android.content.ContextWrapper.attachBaseContext]. - * - * This may be called multiple times for different [Context]s, e.g. when the app is killed by the system. - * - * For bridge methods that require access to [Context], register them here. - */ - open fun onContext(context: Context) {} - - /** - * Called after an [Activity.onCreate] method is executed. - * - * UI code that requires an [Activity] context can be placed here. - * - * Bridge methods must not be registered here, as it can be too late and cause crashes. - * - * If you need to register bridge methods that require an [Activity], do it in [onContext] and make it async. - * - * Once an [Activity] is available, execute the queued calls. Then from JS, call the method asynchronously. - */ - open fun onActivity(activity: Activity) {} - - protected fun Context.getAppInfo(): AppInfo { - val pInfo = packageManager.getPackageInfo(packageName, 0) - val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) pInfo.longVersionCode - else @Suppress("DEPRECATION") pInfo.versionCode.toLong() - - return AppInfo( - packageManager.getApplicationLabel(applicationInfo).toString(), - packageName, - pInfo.versionName ?: versionCode.toString(), - versionCode, - ) - } - - protected fun File.asDir() { - if (!this.isDirectory) this.delete() - this.mkdirs() - } - - protected fun File.asFile() { - if (!this.isFile) this.deleteRecursively() - } - - protected fun Class<*>.method( - name: String, vararg params: Class<*>? - ): Method = getDeclaredMethod(name, *params).apply { - isAccessible = true - } - - protected fun ClassLoader.safeLoadClass(name: String): Class<*>? = runCatching { loadClass(name) }.getOrNull() - - protected fun Method.hook(hook: XC_MethodHook): XC_MethodHook.Unhook = XposedBridge.hookMethod(this, hook) - - protected fun Method.hook(block: MethodHookBuilder.() -> Unit): XC_MethodHook.Unhook = - hook(MethodHookBuilder().apply(block).build()) - - protected fun Class<*>.hookMethod( - name: String, vararg params: Class<*>?, block: MethodHookBuilder.() -> Unit - ): XC_MethodHook.Unhook = method(name, *params).hook(MethodHookBuilder().apply(block).build()) - - protected class MethodHookBuilder { - private var beforeBlock: (HookScope.() -> Unit)? = null - private var afterBlock: (HookScope.() -> Unit)? = null - - fun before(block: HookScope.() -> Unit) { - beforeBlock = block - } - - fun after(block: HookScope.() -> Unit) { - afterBlock = block - } - - fun build(): XC_MethodHook = object : XC_MethodHook() { - override fun beforeHookedMethod(param: MethodHookParam) { - val b = beforeBlock - if (b != null) { - val scope = HookScope( - param = param, proceed = { p -> - super.beforeHookedMethod(p) - }) - scope.b() - } else { - super.beforeHookedMethod(param) - } - } - - override fun afterHookedMethod(param: MethodHookParam) { - val a = afterBlock - if (a != null) { - val scope = HookScope( - param = param, proceed = { p -> - super.afterHookedMethod(p) - }) - scope.a() - } else { - super.afterHookedMethod(param) - } - } - } - } - - /** - * Scope object passed to before/after hook blocks. - * - * Provides: - * - Access to the [param] object - * - [proceed] to call the original XC_MethodHook super method - * - Accessors for `thisObject`, `args`, `result`, and `throwable` - * - * @property param The [XC_MethodHook.MethodHookParam] for the current hook. - * @property proceed Function that calls the super method. - */ - protected class HookScope internal constructor( - val param: XC_MethodHook.MethodHookParam, private val proceed: (XC_MethodHook.MethodHookParam) -> Unit - ) { - /** - * Continues with the default XC_MethodHook super behavior. - * Equivalent to calling `super.beforeHookedMethod(param)` or - * `super.afterHookedMethod(param)` depending on the phase. - */ - fun proceed() = proceed(param) - - val thisObject: Any? get() = param.thisObject - - val args: Array get() = param.args - - var result: Any? - get() = param.result - set(value) { - param.result = value - } - - var throwable: Throwable? - get() = param.throwable - set(value) { - param.throwable = value - } - } -} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Utils.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Utils.kt deleted file mode 100644 index f92abbf..0000000 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Utils.kt +++ /dev/null @@ -1,58 +0,0 @@ -package GoonXposed.xposed - -import android.app.AlertDialog -import android.app.AndroidAppHelper -import android.content.Context -import android.content.Intent -import GoonXposed.xposed.modules.UpdaterModule -import kotlinx.serialization.json.Json -import java.io.File -import kotlin.system.exitProcess - -class Utils { - companion object { - val JSON = Json { ignoreUnknownKeys = true } - - fun reloadApp() { - val application = AndroidAppHelper.currentApplication() - val intent = application.packageManager.getLaunchIntentForPackage(application.packageName) - application.startActivity(Intent.makeRestartActivityTask(intent!!.component)) - exitProcess(0) - } - - fun showRecoveryAlert(context: Context) { - AlertDialog.Builder(context).setTitle("GoonCord Recovery Options") - .setItems(arrayOf("Reload", "Delete Script", "Reset Loader Config")) { _, which -> - when (which) { - 0 -> { - reloadApp() - } - - 1 -> { - val bundleFile = File( - context.dataDir, "${Constants.CACHE_DIR}/${Constants.MAIN_SCRIPT_FILE}" - ) - - if (bundleFile.exists()) bundleFile.delete() - - reloadApp() - } - - 2 -> { - UpdaterModule.resetLoaderConfig(context) - reloadApp() - } - } - }.show() - } - } - - object Log { - fun e(msg: String) = android.util.Log.e(Constants.LOG_TAG, msg) - fun e(msg: String, throwable: Throwable) = android.util.Log.e(Constants.LOG_TAG, msg, throwable) - fun i(msg: String) = android.util.Log.i(Constants.LOG_TAG, msg) - fun i(msg: String, throwable: Throwable) = android.util.Log.i(Constants.LOG_TAG, msg, throwable) - fun w(msg: String) = android.util.Log.w(Constants.LOG_TAG, msg) - fun w(msg: String, throwable: Throwable) = android.util.Log.w(Constants.LOG_TAG, msg, throwable) - } -} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/CacheModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/CacheModule.kt deleted file mode 100644 index debbbc8..0000000 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/CacheModule.kt +++ /dev/null @@ -1,270 +0,0 @@ -package GoonXposed.xposed.modules - -import android.content.Context -import android.util.AtomicFile -import GoonXposed.xposed.Module -import GoonXposed.xposed.Utils.Log -import GoonXposed.xposed.modules.bridge.BridgeModule -import java.io.* - -/** - * A module for caching versioned modules finds and blacklist, and assets mappings. - * - * See [ModulesCache] and [AssetsCache] for the cache structure. - * - * ## Methods - * - * ### Modules cache - * - * - `Shiggy.caches.modules.read() : { blacklist: number[], finds: { [filter: string]: { [id: string]: number } | null }, version: number } | null` - * - Reads the modules cache. Returns `null` if the cache file does not exist or is invalid. - * - * - `Shiggy.caches.modules.write(blacklist: number[], finds: { [filter: string]: { [id: string]: number } | null }) : void` - * - Writes the modules cache. - * - * ### Assets cache - * - * - `Shiggy.caches.assets.read() : { data: { [name: string]: { [type: string]: number } }, version: number } | null` - * - Reads the assets cache. Returns `null` if the cache file does not exist or is invalid. - * - * - `Shiggy.caches.assets.write(data: { [name: string]: { [type: string]: number } }) : void` - * - Writes the assets cache. - * - * The caches are versioned and tied to the app version code. When the app is updated, - * the caches are invalidated and new cache files are created. - */ -object CacheModule : Module() { - - private const val CACHE_DIR = "revenge" - - private const val MODULES_CACHE_PREFIX = "modules" - private const val ASSETS_CACHE_PREFIX = "assets" - - - private lateinit var modulesCache: ModulesCache - private lateinit var assetsCache: AssetsCache - - override fun onContext(context: Context) = with(context) { - val revengeCacheDir = File(cacheDir, CACHE_DIR).apply { asDir() } - val (_, _, _, versionCode) = getAppInfo() - - val modulesCacheFile = File( - revengeCacheDir, "$MODULES_CACHE_PREFIX.$versionCode" - ).apply { asFile() } - - val assetsCacheFile = File( - revengeCacheDir, "$ASSETS_CACHE_PREFIX.$versionCode" - ).apply { asFile() } - - BridgeModule.registerMethod("revenge.caches.modules.read") { - if (::modulesCache.isInitialized) modulesCache.toMap() else ModulesCache.loadFromFileOrNull(modulesCacheFile) - ?.let { - modulesCache = it - it.toMap() - } - } - - BridgeModule.registerMethod("revenge.caches.modules.write") { - val (blacklist, finds) = it - @Suppress("UNCHECKED_CAST") - modulesCache = ModulesCache( - blacklist as ArrayList, finds as HashMap?> - ).apply { saveToFile(modulesCacheFile) } - Log.i("Modules cache saved: ${modulesCacheFile.absolutePath} (blacklisted: ${blacklist.size}, finds: ${finds.size})") - } - - BridgeModule.registerMethod("revenge.caches.assets.read") { - if (::assetsCache.isInitialized) assetsCache.toMap() else AssetsCache.loadFromFileOrNull(assetsCacheFile) - ?.let { - assetsCache = it - it.toMap() - } - } - - BridgeModule.registerMethod("revenge.caches.assets.write") { - val (data) = it - @Suppress("UNCHECKED_CAST") - assetsCache = - AssetsCache(data as HashMap>).apply { saveToFile(assetsCacheFile) } - Log.i("Assets cache saved: ${assetsCacheFile.absolutePath} (count: ${data.size})") - } - } -} - -private class CacheVersionMismatchException(expected: Int, actual: Int) : - Throwable("Expected cache version: $expected, but got version: $actual") - -data class ModulesCache( - val blacklist: List, val finds: Map?> -) { - fun saveToFile(file: File) { - val atomic = AtomicFile(file) - var fos: FileOutputStream? = null - try { - fos = atomic.startWrite() - val out = DataOutputStream(BufferedOutputStream(fos)) - - out.writeInt(VERSION) - - // Write blacklist - out.writeInt(blacklist.size) - for (num in blacklist) out.writeInt(num.toInt()) - - // Write finds - out.writeInt(finds.size) - for ((filter, matches) in finds) { - out.writeUTF(filter) - if (matches == null) { - out.writeBoolean(false) // null marker - } else { - out.writeBoolean(true) // not null - out.writeInt(matches.size) - for ((id, flag) in matches) { - out.writeInt(id.toInt()) - out.writeInt(flag.toInt()) - } - } - } - - out.flush() - atomic.finishWrite(fos) - } catch (t: Throwable) { - if (fos != null) atomic.failWrite(fos) - throw t - } - } - - fun toMap(): Map { - val findsMap = finds.mapValues { (_, matches) -> matches?.map { (k, v) -> k to v }?.toMap() } - - return mapOf("blacklist" to blacklist, "finds" to findsMap, "version" to VERSION) - } - - companion object { - const val VERSION = 3 - - fun loadFromFileOrNull(file: File): ModulesCache? { - if (!file.exists() || file.length() <= 0L) return null - val atomic = AtomicFile(file) - try { - DataInputStream(BufferedInputStream(atomic.openRead())).use { input -> - val version = input.readInt() - if (version != VERSION) throw CacheVersionMismatchException(VERSION, version) - - val blacklistSize = input.readInt() - val blacklist = MutableList(blacklistSize) { input.readInt().toDouble() } - - val findsSize = input.readInt() - val finds = mutableMapOf?>() - repeat(findsSize) { - val filter = input.readUTF() - val hasMatches = input.readBoolean() - if (!hasMatches) { - finds[filter] = null - } else { - val mapSize = input.readInt() - val matches = mutableMapOf() - repeat(mapSize) { - val id = input.readInt().toString() - val flag = input.readInt().toDouble() - matches[id] = flag - } - finds[filter] = matches - } - } - - return ModulesCache(blacklist, finds) - } - } catch (e: CacheVersionMismatchException) { - Log.i("Modules cache version mismatch: ${e.message}") - } catch (e: EOFException) { - Log.e("Modules cache corrupt: ${e.message}") - } catch (e: IOException) { - Log.e("Failed to read modules cache: ${e.message}") - } - runCatching { file.delete() } - return null - } - } -} - -data class AssetsCache( - val data: Map> -) { - fun saveToFile(file: File) { - val atomic = AtomicFile(file) - var fos: FileOutputStream? = null - try { - fos = atomic.startWrite() - val out = DataOutputStream(BufferedOutputStream(fos)) - - out.writeInt(VERSION) - - out.writeInt(data.size) - for ((name, mappings) in data) { - out.writeUTF(name) - out.writeInt(mappings.size) - for ((type, id) in mappings) { - out.writeUTF(type) - out.writeInt(id.toInt()) - } - } - - out.flush() - atomic.finishWrite(fos) - } catch (t: Throwable) { - if (fos != null) atomic.failWrite(fos) - throw t - } - } - - fun toMap(): Map { - return mapOf( - "data" to data.mapValues { (_, mappings) -> - mappings.map { (type, id) -> type to id }.toMap() - }, "version" to VERSION - ) - } - - companion object { - const val VERSION = 2 - - fun loadFromFileOrNull(file: File): AssetsCache? { - if (!file.exists() || file.length() <= 0L) return null - val atomic = AtomicFile(file) - try { - DataInputStream(BufferedInputStream(atomic.openRead())).use { input -> - val version = input.readInt() - if (version != VERSION) throw CacheVersionMismatchException(VERSION, version) - - val dataSize = input.readInt() - val data = mutableMapOf>() - - repeat(dataSize) { - val name = input.readUTF() - val mappingsSize = input.readInt() - val mappings = mutableMapOf() - - repeat(mappingsSize) { - val type = input.readUTF() - val id = input.readInt().toDouble() - mappings[type] = id - } - - data[name] = mappings - } - - return AssetsCache(data) - } - } catch (e: CacheVersionMismatchException) { - Log.i("Assets cache version mismatch: ${e.message}") - } catch (e: EOFException) { - Log.e("Assets cache corrupt: ${e.message}") - } catch (e: IOException) { - Log.e("Failed to read assets cache: ${e.message}") - } - runCatching { file.delete() } - return null - } - } -} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/FixResourcesModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/FixResourcesModule.kt deleted file mode 100644 index c6f39fe..0000000 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/FixResourcesModule.kt +++ /dev/null @@ -1,23 +0,0 @@ -package GoonXposed.xposed.modules - -import android.content.res.Resources -import de.robv.android.xposed.callbacks.XC_LoadPackage -import GoonXposed.xposed.Constants.Companion.TARGET_PACKAGE -import GoonXposed.xposed.Module - -/** - * Hooks [Resources.getIdentifier] to fix resource package name mismatch. - */ -object FixResourcesModule : Module() { - override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { - if (packageName != TARGET_PACKAGE) { - Resources::class.java.hookMethod( - "getIdentifier", String::class.java, String::class.java, String::class.java - ) { - before { - if (args[2] == packageName) args[2] = TARGET_PACKAGE - } - } - } - } -} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/HookScriptLoaderModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/HookScriptLoaderModule.kt deleted file mode 100644 index a1dbf30..0000000 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/HookScriptLoaderModule.kt +++ /dev/null @@ -1,121 +0,0 @@ -package GoonXposed.xposed.modules - -import android.content.res.AssetManager -import android.content.res.XModuleResources -import de.robv.android.xposed.IXposedHookZygoteInit -import de.robv.android.xposed.XposedBridge -import de.robv.android.xposed.callbacks.XC_LoadPackage -import GoonXposed.xposed.Constants -import GoonXposed.xposed.HookStateHolder -import GoonXposed.xposed.Module -import GoonXposed.xposed.Utils.Log -import GoonXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.runBlocking -import java.io.File -import java.lang.reflect.Method - -/** - * Hooks React Native's script loading methods to load custom scripts and bundles. - * - * Preload scripts should be placed in the [PRELOADS_DIR] directory inside the module's files directory. - * - * The main bundle should be placed in the [Constants.CACHE_DIR] directory named [Constants.MAIN_SCRIPT_FILE]. - * If the bundle file does not exist, it will attempt to load `assets://Shiggy.bundle` from the module's assets. - */ -object HookScriptLoaderModule : Module() { - private lateinit var preloadsDir: File - private lateinit var mainScript: File - - private lateinit var modulePath: String - private lateinit var resources: XModuleResources - - const val PRELOADS_DIR = "preloads" - - override fun onInit(startupParam: IXposedHookZygoteInit.StartupParam) { - this@HookScriptLoaderModule.modulePath = startupParam.modulePath - } - - 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() } - - preloadsDir = File(filesDir, PRELOADS_DIR).apply { asDir() } - mainScript = File(cacheDir, Constants.MAIN_SCRIPT_FILE).apply { asFile() } - - listOf( - "com.facebook.react.runtime.ReactInstance\$loadJSBundle$1", - "com.facebook.react.runtime.ReactInstance$1", - // TODO: Remove once Discord fully switches to Bridgeless - "com.facebook.react.bridge.CatalystInstanceImpl" - ).mapNotNull { classLoader.safeLoadClass(it) }.forEach { hook(it) } - } - - private fun hook(instance: Class<*>) = runCatching { - val loadScriptFromAssets = instance.method( - "loadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType - ) - - val loadScriptFromFile = instance.method( - "loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType - ) - - loadScriptFromAssets.hook { - before { - Log.i("Received call to loadScriptFromAssets: ${args[1]} (sync: ${args[2]})") - runCustomScripts(loadScriptFromFile, loadScriptFromAssets) - } - } - - loadScriptFromFile.hook { - before { - Log.i("Received call to loadScriptFromFile: ${args[0]} (sync: ${args[2]})") - runCustomScripts(loadScriptFromFile, loadScriptFromAssets) - } - } - }.onFailure { - Log.e("Failed to hook script loading methods in ${instance.name}:", it) - } - - private fun HookScope.runCustomScripts(loadScriptFromFile: Method, loadScriptFromAssets: Method) { - Log.i("Running custom scripts...") - - runBlocking { - val ready = async { HookStateHolder.readyDeferred.join() } - val download = async { UpdaterModule.downloadScript().join() } - - awaitAll(ready, download) - } - - val loadSynchronously = args[2] - val runScriptFile = { file: File -> - Log.i("Loading script: ${file.absolutePath}") - - XposedBridge.invokeOriginalMethod( - loadScriptFromFile, thisObject, arrayOf(file.absolutePath, file.absolutePath, loadSynchronously) - ) - - Unit - } - - try { - preloadsDir.walk().filter { it.isFile }.forEach(runScriptFile) - - if (mainScript.exists()) runScriptFile(mainScript) - else { - Log.i("Main script does not exist, falling back") - - if (!::resources.isInitialized) resources = XModuleResources.createInstance(modulePath, null) - - XposedBridge.invokeOriginalMethod( - loadScriptFromAssets, - thisObject, - arrayOf(resources.assets, "assets://Shiggy.bundle", loadSynchronously) - ) - } - } catch (e: Throwable) { - Log.e("Unable to run scripts:", e) - } - } -} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt deleted file mode 100644 index 44d56cb..0000000 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt +++ /dev/null @@ -1,1051 +0,0 @@ -package GoonXposed.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.util.TypedValue -import android.view.Gravity -import android.view.View -import android.view.ViewGroup -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 -import de.robv.android.xposed.callbacks.XC_LoadPackage -import GoonXposed.xposed.Module -import GoonXposed.xposed.Utils.Companion.reloadApp -import GoonXposed.xposed.Utils.Log -import kotlinx.coroutines.* -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 - private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) - private var contextForMenu: Context? = null - - override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { - this@LogBoxModule.packageParam = packageParam - - try { - val dcdReactNativeHostClass = classLoader.loadClass("com.discord.bridge.DCDReactNativeHost") - val getUseDeveloperSupportMethod = - dcdReactNativeHostClass.methods.first { it.name == "getUseDeveloperSupport" } - - getUseDeveloperSupportMethod.hook { - before { - result = true - } - } - Log.e("Successfully hooked DCDReactNativeHost") - } catch (e: Exception) { - Log.e("Failed to hook DCDReactNativeHost: ${e.message}") - } - - return@with - } - - override fun onContext(context: Context) { - try { - Log.e("onContext called with context: $context") - contextForMenu = context - - val possibleClasses = listOf( - "com.facebook.react.devsupport.BridgeDevSupportManager", - "com.facebook.react.devsupport.BridgelessDevSupportManager", - "com.facebook.react.devsupport.DevSupportManagerImpl", - "com.facebook.react.devsupport.DevSupportManagerBase", - "com.facebook.react.devsupport.DefaultDevSupportManager" - ) - - var foundAny = false - possibleClasses.forEach { className -> - try { - val clazz = packageParam.classLoader.loadClass(className) - Log.e("Found class: $className") - hookDevSupportManager(clazz, context) - foundAny = true - } catch (e: Exception) { - 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") - dexFile.isAccessible = true - Log.e("Searching for DevSupport classes in classloader...") - } catch (e: Exception) { - Log.e("Could not search for classes: ${e.message}") - } - } - - 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" } - if (handleReloadJSMethod != null) { - XposedBridge.hookMethod(handleReloadJSMethod, object : XC_MethodReplacement() { - override fun replaceHookedMethod(param: MethodHookParam): Any? { - Log.e("handleReloadJS called - reloading app") - reloadApp() - return null - } - }) - } - } catch (e: Exception) { - Log.e("Failed to hook handleReloadJS: ${e.message}") - } - - try { - val showDevOptionsDialogMethod = clazz.methods.firstOrNull { it.name == "showDevOptionsDialog" } - if (showDevOptionsDialogMethod != null) { - XposedBridge.hookMethod(showDevOptionsDialogMethod, object : XC_MethodReplacement() { - override fun replaceHookedMethod(param: MethodHookParam): Any? { - try { - var activityContext: Context? = null - try { - activityContext = getContextFromDevSupport(clazz, param.thisObject) - if (activityContext != null) { - Log.e("Successfully got context from DevSupport") - } - } 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() - } - return null - } - }) - } - } catch (e: Exception) { - } - } catch (e: Exception) { - } - } - - private fun getContextFromDevSupport(clazz: Class<*>, instance: Any?): Context? { - if (instance == null) { - 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", - "reactInstanceDevHelper", - "mReactInstanceManager", - "mApplicationContext" - ) - - for (helperName in helpers) { - try { - Log.e("Trying field: $helperName") - val helperField = XposedHelpers.findFieldIfExists(clazz, helperName) - if (helperField == null) { - 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" - } - - if (getCurrentActivityMethod != null) { - val ctx = getCurrentActivityMethod.invoke(helper) as? Context - if (ctx != null) { - Log.e("Got context from $helperName.getCurrentActivity()") - return ctx - } - } - } catch (e: Exception) { - Log.e("Error trying $helperName: ${e.message}") - } - } - - Log.e("Could not get context from DevSupport object using any method") - null - } catch (e: Exception) { - Log.e("Failed to get context (outer catch): ${e.message}") - null - } - } - - // Material 3 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 isDarkMode(context: Context): Boolean { - return (context.resources.configuration.uiMode and - android.content.res.Configuration.UI_MODE_NIGHT_MASK) == - android.content.res.Configuration.UI_MODE_NIGHT_YES - } - - private fun getM3Colors(context: Context): M3Colors { - val isDark = isDarkMode(context) - return if (isDark) { - M3Colors( - surface = Color.parseColor("#1C1B1F"), - surfaceVariant = Color.parseColor("#49454F"), - onSurface = Color.parseColor("#E6E1E5"), - onSurfaceVariant = Color.parseColor("#CAC4D0"), - primary = Color.parseColor("#D0BCFF"), - onPrimary = Color.parseColor("#381E72"), - primaryContainer = Color.parseColor("#4F378B"), - onPrimaryContainer = Color.parseColor("#EADDFF"), - secondaryContainer = Color.parseColor("#4A4458"), - onSecondaryContainer = Color.parseColor("#E8DEF8"), - error = Color.parseColor("#F2B8B5"), - onError = Color.parseColor("#601410") - ) - } else { - M3Colors( - surface = Color.parseColor("#FFFBFE"), - surfaceVariant = Color.parseColor("#E7E0EC"), - onSurface = Color.parseColor("#1C1B1F"), - onSurfaceVariant = Color.parseColor("#49454F"), - primary = Color.parseColor("#6750A4"), - onPrimary = Color.parseColor("#FFFFFF"), - primaryContainer = Color.parseColor("#EADDFF"), - onPrimaryContainer = Color.parseColor("#21005D"), - secondaryContainer = Color.parseColor("#E8DEF8"), - onSecondaryContainer = Color.parseColor("#1D192B"), - error = Color.parseColor("#B3261E"), - onError = Color.parseColor("#FFFFFF") - ) - } - } - - 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 secondaryContainer: Int, - val onSecondaryContainer: Int, - val error: Int, - val onError: Int - ) - - private fun createM3Background(context: Context, color: Int, cornerRadius: Float): GradientDrawable { - return GradientDrawable().apply { - setColor(color) - setCornerRadius(dpToPx(context, cornerRadius.toInt()).toFloat()) - } - } - - private fun createM3Button(context: Context, text: String, colors: M3Colors, onClick: () -> Unit): View { - return LinearLayout(context).apply { - orientation = LinearLayout.HORIZONTAL - gravity = Gravity.CENTER - background = createM3Background(context, colors.primaryContainer, 20f) - setPadding( - dpToPx(context, 24), - dpToPx(context, 10), - dpToPx(context, 24), - dpToPx(context, 10) - ) - clipToOutline = true - isClickable = true - isFocusable = true - - val params = LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - dpToPx(context, 40) - ).apply { - setMargins(0, dpToPx(context, 8), 0, 0) - } - layoutParams = params - - addView(TextView(context).apply { - this.text = text - setTextColor(colors.onPrimaryContainer) - textSize = 14f - typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL) - gravity = Gravity.CENTER - }) - - // Create a custom ripple effect that stays within bounds - val rippleDrawable = android.graphics.drawable.RippleDrawable( - ColorStateList.valueOf(Color.argb(30, 255, 255, 255)), - createM3Background(context, colors.primaryContainer, 20f), - null - ) - background = rippleDrawable - - setOnClickListener { onClick() } - } - } - - private fun showRecoveryMenu(context: Context) { - Log.e("showRecoveryMenu called with context: $context") - try { - val colors = getM3Colors(context) - 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, 28f) - } - - // Title - val titleView = TextView(context).apply { - text = "ShiggyXposed Recovery" - textSize = 22f - setTextColor(colors.onSurface) - typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD) - setPadding(0, 0, 0, dpToPx(context, 16)) - gravity = Gravity.CENTER - } - container.addView(titleView) - - // Subtitle - val subtitleView = TextView(context).apply { - text = "Select an option to continue" - textSize = 14f - setTextColor(colors.onSurfaceVariant) - typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.NORMAL) - setPadding(0, 0, 0, dpToPx(context, 24)) - gravity = Gravity.CENTER - } - container.addView(subtitleView) - - // Safe Mode Button - val safeModeText = if (isSafeModeEnabled(context)) "Disable Safe Mode" else "Enable Safe Mode" - container.addView(createM3Button(context, safeModeText, colors) { - dialog.dismiss() - handleMenuSelection(context, 0) - }) - - // Refetch Bundle Button - container.addView(createM3Button(context, "Refetch Bundle", colors) { - dialog.dismiss() - handleMenuSelection(context, 1) - }) - - // Load Custom Bundle Button - container.addView(createM3Button(context, "Load Custom Bundle", colors) { - dialog.dismiss() - handleMenuSelection(context, 2) - }) - - // Reload App Button - container.addView(createM3Button(context, "Reload App", colors) { - dialog.dismiss() - handleMenuSelection(context, 3) - }) - - // Clear Cache & Reset Button - container.addView(createM3Button(context, "Clear Cache & Reset", colors) { - dialog.dismiss() - handleMenuSelection(context, 4) - }) - - dialog = AlertDialog.Builder(context) - .setView(container) - .create() - - dialog.window?.setBackgroundDrawable( - createM3Background(context, Color.TRANSPARENT, 28f) - ) - - dialog.show() - Log.e("Recovery menu shown successfully") - } catch (e: Exception) { - Log.e("Error showing recovery menu: ${e.message}", e) - throw e - } - } - - private fun handleMenuSelection(context: Context, index: Int) { - when (index) { - 0 -> toggleSafeMode(context) - 1 -> showConfirmAction( - context, "Refetch Bundle", - "This will download the latest bundle from Github." - ) { refetchBundle(context) } - - 2 -> showCustomBundleDialog(context) - 3 -> reloadApp() - 4 -> showConfirmAction( - context, "Clear Cache & Reset", - "This will clear all cached bundles and reset to default settings." - ) { clearCacheAndReset(context) } - } - } - - private fun showConfirmAction(context: Context, title: String, message: String, action: () -> Unit) { - val colors = getM3Colors(context) - - 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, 28f) - } - - val titleView = TextView(context).apply { - text = title - textSize = 20f - 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)) - lineHeight = dpToPx(context, 20) - } - container.addView(messageView) - - val buttonContainer = LinearLayout(context).apply { - orientation = LinearLayout.HORIZONTAL - gravity = Gravity.END - } - - // Cancel button - 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, 16), - dpToPx(context, 10), - dpToPx(context, 16), - dpToPx(context, 10) - ) - } - - // Confirm button - 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, 16), - dpToPx(context, 10), - dpToPx(context, 16), - dpToPx(context, 10) - ) - } - - 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, 28f) - ) - - dialog.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) { - Log.e("Error checking safe mode: ${e.message}") - 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") - if (themeId.isNotEmpty()) { - safeMode.put("currentThemeId", themeId) - themeFile.delete() - } - } - - settings.put("safeMode", safeMode) - settingsFile.writeText(settings.toString()) - - showM3Toast(context, "Safe Mode ${if (newState) "Enabled" else "Disabled"}") - reloadApp() - - } catch (e: Exception) { - Log.e("Error toggling safe mode: ${e.message}") - showError(context, "Failed to toggle safe mode", e.message) - } - } - - private fun showM3Toast(context: Context, message: String) { - Toast.makeText(context, message, Toast.LENGTH_SHORT).show() - } - - 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 clearCacheAndReset(context: Context) { - scope.launch(Dispatchers.IO) { - try { - val pyoncordDir = getPyoncordDirectory(context) - val bundleFile = File(pyoncordDir, "bundle.js") - val configFile = File(pyoncordDir, "loader.json") - - bundleFile.delete() - - if (configFile.exists()) { - val config = JSONObject(configFile.readText()) - val customLoadUrl = config.optJSONObject("customLoadUrl") ?: JSONObject() - customLoadUrl.put("enabled", false) - customLoadUrl.put("url", "http://localhost:4040/shiggycord.js") - config.put("customLoadUrl", customLoadUrl) - configFile.writeText(config.toString()) - } - - withContext(Dispatchers.Main) { - Toast.makeText(context, "Cache cleared successfully", Toast.LENGTH_SHORT).show() - reloadApp() - } - - } catch (e: Exception) { - Log.e("Error clearing cache: ${e.message}") - withContext(Dispatchers.Main) { - Toast.makeText(context, "Failed to clear cache", Toast.LENGTH_SHORT).show() - } - } - } - } - - private fun showCustomBundleDialog(context: Context) { - val colors = getM3Colors(context) - val filesDir = File(context.filesDir, "pyoncord") - val configFile = File(filesDir, "loader.json") - var currentUrl: String? = null - var isEnabled = false - - if (configFile.exists()) { - try { - val json = JSONObject(configFile.readText()) - val customLoadUrl = json.optJSONObject("customLoadUrl") - if (customLoadUrl != null) { - isEnabled = customLoadUrl.optBoolean("enabled", false) - currentUrl = customLoadUrl.optString("url", "") - } - } 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, 28f) - } - - val titleView = TextView(context).apply { - text = "Custom Bundle URL" - textSize = 20f - setTextColor(colors.onSurface) - typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD) - setPadding(0, 0, 0, dpToPx(context, 16)) - } - container.addView(titleView) - - // Toggle switch container - val toggleContainer = LinearLayout(context).apply { - orientation = LinearLayout.HORIZONTAL - gravity = Gravity.CENTER_VERTICAL - val params = LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.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, - ViewGroup.LayoutParams.WRAP_CONTENT, - 1f - ) - } - - val toggleSwitch = createM3Switch(context, colors, isEnabled) - - toggleContainer.addView(toggleLabel) - toggleContainer.addView(toggleSwitch) - container.addView(toggleContainer) - - val urlInput = EditText(context).apply { - hint = "http://localhost:4040/shiggycord.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) - ) - isEnabled - alpha = if (isEnabled) 1f else 0.5f - val params = LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ).apply { - setMargins(0, 0, 0, dpToPx(context, 24)) - } - layoutParams = params - } - container.addView(urlInput) - - toggleSwitch.setOnClickListener { - val currentState = toggleSwitch.tag as Boolean - val newState = !currentState - toggleSwitch.tag = newState - updateSwitchAppearance(context, toggleSwitch, colors, newState) - - urlInput.animate() - .alpha(if (newState) 1f else 0.5f) - .setDuration(250) - .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, 16), - dpToPx(context, 10), - dpToPx(context, 16), - dpToPx(context, 10) - ) - } - - 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, 16), - dpToPx(context, 10), - dpToPx(context, 16), - dpToPx(context, 10) - ) - } - - val dialog = AlertDialog.Builder(context) - .setView(container) - .create() - - cancelButton.setOnClickListener { dialog.dismiss() } - saveButton.setOnClickListener { - try { - val url = urlInput.text?.toString()?.trim() ?: "" - val enabled = toggleSwitch.tag as Boolean - - if (enabled && url.isNotEmpty()) { - setCustomBundleURL(context, url, true) - dialog.dismiss() - } else if (!enabled) { - setCustomBundleURL(context, url.ifEmpty { "http://localhost:4040/shiggycord.js" }, false) - dialog.dismiss() - } else { - Toast.makeText(context, "Please enter a URL", Toast.LENGTH_SHORT).show() - } - } catch (e: Exception) { - Toast.makeText(context, "Failed to set custom bundle: ${e.message}", Toast.LENGTH_LONG).show() - } - } - - buttonContainer.addView(cancelButton) - buttonContainer.addView(saveButton) - container.addView(buttonContainer) - - dialog.window?.setBackgroundDrawable( - createM3Background(context, Color.TRANSPARENT, 28f) - ) - - dialog.show() - } - - private fun createM3Switch(context: Context, colors: M3Colors, isChecked: Boolean): View { - val switchView = LinearLayout(context).apply { - orientation = LinearLayout.HORIZONTAL - gravity = Gravity.CENTER_VERTICAL - tag = isChecked - layoutParams = LinearLayout.LayoutParams( - dpToPx(context, 52), - dpToPx(context, 32) - ) - } - - updateSwitchAppearance(context, switchView, colors, isChecked) - return switchView - } - - private fun updateSwitchAppearance(context: Context, switchView: View, colors: M3Colors, isChecked: Boolean) { - val trackColor = if (isChecked) colors.primary else colors.surfaceVariant - val thumbColor = if (isChecked) colors.onPrimary else colors.onSurfaceVariant - - val currentTrackColor = if (switchView.background is GradientDrawable) { - (switchView.background as GradientDrawable).color?.defaultColor - ?: (if (!isChecked) colors.primary else colors.surfaceVariant) - } else { - if (!isChecked) colors.primary else colors.surfaceVariant - } - - val trackColorAnimator = ValueAnimator.ofObject(ArgbEvaluator(), currentTrackColor, trackColor) - trackColorAnimator.duration = 250 - trackColorAnimator.interpolator = DecelerateInterpolator() - trackColorAnimator.addUpdateListener { animator -> - val animatedColor = animator.animatedValue as Int - val track = GradientDrawable().apply { - setColor(animatedColor) - setCornerRadius(dpToPx(context, 16).toFloat()) - } - switchView.background = track - } - - if (switchView is LinearLayout) { - val thumb = if (switchView.childCount > 0) { - switchView.getChildAt(0) - } else { - // Create new thumb if none exists - View(context).apply { - val size = dpToPx(context, 24) - layoutParams = LinearLayout.LayoutParams(size, size).apply { - marginStart = dpToPx(context, 4) - marginEnd = dpToPx(context, 24) - } - switchView.addView(this) - } - } - - val currentParams = thumb.layoutParams as LinearLayout.LayoutParams - val targetMarginStart = if (isChecked) dpToPx(context, 24) else dpToPx(context, 4) - - val marginAnimator = ValueAnimator.ofInt(currentParams.marginStart, targetMarginStart) - marginAnimator.duration = 250 - marginAnimator.interpolator = DecelerateInterpolator() - marginAnimator.addUpdateListener { animator -> - val animatedMargin = animator.animatedValue as Int - val params = thumb.layoutParams as LinearLayout.LayoutParams - params.marginStart = animatedMargin - params.marginEnd = dpToPx(context, 28) - animatedMargin - thumb.layoutParams = params - } - - val currentThumbColor = if (thumb.background is GradientDrawable) { - (thumb.background as GradientDrawable).color?.defaultColor - ?: (if (!isChecked) colors.onPrimary else colors.onSurfaceVariant) - } else { - if (!isChecked) colors.onPrimary else colors.onSurfaceVariant - } - - val thumbColorAnimator = ValueAnimator.ofObject(ArgbEvaluator(), currentThumbColor, thumbColor) - thumbColorAnimator.duration = 250 - thumbColorAnimator.interpolator = DecelerateInterpolator() - thumbColorAnimator.addUpdateListener { animator -> - val animatedColor = animator.animatedValue as Int - thumb.background = GradientDrawable().apply { - setColor(animatedColor) - shape = GradientDrawable.OVAL - } - } - - val animatorSet = AnimatorSet() - animatorSet.playTogether(trackColorAnimator, marginAnimator, thumbColorAnimator) - animatorSet.start() - } - } - - private fun setCustomBundleURL(context: Context, url: String, enabled: Boolean) { - try { - val pyoncordDir = getPyoncordDirectory(context) - val configFile = File(pyoncordDir, "loader.json") - - val config = if (configFile.exists()) { - JSONObject(configFile.readText()) - } else { - JSONObject().apply { - put("loadReactDevTools", false) - } - } - - val customLoadUrl = config.optJSONObject("customLoadUrl") ?: JSONObject() - customLoadUrl.put("enabled", enabled) - customLoadUrl.put("url", url) - config.put("customLoadUrl", customLoadUrl) - - configFile.writeText(config.toString()) - - if (enabled) { - File(pyoncordDir, "bundle.js").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 disableCustomBundle(context: Context) { - try { - val configFile = File(getPyoncordDirectory(context), "loader.json") - val config = if (configFile.exists()) { - JSONObject(configFile.readText()) - } else { - JSONObject().apply { - put("loadReactDevTools", false) - } - } - - val customLoadUrl = config.optJSONObject("customLoadUrl") ?: JSONObject() - customLoadUrl.put("enabled", false) - customLoadUrl.put("url", "http://localhost:4040/shiggycord.js") - config.put("customLoadUrl", customLoadUrl) - - configFile.writeText(config.toString()) - Toast.makeText(context, "Custom bundle disabled", Toast.LENGTH_SHORT).show() - reloadApp() - } catch (e: Exception) { - Toast.makeText(context, "Failed to disable custom bundle", Toast.LENGTH_SHORT).show() - } - } - - 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() - backupFile.delete() - - if (configFile.exists()) { - val config = JSONObject(configFile.readText()) - config.put("customLoadUrlEnabled", false) - configFile.writeText(config.toString()) - } - - reloadApp() - } catch (e: Exception) { - Log.e("Error resetting bundle: ${e.message}") - showError(context, "Failed to reset bundle", e.message) - } - } - - private fun getPyoncordDirectory(context: Context): File { - val dir = File(context.filesDir, "pyoncord") - if (!dir.exists()) { - dir.mkdirs() - } - return dir - } - - private fun showError(context: Context, title: String, message: String?) { - val colors = getM3Colors(context) - - 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, 28f) - } - - val titleView = TextView(context).apply { - text = title - textSize = 20f - 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)) - lineHeight = dpToPx(context, 20) - } - 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, 16), - dpToPx(context, 10), - dpToPx(context, 16), - dpToPx(context, 10) - ) - } - - val dialog = AlertDialog.Builder(context) - .setView(container) - .create() - - okButton.setOnClickListener { dialog.dismiss() } - container.addView(okButton) - - dialog.window?.setBackgroundDrawable( - createM3Background(context, Color.TRANSPARENT, 28f) - ) - - dialog.show() - } -} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PayloadGlobalModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PayloadGlobalModule.kt deleted file mode 100644 index 03d0601..0000000 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PayloadGlobalModule.kt +++ /dev/null @@ -1,80 +0,0 @@ -package GoonXposed.xposed.modules - -import android.content.res.AssetManager -import de.robv.android.xposed.XC_MethodHook -import de.robv.android.xposed.XposedBridge -import de.robv.android.xposed.callbacks.XC_LoadPackage -import GoonXposed.xposed.BuildConfig -import GoonXposed.xposed.Constants -import GoonXposed.xposed.Module -import GoonXposed.xposed.Utils.Companion.JSON -import GoonXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.put -import java.io.File - -// TODO: Remove this redundant module and merge its functionality into AdditionalBridgeMethodsModule if needed -class PayloadGlobalModule(private val modules: List) : Module() { - private companion object { - const val GLOBAL_NAME = "__PYON_LOADER__" - } - - private fun getPayloadString(): String = JSON.encodeToString( - buildJsonObject { - put("loaderName", Constants.LOADER_NAME) - put("loaderVersion", BuildConfig.VERSION_NAME) - @Suppress("DEPRECATION") for (module in modules) module.buildPayload(this) - }) - - override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { - val catalystInstance = classLoader.safeLoadClass("com.facebook.react.bridge.CatalystInstanceImpl") - val scriptLoader = classLoader.safeLoadClass("com.facebook.react.runtime.ReactInstance\$loadJSBundle$1") - ?: classLoader.safeLoadClass("com.facebook.react.runtime.ReactInstance$1") - - val setGlobalVariable: (XC_MethodHook.MethodHookParam, String, String) -> Unit = { param, key, json -> - runCatching { - // Attempt old CatalystInstanceImpl method - catalystInstance!!.method( - "setGlobalVariable", String::class.java, String::class.java - ).invoke(param.thisObject, key, json) - }.onFailure { - // Failed (likely because it's a stub), use a workaround - val preloadsDir = File("${appInfo.dataDir}/${Constants.FILES_DIR}", PRELOADS_DIR) - File(preloadsDir, "rv_globals_$key.js").apply { - writeText("this[${JSON.encodeToString(key)}]=$json") - - XposedBridge.invokeOriginalMethod( - scriptLoader!!.method( - "loadScriptFromFile", - String::class.java, - String::class.java, - Boolean::class.javaPrimitiveType - ), param.thisObject, arrayOf(absolutePath, absolutePath, param.args[2]) - ) - - delete() - } - } - } - - val hook = MethodHookBuilder().run { - before { - setGlobalVariable(param, GLOBAL_NAME, getPayloadString()) - } - - build() - } - - listOf(catalystInstance, scriptLoader).forEach { if (it != null) hook(it, hook) } - } - - private fun hook(instance: Class<*>, hook: XC_MethodHook) { - instance.method( - "loadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType, - ).hook(hook) - - instance.method( - "loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType - ).hook(hook) - } -} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PluginsModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PluginsModule.kt deleted file mode 100644 index 8aae94b..0000000 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/PluginsModule.kt +++ /dev/null @@ -1,124 +0,0 @@ -package GoonXposed.xposed.modules - -import android.content.Context -import android.util.AtomicFile -import GoonXposed.xposed.Module -import GoonXposed.xposed.Utils.Log -import GoonXposed.xposed.modules.bridge.BridgeModule -import java.io.* - -/** - * Manages the states of plugins, allowing reading and writing of plugin states to a file. - * - * ## Methods - * - * - `Shiggy.plugins.states.read(): { flags: { [pluginId: string]: number } }` - * - Reads the current plugin states from the file and returns them as a map. - * - * - `Shiggy.plugins.states.write(flags: { [pluginId: string]: number }): void` - * - Writes the provided plugin states to the file. - */ -class PluginsModule : Module() { - private companion object { - const val DATA_DIR = "Shiggy/plugins" - const val STATES_FILE = "states" - } - - private lateinit var states: PluginStates - - override fun onContext(context: Context) = with(context) { - val dataDir = File(filesDir, DATA_DIR) - - val statesFile = File( - dataDir, STATES_FILE - ).apply { asFile() } - - BridgeModule.registerMethod("Shiggy.plugins.states.read") { - if (::states.isInitialized) states.toMap() else - PluginStates.loadFromFileOrNull(statesFile)?.let { - states = it - it.toMap() - } - } - - BridgeModule.registerMethod("Shiggy.plugins.states.write") { - val (flags) = it - @Suppress("UNCHECKED_CAST") - states = PluginStates( - flags as Map - ).apply { saveToFile(statesFile) } - Log.i("Plugin states saved: ${statesFile.absolutePath}") - } - } -} - -private class UnsupportedPluginStatesVersionException(version: Int) : - Throwable("Unsupported plugin states version: $version") - -data class PluginStates( - val flags: Map -) { - fun saveToFile(file: File) { - val atomic = AtomicFile(file) - var fos: FileOutputStream? = null - try { - fos = atomic.startWrite() - val out = DataOutputStream(BufferedOutputStream(fos)) - - out.writeInt(CURRENT_VERSION) - - // Write flags - out.writeInt(flags.size) - for ((id, flags) in flags) { - out.writeUTF(id) - out.writeInt(flags.toInt()) - } - - out.flush() - atomic.finishWrite(fos) - } catch (t: Throwable) { - if (fos != null) atomic.failWrite(fos) - throw t - } - } - - fun toMap(): Map = mapOf("flags" to flags) - - companion object { - const val CURRENT_VERSION = 1 - - fun loadFromFileOrNull(file: File): PluginStates? { - if (!file.exists() || file.length() <= 0L) return null - val atomic = AtomicFile(file) - - try { - DataInputStream(BufferedInputStream(atomic.openRead())).use { input -> - when (val version = input.readInt()) { - 1 -> return loadV1FromFileOrNull(input) - else -> throw UnsupportedPluginStatesVersionException(version) - } - } - } catch (e: UnsupportedPluginStatesVersionException) { - Log.i(e.message!!) - } catch (e: EOFException) { - Log.e("Plugin states corrupt: ${e.message}") - } catch (e: IOException) { - Log.e("Failed to read plugin states: ${e.message}") - } - - runCatching { file.delete() } - return null - } - - private fun loadV1FromFileOrNull(input: DataInputStream): PluginStates { - val flagsSize = input.readInt() - val flags = mutableMapOf() - - repeat(flagsSize) { - flags[input.readUTF()] = input.readInt().toDouble() - } - - return PluginStates(flags) - } - } -} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/UpdaterModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/UpdaterModule.kt deleted file mode 100644 index 2ee6296..0000000 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/UpdaterModule.kt +++ /dev/null @@ -1,155 +0,0 @@ -package GoonXposed.xposed.modules - -import android.app.Activity -import android.app.AlertDialog -import android.content.Context -import android.util.AtomicFile -import android.widget.Toast -import androidx.core.util.writeBytes -import de.robv.android.xposed.callbacks.XC_LoadPackage -import GoonXposed.xposed.Constants -import GoonXposed.xposed.Module -import GoonXposed.xposed.Utils -import GoonXposed.xposed.Utils.Companion.JSON -import GoonXposed.xposed.Utils.Companion.reloadApp -import GoonXposed.xposed.Utils.Log -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.engine.cio.* -import io.ktor.client.plugins.* -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* -import kotlinx.coroutines.* -import kotlinx.serialization.Serializable -import java.io.File -import java.lang.ref.WeakReference - -@Serializable -data class CustomLoadUrl( - val enabled: Boolean = false, val url: String = "" -) - -@Serializable -data class LoaderConfig( - val customLoadUrl: CustomLoadUrl = CustomLoadUrl() -) - -/** - * Module that updates the JS bundle by downloading it from a remote URL. - * - * Shows dialogs when failed allowing retry. - */ -object UpdaterModule : Module() { - private lateinit var config: LoaderConfig - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - private var lastActivity: WeakReference? = null - - private lateinit var cacheDir: File - private lateinit var bundle: File - private lateinit var etag: File - - private const val TIMEOUT_CACHED = 5000L - private const val TIMEOUT = 10000L - private const val ETAG_FILE = "etag.txt" - private const val CONFIG_FILE = "loader.json" - - private const val DEFAULT_BUNDLE_URL = - "https://github.com/OTKUSteyler/GoonCord/releases/download/latest/gooncord.js" - - override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { - cacheDir = File(appInfo.dataDir, Constants.CACHE_DIR).apply { mkdirs() } - val filesDir = File(appInfo.dataDir, Constants.FILES_DIR).apply { mkdirs() } - - bundle = File(cacheDir, Constants.MAIN_SCRIPT_FILE) - etag = File(cacheDir, ETAG_FILE) - - val configFile = File(filesDir, CONFIG_FILE) - - config = runCatching { - if (configFile.exists()) { - JSON.decodeFromString(configFile.readText()) - } else LoaderConfig() - }.getOrDefault(LoaderConfig()) - } - - fun downloadScript(activity: Activity? = null): Job = scope.launch { - try { - HttpClient(CIO) { - expectSuccess = false - install(UserAgent) { agent = Constants.USER_AGENT } - install(HttpRedirect) {} - }.use { client -> - val url = config.customLoadUrl.takeIf { it.enabled }?.url ?: DEFAULT_BUNDLE_URL - Log.i("Fetching JS bundle from: $url") - - val response: HttpResponse = client.get(url) { - headers { - if (etag.exists() && bundle.exists()) { - append(HttpHeaders.IfNoneMatch, etag.readText()) - } - } - - // Retries don't need timeout - if (activity == null) { - timeout { - requestTimeoutMillis = if (!bundle.exists()) TIMEOUT else TIMEOUT_CACHED - } - } - } - - when (response.status) { - HttpStatusCode.OK -> { - val bytes: ByteArray = response.body() - AtomicFile(bundle).writeBytes(bytes) - - val newTag = response.headers[HttpHeaders.ETag] - if (!newTag.isNullOrEmpty()) etag.writeText(newTag) else etag.delete() - - Log.i("Bundle updated (${bytes.size} bytes)") - - // This is a retry, so we show a dialog - if (activity != null) { - withContext(Dispatchers.Main) { - AlertDialog.Builder(activity).setTitle("GoonCord Update Successful") - .setMessage("A reload is required for changes to take effect.") - .setPositiveButton("Reload") { dialog, _ -> - reloadApp() - dialog.dismiss() - }.setCancelable(false).show() - } - } - } - - HttpStatusCode.NotModified -> { - Log.i("Server responded with 304, no changes") - } - - else -> { - throw ResponseException(response, "Received status: ${response.status}") - } - } - } - } catch (e: Throwable) { - Log.e("Failed to download script", e) - showErrorDialog(e) - } - } - - override fun onActivity(activity: Activity) { - lastActivity = WeakReference(activity) - } - - fun showErrorDialog(e: Throwable) { - val activity = lastActivity?.get() ?: return - - activity.runOnUiThread { - } - } - - fun resetLoaderConfig(context: Context) { - val filesDir = File(context.dataDir, Constants.FILES_DIR) - val config = File(filesDir, CONFIG_FILE) - if (config.exists()) config.delete() - } -} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/appearance/FontsModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/appearance/FontsModule.kt deleted file mode 100644 index 2a121e7..0000000 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/appearance/FontsModule.kt +++ /dev/null @@ -1,228 +0,0 @@ -package GoonXposed.xposed.modules.appearance - -import android.content.res.AssetManager -import android.graphics.Typeface -import android.graphics.Typeface.CustomFallbackBuilder -import android.graphics.fonts.Font -import android.graphics.fonts.FontFamily -import android.os.Build -import de.robv.android.xposed.XC_MethodReplacement -import de.robv.android.xposed.XposedHelpers -import de.robv.android.xposed.callbacks.XC_LoadPackage -import GoonXposed.xposed.Constants -import GoonXposed.xposed.Module -import GoonXposed.xposed.Utils.Companion.JSON -import GoonXposed.xposed.Utils.Log -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.engine.cio.* -import io.ktor.client.plugins.* -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* -import kotlinx.coroutines.* -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonObjectBuilder -import kotlinx.serialization.json.put -import java.io.File -import java.io.IOException - -@Serializable -data class FontDefinition( - val name: String? = null, - val description: String? = null, - val spec: Int? = null, - val main: Map, -) - -object FontsModule : Module() { - private val EXTENSIONS = arrayOf("", "_bold", "_italic", "_bold_italic") - private val FILE_EXTENSIONS = arrayOf(".ttf", ".otf") - private const val FONTS_ASSET_PATH = "fonts/" - - private lateinit var fontsDir: File - private lateinit var fontsDownloadsDir: File - private var fontsAbsPath: String? = null - - override fun buildPayload(builder: JsonObjectBuilder) { - builder.apply { - put("fontPatch", 2) - } - } - - override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { - XposedHelpers.findAndHookMethod( - "com.facebook.react.views.text.ReactFontManager\$Companion", - classLoader, - "createAssetTypeface", - String::class.java, - Int::class.java, - "android.content.res.AssetManager", - object : XC_MethodReplacement() { - override fun replaceHookedMethod(param: MethodHookParam): Typeface? { - val fontFamilyName: String = param.args[0].toString() - val style: Int = param.args[1] as Int - val assetManager: AssetManager = param.args[2] as AssetManager - return createAssetTypeface(fontFamilyName, style, assetManager) - } - }) - - val fontDefFile = File(appInfo.dataDir, "${Constants.FILES_DIR}/fonts.json").apply { asFile() } - if (!fontDefFile.exists()) return@with - - val fontDef = try { - JSON.decodeFromString(fontDefFile.readText()) - } catch (_: Throwable) { - return@with - } - - fontsDownloadsDir = File(appInfo.dataDir, "${Constants.FILES_DIR}/downloads/fonts").apply { asDir() } - fontsDir = File(fontsDownloadsDir, fontDef.name!!).apply { asDir() } - fontsAbsPath = fontsDir.absolutePath + "/" - - fontsDir.listFiles()?.forEach { file -> - val fileName = file.name - if (!fileName.startsWith(".")) { - val fontName = fileName.split('.')[0] - if (fontDef.main.keys.none { it == fontName }) { - Log.i("Deleting font file: $fileName") - file.delete() - } - } - } - - // These files should be downloaded by the JS side, but oh well - CoroutineScope(Dispatchers.IO).launch { - fontDef.main.keys.map { name -> - async { - val url = fontDef.main.getValue(name) - try { - Log.i("Downloading $name from $url") - val file = - File(fontsDir, "$name${FILE_EXTENSIONS.first { url.endsWith(it) }}").apply { asFile() } - if (file.exists()) return@async - - val client = HttpClient(CIO) { - install(UserAgent) { agent = Constants.USER_AGENT } - } - - val response: HttpResponse = client.get(url) - if (response.status == HttpStatusCode.OK) file.writeBytes(response.body()) - - return@async - } catch (e: Throwable) { - Log.e("Failed to download fonts ($name from $url)", e) - } - } - }.awaitAll() - } - } - - private fun createAssetTypefaceWithFallbacks( - fontFamilyNames: Array, style: Int, assetManager: AssetManager - ): Typeface? { - val fontFamilies: MutableList = ArrayList() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - // Iterate over the list of fontFamilyNames, constructing new FontFamily objects - // for use in the CustomFallbackBuilder below. - for (fontFamilyName in fontFamilyNames) { - try { - for (fileExtension in FILE_EXTENSIONS) { - val (customName, refName) = fontFamilyName.split(":") - val file = File(fontsDownloadsDir, "$customName/$refName.$fileExtension").apply { asFile() } - val font = Font.Builder(file).build() - val family = FontFamily.Builder(font).build() - fontFamilies.add(family) - } - } catch (_: Throwable) { - } - - for (fontRootPath in arrayOf(fontsAbsPath, FONTS_ASSET_PATH).filterNotNull()) { - for (fileExtension in FILE_EXTENSIONS) { - val fileName = - StringBuilder().append(fontRootPath).append(fontFamilyName).append(fileExtension).toString() - try { - val builder = if (fileName[0] == '/') Font.Builder(File(fileName)) else Font.Builder( - assetManager, fileName - ) - val font = builder.build() - val family = FontFamily.Builder(font).build() - fontFamilies.add(family) - } catch (_: RuntimeException) { - // If the typeface asset does not exist, try another extension. - continue - } catch (_: IOException) { - // If the font asset does not exist, try another extension. - continue - } - } - } - } - - // If there's some problem constructing fonts, fall back to the default behavior. - if (fontFamilies.isEmpty()) return createAssetTypeface(fontFamilyNames[0], style, assetManager) - - val fallbackBuilder = CustomFallbackBuilder(fontFamilies[0]) - for (i in 1 until fontFamilies.size) { - fallbackBuilder.addCustomFallback(fontFamilies[i]) - } - return fallbackBuilder.build() - } - return null - } - - private fun createAssetTypeface( - fontFamilyName: String, style: Int, assetManager: AssetManager - ): Typeface? { - // This logic attempts to safely check if the frontend code is attempting to use - // fallback fonts, and if it is, to use the fallback typeface creation logic. - var fontFamilyName: String = fontFamilyName - val fontFamilyNames = fontFamilyName.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - for (i in fontFamilyNames.indices) { - fontFamilyNames[i] = fontFamilyNames[i].trim() - } - - // If there are multiple font family names: - // For newer versions of Android, construct a Typeface with fallbacks - // For older versions of Android, ignore all the fallbacks and just use the first font family - if (fontFamilyNames.size > 1) { - fontFamilyName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - return createAssetTypefaceWithFallbacks(fontFamilyNames, style, assetManager) - } else { - fontFamilyNames[0] - } - } - - val extension = EXTENSIONS[style] - - try { - for (fileExtension in FILE_EXTENSIONS) { - val (customName, refName) = fontFamilyName.split(":") - val file = File(fontsDownloadsDir, "$customName/$refName.$fileExtension").apply { asFile() } - if (!file.exists()) throw Exception() - return Typeface.createFromFile(file.absolutePath) - } - } catch (_: Throwable) { - } - - // Lastly, after all those checks above, this is the original RN logic for - // getting the typeface. - for (fontRootPath in arrayOf(fontsAbsPath, FONTS_ASSET_PATH).filterNotNull()) { - for (fileExtension in FILE_EXTENSIONS) { - val fileName = - StringBuilder().append(fontRootPath).append(fontFamilyName).append(extension).append(fileExtension) - .toString() - - return try { - if (fileName[0] == '/') Typeface.createFromFile(fileName) - else Typeface.createFromAsset(assetManager, fileName) - } catch (_: RuntimeException) { - // If the typeface asset does not exist, try another extension. - continue - } - } - } - - return Typeface.create(fontFamilyName, style) - } -} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/appearance/SysColorsModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/appearance/SysColorsModule.kt deleted file mode 100644 index 3807b09..0000000 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/appearance/SysColorsModule.kt +++ /dev/null @@ -1,43 +0,0 @@ -package GoonXposed.xposed.modules.appearance - -import android.R.color -import android.app.AndroidAppHelper -import android.content.Context -import android.os.Build -import androidx.core.content.ContextCompat -import GoonXposed.xposed.Module -import kotlinx.serialization.json.* -import java.lang.ref.WeakReference - -object SysColorsModule : Module() { - private lateinit var context: WeakReference - private fun isSupported() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S - - override fun buildPayload(builder: JsonObjectBuilder) { - context = WeakReference(AndroidAppHelper.currentApplication()) - val accents = arrayOf("accent1", "accent2", "accent3", "neutral1", "neutral2") - val shades = arrayOf(0, 10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000) - - builder.apply { - put("isSysColorsSupported", isSupported()) - if (isSupported()) putJsonObject("sysColors") { - for (accent in accents) putJsonArray(accent) { - for (shade in shades) { - val colorName = "system_" + accent + "_" + shade - - val colorResourceId = runCatching { - color::class.java.getField(colorName).getInt(null) - }.getOrElse { 0 } - - add(convertToColor(colorResourceId)) - } - } - } - } - } - - private fun convertToColor(id: Int): String { - val clr = if (isSupported()) ContextCompat.getColor(context.get()!!, id) else 0 - return String.format("#%06X", 0xFFFFFF and clr) - } -} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/appearance/ThemesModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/appearance/ThemesModule.kt deleted file mode 100644 index 2e08714..0000000 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/appearance/ThemesModule.kt +++ /dev/null @@ -1,159 +0,0 @@ -package GoonXposed.xposed.modules.appearance - -import android.content.Context -import android.content.res.Resources -import androidx.core.graphics.toColorInt -import de.robv.android.xposed.callbacks.XC_LoadPackage -import GoonXposed.xposed.Constants -import GoonXposed.xposed.Module -import GoonXposed.xposed.Utils.Companion.JSON -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonObjectBuilder -import kotlinx.serialization.json.encodeToJsonElement -import kotlinx.serialization.json.put -import java.io.File - -@Serializable -data class Author( - val name: String, val id: String? = null -) - -@Serializable -data class ThemeData( - val name: String, - val description: String? = null, - val authors: List? = null, - val spec: Int, - val semanticColors: Map>? = null, - val rawColors: Map? = null -) - -@Serializable -data class Theme( - val id: String, val selected: Boolean, val data: ThemeData -) - -object ThemesModule : Module() { - private lateinit var param: XC_LoadPackage.LoadPackageParam - - private var theme: Theme? = null - private val rawColorMap = mutableMapOf() - - private const val THEME_FILE = "current-theme.json" - - - @ExperimentalSerializationApi - override fun buildPayload(builder: JsonObjectBuilder) { - builder.apply { - put("hasThemeSupport", true) - if (theme != null) put("storedTheme", JSON.encodeToJsonElement(theme!!)) - else put("storedTheme", null) - } - } - - private fun String.fromScreamingSnakeToCamelCase() = - this.split("_").joinToString("") { it -> it.lowercase().replaceFirstChar { it.uppercase() } } - - override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) { - param = packageParam - theme = getTheme() - hookTheme() - } - - private fun File.isValidThemeFile(): Boolean { - if (!this.exists()) return false - - val text = this.readText() - return text.isNotBlank() && text != "{}" && text != "null" - } - - private fun getTheme(): Theme? { - val themeFile = File(param.appInfo.dataDir, "${Constants.FILES_DIR}/${THEME_FILE}").apply { asFile() } - if (!themeFile.isValidThemeFile()) return null - - return try { - val themeText = themeFile.readText() - JSON.decodeFromString(themeText) - } catch (_: Exception) { - null - } - } - - fun hookTheme() { - val themeManager = param.classLoader.loadClass("com.discord.theme.utils.ColorUtilsKt") - val darkTheme = param.classLoader.loadClass("com.discord.theme.DarkerTheme") - val lightTheme = param.classLoader.loadClass("com.discord.theme.LightTheme") - - val theme = this.theme ?: return - - // Apply rawColors - theme.data.rawColors?.forEach { (key, value) -> - rawColorMap[key.lowercase()] = hexStringToColorInt(value) - } - - // Apply semanticColors - theme.data.semanticColors?.forEach { (key, value) -> - // TEXT_NORMAL -> getTextNormal - val methodName = "get${key.fromScreamingSnakeToCamelCase()}" - value.forEachIndexed { index, v -> - when (index) { - 0 -> hookThemeMethod(darkTheme, methodName, hexStringToColorInt(v)) - 1 -> hookThemeMethod(lightTheme, methodName, hexStringToColorInt(v)) - } - } - } - - // If there's any rawColors value, hook the color getter - if (!theme.data.rawColors.isNullOrEmpty()) { - val getColorCompat = themeManager.getDeclaredMethod( - "getColorCompat", - Resources::class.java, - Int::class.javaPrimitiveType, - Resources.Theme::class.java, - ) - - val getColorCompatLegacy = themeManager.getDeclaredMethod( - "getColorCompat", Context::class.java, Int::class.javaPrimitiveType - ) - - val patch = MethodHookBuilder().run { - before { - val arg1 = args[0] - val resources = if (arg1 is Context) arg1.resources else (arg1 as Resources) - val name = resources.getResourceEntryName(args[1] as Int) - - if (rawColorMap[name] != null) result = rawColorMap[name] - } - - build() - } - - getColorCompat.hook(patch) - getColorCompatLegacy.hook(patch) - } - } - - // Parse HEX colour string to INT. Takes "#RRGGBBAA" or "#RRGGBB" - private fun hexStringToColorInt(hexString: String): Int { - return if (hexString.length == 9) { - // Rearrange RRGGBBAA -> AARRGGBB so parseColor() is happy - val alpha = hexString.substring(7, 9) - val rrggbb = hexString.substring(1, 7) - "#$alpha$rrggbb".toColorInt() - } else hexString.toColorInt() - } - - private fun hookThemeMethod(themeClass: Class<*>, methodName: String, themeValue: Int) { - try { - themeClass.getDeclaredMethod(methodName).let { method -> - method.hook { - before { - result = themeValue - } - } - } - } catch (_: NoSuchMethodException) { - } - } -} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/bridge/AdditionalBridgeMethodsModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/bridge/AdditionalBridgeMethodsModule.kt deleted file mode 100644 index 178e0d9..0000000 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/bridge/AdditionalBridgeMethodsModule.kt +++ /dev/null @@ -1,94 +0,0 @@ -package GoonXposed.xposed.modules.bridge - -import android.app.Activity -import android.app.AlertDialog -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.os.Build -import android.widget.Toast -import GoonXposed.xposed.Module -import GoonXposed.xposed.Utils -import java.io.File - -object AdditionalBridgeMethodsModule : Module() { - override fun onContext(context: Context) = with(context) { - BridgeModule.registerMethod("Shiggy.fs.getConstants") { - mapOf( - "data" to dataDir.absolutePath, - "files" to filesDir.absolutePath, - "cache" to cacheDir.absolutePath, - ) - } - - BridgeModule.registerMethod("Shiggy.fs.delete") { - val (path) = it - File(path as String).run { - if (this.isDirectory) this.deleteRecursively() - else this.delete() - } - } - - BridgeModule.registerMethod("Shiggy.fs.exists") { - val (path) = it - File(path as String).exists() - } - - BridgeModule.registerMethod("Shiggy.fs.read") { it -> - val (path) = it - val file = File(path as String).apply { openFileGuarded() } - - file.bufferedReader().use { it.readText() } - } - - BridgeModule.registerMethod("Shiggy.fs.write") { - val (path, contents) = it - val file = File(path as String).apply { openFileGuarded() } - - file.writeText(contents as String) - } - } - - override fun onActivity(activity: Activity) = with(activity) { - BridgeModule.registerMethod("Shiggy.alertError") { - val (error, version) = it - val app = getAppInfo() - val errorString = "$error" - - val clipboard = applicationContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText("Stack Trace", errorString) - - AlertDialog.Builder(this) - .setTitle("GoonCord Error") - .setMessage( - """ - GoonCord: $version - ${app.name}: ${app.version} (${app.versionCode}) - Device: ${Build.MANUFACTURER} ${Build.MODEL} - - - """.trimIndent() + errorString - ) - .setPositiveButton("OK") { dialog, _ -> - dialog.dismiss() - } - .setNeutralButton("Copy") { dialog, _ -> - clipboard.setPrimaryClip(clip) - Toast.makeText(applicationContext, "Copied stack trace", Toast.LENGTH_SHORT).show() - dialog.dismiss() - } - .show() - - null - } - - BridgeModule.registerMethod("Shiggy.showRecoveryAlert") { - Utils.showRecoveryAlert(this) - } - } - - private fun File.openFileGuarded() { - if (!this.exists()) throw Error("Path does not exist: $path") - if (!this.isFile) throw Error("Path is not a file: $path") - } -} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/bridge/BridgeModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/bridge/BridgeModule.kt deleted file mode 100644 index 17d6474..0000000 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/bridge/BridgeModule.kt +++ /dev/null @@ -1,157 +0,0 @@ -package GoonXposed.xposed.modules.bridge - -import de.robv.android.xposed.callbacks.XC_LoadPackage -import GoonXposed.xposed.BuildConfig -import GoonXposed.xposed.Constants -import GoonXposed.xposed.Module -import GoonXposed.xposed.Utils.Log -import java.lang.reflect.Method - -/** - * See for possible return types: - * https://github.com/facebook/react-native/blob/c23e84ae9/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/Arguments.kt#L19 - * - * You may return a [Unit] and the resulting value will be `null`. - */ -typealias BridgeMethodCallback = (args: BridgeMethodArgs) -> Any? - -typealias BridgeMethodArgs = ArrayList - -/** - * A module that exposes a bridge for calling methods from JavaScript. - * - * To call a method, pass an object with the following structure to a hooked method: - * ```js - * { - * Shiggy: { - * method: "method.name", - * args: [arg1, arg2, ...] - * } - * } - * ``` - * - * For methods that accept additional arguments, only the first object argument is used for the bridge call. - * - * The result will be an object with either a `result` or `error` key: - * ```js - * { - * result: ... // The return value of the method - * } - * ``` - * or - * ```js - * { - * error: "Error message" - * } - * ``` - */ -object BridgeModule : Module() { - private lateinit var readableMapGetString: Method - private lateinit var readableMapToHashMap: Method - private lateinit var argumentsMakeNative: Method - - private const val CALL_DATA_KEY = "Shiggy" - private const val METHOD_NAME_KEY = "method" - private const val METHOD_ARGS_KEY = "args" - - private val methods: MutableMap = mutableMapOf() - - /** - * Registers a bridge method that can be called from JavaScript. - * - * If the method is already registered, it will be overwritten. - * - * @param name The name of the method to register. - * @param callback The callback to invoke when the method is called. - */ - fun registerMethod(name: String, callback: BridgeMethodCallback) { - if (methods.containsKey(name)) Log.w("Bridge method already exists and will be overridden: $name") - methods[name] = callback - } - - override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { - val arguments = classLoader.loadClass("com.facebook.react.bridge.Arguments") - val readableMap = classLoader.loadClass("com.facebook.react.bridge.ReadableMap") - val promise = classLoader.loadClass("com.facebook.react.bridge.Promise") - - val promiseResolve = promise.method("resolve", Object::class.java) - argumentsMakeNative = arguments.method("makeNativeObject", Object::class.java) - readableMapGetString = readableMap.method("getString", String::class.java) - readableMapToHashMap = readableMap.method("toHashMap") - - classLoader.loadClass("com.horcrux.svg.RNSVGRenderableManager") - .hookMethod("getBBox", Double::class.javaObjectType, readableMap) { - before { - callBridgeMethod(readableMapToHashMap(args[1]!!))?.let { result = it.toNativeObject() } - } - } - - classLoader.loadClass("com.facebook.react.modules.blob.FileReaderModule") - .hookMethod("readAsDataURL", readableMap, promise) { - before { - val (readableMap, promise) = args - callBridgeMethod(readableMapToHashMap(readableMap!!))?.let { - promiseResolve.invoke(promise, it.toNativeObject()) - result = null - } - } - } - - // Clear methods every package load. Plugins should re-register on each load. - methods.clear() - registerDefaultMethods() - - return@with - } - - private fun registerDefaultMethods() { - methods["Shiggy.info"] = { - mapOf( - "name" to Constants.LOADER_NAME, "version" to BuildConfig.VERSION_CODE - ) - } - - methods["Shiggy.test"] = { - mapOf( - "string" to "string", - "number" to 7256, - "array" to listOf("testing", 527737, listOf(true)), - "object" to mapOf("nested" to true), - "boolean" to false, - "args" to it, - ) - } - } - - private fun Any?.toNativeObject(): Any? = argumentsMakeNative.invoke( - null, when (this) { - Unit -> null - else -> this - } - ) - - private fun readableMapToHashMap(map: Any): HashMap { - @Suppress("UNCHECKED_CAST") return readableMapToHashMap.invoke(map) as HashMap - } - - private fun callBridgeMethod(hashMap: HashMap): Map? = try { - val (method, args) = getBridgeCallData(hashMap) ?: return null - val ret = method(args).toNativeObject() - mapOf("result" to ret) - } catch (e: Throwable) { - mapOf("error" to e.stackTraceToString()) - } - - private fun getBridgeCallData(hashMap: HashMap): Pair? { - @Suppress("UNCHECKED_CAST") val data = hashMap[CALL_DATA_KEY] as HashMap? - data ?: return null - - @Suppress("UNCHECKED_CAST") val name = data[METHOD_NAME_KEY] as String - val method = methods[name] - method ?: throw Error("Method not registered: $name") - - @Suppress("UNCHECKED_CAST") val args = data[METHOD_ARGS_KEY] as BridgeMethodArgs - - return Pair(method, args) - } -} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/no_track/BlockCrashReportingModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/no_track/BlockCrashReportingModule.kt deleted file mode 100644 index c15d72b..0000000 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/no_track/BlockCrashReportingModule.kt +++ /dev/null @@ -1,50 +0,0 @@ -package GoonXposed.xposed.modules.no_track - -import android.content.Context -import de.robv.android.xposed.callbacks.XC_LoadPackage -import GoonXposed.xposed.Module -import GoonXposed.xposed.Utils.Log - -/** - * Hooks Discord's crash reporting to disable Sentry initialization. - * While Discord doesn't have Sentry auto-initialization on by default, we still hook the content provider just to be safe. - */ -object BlockCrashReportingModule : Module() { - override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { - val crashReportingClass = classLoader.safeLoadClass("com.discord.crash_reporting.CrashReporting") - crashReportingClass?.apply { - - // This only exists on 30720x and above - runCatching { - hookMethod("isDisabled") { - before { - Log.i("Forced CrashReporting.isDisabled() to true") - result = true - } - } - }.onFailure { - // In older versions, this hook works fine. - // Hooking this on 30720x will result in a crash after a few seconds, - // since Discord asserts initialization when setting a Sentry tag. - hookMethod( - "init", Context::class.java, String::class.java - ) { - before { - Log.i("Blocked CrashReporting initialization") - result = null - } - } - } - } - - val sentryInitProviderClass = classLoader.safeLoadClass("io.sentry.android.core.SentryInitProvider") - sentryInitProviderClass?.hookMethod("onCreate") { - before { - Log.i("Blocked SentryInitProvider initialization") - result = true - } - } - - return@with - } -} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/no_track/BlockDeepLinksTrackingModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/no_track/BlockDeepLinksTrackingModule.kt deleted file mode 100644 index e47548d..0000000 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/no_track/BlockDeepLinksTrackingModule.kt +++ /dev/null @@ -1,25 +0,0 @@ -package GoonXposed.xposed.modules.no_track - -import android.content.Context -import de.robv.android.xposed.callbacks.XC_LoadPackage -import GoonXposed.xposed.Module -import GoonXposed.xposed.Utils.Log - -/** - * Hooks Discord's deep links tracking to disable AppsFlyer initialization. - */ -object BlockDeepLinksTrackingModule : Module() { - override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { - val deepLinksClass = classLoader.safeLoadClass("com.discord.deep_link.DeepLinks") - deepLinksClass?.hookMethod( - "init", Context::class.java - ) { - before { - Log.i("Blocked DeepLinks tracking initialization") - result = null - } - } - - return@with - } -}