diff --git a/app/app/build.gradle.kts b/app/app/build.gradle.kts
deleted file mode 100644
index 2b9856c..0000000
--- a/app/app/build.gradle.kts
+++ /dev/null
@@ -1,51 +0,0 @@
-plugins {
- id("com.android.application")
- id("org.jetbrains.kotlin.android")
- kotlin("plugin.serialization") version "2.1.10"
-}
-
-android {
- namespace = "GoonXposed.xposed"
- compileSdk = 36
-
- defaultConfig {
- applicationId = "GoonXposed.xposed"
- minSdk = 24
- targetSdk = 36
- versionCode = 1220
- versionName = "1.2.2"
- }
-
- buildTypes {
- debug {
- isDebuggable = true
- isMinifyEnabled = false
- applicationIdSuffix = ".debug"
- }
- release {
- isDebuggable = false
- isMinifyEnabled = false
- }
- }
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
- }
- kotlinOptions {
- jvmTarget = "1.8"
- }
- buildFeatures {
- buildConfig = true
- }
-}
-
-dependencies {
- compileOnly("de.robv.android.xposed:api:82")
-
- implementation("androidx.core:core-ktx:1.17.0")
- implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2")
- implementation("io.ktor:ktor-client-core:3.3.1")
- implementation("io.ktor:ktor-client-cio:3.3.1")
- implementation("com.google.android.material:material:1.12.0")
-}
diff --git a/app/app/src/main/AndroidManifest.xml b/app/app/src/main/AndroidManifest.xml
deleted file mode 100644
index 40f8598..0000000
--- a/app/app/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/app/app/src/main/assets/xposed_init b/app/app/src/main/assets/xposed_init
deleted file mode 100644
index d1f1c51..0000000
--- a/app/app/src/main/assets/xposed_init
+++ /dev/null
@@ -1 +0,0 @@
-GoonXposed.xposed.Main
diff --git a/app/app/src/main/kotlin/GoonXposed/xposed/xposed/Constants.kt b/app/app/src/main/kotlin/GoonXposed/xposed/xposed/Constants.kt
deleted file mode 100644
index 1c91b08..0000000
--- a/app/app/src/main/kotlin/GoonXposed/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/app/src/main/kotlin/GoonXposed/xposed/xposed/Main.kt b/app/app/src/main/kotlin/GoonXposed/xposed/xposed/Main.kt
deleted file mode 100644
index f3b328e..0000000
--- a/app/app/src/main/kotlin/GoonXposed/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/app/src/main/kotlin/GoonXposed/xposed/xposed/Module.kt b/app/app/src/main/kotlin/GoonXposed/xposed/xposed/Module.kt
deleted file mode 100644
index 41f878f..0000000
--- a/app/app/src/main/kotlin/GoonXposed/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/app/src/main/kotlin/GoonXposed/xposed/xposed/Utils.kt b/app/app/src/main/kotlin/GoonXposed/xposed/xposed/Utils.kt
deleted file mode 100644
index f92abbf..0000000
--- a/app/app/src/main/kotlin/GoonXposed/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/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/CacheModule.kt b/app/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/CacheModule.kt
deleted file mode 100644
index debbbc8..0000000
--- a/app/app/src/main/kotlin/GoonXposed/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/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/FixResourcesModule.kt b/app/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/FixResourcesModule.kt
deleted file mode 100644
index c6f39fe..0000000
--- a/app/app/src/main/kotlin/GoonXposed/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/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/HookScriptLoaderModule.kt b/app/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/HookScriptLoaderModule.kt
deleted file mode 100644
index a1dbf30..0000000
--- a/app/app/src/main/kotlin/GoonXposed/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/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/LogBoxModule.kt b/app/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/LogBoxModule.kt
deleted file mode 100644
index 44d56cb..0000000
--- a/app/app/src/main/kotlin/GoonXposed/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/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/PayloadGlobalModule.kt b/app/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/PayloadGlobalModule.kt
deleted file mode 100644
index 03d0601..0000000
--- a/app/app/src/main/kotlin/GoonXposed/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/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/PluginsModule.kt b/app/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/PluginsModule.kt
deleted file mode 100644
index 8aae94b..0000000
--- a/app/app/src/main/kotlin/GoonXposed/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/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/UpdaterModule.kt b/app/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/UpdaterModule.kt
deleted file mode 100644
index 2ee6296..0000000
--- a/app/app/src/main/kotlin/GoonXposed/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/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/appearance/FontsModule.kt b/app/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/appearance/FontsModule.kt
deleted file mode 100644
index 2a121e7..0000000
--- a/app/app/src/main/kotlin/GoonXposed/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/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/appearance/SysColorsModule.kt b/app/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/appearance/SysColorsModule.kt
deleted file mode 100644
index 3807b09..0000000
--- a/app/app/src/main/kotlin/GoonXposed/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/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/appearance/ThemesModule.kt b/app/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/appearance/ThemesModule.kt
deleted file mode 100644
index 2e08714..0000000
--- a/app/app/src/main/kotlin/GoonXposed/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/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/bridge/AdditionalBridgeMethodsModule.kt b/app/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/bridge/AdditionalBridgeMethodsModule.kt
deleted file mode 100644
index 178e0d9..0000000
--- a/app/app/src/main/kotlin/GoonXposed/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/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/bridge/BridgeModule.kt b/app/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/bridge/BridgeModule.kt
deleted file mode 100644
index 17d6474..0000000
--- a/app/app/src/main/kotlin/GoonXposed/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/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/no_track/BlockCrashReportingModule.kt b/app/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/no_track/BlockCrashReportingModule.kt
deleted file mode 100644
index c15d72b..0000000
--- a/app/app/src/main/kotlin/GoonXposed/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/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/no_track/BlockDeepLinksTrackingModule.kt b/app/app/src/main/kotlin/GoonXposed/xposed/xposed/modules/no_track/BlockDeepLinksTrackingModule.kt
deleted file mode 100644
index e47548d..0000000
--- a/app/app/src/main/kotlin/GoonXposed/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
- }
-}
diff --git a/app/app/src/main/kotlin/io/github/revenge/xposed/Utils.kt b/app/app/src/main/kotlin/io/github/revenge/xposed/Utils.kt
deleted file mode 100644
index 79a1322..0000000
--- a/app/app/src/main/kotlin/io/github/revenge/xposed/Utils.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package GoonXposed.xposed
-
-import android.app.AndroidAppHelper
-import android.content.Intent
-import kotlin.system.exitProcess
-import kotlinx.serialization.json.Json
-
-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)
- }
- }
-
- 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/app/src/main/kotlin/io/github/revenge/xposed/modules/FixResourcesModule.kt b/app/app/src/main/kotlin/io/github/revenge/xposed/modules/FixResourcesModule.kt
deleted file mode 100644
index c6f39fe..0000000
--- a/app/app/src/main/kotlin/io/github/revenge/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/app/src/main/kotlin/io/github/revenge/xposed/modules/HookScriptLoaderModule.kt b/app/app/src/main/kotlin/io/github/revenge/xposed/modules/HookScriptLoaderModule.kt
deleted file mode 100644
index a1dbf30..0000000
--- a/app/app/src/main/kotlin/io/github/revenge/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/app/src/main/kotlin/io/github/revenge/xposed/modules/LogBoxModule.kt b/app/app/src/main/kotlin/io/github/revenge/xposed/modules/LogBoxModule.kt
deleted file mode 100644
index 41042fc..0000000
--- a/app/app/src/main/kotlin/io/github/revenge/xposed/modules/LogBoxModule.kt
+++ /dev/null
@@ -1,601 +0,0 @@
-package GoonXposed.xposed.modules
-
-import android.app.AlertDialog
-import android.content.Context
-import android.content.Intent
-import android.net.Uri
-import android.os.Build
-import android.widget.EditText
-import android.widget.Toast
-import de.robv.android.xposed.callbacks.XC_LoadPackage
-import GoonXposed.xposed.BuildConfig
-import GoonXposed.xposed.Module
-import GoonXposed.xposed.Utils
-import GoonXposed.xposed.Utils.Log
-import de.robv.android.xposed.XposedHelpers
-import kotlinx.coroutines.*
-import org.json.JSONArray
-import org.json.JSONObject
-import java.io.File
-import java.net.HttpURLConnection
-import java.net.URL
-import GoonXposed.xposed.Utils.Companion.reloadApp
-
-object LogBoxModule : Module() {
- lateinit var packageParam: XC_LoadPackage.LoadPackageParam
- private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
-
- override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
- this@LogBoxModule.packageParam = packageParam
-
- val dcdReactNativeHostClass = classLoader.loadClass("com.discord.bridge.DCDReactNativeHost")
- val getUseDeveloperSupportMethod = dcdReactNativeHostClass.methods.first { it.name == "getUseDeveloperSupport" }
-
- getUseDeveloperSupportMethod.hook {
- before {
- result = true
- }
- }
-
- listOf(
- "com.facebook.react.devsupport.BridgeDevSupportManager",
- "com.facebook.react.devsupport.BridgelessDevSupportManager"
- ).mapNotNull { packageParam.classLoader.safeLoadClass(it) }.forEach {
- hookDevSupportManager(it)
- }
-
- return@with
- }
-
- private fun hookDevSupportManager(clazz: Class<*>) {
- val showDevOptionsDialogMethod = clazz.methods.first { it.name == "showDevOptionsDialog" }
-
- showDevOptionsDialogMethod.hook {
- before {
- try {
- val context = getContextFromDevSupport(clazz, thisObject)
- if (context != null) {
- showRecoveryMenu(context)
- }
- result = null
- } catch (e: Exception) {
- Log.e("Failed to show recovery menu: ${e.message}")
- }
- }
- }
- }
-
- private fun getContextFromDevSupport(clazz: Class<*>, instance: Any?): Context? {
- return try {
- val mReactInstanceDevHelperField = XposedHelpers.findField(clazz, "mReactInstanceDevHelper")
- val helper = mReactInstanceDevHelperField.get(instance)
- val getCurrentActivityMethod = helper?.javaClass?.methods?.first { it.name == "getCurrentActivity" }
- getCurrentActivityMethod?.invoke(helper) as? Context
- } catch (e: Exception) {
- Log.e("Failed to get context: ${e.message}")
- null
- }
- }
-
- private fun reloadReactNative(context: Context) {
- try {
- val reactInstanceManagerClass =
- packageParam.classLoader.loadClass("com.facebook.react.ReactInstanceManager")
- val reactApplicationClass = packageParam.classLoader.loadClass("com.facebook.react.ReactApplication")
-
- val app = context.applicationContext
-
- if (reactApplicationClass.isInstance(app)) {
- val getReactNativeHostMethod = reactApplicationClass.methods.first { it.name == "getReactNativeHost" }
- val reactNativeHost = getReactNativeHostMethod.invoke(app)
-
- val getReactInstanceManagerMethod =
- reactNativeHost.javaClass.methods.first { it.name == "getReactInstanceManager" }
- val reactInstanceManager = getReactInstanceManagerMethod.invoke(reactNativeHost)
-
- if (reactInstanceManager != null) {
- val reloadMethod =
- reactInstanceManagerClass.methods.first { it.name == "recreateReactContextInBackground" }
- reloadMethod.invoke(reactInstanceManager)
- } else {
- showError(context, "Error", "Could not get React Instance Manager")
- }
- } else {
- showError(context, "Error", "Application is not a React Application")
- }
- } catch (e: Exception) {
- Log.e("Failed to Reload App: ${e.message}")
- showError(context, "Failed to reload", "Error: ${e.message}")
- }
- }
-
- private fun showRecoveryMenu(context: Context) {
- val options = arrayOf(
- if (isSafeModeEnabled(context)) "Disable Safe Mode" else "Enable Safe Mode",
- "Refetch Bundle",
- "Load Custom Bundle",
- "Reload App",
- "Clear Cache & Reset"
- )
-
- val adapter =
- object : android.widget.ArrayAdapter(context, android.R.layout.select_dialog_item, options) {
- override fun getView(
- position: Int,
- convertView: android.view.View?,
- parent: android.view.ViewGroup
- ): android.view.View {
- val view = super.getView(position, convertView, parent)
- (view as? android.widget.TextView)?.apply {
- setTextColor(0xFFFFFFFF.toInt())
- textSize = 14f
- setPadding(
- (16 * context.resources.displayMetrics.density).toInt(),
- (12 * context.resources.displayMetrics.density).toInt(),
- (16 * context.resources.displayMetrics.density).toInt(),
- (12 * context.resources.displayMetrics.density).toInt()
- )
- setTypeface(typeface, android.graphics.Typeface.NORMAL)
- }
- view.setBackgroundColor(0xFF2D2D2D.toInt())
- return view
- }
- }
-
- val dialog = AlertDialog.Builder(context)
- .setTitle("GoonXposed Recovery Menu")
- .setAdapter(adapter) { _, which ->
- handleMenuSelection(context, which)
- }
- .setNegativeButton("Close", null)
- .create()
-
- dialog.setOnShowListener {
- dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.apply {
- setTextColor(0xFFBB86FC.toInt())
- setBackgroundColor(0xFF1E1E1E.toInt())
- textSize = 12f
- setPadding(
- (20 * context.resources.displayMetrics.density).toInt(),
- (8 * context.resources.displayMetrics.density).toInt(),
- (20 * context.resources.displayMetrics.density).toInt(),
- (8 * context.resources.displayMetrics.density).toInt()
- )
- }
-
- dialog.window?.setBackgroundDrawable(android.graphics.drawable.ColorDrawable(0xFF1E1E1E.toInt()))
-
- val titleId = context.resources.getIdentifier("alertTitle", "id", "android")
- val titleView = dialog.findViewById(titleId)
- titleView?.apply {
- setTextColor(0xFFBB86FC.toInt())
- textSize = 16f
- setTypeface(typeface, android.graphics.Typeface.BOLD)
- }
-
- dialog.listView?.setBackgroundColor(0xFF1E1E1E.toInt())
- dialog.listView?.divider = android.graphics.drawable.ColorDrawable(0xFF444444.toInt())
- dialog.listView?.dividerHeight = 1
- }
- dialog.show()
- }
-
- private fun handleMenuSelection(context: Context, index: Int) {
- when (index) {
- 0 -> toggleSafeMode(context)
- 1 -> showConfirmAction(
- context, "Refetch Bundle",
- "This will download the latest bundle from the official server."
- ) { 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 dialog = AlertDialog.Builder(context)
- .setTitle(title)
- .setMessage(message)
- .setPositiveButton("Confirm") { _, _ -> action() }
- .setNegativeButton("Cancel", null)
- .create()
-
- dialog.setOnShowListener {
- dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.apply {
- setTextColor(0xFFFFFFFF.toInt())
- setBackgroundColor(0xFF1E1E1E.toInt())
- textSize = 12f
- }
- dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.apply {
- setTextColor(0xFFBB86FC.toInt())
- setBackgroundColor(0xFF1E1E1E.toInt())
- textSize = 12f
- setTypeface(typeface, android.graphics.Typeface.BOLD)
- }
-
- dialog.window?.setBackgroundDrawable(android.graphics.drawable.ColorDrawable(0xFF1E1E1E.toInt()))
-
- val titleId = context.resources.getIdentifier("alertTitle", "id", "android")
- val titleView = dialog.findViewById(titleId)
- titleView?.apply {
- setTextColor(0xFFBB86FC.toInt())
- textSize = 16f
- }
-
- val messageId = context.resources.getIdentifier("message", "id", "android")
- val messageView = dialog.findViewById(messageId)
- messageView?.apply {
- setTextColor(0xFFFFFFFF.toInt())
- textSize = 14f
- setPadding(
- (5 * context.resources.displayMetrics.density).toInt(),
- paddingTop,
- (5 * context.resources.displayMetrics.density).toInt(),
- paddingBottom
- )
- }
- }
- dialog.show()
- }
-
- private fun showCustomBundleDialog(context: Context) {
- val filesDir = File(context.filesDir, "pyoncord")
- val configFile = File(filesDir, "loader.json")
- var currentUrl: String? = null
-
- if (configFile.exists()) {
- try {
- val json = JSONObject(configFile.readText())
- val customLoadUrl = json.optJSONObject("customLoadUrl")
- if (customLoadUrl != null && customLoadUrl.optBoolean("enabled", false)) {
- currentUrl = customLoadUrl.optString("url", "")
- }
- } catch (_: Exception) {
- }
- }
-
- val urlInput = EditText(context).apply {
- hint = "http://localhost:4040/shiggycord.js"
- setTextColor(0xFFFFFFFF.toInt())
- setBackgroundColor(0xFF2D2D2D.toInt())
- setHintTextColor(0xFF888888.toInt())
- inputType = android.text.InputType.TYPE_TEXT_VARIATION_URI
- setText(currentUrl ?: "")
- gravity = android.view.Gravity.START
- textAlignment = android.view.View.TEXT_ALIGNMENT_VIEW_START
- textSize = 14f
- setPadding(
- (12 * context.resources.displayMetrics.density).toInt(),
- (10 * context.resources.displayMetrics.density).toInt(),
- (12 * context.resources.displayMetrics.density).toInt(),
- (10 * context.resources.displayMetrics.density).toInt()
- )
- }
-
- val container = android.widget.FrameLayout(context)
- val params = android.widget.FrameLayout.LayoutParams(
- (context.resources.displayMetrics.widthPixels * 0.85).toInt(),
- android.widget.FrameLayout.LayoutParams.WRAP_CONTENT
- )
- params.leftMargin = (15 * context.resources.displayMetrics.density).toInt()
- params.rightMargin = (15 * context.resources.displayMetrics.density).toInt()
- urlInput.layoutParams = params
- container.addView(urlInput)
-
- val dialog = AlertDialog.Builder(context)
- .setTitle("Custom Bundle URL")
- .setView(container)
- .setPositiveButton("Save", null)
- .setNegativeButton("Cancel", null)
- .create()
-
- dialog.setOnShowListener {
- dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.apply {
- setTextColor(0xFFBB86FC.toInt())
- setBackgroundColor(0xFF1E1E1E.toInt())
- textSize = 12f
- }
- dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.apply {
- setTextColor(0xFFBB86FC.toInt())
- setBackgroundColor(0xFF1E1E1E.toInt())
- textSize = 12f
- setTypeface(typeface, android.graphics.Typeface.BOLD)
- }
-
- dialog.window?.setBackgroundDrawable(android.graphics.drawable.ColorDrawable(0xFF1E1E1E.toInt()))
-
- val titleId = context.resources.getIdentifier("alertTitle", "id", "android")
- val titleView = dialog.findViewById(titleId)
- titleView?.apply {
- setTextColor(0xFFBB86FC.toInt())
- textSize = 16f
- }
- }
-
- dialog.show()
-
- dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener {
- try {
- val url = urlInput.text?.toString()?.trim() ?: ""
- if (url.isNotEmpty()) {
- setCustomBundleURL(context, url)
- dialog.dismiss()
- } else {
- disableCustomBundle(context)
- dialog.dismiss()
- }
- } catch (e: Exception) {
- Toast.makeText(context, "Failed to set custom bundle: ${e.message}", Toast.LENGTH_LONG).show()
- }
- }
- }
-
- private fun showError(context: Context, title: String, message: String?) {
- val dialog = AlertDialog.Builder(context)
- .setTitle(title)
- .setMessage(message ?: "An unknown error occurred")
- .setPositiveButton("Close", null)
- .create()
-
- dialog.setOnShowListener {
- dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.apply {
- setTextColor(0xFFBB86FC.toInt())
- setBackgroundColor(0xFF1E1E1E.toInt())
- textSize = 12f
- }
-
- dialog.window?.setBackgroundDrawable(android.graphics.drawable.ColorDrawable(0xFF1E1E1E.toInt()))
-
- val titleId = context.resources.getIdentifier("alertTitle", "id", "android")
- val titleView = dialog.findViewById(titleId)
- titleView?.setTextColor(0xFFBB86FC.toInt())
-
- val messageId = context.resources.getIdentifier("message", "id", "android")
- val messageView = dialog.findViewById(messageId)
- messageView?.setTextColor(0xFFFFFFFF.toInt())
- }
- dialog.show()
- }
-
- 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 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 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())
-
- Toast.makeText(context, "Safe Mode ${if (newState) "Enabled" else "Disabled"}", Toast.LENGTH_SHORT).show()
-
- reloadApp()
-
- } catch (e: Exception) {
- Log.e("Error toggling safe mode: ${e.message}")
- showError(context, "Failed to toggle safe mode", e.message)
- }
- }
-
- private fun refetchBundle(context: Context) {
- try {
- val pyoncordDir = getPyoncordDirectory(context)
- val bundleFile = File(pyoncordDir, "bundle.js")
- val backupFile = File(pyoncordDir, "bundle.js.backup")
-
- if (bundleFile.exists()) {
- backupFile.delete()
- bundleFile.renameTo(backupFile)
- Log.e("Bundle moved to backup")
- }
-
- reloadApp()
-
- } catch (e: Exception) {
- Log.e("Error refetching bundle: ${e.message}")
- showError(context, "Failed to refetch bundle", e.message)
- }
- }
-
- private fun resetBundle(context: Context) {
- try {
- val pyoncordDir = getPyoncordDirectory(context)
- val bundleFile = File(pyoncordDir, "bundle.js")
- val configFile = File(pyoncordDir, "loader_config.json")
-
- bundleFile.delete()
-
- if (configFile.exists()) {
- val config = JSONObject(configFile.readText())
- config.put("customLoadUrlEnabled", false)
- config.put("customLoadUrl", "http://localhost:4040/ShiggyCord.js")
- 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 validateAndLoadBundle(context: Context, urlString: String) {
- scope.launch(Dispatchers.IO) {
- try {
- val url = URL(urlString)
- if (url.protocol != "http" && url.protocol != "https") {
- withContext(Dispatchers.Main) {
- showError(context, "Invalid URL", "URL must start with http:// or https://")
- }
- return@launch
- }
-
- val connection = url.openConnection() as HttpURLConnection
- connection.requestMethod = "GET"
- connection.connectTimeout = 10000
- connection.readTimeout = 10000
-
- val responseCode = connection.responseCode
- if (responseCode != 200) {
- withContext(Dispatchers.Main) {
- showError(context, "Error", "Server returned error $responseCode")
- }
- return@launch
- }
-
- val contentType = connection.contentType
- if (contentType != null && !contentType.contains("javascript") && !contentType.contains("text")) {
- withContext(Dispatchers.Main) {
- showError(context, "Invalid Content", "URL must point to a JavaScript file")
- }
- return@launch
- }
-
- withContext(Dispatchers.Main) {
- setCustomBundleURL(context, urlString)
- }
- } catch (e: Exception) {
- withContext(Dispatchers.Main) {
- showError(context, "Connection Error", "Could not reach URL: ${e.message}")
- }
- }
- }
- }
-
- private fun setCustomBundleURL(context: Context, url: String) {
- try {
- val pyoncordDir = getPyoncordDirectory(context)
- val configFile = File(pyoncordDir, "loader.json")
-
- val config = if (configFile.exists()) {
- JSONObject(configFile.readText())
- } else {
- JSONObject().apply {
- put("loadReactDevTools", false)
- }
- }
-
- val customLoadUrl = config.optJSONObject("customLoadUrl") ?: JSONObject()
- customLoadUrl.put("enabled", true)
- customLoadUrl.put("url", url)
- config.put("customLoadUrl", customLoadUrl)
-
- configFile.writeText(config.toString())
-
- File(pyoncordDir, "bundle.js").delete()
-
- Toast.makeText(context, "Custom bundle set", Toast.LENGTH_SHORT).show()
-
- reloadApp()
- } catch (e: Exception) {
- Log.e("Error setting custom bundle URL: ${e.message}")
- showError(context, "Failed to save configuration", e.message)
- }
- }
-
- private fun getPyoncordDirectory(context: Context): File {
- val dir = File(context.filesDir, "pyoncord")
- if (!dir.exists()) {
- dir.mkdirs()
- }
- return dir
- }
-
- override fun onContext(context: Context) {
- }
-}
diff --git a/app/app/src/main/kotlin/io/github/revenge/xposed/modules/PayloadGlobalModule.kt b/app/app/src/main/kotlin/io/github/revenge/xposed/modules/PayloadGlobalModule.kt
deleted file mode 100644
index 03d0601..0000000
--- a/app/app/src/main/kotlin/io/github/revenge/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/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt b/app/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt
deleted file mode 100644
index d8cc257..0000000
--- a/app/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt
+++ /dev/null
@@ -1,152 +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/GoonManager/releases/latest/download/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
- }
-
- 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/app/src/main/res/values/strings.xml b/app/app/src/main/res/values/strings.xml
deleted file mode 100644
index bff2089..0000000
--- a/app/app/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
- GoonXposed
-