Compare commits

...
Sign in to create a new pull request.

34 commits

Author SHA1 Message Date
Rosie Val
2864920cf2 fix: remove broken perf patches that hang kv storage, fix resource package mappings, and fix fonts hook 2026-09-25 17:43:43 -04:00
Rosie Val
3e47076257 feat(compat): add GmsCompatModule for microG and de-Googled device support 2026-09-25 17:18:04 -04:00
Rosie Val
d91fdb13f9 fix(module): add missing Log import in Module.kt 2026-09-25 17:10:41 -04:00
Rosie Val
47075c116e fix: resolve StackOverflowError in Module hookMethod, fix resource identifier mapping, and auto-publish release APKs in CI 2026-09-25 17:04:50 -04:00
Rosie Val
96e2755509 feat(scope): expand xposed_scope to dev.shiggy.cord and Discord variants 2026-09-25 16:41:48 -04:00
Rosie Val
38f45a7e0b fix(hook): prevent startup deadlock, fix target package/activity filters, restore bundle asset and shake detector 2026-09-25 16:34:09 -04:00
Rosie Val
43f024969d fix(ci): fix compilation errors from package rename, restore missing imports and helpers, fix 404 bundle URL 2026-09-25 16:14:38 -04:00
OTKUSteyler
109f7d5775
Update SysColorsModule.kt 2026-09-25 19:49:01 +02:00
OTKUSteyler
3146978ff2
Update PayloadGlobalModule.kt 2026-09-25 19:41:40 +02:00
OTKUSteyler
fe77d0624b
Update Main.kt 2026-09-25 19:32:03 +02:00
OTKUSteyler
748bf1820c
Update ThemesModule.kt 2026-09-25 19:23:07 +02:00
OTKUSteyler
488f59c84a
Update FontsModule.kt 2026-09-25 19:22:28 +02:00
OTKUSteyler
afb552f3ce
Update build.gradle.kts 2026-09-25 19:21:17 +02:00
OTKUSteyler
7811c97fd7
Add files via upload 2026-09-25 19:20:19 +02:00
OTKUSteyler
dff5dd3f95
Add files via upload 2026-09-25 19:19:20 +02:00
OTKUSteyler
7cfc42ce8c
Add files via upload 2026-09-25 19:18:33 +02:00
OTKUSteyler
9e2fa39ed2
Update CacheModule.kt 2026-09-25 19:18:00 +02:00
OTKUSteyler
122a19bfe0
Update FixResourcesModule.kt 2026-09-25 19:17:25 +02:00
OTKUSteyler
404c0c63ff
Update HookScriptLoaderModule.kt 2026-09-25 19:16:59 +02:00
OTKUSteyler
aab7f6ad27
Update PerfPatchesModule.kt 2026-09-25 19:16:02 +02:00
OTKUSteyler
1ac17ff0dd
Update PluginsModule.kt 2026-09-25 19:15:26 +02:00
OTKUSteyler
9afa0c264a
Update UpdaterModule.kt 2026-09-25 19:14:49 +02:00
OTKUSteyler
852848f0dd
Update Constants.kt 2026-09-25 19:13:14 +02:00
OTKUSteyler
d83c3c8ea5
Update Module.kt 2026-09-25 19:12:41 +02:00
OTKUSteyler
fc28e79bb4
Update Utils.kt 2026-09-25 19:12:20 +02:00
OTKUSteyler
eece858568
Update AndroidManifest.xml 2026-09-25 19:11:13 +02:00
OTKUSteyler
a045a5edad
Update strings.xml 2026-09-25 19:10:31 +02:00
OTKUSteyler
7cc04647e2
Update xposed_init 2026-09-25 19:10:12 +02:00
OTKUSteyler
fb8a4ac305
Update README.md 2026-09-25 18:36:42 +02:00
OTKUSteyler
3e3550f5d3
Update README.md 2026-09-25 18:35:21 +02:00
OTKUSteyler
f1fb057aa0
Update README.md 2026-09-25 18:19:48 +02:00
kmmiio99o
b84bd3c346
chore: bump to 1.3.6 2026-09-25 16:58:28 +02:00
kmmiio99o
0b39a29e8e
fix: prevent module load failures from hiding fonts and themes pages 2026-09-25 16:58:08 +02:00
kmmiio99o
7d15a63b4c
feat(build): disable manifying 2026-09-25 16:42:11 +02:00
41 changed files with 900 additions and 742 deletions

View file

@ -1,6 +1,13 @@
name: Build APK
on: [push]
on:
push:
branches: [ "master" ]
tags: [ "v*", "V*" ]
workflow_dispatch:
permissions:
contents: write
jobs:
build:
@ -65,3 +72,23 @@ jobs:
with:
name: app-release
path: /tmp/ShiggyXposedbuild/apks/app-release.apk
- name: Publish APKs to GitHub Release
if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/master'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
TAG_NAME="${GITHUB_REF#refs/tags/}"
else
TAG_NAME=$(gh release view --json tagName -q .tagName 2>/dev/null || echo "")
fi
if [ -n "$TAG_NAME" ]; then
echo "Uploading APKs to release $TAG_NAME..."
gh release upload "$TAG_NAME" \
/tmp/ShiggyXposedbuild/apks/app-release.apk \
/tmp/ShiggyXposedbuild/apks/app-debug.apk \
--clobber
else
echo "No release tag found to upload assets to."
fi

View file

@ -1,6 +1,6 @@
# ShiggyXposed
# GoonXposed
ShiggyXposed is an Xposed module (Kotlin) that injects the ShiggyCord client modifications into the official Discord Android application. It provides an in-app recovery/dev menu (LogBox) that lets you manage bundle loading, themes, and other developer helpers.
GoonXposed is an Xposed module (Kotlin) that injects the GoonCord client modifications into the official Discord Android application. It provides an in-app recovery/dev menu (LogBox) that lets you manage bundle loading, themes, and other developer helpers.
This README explains prerequisites, installation, usage, troubleshooting, and credits. If you want help, open an issue on the repository or join the project's support server (links below).
@ -26,13 +26,13 @@ This README explains prerequisites, installation, usage, troubleshooting, and cr
## What is this
ShiggyXposed injects a patched ShiggyCord bundle into the official Discord application by using the Xposed framework. It's intended for users who run a rooted Android device and want to apply client-side modifications to Discord.
GoonXposed injects a patched GoonCord bundle into the official Discord application by using the Xposed framework. It's intended for users who run a rooted Android device and want to apply client-side modifications to Discord.
---
## Features
- Inject a custom ShiggyCord bundle into Discord.
- Inject a custom GoonCord bundle into Discord.
- Toggle bundle injection on/off (marker-based).
- Load a custom bundle URL and toggle it on/off.
- Recovery/LogBox action sheet for reload, safe-mode, refetch/reset, and options.
@ -44,7 +44,7 @@ ShiggyXposed injects a patched ShiggyCord bundle into the official Discord appli
## Prerequisites
Before installing and using ShiggyXposed:
Before installing and using GoonXposed:
- A rooted Android device. Supported root solutions you might use:
- Magisk, KernelSU, KernelSUNext, or Sukisu Ultra.
@ -57,10 +57,10 @@ Before installing and using ShiggyXposed:
## Installation
1. Download the latest APK from the repository Releases:
- https://github.com/kmmiio99o/ShiggyXposed/releases
- https://github.com/OTKUSteyler/GoonXposed/releases
2. Install the APK on your device (standard package install).
3. Open your LSPosed manager (or chosen Xposed manager).
4. Enable the ShiggyXposed module and ensure the module's scope includes the Discord app.
4. Enable the GoonXposed module and ensure the module's scope includes the Discord app.
5. Reboot your device (or restart the system process) for changes to take effect.
---
@ -124,7 +124,7 @@ Notes:
## Troubleshooting & FAQ
1. Module not working or not showing in Discord?
- Ensure ShiggyXposed is enabled in LSPosed and Discord is included in the module's scope.
- Ensure GoonXposed is enabled in LSPosed and Discord is included in the module's scope.
- Reboot after enabling the module.
- Confirm you have the compatible LSPosed fork installed (JingMatrix recommended).
@ -182,10 +182,12 @@ Please refer to the `LICENSE` file in the repository root for the project licens
## Contact & Links
- Releases: https://github.com/kmmiio99o/ShiggyXposed/releases
- Repository: https://github.com/kmmiio99o/ShiggyXposed
- Releases: https://github.com/OTKUSteyler/GoonXposed/releases
- Repository: https://github.com/OTKUSteyler/GoonXposed
- Support server: https://discord.gg/nQykFF9Ud6
---
Thank you for using ShiggyXposed.
Thank you for using GoonXposed.
![OTKUSteyler](https://count.getloli.com/@OTKUSteyler?name=OTKUSteyler&theme=miku&padding=7&offset=0&align=top&scale=1&pixelated=1&darkmode=1)

View file

@ -5,23 +5,26 @@ plugins {
}
android {
namespace = "ShiggyXposed.xposed"
namespace = "GoonXposed.xposed"
compileSdk = 36
defaultConfig {
applicationId = "ShiggyXposed.xposed"
applicationId = "GoonXposed.xposed"
minSdk = 24
targetSdk = 36
versionCode = 1350
versionName = "1.3.5"
versionCode = 1358
versionName = "1.3.8"
}
signingConfigs {
create("release") {
storeFile = file("../release.keystore")
storePassword = "shiggyxposed"
keyAlias = "shiggyxposed"
keyPassword = "shiggyxposed"
val keystoreFile = file("../release.keystore")
if (keystoreFile.exists()) {
storeFile = keystoreFile
storePassword = "goonxposed"
keyAlias = "goonxposed"
keyPassword = "goonxposed"
}
}
}
@ -33,10 +36,13 @@ android {
}
release {
isDebuggable = false
isMinifyEnabled = true
isMinifyEnabled = false
val keystoreFile = file("../release.keystore")
if (keystoreFile.exists()) {
signingConfig = signingConfigs.getByName("release")
}
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8

View file

@ -13,9 +13,12 @@
<meta-data android:name="xposedmodule" android:value="true" />
<meta-data
android:name="xposeddescription"
android:value="Xposed module to inject ShiggyCord into discord"
android:value="Xposed module to inject GoonCord into discord"
/>
<meta-data android:name="xposedminversion" android:value="82" />
<meta-data android:name="xposedscope" android:value="com.discord" />
<meta-data
android:name="xposedscope"
android:resource="@array/xposed_scope"
/>
</application>
</manifest>

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
ShiggyXposed.xposed.Main
GoonXposed.xposed.Main

View file

@ -1,4 +1,4 @@
package ShiggyXposed.xposed
package GoonXposed.xposed
class Constants {
companion object {
@ -10,10 +10,10 @@ class Constants {
const val MAIN_SCRIPT_FILE = "bundle.js"
const val LOG_TAG = "Shiggy"
const val LOG_TAG = "Goon"
const val LOADER_NAME = "ShiggyXposed"
const val LOADER_NAME = "GoonXposed"
const val USER_AGENT = "ShiggyXposed"
const val USER_AGENT = "GoonXposed"
}
}

View file

@ -1,4 +1,4 @@
package ShiggyXposed.xposed
package GoonXposed.xposed
import android.app.Activity
import android.content.Context
@ -7,16 +7,16 @@ 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 ShiggyXposed.xposed.Utils.Log
import ShiggyXposed.xposed.modules.*
import ShiggyXposed.xposed.modules.appearance.FontsModule
import ShiggyXposed.xposed.modules.appearance.SysColorsModule
import ShiggyXposed.xposed.modules.appearance.ThemesModule
import ShiggyXposed.xposed.modules.bridge.AdditionalBridgeMethodsModule
import ShiggyXposed.xposed.modules.bridge.BridgeModule
import ShiggyXposed.xposed.modules.no_track.BlockCrashReportingModule
import ShiggyXposed.xposed.modules.no_track.BlockDeepLinksTrackingModule
import ShiggyXposed.xposed.modules.LogBox.*
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 GoonXposed.xposed.modules.LogBox.*
import kotlinx.coroutines.CompletableDeferred
object HookStateHolder {
@ -50,7 +50,8 @@ class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
PerfPatchesModule,
FontsModule,
ThemesModule,
SysColorsModule
SysColorsModule,
GmsCompatModule
)
init {
@ -58,13 +59,41 @@ class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
}
override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) {
for (module in modules) module.onInit(startupParam)
dispatch("onInit") { module -> module.onInit(startupParam) }
}
private fun isTargetPackage(pkg: String): Boolean {
return pkg == Constants.TARGET_PACKAGE
|| pkg.startsWith("com.discord")
|| pkg == "dev.shiggy.cord"
|| pkg.contains("discord")
|| pkg.contains("gooncord")
}
override fun handleLoadPackage(param: XC_LoadPackage.LoadPackageParam) = with(param) {
if (!isTargetPackage(packageName)) return
if (processName.contains(":")) return
if (hooked) return
val reactActivity = classLoader.loadClass(Constants.TARGET_ACTIVITY)
val activityClassNames = listOf(
Constants.TARGET_ACTIVITY,
"$packageName.react_activities.ReactActivity",
"com.discord.react_activities.ReactActivity",
"com.facebook.react.ReactActivity",
"com.discord.main.MainActivity",
"com.discord.main.MainDefault"
)
var reactActivity: Class<*>? = null
for (className in activityClassNames) {
try {
reactActivity = classLoader.loadClass(className)
if (reactActivity != null) break
} catch (_: Throwable) {}
}
if (reactActivity == null) {
Log.w("Target activity not found by name, falling back to Activity base class")
}
ContextWrapper::class.java.hookMethod("attachBaseContext", Context::class.java) {
after {
@ -75,10 +104,18 @@ class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
}
}
reactActivity.hookMethod("onCreate", Bundle::class.java) {
val targetActivityClass = reactActivity ?: Activity::class.java
targetActivityClass.hookMethod("onCreate", Bundle::class.java) {
after {
val act = thisObject as Activity
Log.i("Received Activity")
if (reactActivity == null &&
!act.javaClass.name.contains("ReactActivity", ignoreCase = true) &&
!act.javaClass.name.contains("MainActivity", ignoreCase = true) &&
!act.javaClass.name.contains("MainDefault", ignoreCase = true)
) {
return@after
}
Log.i("Received Activity: ${act.javaClass.name}")
if (!HookStateHolder.gotContext) {
Log.w("Activity created before we got Context, process may have been recreated!")
@ -90,20 +127,53 @@ class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
}
}
targetActivityClass.hookMethod("onResume") {
after {
val act = thisObject as Activity
this@Main.onResume(act)
}
}
targetActivityClass.hookMethod("onPause") {
after {
val act = thisObject as Activity
this@Main.onPause(act)
}
}
this@Main.onLoad(param)
hooked = true
}
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {
for (module in modules) module.onLoad(packageParam)
dispatch("onLoad") { module -> module.onLoad(packageParam) }
}
override fun onContext(context: Context) {
for (module in modules) module.onContext(context)
dispatch("onContext") { module -> module.onContext(context) }
}
override fun onActivity(activity: Activity) {
for (module in modules) module.onActivity(activity)
dispatch("onActivity") { module -> module.onActivity(activity) }
}
override fun onResume(activity: Activity) {
dispatch("onResume") { module -> module.onResume(activity) }
}
override fun onPause(activity: Activity) {
dispatch("onPause") { module -> module.onPause(activity) }
}
private fun dispatch(stage: String, block: (Module) -> Unit) {
for (module in modules) {
val name = module.javaClass.simpleName.ifEmpty { module.javaClass.name }
try {
block(module)
} catch (e: Throwable) {
Log.e("Module $name failed during $stage", e)
}
}
}
}

View file

@ -1,12 +1,15 @@
package ShiggyXposed.xposed
package GoonXposed.xposed
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
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.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage
import GoonXposed.xposed.Utils.Log
import kotlinx.serialization.json.JsonObjectBuilder
import java.io.File
import java.lang.reflect.Method
@ -18,13 +21,18 @@ data class AppInfo(
val versionCode: Long,
)
abstract class Module {
/**
* Base class for every Xposed side module.
*
* The [Main] loader forwards the various [XC_LoadPackage] lifecycle callbacks to every
* registered [Module] instance. Modules may override any subset of the hooks below.
*/
open 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) {
}
open fun buildPayload(builder: JsonObjectBuilder) {}
/**
* Called during Zygote initialization.
@ -63,110 +71,28 @@ abstract class Module {
*/
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()
open fun onResume(activity: Activity) {}
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)
}
}
}
open fun onPause(activity: Activity) {}
}
/**
* 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.
* Runtime wrapper around an [XC_MethodHook.MethodHookParam] used as the receiver of every
* `before`/`after` hook block, exposing [thisObject], [args], [result] and [param] directly.
*/
protected class HookScope internal constructor(
val param: XC_MethodHook.MethodHookParam, private val proceed: (XC_MethodHook.MethodHookParam) -> Unit
class HookScope(
val param: XC_MethodHook.MethodHookParam,
private val proceed: ((XC_MethodHook.MethodHookParam) -> Unit)? = null
) {
/**
* 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)
fun proceed() {
proceed?.invoke(param)
}
val thisObject: Any? get() = param.thisObject
val thisObject: Any?
get() = param.thisObject
val args: Array<Any?> get() = param.args
val args: Array<Any?>
get() = param.args
var result: Any?
get() = param.result
@ -180,4 +106,105 @@ abstract class Module {
param.throwable = value
}
}
/**
* DSL builder for lazily constructed [XC_MethodHook] instances.
*
* ```kotlin
* val hook = MethodHookBuilder().run {
* before { result = null }
* after { Log.i("done") }
* build()
* }
* ```
*/
class MethodHookBuilder {
private val beforeCallbacks = mutableListOf<HookScope.() -> Unit>()
private val afterCallbacks = mutableListOf<HookScope.() -> Unit>()
fun before(callback: HookScope.() -> Unit) {
beforeCallbacks += callback
}
fun after(callback: HookScope.() -> Unit) {
afterCallbacks += callback
}
fun build(): XC_MethodHook = object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) {
val scope = HookScope(param = param, proceed = { p -> super.beforeHookedMethod(p) })
beforeCallbacks.forEach { scope.run(it) }
}
override fun afterHookedMethod(param: MethodHookParam) {
val scope = HookScope(param = param, proceed = { p -> super.afterHookedMethod(p) })
afterCallbacks.forEach { scope.run(it) }
}
}
companion object {
fun from(block: MethodHookBuilder.() -> Unit): XC_MethodHook = MethodHookBuilder().run {
block()
build()
}
}
}
fun ClassLoader.safeLoadClass(name: String): Class<*>? = try {
loadClass(name)
} catch (e: Throwable) {
null
}
fun Class<*>.method(name: String, vararg parameterTypes: Class<*>?): Method =
XposedHelpers.findMethodExact(this, name, *parameterTypes)
fun Method.hook(hook: XC_MethodHook): XC_MethodHook.Unhook = XposedBridge.hookMethod(this, hook)
fun Method.hook(block: MethodHookBuilder.() -> Unit): XC_MethodHook.Unhook {
val builder = MethodHookBuilder()
builder.block()
return XposedBridge.hookMethod(this, builder.build())
}
fun Class<*>.hookMethod(
name: String,
vararg parameterTypes: Class<*>?,
block: MethodHookBuilder.() -> Unit
): XC_MethodHook.Unhook? = runCatching {
val method = method(name, *parameterTypes)
method.hook(block)
}.onFailure {
Log.w("Failed to hook method $name on ${this.name}: ${it.message}")
}.getOrNull()
fun File.asDir(): File {
if (!exists() || !isDirectory) mkdirs()
return this
}
fun File.asFile(): File {
parentFile?.mkdirs()
return this
}
fun Context.getAppInfo(): AppInfo {
val label = applicationInfo.loadLabel(packageManager).toString()
val packageInfo = try {
packageManager.getPackageInfo(packageName, 0)
} catch (e: PackageManager.NameNotFoundException) {
null
}
val versionCode = if (packageInfo != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) packageInfo.longVersionCode
else @Suppress("DEPRECATION") packageInfo.versionCode.toLong()
} else 0L
return AppInfo(
name = label,
packageName = packageName,
version = packageInfo?.versionName ?: "",
versionCode = versionCode
)
}

View file

@ -1,10 +1,10 @@
package ShiggyXposed.xposed
package GoonXposed.xposed
import android.app.AlertDialog
import android.app.AndroidAppHelper
import android.content.Context
import android.content.Intent
import ShiggyXposed.xposed.modules.UpdaterModule
import GoonXposed.xposed.modules.UpdaterModule
import kotlinx.serialization.json.Json
import java.io.File
import kotlin.system.exitProcess

View file

@ -1,10 +1,13 @@
package ShiggyXposed.xposed.modules
package GoonXposed.xposed.modules
import android.content.Context
import android.util.AtomicFile
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils.Log
import ShiggyXposed.xposed.modules.bridge.BridgeModule
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.asDir
import GoonXposed.xposed.asFile
import GoonXposed.xposed.getAppInfo
import GoonXposed.xposed.modules.bridge.BridgeModule
import java.io.*
/**
@ -35,7 +38,7 @@ import java.io.*
*/
object CacheModule : Module() {
private const val CACHE_DIR = "Shiggy"
private const val CACHE_DIR = "Goon"
private const val MODULES_CACHE_PREFIX = "modules"
private const val ASSETS_CACHE_PREFIX = "assets"

View file

@ -1,9 +1,10 @@
package ShiggyXposed.xposed.modules
package GoonXposed.xposed.modules
import android.content.res.Resources
import de.robv.android.xposed.callbacks.XC_LoadPackage
import ShiggyXposed.xposed.Constants.Companion.TARGET_PACKAGE
import ShiggyXposed.xposed.Module
import GoonXposed.xposed.Constants.Companion.TARGET_PACKAGE
import GoonXposed.xposed.Module
import GoonXposed.xposed.hookMethod
/**
* Hooks [Resources.getIdentifier] to fix resource package name mismatch.
@ -15,7 +16,13 @@ object FixResourcesModule : Module() {
"getIdentifier", String::class.java, String::class.java, String::class.java
) {
before {
if (args[2] == packageName) args[2] = TARGET_PACKAGE
if (args[2] == packageName) {
args[2] = TARGET_PACKAGE
}
val name = args[0] as? String
if (name != null && name.startsWith("$packageName:")) {
args[0] = name.replaceFirst("$packageName:", "$TARGET_PACKAGE:")
}
}
}
}

View file

@ -0,0 +1,69 @@
package GoonXposed.xposed.modules
import android.content.Context
import de.robv.android.xposed.callbacks.XC_LoadPackage
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.hookMethod
import GoonXposed.xposed.safeLoadClass
/**
* Compatibility layer for environments without Google Play Services (microG, GrapheneOS, de-Googled ROMs).
*
* Mocks GoogleApiAvailability and GooglePlayServicesUtil availability checks so Discord does not
* hang, crash, or show "Google Play Services not available" prompts, and provides safe stubs for
* Google Advertising ID queries.
*/
object GmsCompatModule : Module() {
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
hookGoogleApiAvailability(classLoader)
hookAdvertisingId(classLoader)
}
private fun hookGoogleApiAvailability(classLoader: ClassLoader) {
listOf(
"com.google.android.gms.common.GoogleApiAvailability",
"com.google.android.gms.common.GoogleApiAvailabilityLight",
"com.google.android.gms.common.GooglePlayServicesUtil",
"com.google.android.gms.common.GooglePlayServicesUtilLight"
).forEach { className ->
val clazz = classLoader.safeLoadClass(className) ?: return@forEach
// ConnectionResult.SUCCESS = 0
clazz.hookMethod("isGooglePlayServicesAvailable", Context::class.java) {
before {
result = 0
}
}
clazz.hookMethod("isGooglePlayServicesAvailable", Context::class.java, Int::class.javaPrimitiveType) {
before {
result = 0
}
}
clazz.hookMethod("isUserResolvableError", Int::class.javaPrimitiveType) {
before {
result = false
}
}
}
}
private fun hookAdvertisingId(classLoader: ClassLoader) {
val adClientClass = classLoader.safeLoadClass("com.google.android.gms.ads.identifier.AdvertisingIdClient") ?: return
val adInfoClass = classLoader.safeLoadClass("com.google.android.gms.ads.identifier.AdvertisingIdClient\$Info")
if (adInfoClass != null) {
adClientClass.hookMethod("getAdvertisingIdInfo", Context::class.java) {
before {
try {
val constructor = adInfoClass.getDeclaredConstructor(String::class.java, Boolean::class.javaPrimitiveType)
constructor.isAccessible = true
result = constructor.newInstance("00000000-0000-0000-0000-000000000000", true)
Log.i("GmsCompat: returned mock AdvertisingIdInfo")
} catch (e: Throwable) {
Log.w("GmsCompat: failed to create mock AdvertisingIdInfo: ${e.message}")
}
}
}
}
}
}

View file

@ -1,18 +1,25 @@
package ShiggyXposed.xposed.modules
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 ShiggyXposed.xposed.Constants
import ShiggyXposed.xposed.HookStateHolder
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils.Log
import ShiggyXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
import GoonXposed.xposed.Constants
import GoonXposed.xposed.HookScope
import GoonXposed.xposed.HookStateHolder
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.asDir
import GoonXposed.xposed.asFile
import GoonXposed.xposed.hook
import GoonXposed.xposed.method
import GoonXposed.xposed.safeLoadClass
import GoonXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import java.io.File
import java.lang.reflect.Method
@ -52,49 +59,53 @@ object HookScriptLoaderModule : Module() {
).mapNotNull { classLoader.safeLoadClass(it) }.forEach { hook(it) }
}
private fun hook(instance: Class<*>) = runCatching {
val loadScriptFromAssets = instance.method(
private fun hook(instance: Class<*>) {
val loadScriptFromAssets = runCatching {
instance.method(
"loadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType
)
}.getOrNull()
val loadScriptFromFile = instance.method(
val loadScriptFromFile = runCatching {
instance.method(
"loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType
)
}.getOrNull()
loadScriptFromAssets.hook {
loadScriptFromAssets?.hook {
before {
Log.i("Received call to loadScriptFromAssets: ${args[1]} (sync: ${args[2]})")
runCustomScripts(loadScriptFromFile, loadScriptFromAssets)
}
}
loadScriptFromFile.hook {
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) {
private fun HookScope.runCustomScripts(loadScriptFromFile: Method?, loadScriptFromAssets: Method?) {
Log.i("Running custom scripts...")
runBlocking {
val ready = async { HookStateHolder.readyDeferred.join() }
val isCustomUrl = UpdaterModule.isCustomUrlEnabled
if (!mainScript.exists() || isCustomUrl) {
val reason = if (isCustomUrl) "Custom URL enabled" else "Main script does not exist"
Log.i("$reason, downloading before load...")
val download =
async { UpdaterModule.downloadScript(showUpdateDialog = false).join() }
awaitAll(ready, download)
try {
withTimeout(10000L) {
UpdaterModule.downloadScript(showUpdateDialog = false).join()
}
} catch (e: Throwable) {
Log.w("Bundle download did not complete in time", e)
}
} else {
Log.i("Main script exists, updating in background...")
UpdaterModule.downloadScript(showUpdateDialog = true)
ready.await()
}
}
@ -102,9 +113,11 @@ object HookScriptLoaderModule : Module() {
val runScriptFile = { file: File ->
Log.i("Loading script: ${file.absolutePath}")
if (loadScriptFromFile != null) {
XposedBridge.invokeOriginalMethod(
loadScriptFromFile, thisObject, arrayOf(file.absolutePath, file.absolutePath, loadSynchronously)
)
}
Unit
}
@ -123,15 +136,25 @@ object HookScriptLoaderModule : Module() {
if (mainScript.exists()) {
runScriptFile(mainScript)
} else {
Log.i("Main script does not exist, falling back")
Log.i("Main script does not exist, checking for fallback asset")
if (!::resources.isInitialized) resources = XModuleResources.createInstance(modulePath, null)
var hasFallback = false
try {
resources.assets.open("Shiggy.bundle").use { hasFallback = true }
} catch (_: Throwable) {}
if (hasFallback && loadScriptFromAssets != null) {
Log.i("Loading fallback assets://Shiggy.bundle")
XposedBridge.invokeOriginalMethod(
loadScriptFromAssets,
thisObject,
arrayOf(resources.assets, "assets://Shiggy.bundle", loadSynchronously)
)
} else {
Log.w("No fallback bundle asset found; letting host app load default JS")
}
}
}
} catch (e: Throwable) {

View file

@ -1,4 +1,4 @@
package ShiggyXposed.xposed.modules.LogBox
package GoonXposed.xposed.modules.LogBox
import android.app.AlertDialog
import android.content.Context
@ -62,7 +62,7 @@ object CustomBundleDialog {
container.addView(toggleContainer)
val urlInput = EditText(context).apply {
hint = "http://localhost:4040/shiggycord.js"
hint = "http://localhost:4040/gooncord.js"
setTextColor(colors.onSurface)
setHintTextColor(colors.onSurfaceVariant)
background = LogBoxUtils.createM3Background(context, colors.surfaceVariant, 12f)

View file

@ -1,4 +1,4 @@
package ShiggyXposed.xposed.modules.LogBox
package GoonXposed.xposed.modules.LogBox
import android.app.AlertDialog
import android.content.Context

View file

@ -1,12 +1,12 @@
package ShiggyXposed.xposed.modules.LogBox
package GoonXposed.xposed.modules.LogBox
import android.content.Context
import android.widget.Toast
import org.json.JSONObject
import java.io.File
import ShiggyXposed.xposed.Utils.Log
import ShiggyXposed.xposed.Utils.Companion.reloadApp
import ShiggyXposed.xposed.Constants
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.Utils.Companion.reloadApp
import GoonXposed.xposed.Constants
import kotlinx.coroutines.*
object LogBoxActions {

View file

@ -1,4 +1,4 @@
package ShiggyXposed.xposed.modules.LogBox
package GoonXposed.xposed.modules.LogBox
import android.content.Context
import android.content.res.ColorStateList

View file

@ -1,4 +1,4 @@
package ShiggyXposed.xposed.modules.LogBox
package GoonXposed.xposed.modules.LogBox
import android.graphics.Color

View file

@ -1,11 +1,11 @@
package ShiggyXposed.xposed.modules.LogBox
package GoonXposed.xposed.modules.LogBox
import android.app.AlertDialog
import android.content.Context
import android.graphics.Color
import android.view.ViewGroup
import android.widget.LinearLayout
import ShiggyXposed.xposed.Utils.Log
import GoonXposed.xposed.Utils.Log
object LogBoxDialogs {

View file

@ -1,4 +1,4 @@
package ShiggyXposed.xposed.modules.LogBox
package GoonXposed.xposed.modules.LogBox
import android.app.AlertDialog
import android.content.Context
@ -14,8 +14,8 @@ import android.content.res.ColorStateList
import android.view.animation.DecelerateInterpolator
import org.json.JSONObject
import java.io.File
import ShiggyXposed.xposed.Utils.Log
import ShiggyXposed.xposed.Utils
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.Utils
object LogBoxMenu {
@ -30,7 +30,7 @@ object LogBoxMenu {
val container = createMenuContainer(context, colors)
// Title
container.addView(createTitle(context, colors, "ShiggyXposed Recovery", center = true))
container.addView(createTitle(context, colors, "GoonXposed Recovery", center = true))
// Subtitle
container.addView(createSubtitle(context, colors, "Select an option to continue"))
@ -782,7 +782,7 @@ object LogBoxMenu {
) { LogBoxActions.refetchBundle(context) }
2 -> showCustomBundleDialog(context)
3 -> ShiggyXposed.xposed.Utils.Companion.reloadApp()
3 -> Utils.reloadApp()
4 -> showConfirmAction(
context, "Clear Cache & Reset",
"This will clear all cached bundles and reset to default settings."

View file

@ -1,36 +1,67 @@
package ShiggyXposed.xposed.modules.LogBox
package GoonXposed.xposed.modules.LogBox
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils.Log
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.Utils.Companion.reloadApp
import android.app.Activity
import android.content.Context
import de.robv.android.xposed.XC_MethodHook
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 kotlinx.coroutines.*
import java.lang.ref.WeakReference
object LogBoxModule : Module() {
lateinit var packageParam: XC_LoadPackage.LoadPackageParam
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
var contextForMenu: Context? = null
private var currentActivity: WeakReference<Activity>? = null
private val shakeDetector = ShakeDetector {
val act = currentActivity?.get() ?: return@ShakeDetector
act.runOnUiThread {
try {
Log.i("Shake detected! Opening Recovery Menu...")
LogBoxNavigation.showRecoveryMenu(act)
} catch (e: Throwable) {
Log.e("Failed to show recovery menu on shake: ${e.message}", e)
}
}
}
override fun onActivity(activity: Activity) {
currentActivity = WeakReference(activity)
contextForMenu = activity
}
override fun onResume(activity: Activity) {
currentActivity = WeakReference(activity)
contextForMenu = activity
try {
shakeDetector.start(activity)
Log.i("ShakeDetector started for activity: ${activity.javaClass.simpleName}")
} catch (e: Throwable) {
Log.e("Failed to start ShakeDetector: ${e.message}", e)
}
}
override fun onPause(activity: Activity) {
try {
shakeDetector.stop()
Log.i("ShakeDetector stopped")
} catch (e: Throwable) {
Log.e("Failed to stop ShakeDetector: ${e.message}", e)
}
}
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}")
}
// Do not force getUseDeveloperSupport = true, as it causes React Native to loop
// attempting to connect to a Metro bundler on localhost:8081 and hang the app loading screen.
// Shake detector and recovery menu work independently without it.
return@with
}
@ -95,7 +126,7 @@ object LogBoxModule : Module() {
XposedBridge.hookMethod(handleReloadJSMethod, object : XC_MethodReplacement() {
override fun replaceHookedMethod(param: MethodHookParam): Any? {
Log.e("handleReloadJS called - reloading app")
ShiggyXposed.xposed.Utils.Companion.reloadApp()
reloadApp()
return null
}
})
@ -120,7 +151,7 @@ object LogBoxModule : Module() {
Log.e("Failed to get context from DevSupport (non-fatal): ${e.message}")
}
val finalContext = activityContext ?: contextForMenu ?: context
val finalContext = currentActivity?.get() ?: activityContext ?: contextForMenu ?: context
Log.e("Using context: $finalContext (type: ${finalContext.javaClass.name})")
LogBoxNavigation.showRecoveryMenu(finalContext)

View file

@ -1,4 +1,4 @@
package ShiggyXposed.xposed.modules.LogBox
package GoonXposed.xposed.modules.LogBox
import android.content.Context

View file

@ -1,10 +1,10 @@
package ShiggyXposed.xposed.modules.LogBox
package GoonXposed.xposed.modules.LogBox
import android.content.Context
import android.graphics.Color
import org.json.JSONObject
import java.io.File
import ShiggyXposed.xposed.Utils.Log
import GoonXposed.xposed.Utils.Log
object LogBoxTheme {

View file

@ -1,4 +1,4 @@
package ShiggyXposed.xposed.modules.LogBox
package GoonXposed.xposed.modules.LogBox
import android.content.Context
import android.util.TypedValue
@ -9,8 +9,8 @@ import android.view.View
import android.widget.Toast
import org.json.JSONObject
import java.io.File
import ShiggyXposed.xposed.Constants
import ShiggyXposed.xposed.Utils.Log
import GoonXposed.xposed.Constants
import GoonXposed.xposed.Utils.Log
object LogBoxUtils {

View file

@ -1,4 +1,4 @@
package ShiggyXposed.xposed.modules.LogBox
package GoonXposed.xposed.modules.LogBox
import android.app.AlertDialog
import android.content.Context
@ -19,40 +19,29 @@ object OptionsMenu {
container.addView(LogBoxComponents.createM3Button(context, injectionText, colors) {
dialog.dismiss()
LogBoxActions.toggleBundleInjection(context)
GoonXposed.xposed.Utils.Companion.reloadApp()
})
container.addView(LogBoxComponents.createM3Button(context, "Refetch Bundle", colors) {
dialog.dismiss()
RecoveryMenu::class.java.getDeclaredMethod(
"showConfirmAction",
Context::class.java,
String::class.java,
String::class.java,
Runnable::class.java
).apply { isAccessible = true }.invoke(
null,
RecoveryMenu.showConfirmAction(
context,
"Refetch Bundle",
"This will download the latest bundle from Github.",
Runnable { LogBoxActions.refetchBundle(context) }
)
"This will download the latest bundle from Github."
) {
LogBoxActions.refetchBundle(context)
}
})
container.addView(LogBoxComponents.createM3Button(context, "Clear Cache & Reset", colors) {
dialog.dismiss()
RecoveryMenu::class.java.getDeclaredMethod(
"showConfirmAction",
Context::class.java,
String::class.java,
String::class.java,
Runnable::class.java
).apply { isAccessible = true }.invoke(
null,
RecoveryMenu.showConfirmAction(
context,
"Clear Cache & Reset",
"This will clear all cached bundles and reset to default settings.",
Runnable { LogBoxActions.clearCacheAndReset(context) }
)
"This will clear all cached bundles and reset to default settings."
) {
LogBoxActions.clearCacheAndReset(context)
}
})
container.addView(LogBoxComponents.createM3Button(context, "Back", colors) {

View file

@ -1,9 +1,9 @@
package ShiggyXposed.xposed.modules.LogBox
package GoonXposed.xposed.modules.LogBox
import android.app.AlertDialog
import android.content.Context
import android.widget.LinearLayout
import ShiggyXposed.xposed.Utils.Log
import GoonXposed.xposed.Utils.Log
object RecoveryMenu {
@ -15,7 +15,7 @@ object RecoveryMenu {
val container = LogBoxComponents.createMenuContainer(context, colors)
container.addView(LogBoxComponents.createTitle(context, colors, "ShiggyXposed Recovery", center = true))
container.addView(LogBoxComponents.createTitle(context, colors, "GoonXposed Recovery", center = true))
container.addView(LogBoxComponents.createSubtitle(context, colors, "Select an option to continue"))
val safeModeText = if (LogBoxUtils.isSafeModeEnabled(context)) "Disable Safe Mode" else "Enable Safe Mode"
@ -63,7 +63,7 @@ object RecoveryMenu {
}
2 -> CustomBundleDialog.showCustomBundleDialog(context)
3 -> ShiggyXposed.xposed.Utils.Companion.reloadApp()
3 -> GoonXposed.xposed.Utils.Companion.reloadApp()
4 -> showConfirmAction(
context,
"Clear Cache & Reset",
@ -76,7 +76,7 @@ object RecoveryMenu {
}
}
private fun showConfirmAction(context: Context, title: String, message: String, action: () -> Unit) {
fun showConfirmAction(context: Context, title: String, message: String, action: () -> Unit) {
val colors = LogBoxTheme.getM3Colors(context)
val container = LogBoxComponents.createMenuContainer(context, colors)

View file

@ -0,0 +1,56 @@
package GoonXposed.xposed.modules.LogBox
import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import kotlin.math.sqrt
class ShakeDetector(private val onShake: () -> Unit) : SensorEventListener {
private var sensorManager: SensorManager? = null
private var accelerometer: Sensor? = null
private var lastShakeTimestamp: Long = 0
fun start(context: Context): Boolean {
if (sensorManager != null) return true
sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager
accelerometer = sensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
return accelerometer?.let {
sensorManager?.registerListener(this, it, SensorManager.SENSOR_DELAY_UI)
} ?: false
}
fun stop() {
sensorManager?.unregisterListener(this)
sensorManager = null
accelerometer = null
}
override fun onSensorChanged(event: SensorEvent) {
if (event.sensor.type != Sensor.TYPE_ACCELEROMETER) return
val x = event.values[0] / SensorManager.GRAVITY_EARTH
val y = event.values[1] / SensorManager.GRAVITY_EARTH
val z = event.values[2] / SensorManager.GRAVITY_EARTH
// gForce will be close to 1 when there is no movement.
val gForce = sqrt((x * x + y * y + z * z).toDouble()).toFloat()
if (gForce > SHAKE_THRESHOLD_GRAVITY) {
val now = System.currentTimeMillis()
// Ignore shake events too close to each other (1 second debounce)
if (now - lastShakeTimestamp > SHAKE_SLOP_TIME_MS) {
lastShakeTimestamp = now
onShake()
}
}
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
companion object {
private const val SHAKE_THRESHOLD_GRAVITY = 2.7f
private const val SHAKE_SLOP_TIME_MS = 1000L
}
}

View file

@ -1,14 +1,19 @@
package ShiggyXposed.xposed.modules
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 ShiggyXposed.xposed.BuildConfig
import ShiggyXposed.xposed.Constants
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils.Companion.JSON
import ShiggyXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
import GoonXposed.xposed.BuildConfig
import GoonXposed.xposed.Constants
import GoonXposed.xposed.MethodHookBuilder
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Companion.JSON
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.hook
import GoonXposed.xposed.method
import GoonXposed.xposed.safeLoadClass
import GoonXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import java.io.File
@ -23,7 +28,11 @@ class PayloadGlobalModule(private val modules: List<Module>) : Module() {
buildJsonObject {
put("loaderName", Constants.LOADER_NAME)
put("loaderVersion", BuildConfig.VERSION_NAME)
@Suppress("DEPRECATION") for (module in modules) module.buildPayload(this)
@Suppress("DEPRECATION") for (module in modules) runCatching {
module.buildPayload(this)
}.onFailure {
Log.e("Module ${module.javaClass.simpleName} failed while building payload", it)
}
})
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {

View file

@ -1,11 +1,11 @@
package ShiggyXposed.xposed.modules
package GoonXposed.xposed.modules
import de.robv.android.xposed.callbacks.XC_LoadPackage
import kotlinx.serialization.json.JsonObjectBuilder
import kotlinx.serialization.json.put
import ShiggyXposed.xposed.Constants
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils.Log
import GoonXposed.xposed.Constants
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import java.io.File
/**
@ -25,293 +25,20 @@ object PerfPatchesModule : Module() {
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {
try {
// Build preloads path inside the app data files directory
val filesDir = File(packageParam.appInfo.dataDir, Constants.FILES_DIR).apply { mkdirs() }
val preloadsDir = File(filesDir, HookScriptLoaderModule.PRELOADS_DIR).apply { mkdirs() }
val filesDir = File(packageParam.appInfo.dataDir, Constants.FILES_DIR)
val preloadsDir = File(filesDir, HookScriptLoaderModule.PRELOADS_DIR)
val out = File(preloadsDir, FILE_NAME)
// Defensive JS runtime patch. Avoid template literals with ${} to prevent Kotlin interpolation.
val js = """
(function(){
try {
if (!globalThis.__SHIGGY_PERF_PATCHES__) {
globalThis.__SHIGGY_PERF_PATCHES__ = { installedAt: Date.now(), enabled: true };
}
function safe(fn) {
return function() {
try { return fn.apply(this, arguments); }
catch (e) { try { console && console.warn && console.warn('shiggy patch error', e); } catch(_){} }
};
}
function memoize(fn, maxEntries) {
if (typeof fn !== 'function') return fn;
var cache = new Map();
maxEntries = Number(maxEntries) || 128;
return function() {
var key;
try { key = JSON.stringify(Array.prototype.slice.call(arguments)); }
catch (e) { key = String(arguments.length) + ':' + String(arguments[0]); }
if (cache.has(key)) return cache.get(key);
var res = fn.apply(this, arguments);
cache.set(key, res);
if (cache.size > maxEntries) {
var it = cache.keys().next();
if (!it.done) cache.delete(it.value);
}
return res;
};
}
function createBatched(fn, intervalMs) {
if (typeof fn !== 'function') return fn;
var buf = [];
var timer = null;
intervalMs = Number(intervalMs) || 2000;
function flush() {
var batch = buf.splice(0, buf.length);
if (!batch.length) return;
try { fn.call(null, batch); }
catch (e) {
// Fallback: call per event asynchronously
try {
batch.forEach(function(ev){
setTimeout(function(){ try { fn(ev); } catch (_) {} }, 0);
});
} catch (_) {}
}
}
return function(event) {
try {
buf.push(event);
if (!timer) timer = setTimeout(function(){ timer = null; flush(); }, intervalMs);
} catch (e) {}
};
}
function patchIfExists(path, wrapper) {
try {
var parts = path.split('.');
var obj = globalThis;
for (var i = 0; i < parts.length - 1; i++) {
obj = obj && obj[parts[i]];
}
if (!obj) return false;
var key = parts[parts.length - 1];
if (typeof obj[key] !== 'function') return false;
obj[key] = wrapper(obj[key]);
return true;
} catch (e) { return false; }
}
// Try memoizing a few well-known functions
['computeHappeningNowState', 'computeNowState'].forEach(function(name){
try { patchIfExists(name, function(orig){ return memoize(orig, 128); }); } catch(_) {}
});
// Try caching RowGenerator.prototype.generate by instance signature
try {
var RG = globalThis && globalThis.RowGenerator;
if (RG && RG.prototype && typeof RG.prototype.generate === 'function') {
(function(){
var orig = RG.prototype.generate;
RG.prototype.generate = function() {
try {
var sig;
if (arguments && arguments.length) {
var a0 = arguments[0];
if (Array.isArray(a0)) {
sig = 'arr:' + a0.length + ':' + a0.slice(0,8).map(function(x){ return x && x.id ? x.id : ''; }).join(',');
} else if (a0 && a0.id) {
sig = 'id:' + a0.id;
} else {
try { sig = JSON.stringify(a0).slice(0,200); } catch(e){ sig = String(a0); }
}
} else sig = 'noargs';
if (this.__shiggy_sig === sig && typeof this.__shiggy_cache !== 'undefined') {
return this.__shiggy_cache;
}
var res = orig.apply(this, arguments);
this.__shiggy_sig = sig;
this.__shiggy_cache = res;
return res;
} catch (e) { return orig.apply(this, arguments); }
};
})();
}
} catch (e) {}
// Batch analytics-like methods (best-effort)
['app_ui_viewed', 'logEvent', 'Analytics.log', 'Analytics.track', 'analytics.trackEvent'].forEach(function(name){
try {
var parts = name.split('.');
var obj = globalThis;
for (var i = 0; i < parts.length - 1; i++) obj = obj && obj[parts[i]];
if (!obj) return;
var key = parts[parts.length - 1];
if (typeof obj[key] === 'function') obj[key] = createBatched(obj[key], 2000);
} catch (e) {}
});
// Make console methods async to avoid blocking hot-paths during startup
(function(){
try {
if (typeof console !== 'undefined') {
['log','info','warn','error','debug','trace'].forEach(function(m){
try {
if (typeof console[m] === 'function') {
var orig = console[m];
console[m] = function() {
var args = Array.prototype.slice.call(arguments);
setTimeout(function(){ try { orig.apply(console, args); } catch(_){} }, 0);
};
}
} catch(e){}
});
}
} catch(e){}
})();
// Deduplicate and slightly delay repeated kv calls to reduce IO contention on startup
(function(){
try {
var pending = new Map();
// Lightweight stats for dedupe wrapper to help telemetry/debugging in-case we need to tune delays.
try {
globalThis.__SHIGGY_PERF_DEDUPE_STATS__ = globalThis.__SHIGGY_PERF_DEDUPE_STATS__ || {
methods: {},
totalRequests: 0,
totalHits: 0,
totalMisses: 0,
pending: 0,
lastUpdated: Date.now()
};
} catch(e) {}
function _incStats(name, key) {
try {
var s = globalThis.__SHIGGY_PERF_DEDUPE_STATS__;
if (!s) return;
s.lastUpdated = Date.now();
var m = s.methods[name] || (s.methods[name] = { requests: 0, hits: 0, misses: 0, pending: 0 });
if (key === 'requests') { m.requests++; s.totalRequests++; }
else if (key === 'hits') { m.hits++; s.totalHits++; }
else if (key === 'misses') { m.misses++; s.totalMisses++; }
else if (key === 'pending_inc') { m.pending++; s.pending++; }
else if (key === 'pending_dec') { m.pending = Math.max(0, m.pending - 1); s.pending = Math.max(0, s.pending - 1); }
} catch(e) {}
}
function wrapAsyncDedupe(kvObj, methodName, delayMs){
try {
var orig = kvObj[methodName];
if (typeof orig !== 'function') return;
kvObj[methodName] = function() {
var argsArr = Array.prototype.slice.call(arguments);
var key;
try { key = JSON.stringify([methodName, argsArr]); } catch(e){ key = methodName + ':' + String(argsArr[0]); }
_incStats(methodName, 'requests');
if (pending.has(key)) { _incStats(methodName, 'hits'); return pending.get(key); }
_incStats(methodName, 'misses');
var self = this;
var p = new Promise(function(resolve, reject){
setTimeout(function(){
try {
var res = orig.apply(self, argsArr);
if (res && typeof res.then === 'function') res.then(resolve, reject);
else resolve(res);
} catch (e) { reject(e); }
}, Number(delayMs) || 20);
});
pending.set(key, p);
_incStats(methodName, 'pending_inc');
var cleanup = function(){ pending.delete(key); _incStats(methodName, 'pending_dec'); };
p.then(cleanup, cleanup);
return p;
};
} catch(e){}
}
if (globalThis.kv) {
if (globalThis.kv.get_many) wrapAsyncDedupe(globalThis.kv, 'get_many', 20);
if (globalThis.kv.get_kv_entries) wrapAsyncDedupe(globalThis.kv, 'get_kv_entries', 20);
}
} catch(e){}
})();
// Defer heavy filesystem/storage calls slightly to prioritize UI rendering
(function(){
try {
function makeDeferredIfAsync(obj, name, delayMs) {
try {
var orig = obj[name];
if (typeof orig !== 'function') return;
var isAsync = orig.constructor && orig.constructor.name === 'AsyncFunction';
var src = String(orig);
if (!isAsync && src.indexOf('return new Promise') === -1 && src.indexOf('.then(') === -1) {
// Likely synchronous - skip deferral to avoid changing semantics
return;
}
obj[name] = function() {
var args = Array.prototype.slice.call(arguments);
var self = this;
return new Promise(function(resolve, reject){
setTimeout(function(){
try {
var res = orig.apply(self, args);
if (res && typeof res.then === 'function') res.then(resolve, reject);
else resolve(res);
} catch (e) { reject(e); }
}, Number(delayMs) || 100);
});
};
} catch(e){}
}
if (globalThis.db && typeof globalThis.db.fs_info === 'function') {
makeDeferredIfAsync(globalThis.db, 'fs_info', 100);
}
if (globalThis.Storage && typeof globalThis.Storage.refresh === 'function') {
makeDeferredIfAsync(globalThis.Storage, 'refresh', 50);
}
} catch(e){}
})();
// Expose lightweight API
try {
if (!globalThis.__SHIGGY_PERF_API__) {
globalThis.__SHIGGY_PERF_API__ = {
installedAt: Date.now(),
enabled: true,
uninstall: function() { try { globalThis.__SHIGGY_PERF_API__.enabled = false; } catch(e) {} }
};
}
} catch (e) {}
} catch (e) {
try { console && console.warn && console.warn('PerfPatches: error', e); } catch(_) {}
}
})();
""".trimIndent()
// Write file if absent or changed
try {
if (!out.exists() || out.readText() != js) {
out.writeText(js)
Log.i("PerfPatchesModule: wrote patch to " + out.absolutePath)
} else {
Log.i("PerfPatchesModule: perf patch already up-to-date")
if (out.exists()) {
out.delete()
Log.i("PerfPatchesModule: deleted legacy perf_patches.js")
}
} catch (e: Throwable) {
Log.e("PerfPatchesModule: failed writing patch", e)
}
} catch (e: Throwable) {
Log.e("PerfPatchesModule:onLoad failed", e)
Log.e("PerfPatchesModule: cleanup failed", e)
}
}
@Deprecated("Overrides deprecated Module.buildPayload")
override fun buildPayload(builder: JsonObjectBuilder) {
builder.put("perfPatches", true)
builder.put("perfPatches", false)
}
}

View file

@ -1,10 +1,11 @@
package ShiggyXposed.xposed.modules
package GoonXposed.xposed.modules
import android.content.Context
import android.util.AtomicFile
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils.Log
import ShiggyXposed.xposed.modules.bridge.BridgeModule
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.asFile
import GoonXposed.xposed.modules.bridge.BridgeModule
import java.io.*
/**

View file

@ -1,18 +1,18 @@
package ShiggyXposed.xposed.modules
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 dev.rushii.libunbound.LibUnbound
import ShiggyXposed.xposed.Constants
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils
import ShiggyXposed.xposed.Utils.Companion.JSON
import ShiggyXposed.xposed.Utils.Log
import ShiggyXposed.xposed.modules.bridge.BridgeModule
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.*
@ -22,41 +22,49 @@ import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.encodeToString
import java.io.File
import java.lang.ref.WeakReference
@Serializable
data class CustomLoadUrl(val enabled: Boolean = false, val url: String = "")
data class CustomLoadUrl(
val enabled: Boolean = false, val url: String = ""
)
@Serializable
data class LoaderConfig(val customLoadUrl: CustomLoadUrl = CustomLoadUrl(), val disableInjection: Boolean = false)
@Serializable
data class EndpointInfo(val paths: ArrayList<String>, val hash: String? = null, val version: String)
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
val isCustomUrlEnabled: Boolean
get() = config.customLoadUrl.enabled
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var lastActivity: WeakReference<Activity>? = null
private lateinit var cacheDir: File
private lateinit var bundle: File
private lateinit var etag: File
// Path to the application's data directory so we can read LogBox settings
private lateinit var appDataDir: File
private const val TIMEOUT_STRICT = 15000L
private const val MIN_BYTECODE_SIZE = 512
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_BASE_URL = "https://github.com/kmmiio99o/ShiggyCord/releases/latest/download/"
private const val FALLBACK_BASE_URL = "https://bundle.shiggycord.dev/"
private const val DEFAULT_BUNDLE_NAME = "shiggycord.min.js"
val isCustomUrlEnabled: Boolean
get() = if (::config.isInitialized) config.customLoadUrl.enabled else false
private const val DEFAULT_BUNDLE_URL =
"https://github.com/OTKUSteyler/GoonCord/releases/latest/download/gooncord.js"
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
// store app data dir for later checks (LogBox settings live under files/logbox)
appDataDir = File(appInfo.dataDir)
cacheDir = File(appInfo.dataDir, Constants.CACHE_DIR).apply { mkdirs() }
val filesDir = File(appInfo.dataDir, Constants.FILES_DIR).apply { mkdirs() }
@ -64,100 +72,106 @@ object UpdaterModule : Module() {
etag = File(cacheDir, ETAG_FILE)
val configFile = File(filesDir, CONFIG_FILE)
config = runCatching {
if (configFile.exists()) JSON.decodeFromString<LoaderConfig>(configFile.readText()) else LoaderConfig()
}.getOrDefault(LoaderConfig())
BridgeModule.registerMethod("revenge.updater.clear") {
if (bundle.exists()) bundle.delete()
if (etag.exists()) etag.delete()
null
}
config = runCatching {
if (configFile.exists()) {
JSON.decodeFromString<LoaderConfig>(configFile.readText())
} else LoaderConfig()
}.getOrDefault(LoaderConfig())
}
fun downloadScript(activity: Activity? = null, showUpdateDialog: Boolean = true): Job = scope.launch {
try {
// Respect LogBox setting to disable bundle injections.
// If the file "files/logbox/LOGBOX_SETTINGS" contains the flag bundleInjectionDisabled=true,
// skip attempting to download/update the bundle.
try {
val settingsFile = File(appDataDir, "files/logbox/LOGBOX_SETTINGS")
if (settingsFile.exists()) {
val txt = settingsFile.readText()
// look for a simple true marker; this avoids introducing a new JSON dependency here
if (txt.contains("\"bundleInjectionDisabled\"") && txt.contains("true")) {
Log.i("UpdaterModule: bundle injection disabled by LogBox settings — skipping download")
return@launch
}
}
} catch (ignore: Exception) {
// If anything goes wrong reading the setting, proceed with normal behaviour.
}
HttpClient(CIO) {
expectSuccess = false
install(HttpTimeout) {
requestTimeoutMillis = TIMEOUT_STRICT
connectTimeoutMillis = 5000L
socketTimeoutMillis = TIMEOUT_STRICT
}
install(UserAgent) { agent = Constants.USER_AGENT }
install(HttpRedirect) { checkHttpMethod = false }
install(HttpRedirect) {}
}.use { client ->
val targetUrl = resolveTargetUrl(client)
Log.i("Fetching bundle: $targetUrl")
val url = config.customLoadUrl.takeIf { it.enabled }?.url ?: DEFAULT_BUNDLE_URL
Log.i("Fetching JS bundle from: $url")
val response: HttpResponse = client.get(targetUrl) {
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)
if (bytes.size < MIN_BYTECODE_SIZE) {
throw Exception("Payload too small (${bytes.size} bytes). Possible corrupt build.")
val newTag = response.headers[HttpHeaders.ETag]
if (!newTag.isNullOrEmpty()) etag.writeText(newTag) else etag.delete()
Log.i("Bundle updated (${bytes.size} bytes)")
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()
}
} else if (showUpdateDialog) {
val act = lastActivity?.get()
act?.runOnUiThread {
AlertDialog.Builder(act)
.setTitle("GoonCord Update Downloaded")
.setMessage("A reload is required for changes to take effect.")
.setPositiveButton("Reload") { dialog, _ ->
reloadApp()
dialog.dismiss()
}
.setNegativeButton("Later") { dialog, _ ->
dialog.dismiss()
}
.setCancelable(false)
.show()
}
AtomicFile(bundle).apply {
val stream = startWrite()
try {
stream.write(bytes)
finishWrite(stream)
} catch (e: Exception) {
failWrite(stream)
throw e
}
}
response.headers[HttpHeaders.ETag]?.let { etag.writeText(it) } ?: etag.delete()
Log.i("Bundle updated: ${bytes.size} bytes")
HttpStatusCode.NotModified -> {
Log.i("Server responded with 304, no changes")
}
HttpStatusCode.NotModified -> Log.i("Bundle is up to date (304)")
else -> throw ResponseException(response, "HTTP ${response.status}")
else -> {
throw ResponseException(response, "Received status: ${response.status}")
}
}
}
} catch (e: Throwable) {
Log.e("Updater Error", e)
}
}
private suspend fun resolveTargetUrl(client: HttpClient): String {
if (config.customLoadUrl.enabled && config.customLoadUrl.url.isNotEmpty()) {
return config.customLoadUrl.url
}
return tryResolveUrl(client, DEFAULT_BASE_URL)
?: tryResolveUrl(client, FALLBACK_BASE_URL)
?: DEFAULT_BASE_URL + DEFAULT_BUNDLE_NAME
}
private suspend fun tryResolveUrl(client: HttpClient, baseUrl: String): String? {
return try {
val infoResponse = client.get("${baseUrl}info.json")
if (infoResponse.status == HttpStatusCode.OK) {
val info = JSON.decodeFromString<EndpointInfo>(infoResponse.bodyAsText())
val hermesVersion = withTimeoutOrNull(2000L) {
runCatching { LibUnbound.getHermesRuntimeBytecodeVersion() }.getOrNull()
} ?: 96
val hbcName = "shiggycord.$hermesVersion.hbc"
when {
info.paths.contains(hbcName) -> baseUrl + hbcName
info.paths.contains("shiggycord.min.js") -> baseUrl + "shiggycord.min.js"
else -> baseUrl + DEFAULT_BUNDLE_NAME
}
} else null
} catch (e: Exception) {
null
Log.e("Failed to download script", e)
showErrorDialog(e)
}
}
@ -165,16 +179,16 @@ object UpdaterModule : Module() {
lastActivity = WeakReference(activity)
}
fun setDisableInjection(context: Context, disabled: Boolean) {
val filesDir = File(context.dataDir, Constants.FILES_DIR).apply { mkdirs() }
val configFile = File(filesDir, CONFIG_FILE)
val newCfg = config.copy(disableInjection = disabled)
configFile.writeText(JSON.encodeToString(newCfg))
config = newCfg
Toast.makeText(context, "Injection ${if (disabled) "disabled" else "enabled"}", Toast.LENGTH_SHORT).show()
fun showErrorDialog(e: Throwable) {
val activity = lastActivity?.get() ?: return
activity.runOnUiThread {
}
}
fun isInjectionDisabled(context: Context? = null): Boolean {
return if (::config.isInitialized) config.disableInjection else false
fun resetLoaderConfig(context: Context) {
val filesDir = File(context.dataDir, Constants.FILES_DIR)
val config = File(filesDir, CONFIG_FILE)
if (config.exists()) config.delete()
}
}

View file

@ -1,4 +1,4 @@
package ShiggyXposed.xposed.modules.appearance
package GoonXposed.xposed.modules.appearance
import android.content.res.AssetManager
import android.graphics.Typeface
@ -9,10 +9,12 @@ 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 ShiggyXposed.xposed.Constants
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils.Companion.JSON
import ShiggyXposed.xposed.Utils.Log
import GoonXposed.xposed.Constants
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Companion.JSON
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.asDir
import GoonXposed.xposed.asFile
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
@ -40,6 +42,11 @@ object FontsModule : Module() {
private val FILE_EXTENSIONS = arrayOf(".ttf", ".otf")
private const val FONTS_ASSET_PATH = "fonts/"
private val FONT_MANAGER_CLASSES = arrayOf(
"com.facebook.react.views.text.ReactFontManager\$Companion",
"com.facebook.react.common.assets.ReactFontManager\$Companion",
)
private lateinit var fontsDir: File
private lateinit var fontsDownloadsDir: File
private var fontsAbsPath: String? = null
@ -51,21 +58,33 @@ object FontsModule : Module() {
}
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
XposedHelpers.findAndHookMethod(
"com.facebook.react.common.assets.ReactFontManager\$Companion",
classLoader,
"createAssetTypeface",
String::class.java,
Int::class.java,
"android.content.res.AssetManager",
object : XC_MethodReplacement() {
val replacement = 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)
}
})
}
// React Native moved ReactFontManager between releases, so try every known package.
// Never let this throw: an uncaught error here aborts the whole module chain.
val hooked = FONT_MANAGER_CLASSES.any { className ->
runCatching {
XposedHelpers.findAndHookMethod(
className,
classLoader,
"createAssetTypeface",
String::class.java,
Int::class.javaPrimitiveType ?: java.lang.Integer.TYPE,
"android.content.res.AssetManager",
replacement
)
}.onFailure { Log.e("Failed to hook createAssetTypeface on $className", it) }
.isSuccess
}
if (!hooked) Log.e("Failed to hook createAssetTypeface on all known ReactFontManager classes")
val fontDefFile = File(appInfo.dataDir, "${Constants.FILES_DIR}/fonts.json").apply { asFile() }
if (!fontDefFile.exists()) return@with

View file

@ -1,11 +1,11 @@
package ShiggyXposed.xposed.modules.appearance
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 ShiggyXposed.xposed.Module
import GoonXposed.xposed.Module
import kotlinx.serialization.json.*
import java.lang.ref.WeakReference

View file

@ -1,12 +1,16 @@
package ShiggyXposed.xposed.modules.appearance
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 ShiggyXposed.xposed.Constants
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils.Companion.JSON
import GoonXposed.xposed.Constants
import GoonXposed.xposed.MethodHookBuilder
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Companion.JSON
import GoonXposed.xposed.asFile
import GoonXposed.xposed.hook
import GoonXposed.xposed.method
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObjectBuilder
@ -42,7 +46,7 @@ object ThemesModule : Module() {
private const val THEME_FILE = "current-theme.json"
@Deprecated("This method is deprecated in the parent class")
@ExperimentalSerializationApi
override fun buildPayload(builder: JsonObjectBuilder) {
builder.apply {
@ -81,10 +85,22 @@ object ThemesModule : Module() {
}
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")
try {
val themeManager = runCatching { param.classLoader.loadClass("com.discord.theme.utils.ColorUtilsKt") }.getOrNull()
val darkTheme = runCatching { param.classLoader.loadClass("com.discord.theme.DarkTheme") }.getOrNull()
?: runCatching { param.classLoader.loadClass("com.discord.theme.DarkerTheme") }.getOrNull()
val onyxTheme = runCatching { param.classLoader.loadClass("com.discord.theme.OnyxTheme") }.getOrNull()
val lightTheme = runCatching { param.classLoader.loadClass("com.discord.theme.LightTheme") }.getOrNull()
hookThemeInternal(themeManager, darkTheme, onyxTheme, lightTheme)
} catch (e: Throwable) {
GoonXposed.xposed.Utils.Log.e("Failed to hook theme: ${e.message}")
}
}
private fun hookThemeInternal(
themeManager: Class<*>?, darkTheme: Class<*>?, onyxTheme: Class<*>?, lightTheme: Class<*>?
) {
val theme = this.theme ?: return
// Apply rawColors
@ -97,51 +113,72 @@ object ThemesModule : Module() {
// TEXT_NORMAL -> getTextNormal
val methodName = "get${key.fromScreamingSnakeToCamelCase()}"
value.forEachIndexed { index, v ->
val colorInt = hexStringToColorInt(v)
when (index) {
0 -> hookThemeMethod(darkTheme, methodName, hexStringToColorInt(v))
1 -> hookThemeMethod(lightTheme, methodName, hexStringToColorInt(v))
0 -> {
if (darkTheme != null) hookThemeMethod(darkTheme, methodName, colorInt)
if (onyxTheme != null) hookThemeMethod(onyxTheme, methodName, colorInt)
}
1 -> {
if (lightTheme != null) hookThemeMethod(lightTheme, methodName, colorInt)
}
}
}
}
// If there's any rawColors value, hook the color getter
if (!theme.data.rawColors.isNullOrEmpty()) {
val getColorCompat = themeManager.getDeclaredMethod(
if (!theme.data.rawColors.isNullOrEmpty() && themeManager != null) {
try {
val getColorCompat = runCatching {
themeManager.getDeclaredMethod(
"getColorCompat",
Resources::class.java,
Int::class.javaPrimitiveType,
Resources.Theme::class.java,
)
}.getOrNull()
val getColorCompatLegacy = themeManager.getDeclaredMethod(
val getColorCompatLegacy = runCatching {
themeManager.getDeclaredMethod(
"getColorCompat", Context::class.java, Int::class.javaPrimitiveType
)
}.getOrNull()
val patch = MethodHookBuilder().run {
before {
try {
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]
} catch (_: Throwable) {
}
}
build()
}
getColorCompat.hook(patch)
getColorCompatLegacy.hook(patch)
getColorCompat?.hook(patch)
getColorCompatLegacy?.hook(patch)
} catch (e: Throwable) {
GoonXposed.xposed.Utils.Log.e("Failed to hook getColorCompat: ${e.message}")
}
}
}
// Parse HEX colour string to INT. Takes "#RRGGBBAA" or "#RRGGBB"
private fun hexStringToColorInt(hexString: String): Int {
return if (hexString.length == 9) {
return try {
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()
} catch (_: Throwable) {
0
}
}
private fun hookThemeMethod(themeClass: Class<*>, methodName: String, themeValue: Int) {

View file

@ -1,4 +1,4 @@
package ShiggyXposed.xposed.modules.bridge
package GoonXposed.xposed.modules.bridge
import android.app.Activity
import android.app.AlertDialog
@ -7,9 +7,9 @@ import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import android.widget.Toast
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils
import ShiggyXposed.xposed.Utils.Companion.reloadApp
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils
import GoonXposed.xposed.getAppInfo
import java.io.File
object AdditionalBridgeMethodsModule : Module() {
@ -48,11 +48,6 @@ object AdditionalBridgeMethodsModule : Module() {
file.writeText(contents as String)
}
BridgeModule.registerMethod("Shiggy.app.reload") {
reloadApp()
null
}
}
override fun onActivity(activity: Activity) = with(activity) {
@ -65,10 +60,10 @@ object AdditionalBridgeMethodsModule : Module() {
val clip = ClipData.newPlainText("Stack Trace", errorString)
AlertDialog.Builder(this)
.setTitle("ShiggyCord Error")
.setTitle("GoonCord Error")
.setMessage(
"""
ShiggyCord: $version
GoonCord: $version
${app.name}: ${app.version} (${app.versionCode})
Device: ${Build.MANUFACTURER} ${Build.MODEL}

View file

@ -1,10 +1,12 @@
package ShiggyXposed.xposed.modules.bridge
package GoonXposed.xposed.modules.bridge
import de.robv.android.xposed.callbacks.XC_LoadPackage
import ShiggyXposed.xposed.BuildConfig
import ShiggyXposed.xposed.Constants
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils.Log
import GoonXposed.xposed.BuildConfig
import GoonXposed.xposed.Constants
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.hookMethod
import GoonXposed.xposed.method
import java.lang.reflect.Method
/**

View file

@ -1,9 +1,11 @@
package ShiggyXposed.xposed.modules.no_track
package GoonXposed.xposed.modules.no_track
import android.content.Context
import de.robv.android.xposed.callbacks.XC_LoadPackage
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils.Log
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.hookMethod
import GoonXposed.xposed.safeLoadClass
/**
* Hooks Discord's crash reporting to disable Sentry initialization.

View file

@ -1,9 +1,11 @@
package ShiggyXposed.xposed.modules.no_track
package GoonXposed.xposed.modules.no_track
import android.content.Context
import de.robv.android.xposed.callbacks.XC_LoadPackage
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils.Log
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.hookMethod
import GoonXposed.xposed.safeLoadClass
/**
* Hooks Discord's deep links tracking to disable AppsFlyer initialization.

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="xposed_scope">
<item>com.discord</item>
<item>com.discord.canary</item>
<item>com.discord.ptb</item>
<item>dev.shiggy.cord</item>
</string-array>
</resources>

View file

@ -1,3 +1,3 @@
<resources>
<string name="app_name">ShiggyXposed</string>
<string name="app_name">GoonXposed</string>
</resources>