Compare commits
34 commits
master
...
fix/ci-bui
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2864920cf2 | ||
|
|
3e47076257 | ||
|
|
d91fdb13f9 | ||
|
|
47075c116e | ||
|
|
96e2755509 | ||
|
|
38f45a7e0b | ||
|
|
43f024969d | ||
|
|
109f7d5775 | ||
|
|
3146978ff2 | ||
|
|
fe77d0624b | ||
|
|
748bf1820c | ||
|
|
488f59c84a | ||
|
|
afb552f3ce | ||
|
|
7811c97fd7 | ||
|
|
dff5dd3f95 | ||
|
|
7cfc42ce8c | ||
|
|
9e2fa39ed2 | ||
|
|
122a19bfe0 | ||
|
|
404c0c63ff | ||
|
|
aab7f6ad27 | ||
|
|
1ac17ff0dd | ||
|
|
9afa0c264a | ||
|
|
852848f0dd | ||
|
|
d83c3c8ea5 | ||
|
|
fc28e79bb4 | ||
|
|
eece858568 | ||
|
|
a045a5edad | ||
|
|
7cc04647e2 | ||
|
|
fb8a4ac305 | ||
|
|
3e3550f5d3 | ||
|
|
f1fb057aa0 | ||
|
|
b84bd3c346 | ||
|
|
0b39a29e8e | ||
|
|
7d15a63b4c |
41 changed files with 900 additions and 742 deletions
29
.github/workflows/build.yml
vendored
29
.github/workflows/build.yml
vendored
|
|
@ -1,6 +1,13 @@
|
||||||
name: Build APK
|
name: Build APK
|
||||||
|
|
||||||
on: [push]
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "master" ]
|
||||||
|
tags: [ "v*", "V*" ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
@ -65,3 +72,23 @@ jobs:
|
||||||
with:
|
with:
|
||||||
name: app-release
|
name: app-release
|
||||||
path: /tmp/ShiggyXposedbuild/apks/app-release.apk
|
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
|
||||||
24
README.md
24
README.md
|
|
@ -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).
|
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
|
## 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
|
## Features
|
||||||
|
|
||||||
- Inject a custom ShiggyCord bundle into Discord.
|
- Inject a custom GoonCord bundle into Discord.
|
||||||
- Toggle bundle injection on/off (marker-based).
|
- Toggle bundle injection on/off (marker-based).
|
||||||
- Load a custom bundle URL and toggle it on/off.
|
- Load a custom bundle URL and toggle it on/off.
|
||||||
- Recovery/LogBox action sheet for reload, safe-mode, refetch/reset, and options.
|
- 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
|
## Prerequisites
|
||||||
|
|
||||||
Before installing and using ShiggyXposed:
|
Before installing and using GoonXposed:
|
||||||
|
|
||||||
- A rooted Android device. Supported root solutions you might use:
|
- A rooted Android device. Supported root solutions you might use:
|
||||||
- Magisk, KernelSU, KernelSUNext, or Sukisu Ultra.
|
- Magisk, KernelSU, KernelSUNext, or Sukisu Ultra.
|
||||||
|
|
@ -57,10 +57,10 @@ Before installing and using ShiggyXposed:
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Download the latest APK from the repository Releases:
|
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).
|
2. Install the APK on your device (standard package install).
|
||||||
3. Open your LSPosed manager (or chosen Xposed manager).
|
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.
|
5. Reboot your device (or restart the system process) for changes to take effect.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -124,7 +124,7 @@ Notes:
|
||||||
## Troubleshooting & FAQ
|
## Troubleshooting & FAQ
|
||||||
|
|
||||||
1. Module not working or not showing in Discord?
|
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.
|
- Reboot after enabling the module.
|
||||||
- Confirm you have the compatible LSPosed fork installed (JingMatrix recommended).
|
- 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
|
## Contact & Links
|
||||||
|
|
||||||
- Releases: https://github.com/kmmiio99o/ShiggyXposed/releases
|
- Releases: https://github.com/OTKUSteyler/GoonXposed/releases
|
||||||
- Repository: https://github.com/kmmiio99o/ShiggyXposed
|
- Repository: https://github.com/OTKUSteyler/GoonXposed
|
||||||
- Support server: https://discord.gg/nQykFF9Ud6
|
- Support server: https://discord.gg/nQykFF9Ud6
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Thank you for using ShiggyXposed.
|
Thank you for using GoonXposed.
|
||||||
|
|
||||||
|

|
||||||
|
|
|
||||||
|
|
@ -5,23 +5,26 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "ShiggyXposed.xposed"
|
namespace = "GoonXposed.xposed"
|
||||||
compileSdk = 36
|
compileSdk = 36
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "ShiggyXposed.xposed"
|
applicationId = "GoonXposed.xposed"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 1350
|
versionCode = 1358
|
||||||
versionName = "1.3.5"
|
versionName = "1.3.8"
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
create("release") {
|
create("release") {
|
||||||
storeFile = file("../release.keystore")
|
val keystoreFile = file("../release.keystore")
|
||||||
storePassword = "shiggyxposed"
|
if (keystoreFile.exists()) {
|
||||||
keyAlias = "shiggyxposed"
|
storeFile = keystoreFile
|
||||||
keyPassword = "shiggyxposed"
|
storePassword = "goonxposed"
|
||||||
|
keyAlias = "goonxposed"
|
||||||
|
keyPassword = "goonxposed"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,10 +36,13 @@ android {
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
isDebuggable = false
|
isDebuggable = false
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = false
|
||||||
|
val keystoreFile = file("../release.keystore")
|
||||||
|
if (keystoreFile.exists()) {
|
||||||
signingConfig = signingConfigs.getByName("release")
|
signingConfig = signingConfigs.getByName("release")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,12 @@
|
||||||
<meta-data android:name="xposedmodule" android:value="true" />
|
<meta-data android:name="xposedmodule" android:value="true" />
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="xposeddescription"
|
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="xposedminversion" android:value="82" />
|
||||||
<meta-data android:name="xposedscope" android:value="com.discord" />
|
<meta-data
|
||||||
|
android:name="xposedscope"
|
||||||
|
android:resource="@array/xposed_scope"
|
||||||
|
/>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
||||||
28
app/src/main/assets/Shiggy.bundle
Normal file
28
app/src/main/assets/Shiggy.bundle
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
||||||
ShiggyXposed.xposed.Main
|
GoonXposed.xposed.Main
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package ShiggyXposed.xposed
|
package GoonXposed.xposed
|
||||||
|
|
||||||
class Constants {
|
class Constants {
|
||||||
companion object {
|
companion object {
|
||||||
|
|
@ -10,10 +10,10 @@ class Constants {
|
||||||
const val MAIN_SCRIPT_FILE = "bundle.js"
|
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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package ShiggyXposed.xposed
|
package GoonXposed.xposed
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -7,16 +7,16 @@ import android.os.Bundle
|
||||||
import de.robv.android.xposed.IXposedHookLoadPackage
|
import de.robv.android.xposed.IXposedHookLoadPackage
|
||||||
import de.robv.android.xposed.IXposedHookZygoteInit
|
import de.robv.android.xposed.IXposedHookZygoteInit
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import ShiggyXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
import ShiggyXposed.xposed.modules.*
|
import GoonXposed.xposed.modules.*
|
||||||
import ShiggyXposed.xposed.modules.appearance.FontsModule
|
import GoonXposed.xposed.modules.appearance.FontsModule
|
||||||
import ShiggyXposed.xposed.modules.appearance.SysColorsModule
|
import GoonXposed.xposed.modules.appearance.SysColorsModule
|
||||||
import ShiggyXposed.xposed.modules.appearance.ThemesModule
|
import GoonXposed.xposed.modules.appearance.ThemesModule
|
||||||
import ShiggyXposed.xposed.modules.bridge.AdditionalBridgeMethodsModule
|
import GoonXposed.xposed.modules.bridge.AdditionalBridgeMethodsModule
|
||||||
import ShiggyXposed.xposed.modules.bridge.BridgeModule
|
import GoonXposed.xposed.modules.bridge.BridgeModule
|
||||||
import ShiggyXposed.xposed.modules.no_track.BlockCrashReportingModule
|
import GoonXposed.xposed.modules.no_track.BlockCrashReportingModule
|
||||||
import ShiggyXposed.xposed.modules.no_track.BlockDeepLinksTrackingModule
|
import GoonXposed.xposed.modules.no_track.BlockDeepLinksTrackingModule
|
||||||
import ShiggyXposed.xposed.modules.LogBox.*
|
import GoonXposed.xposed.modules.LogBox.*
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
|
||||||
object HookStateHolder {
|
object HookStateHolder {
|
||||||
|
|
@ -50,7 +50,8 @@ class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||||
PerfPatchesModule,
|
PerfPatchesModule,
|
||||||
FontsModule,
|
FontsModule,
|
||||||
ThemesModule,
|
ThemesModule,
|
||||||
SysColorsModule
|
SysColorsModule,
|
||||||
|
GmsCompatModule
|
||||||
)
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
@ -58,13 +59,41 @@ class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) {
|
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) {
|
override fun handleLoadPackage(param: XC_LoadPackage.LoadPackageParam) = with(param) {
|
||||||
|
if (!isTargetPackage(packageName)) return
|
||||||
|
if (processName.contains(":")) return
|
||||||
if (hooked) 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) {
|
ContextWrapper::class.java.hookMethod("attachBaseContext", Context::class.java) {
|
||||||
after {
|
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 {
|
after {
|
||||||
val act = thisObject as Activity
|
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) {
|
if (!HookStateHolder.gotContext) {
|
||||||
Log.w("Activity created before we got Context, process may have been recreated!")
|
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)
|
this@Main.onLoad(param)
|
||||||
|
|
||||||
hooked = true
|
hooked = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {
|
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) {
|
override fun onContext(context: Context) {
|
||||||
for (module in modules) module.onContext(context)
|
dispatch("onContext") { module -> module.onContext(context) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivity(activity: Activity) {
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
package ShiggyXposed.xposed
|
package GoonXposed.xposed
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import de.robv.android.xposed.IXposedHookZygoteInit
|
import de.robv.android.xposed.IXposedHookZygoteInit
|
||||||
import de.robv.android.xposed.XC_MethodHook
|
import de.robv.android.xposed.XC_MethodHook
|
||||||
import de.robv.android.xposed.XposedBridge
|
import de.robv.android.xposed.XposedBridge
|
||||||
|
import de.robv.android.xposed.XposedHelpers
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
|
import GoonXposed.xposed.Utils.Log
|
||||||
import kotlinx.serialization.json.JsonObjectBuilder
|
import kotlinx.serialization.json.JsonObjectBuilder
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
|
|
@ -18,13 +21,18 @@ data class AppInfo(
|
||||||
val versionCode: Long,
|
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.
|
* 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.")
|
@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.
|
* Called during Zygote initialization.
|
||||||
|
|
@ -63,110 +71,28 @@ abstract class Module {
|
||||||
*/
|
*/
|
||||||
open fun onActivity(activity: Activity) {}
|
open fun onActivity(activity: Activity) {}
|
||||||
|
|
||||||
protected fun Context.getAppInfo(): AppInfo {
|
open fun onResume(activity: Activity) {}
|
||||||
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(
|
open fun onPause(activity: Activity) {}
|
||||||
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.
|
* Runtime wrapper around an [XC_MethodHook.MethodHookParam] used as the receiver of every
|
||||||
*
|
* `before`/`after` hook block, exposing [thisObject], [args], [result] and [param] directly.
|
||||||
* 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(
|
class HookScope(
|
||||||
val param: XC_MethodHook.MethodHookParam, private val proceed: (XC_MethodHook.MethodHookParam) -> Unit
|
val param: XC_MethodHook.MethodHookParam,
|
||||||
|
private val proceed: ((XC_MethodHook.MethodHookParam) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
/**
|
fun proceed() {
|
||||||
* Continues with the default XC_MethodHook super behavior.
|
proceed?.invoke(param)
|
||||||
* 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 thisObject: Any?
|
||||||
|
get() = param.thisObject
|
||||||
|
|
||||||
val args: Array<Any?> get() = param.args
|
val args: Array<Any?>
|
||||||
|
get() = param.args
|
||||||
|
|
||||||
var result: Any?
|
var result: Any?
|
||||||
get() = param.result
|
get() = param.result
|
||||||
|
|
@ -180,4 +106,105 @@ abstract class Module {
|
||||||
param.throwable = value
|
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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
package ShiggyXposed.xposed
|
package GoonXposed.xposed
|
||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.app.AndroidAppHelper
|
import android.app.AndroidAppHelper
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import ShiggyXposed.xposed.modules.UpdaterModule
|
import GoonXposed.xposed.modules.UpdaterModule
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
package ShiggyXposed.xposed.modules
|
package GoonXposed.xposed.modules
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AtomicFile
|
import android.util.AtomicFile
|
||||||
import ShiggyXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import ShiggyXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
import ShiggyXposed.xposed.modules.bridge.BridgeModule
|
import GoonXposed.xposed.asDir
|
||||||
|
import GoonXposed.xposed.asFile
|
||||||
|
import GoonXposed.xposed.getAppInfo
|
||||||
|
import GoonXposed.xposed.modules.bridge.BridgeModule
|
||||||
import java.io.*
|
import java.io.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -35,7 +38,7 @@ import java.io.*
|
||||||
*/
|
*/
|
||||||
object CacheModule : Module() {
|
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 MODULES_CACHE_PREFIX = "modules"
|
||||||
private const val ASSETS_CACHE_PREFIX = "assets"
|
private const val ASSETS_CACHE_PREFIX = "assets"
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
package ShiggyXposed.xposed.modules
|
package GoonXposed.xposed.modules
|
||||||
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import ShiggyXposed.xposed.Constants.Companion.TARGET_PACKAGE
|
import GoonXposed.xposed.Constants.Companion.TARGET_PACKAGE
|
||||||
import ShiggyXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
|
import GoonXposed.xposed.hookMethod
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hooks [Resources.getIdentifier] to fix resource package name mismatch.
|
* 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
|
"getIdentifier", String::class.java, String::class.java, String::class.java
|
||||||
) {
|
) {
|
||||||
before {
|
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:")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,18 +1,25 @@
|
||||||
package ShiggyXposed.xposed.modules
|
package GoonXposed.xposed.modules
|
||||||
|
|
||||||
import android.content.res.AssetManager
|
import android.content.res.AssetManager
|
||||||
import android.content.res.XModuleResources
|
import android.content.res.XModuleResources
|
||||||
import de.robv.android.xposed.IXposedHookZygoteInit
|
import de.robv.android.xposed.IXposedHookZygoteInit
|
||||||
import de.robv.android.xposed.XposedBridge
|
import de.robv.android.xposed.XposedBridge
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import ShiggyXposed.xposed.Constants
|
import GoonXposed.xposed.Constants
|
||||||
import ShiggyXposed.xposed.HookStateHolder
|
import GoonXposed.xposed.HookScope
|
||||||
import ShiggyXposed.xposed.Module
|
import GoonXposed.xposed.HookStateHolder
|
||||||
import ShiggyXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Module
|
||||||
import ShiggyXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
|
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.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
|
|
||||||
|
|
@ -52,49 +59,53 @@ object HookScriptLoaderModule : Module() {
|
||||||
).mapNotNull { classLoader.safeLoadClass(it) }.forEach { hook(it) }
|
).mapNotNull { classLoader.safeLoadClass(it) }.forEach { hook(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hook(instance: Class<*>) = runCatching {
|
private fun hook(instance: Class<*>) {
|
||||||
val loadScriptFromAssets = instance.method(
|
val loadScriptFromAssets = runCatching {
|
||||||
|
instance.method(
|
||||||
"loadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType
|
"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
|
"loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType
|
||||||
)
|
)
|
||||||
|
}.getOrNull()
|
||||||
|
|
||||||
loadScriptFromAssets.hook {
|
loadScriptFromAssets?.hook {
|
||||||
before {
|
before {
|
||||||
Log.i("Received call to loadScriptFromAssets: ${args[1]} (sync: ${args[2]})")
|
Log.i("Received call to loadScriptFromAssets: ${args[1]} (sync: ${args[2]})")
|
||||||
runCustomScripts(loadScriptFromFile, loadScriptFromAssets)
|
runCustomScripts(loadScriptFromFile, loadScriptFromAssets)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadScriptFromFile.hook {
|
loadScriptFromFile?.hook {
|
||||||
before {
|
before {
|
||||||
Log.i("Received call to loadScriptFromFile: ${args[0]} (sync: ${args[2]})")
|
Log.i("Received call to loadScriptFromFile: ${args[0]} (sync: ${args[2]})")
|
||||||
runCustomScripts(loadScriptFromFile, loadScriptFromAssets)
|
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...")
|
Log.i("Running custom scripts...")
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
val ready = async { HookStateHolder.readyDeferred.join() }
|
|
||||||
val isCustomUrl = UpdaterModule.isCustomUrlEnabled
|
val isCustomUrl = UpdaterModule.isCustomUrlEnabled
|
||||||
|
|
||||||
if (!mainScript.exists() || isCustomUrl) {
|
if (!mainScript.exists() || isCustomUrl) {
|
||||||
val reason = if (isCustomUrl) "Custom URL enabled" else "Main script does not exist"
|
val reason = if (isCustomUrl) "Custom URL enabled" else "Main script does not exist"
|
||||||
Log.i("$reason, downloading before load...")
|
Log.i("$reason, downloading before load...")
|
||||||
val download =
|
try {
|
||||||
async { UpdaterModule.downloadScript(showUpdateDialog = false).join() }
|
withTimeout(10000L) {
|
||||||
awaitAll(ready, download)
|
UpdaterModule.downloadScript(showUpdateDialog = false).join()
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.w("Bundle download did not complete in time", e)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.i("Main script exists, updating in background...")
|
Log.i("Main script exists, updating in background...")
|
||||||
UpdaterModule.downloadScript(showUpdateDialog = true)
|
UpdaterModule.downloadScript(showUpdateDialog = true)
|
||||||
ready.await()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,9 +113,11 @@ object HookScriptLoaderModule : Module() {
|
||||||
val runScriptFile = { file: File ->
|
val runScriptFile = { file: File ->
|
||||||
Log.i("Loading script: ${file.absolutePath}")
|
Log.i("Loading script: ${file.absolutePath}")
|
||||||
|
|
||||||
|
if (loadScriptFromFile != null) {
|
||||||
XposedBridge.invokeOriginalMethod(
|
XposedBridge.invokeOriginalMethod(
|
||||||
loadScriptFromFile, thisObject, arrayOf(file.absolutePath, file.absolutePath, loadSynchronously)
|
loadScriptFromFile, thisObject, arrayOf(file.absolutePath, file.absolutePath, loadSynchronously)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Unit
|
Unit
|
||||||
}
|
}
|
||||||
|
|
@ -123,15 +136,25 @@ object HookScriptLoaderModule : Module() {
|
||||||
if (mainScript.exists()) {
|
if (mainScript.exists()) {
|
||||||
runScriptFile(mainScript)
|
runScriptFile(mainScript)
|
||||||
} else {
|
} 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)
|
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(
|
XposedBridge.invokeOriginalMethod(
|
||||||
loadScriptFromAssets,
|
loadScriptFromAssets,
|
||||||
thisObject,
|
thisObject,
|
||||||
arrayOf(resources.assets, "assets://Shiggy.bundle", loadSynchronously)
|
arrayOf(resources.assets, "assets://Shiggy.bundle", loadSynchronously)
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
Log.w("No fallback bundle asset found; letting host app load default JS")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package ShiggyXposed.xposed.modules.LogBox
|
package GoonXposed.xposed.modules.LogBox
|
||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -62,7 +62,7 @@ object CustomBundleDialog {
|
||||||
container.addView(toggleContainer)
|
container.addView(toggleContainer)
|
||||||
|
|
||||||
val urlInput = EditText(context).apply {
|
val urlInput = EditText(context).apply {
|
||||||
hint = "http://localhost:4040/shiggycord.js"
|
hint = "http://localhost:4040/gooncord.js"
|
||||||
setTextColor(colors.onSurface)
|
setTextColor(colors.onSurface)
|
||||||
setHintTextColor(colors.onSurfaceVariant)
|
setHintTextColor(colors.onSurfaceVariant)
|
||||||
background = LogBoxUtils.createM3Background(context, colors.surfaceVariant, 12f)
|
background = LogBoxUtils.createM3Background(context, colors.surfaceVariant, 12f)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package ShiggyXposed.xposed.modules.LogBox
|
package GoonXposed.xposed.modules.LogBox
|
||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
package ShiggyXposed.xposed.modules.LogBox
|
package GoonXposed.xposed.modules.LogBox
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import ShiggyXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
import ShiggyXposed.xposed.Utils.Companion.reloadApp
|
import GoonXposed.xposed.Utils.Companion.reloadApp
|
||||||
import ShiggyXposed.xposed.Constants
|
import GoonXposed.xposed.Constants
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
object LogBoxActions {
|
object LogBoxActions {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package ShiggyXposed.xposed.modules.LogBox
|
package GoonXposed.xposed.modules.LogBox
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package ShiggyXposed.xposed.modules.LogBox
|
package GoonXposed.xposed.modules.LogBox
|
||||||
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
package ShiggyXposed.xposed.modules.LogBox
|
package GoonXposed.xposed.modules.LogBox
|
||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import ShiggyXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
|
|
||||||
object LogBoxDialogs {
|
object LogBoxDialogs {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package ShiggyXposed.xposed.modules.LogBox
|
package GoonXposed.xposed.modules.LogBox
|
||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -14,8 +14,8 @@ import android.content.res.ColorStateList
|
||||||
import android.view.animation.DecelerateInterpolator
|
import android.view.animation.DecelerateInterpolator
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import ShiggyXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
import ShiggyXposed.xposed.Utils
|
import GoonXposed.xposed.Utils
|
||||||
|
|
||||||
object LogBoxMenu {
|
object LogBoxMenu {
|
||||||
|
|
||||||
|
|
@ -30,7 +30,7 @@ object LogBoxMenu {
|
||||||
val container = createMenuContainer(context, colors)
|
val container = createMenuContainer(context, colors)
|
||||||
|
|
||||||
// Title
|
// Title
|
||||||
container.addView(createTitle(context, colors, "ShiggyXposed Recovery", center = true))
|
container.addView(createTitle(context, colors, "GoonXposed Recovery", center = true))
|
||||||
|
|
||||||
// Subtitle
|
// Subtitle
|
||||||
container.addView(createSubtitle(context, colors, "Select an option to continue"))
|
container.addView(createSubtitle(context, colors, "Select an option to continue"))
|
||||||
|
|
@ -782,7 +782,7 @@ object LogBoxMenu {
|
||||||
) { LogBoxActions.refetchBundle(context) }
|
) { LogBoxActions.refetchBundle(context) }
|
||||||
|
|
||||||
2 -> showCustomBundleDialog(context)
|
2 -> showCustomBundleDialog(context)
|
||||||
3 -> ShiggyXposed.xposed.Utils.Companion.reloadApp()
|
3 -> Utils.reloadApp()
|
||||||
4 -> showConfirmAction(
|
4 -> showConfirmAction(
|
||||||
context, "Clear Cache & Reset",
|
context, "Clear Cache & Reset",
|
||||||
"This will clear all cached bundles and reset to default settings."
|
"This will clear all cached bundles and reset to default settings."
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,67 @@
|
||||||
package ShiggyXposed.xposed.modules.LogBox
|
package GoonXposed.xposed.modules.LogBox
|
||||||
|
|
||||||
import ShiggyXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import ShiggyXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
|
import GoonXposed.xposed.Utils.Companion.reloadApp
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import de.robv.android.xposed.XC_MethodHook
|
||||||
import de.robv.android.xposed.XC_MethodReplacement
|
import de.robv.android.xposed.XC_MethodReplacement
|
||||||
import de.robv.android.xposed.XposedBridge
|
import de.robv.android.xposed.XposedBridge
|
||||||
import de.robv.android.xposed.XposedHelpers
|
import de.robv.android.xposed.XposedHelpers
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
object LogBoxModule : Module() {
|
object LogBoxModule : Module() {
|
||||||
lateinit var packageParam: XC_LoadPackage.LoadPackageParam
|
lateinit var packageParam: XC_LoadPackage.LoadPackageParam
|
||||||
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||||
var contextForMenu: Context? = null
|
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) {
|
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
||||||
this@LogBoxModule.packageParam = packageParam
|
this@LogBoxModule.packageParam = packageParam
|
||||||
|
|
||||||
try {
|
// Do not force getUseDeveloperSupport = true, as it causes React Native to loop
|
||||||
val dcdReactNativeHostClass = classLoader.loadClass("com.discord.bridge.DCDReactNativeHost")
|
// attempting to connect to a Metro bundler on localhost:8081 and hang the app loading screen.
|
||||||
val getUseDeveloperSupportMethod =
|
// Shake detector and recovery menu work independently without it.
|
||||||
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
|
return@with
|
||||||
}
|
}
|
||||||
|
|
@ -95,7 +126,7 @@ object LogBoxModule : Module() {
|
||||||
XposedBridge.hookMethod(handleReloadJSMethod, object : XC_MethodReplacement() {
|
XposedBridge.hookMethod(handleReloadJSMethod, object : XC_MethodReplacement() {
|
||||||
override fun replaceHookedMethod(param: MethodHookParam): Any? {
|
override fun replaceHookedMethod(param: MethodHookParam): Any? {
|
||||||
Log.e("handleReloadJS called - reloading app")
|
Log.e("handleReloadJS called - reloading app")
|
||||||
ShiggyXposed.xposed.Utils.Companion.reloadApp()
|
reloadApp()
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -120,7 +151,7 @@ object LogBoxModule : Module() {
|
||||||
Log.e("Failed to get context from DevSupport (non-fatal): ${e.message}")
|
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})")
|
Log.e("Using context: $finalContext (type: ${finalContext.javaClass.name})")
|
||||||
|
|
||||||
LogBoxNavigation.showRecoveryMenu(finalContext)
|
LogBoxNavigation.showRecoveryMenu(finalContext)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package ShiggyXposed.xposed.modules.LogBox
|
package GoonXposed.xposed.modules.LogBox
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
package ShiggyXposed.xposed.modules.LogBox
|
package GoonXposed.xposed.modules.LogBox
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import ShiggyXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
|
|
||||||
object LogBoxTheme {
|
object LogBoxTheme {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package ShiggyXposed.xposed.modules.LogBox
|
package GoonXposed.xposed.modules.LogBox
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
|
|
@ -9,8 +9,8 @@ import android.view.View
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import ShiggyXposed.xposed.Constants
|
import GoonXposed.xposed.Constants
|
||||||
import ShiggyXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
|
|
||||||
object LogBoxUtils {
|
object LogBoxUtils {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package ShiggyXposed.xposed.modules.LogBox
|
package GoonXposed.xposed.modules.LogBox
|
||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -19,40 +19,29 @@ object OptionsMenu {
|
||||||
container.addView(LogBoxComponents.createM3Button(context, injectionText, colors) {
|
container.addView(LogBoxComponents.createM3Button(context, injectionText, colors) {
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
LogBoxActions.toggleBundleInjection(context)
|
LogBoxActions.toggleBundleInjection(context)
|
||||||
|
GoonXposed.xposed.Utils.Companion.reloadApp()
|
||||||
})
|
})
|
||||||
|
|
||||||
container.addView(LogBoxComponents.createM3Button(context, "Refetch Bundle", colors) {
|
container.addView(LogBoxComponents.createM3Button(context, "Refetch Bundle", colors) {
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
RecoveryMenu::class.java.getDeclaredMethod(
|
RecoveryMenu.showConfirmAction(
|
||||||
"showConfirmAction",
|
|
||||||
Context::class.java,
|
|
||||||
String::class.java,
|
|
||||||
String::class.java,
|
|
||||||
Runnable::class.java
|
|
||||||
).apply { isAccessible = true }.invoke(
|
|
||||||
null,
|
|
||||||
context,
|
context,
|
||||||
"Refetch Bundle",
|
"Refetch Bundle",
|
||||||
"This will download the latest bundle from Github.",
|
"This will download the latest bundle from Github."
|
||||||
Runnable { LogBoxActions.refetchBundle(context) }
|
) {
|
||||||
)
|
LogBoxActions.refetchBundle(context)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
container.addView(LogBoxComponents.createM3Button(context, "Clear Cache & Reset", colors) {
|
container.addView(LogBoxComponents.createM3Button(context, "Clear Cache & Reset", colors) {
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
RecoveryMenu::class.java.getDeclaredMethod(
|
RecoveryMenu.showConfirmAction(
|
||||||
"showConfirmAction",
|
|
||||||
Context::class.java,
|
|
||||||
String::class.java,
|
|
||||||
String::class.java,
|
|
||||||
Runnable::class.java
|
|
||||||
).apply { isAccessible = true }.invoke(
|
|
||||||
null,
|
|
||||||
context,
|
context,
|
||||||
"Clear Cache & Reset",
|
"Clear Cache & Reset",
|
||||||
"This will clear all cached bundles and reset to default settings.",
|
"This will clear all cached bundles and reset to default settings."
|
||||||
Runnable { LogBoxActions.clearCacheAndReset(context) }
|
) {
|
||||||
)
|
LogBoxActions.clearCacheAndReset(context)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
container.addView(LogBoxComponents.createM3Button(context, "Back", colors) {
|
container.addView(LogBoxComponents.createM3Button(context, "Back", colors) {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
package ShiggyXposed.xposed.modules.LogBox
|
package GoonXposed.xposed.modules.LogBox
|
||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import ShiggyXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
|
|
||||||
object RecoveryMenu {
|
object RecoveryMenu {
|
||||||
|
|
||||||
|
|
@ -15,7 +15,7 @@ object RecoveryMenu {
|
||||||
|
|
||||||
val container = LogBoxComponents.createMenuContainer(context, colors)
|
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"))
|
container.addView(LogBoxComponents.createSubtitle(context, colors, "Select an option to continue"))
|
||||||
|
|
||||||
val safeModeText = if (LogBoxUtils.isSafeModeEnabled(context)) "Disable Safe Mode" else "Enable Safe Mode"
|
val safeModeText = if (LogBoxUtils.isSafeModeEnabled(context)) "Disable Safe Mode" else "Enable Safe Mode"
|
||||||
|
|
@ -63,7 +63,7 @@ object RecoveryMenu {
|
||||||
}
|
}
|
||||||
|
|
||||||
2 -> CustomBundleDialog.showCustomBundleDialog(context)
|
2 -> CustomBundleDialog.showCustomBundleDialog(context)
|
||||||
3 -> ShiggyXposed.xposed.Utils.Companion.reloadApp()
|
3 -> GoonXposed.xposed.Utils.Companion.reloadApp()
|
||||||
4 -> showConfirmAction(
|
4 -> showConfirmAction(
|
||||||
context,
|
context,
|
||||||
"Clear Cache & Reset",
|
"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 colors = LogBoxTheme.getM3Colors(context)
|
||||||
val container = LogBoxComponents.createMenuContainer(context, colors)
|
val container = LogBoxComponents.createMenuContainer(context, colors)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,19 @@
|
||||||
package ShiggyXposed.xposed.modules
|
package GoonXposed.xposed.modules
|
||||||
|
|
||||||
import android.content.res.AssetManager
|
import android.content.res.AssetManager
|
||||||
import de.robv.android.xposed.XC_MethodHook
|
import de.robv.android.xposed.XC_MethodHook
|
||||||
import de.robv.android.xposed.XposedBridge
|
import de.robv.android.xposed.XposedBridge
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import ShiggyXposed.xposed.BuildConfig
|
import GoonXposed.xposed.BuildConfig
|
||||||
import ShiggyXposed.xposed.Constants
|
import GoonXposed.xposed.Constants
|
||||||
import ShiggyXposed.xposed.Module
|
import GoonXposed.xposed.MethodHookBuilder
|
||||||
import ShiggyXposed.xposed.Utils.Companion.JSON
|
import GoonXposed.xposed.Module
|
||||||
import ShiggyXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
|
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.buildJsonObject
|
||||||
import kotlinx.serialization.json.put
|
import kotlinx.serialization.json.put
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
@ -23,7 +28,11 @@ class PayloadGlobalModule(private val modules: List<Module>) : Module() {
|
||||||
buildJsonObject {
|
buildJsonObject {
|
||||||
put("loaderName", Constants.LOADER_NAME)
|
put("loaderName", Constants.LOADER_NAME)
|
||||||
put("loaderVersion", BuildConfig.VERSION_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) {
|
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
package ShiggyXposed.xposed.modules
|
package GoonXposed.xposed.modules
|
||||||
|
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import kotlinx.serialization.json.JsonObjectBuilder
|
import kotlinx.serialization.json.JsonObjectBuilder
|
||||||
import kotlinx.serialization.json.put
|
import kotlinx.serialization.json.put
|
||||||
import ShiggyXposed.xposed.Constants
|
import GoonXposed.xposed.Constants
|
||||||
import ShiggyXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import ShiggyXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -25,293 +25,20 @@ object PerfPatchesModule : Module() {
|
||||||
|
|
||||||
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {
|
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {
|
||||||
try {
|
try {
|
||||||
// Build preloads path inside the app data files directory
|
val filesDir = File(packageParam.appInfo.dataDir, Constants.FILES_DIR)
|
||||||
val filesDir = File(packageParam.appInfo.dataDir, Constants.FILES_DIR).apply { mkdirs() }
|
val preloadsDir = File(filesDir, HookScriptLoaderModule.PRELOADS_DIR)
|
||||||
val preloadsDir = File(filesDir, HookScriptLoaderModule.PRELOADS_DIR).apply { mkdirs() }
|
|
||||||
val out = File(preloadsDir, FILE_NAME)
|
val out = File(preloadsDir, FILE_NAME)
|
||||||
|
if (out.exists()) {
|
||||||
// Defensive JS runtime patch. Avoid template literals with ${} to prevent Kotlin interpolation.
|
out.delete()
|
||||||
val js = """
|
Log.i("PerfPatchesModule: deleted legacy perf_patches.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")
|
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.e("PerfPatchesModule: failed writing patch", e)
|
Log.e("PerfPatchesModule: cleanup failed", e)
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Log.e("PerfPatchesModule:onLoad failed", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("Overrides deprecated Module.buildPayload")
|
@Deprecated("Overrides deprecated Module.buildPayload")
|
||||||
override fun buildPayload(builder: JsonObjectBuilder) {
|
override fun buildPayload(builder: JsonObjectBuilder) {
|
||||||
builder.put("perfPatches", true)
|
builder.put("perfPatches", false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
package ShiggyXposed.xposed.modules
|
package GoonXposed.xposed.modules
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AtomicFile
|
import android.util.AtomicFile
|
||||||
import ShiggyXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import ShiggyXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
import ShiggyXposed.xposed.modules.bridge.BridgeModule
|
import GoonXposed.xposed.asFile
|
||||||
|
import GoonXposed.xposed.modules.bridge.BridgeModule
|
||||||
import java.io.*
|
import java.io.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
package ShiggyXposed.xposed.modules
|
package GoonXposed.xposed.modules
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.app.AlertDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AtomicFile
|
import android.util.AtomicFile
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.core.util.writeBytes
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import dev.rushii.libunbound.LibUnbound
|
import GoonXposed.xposed.Constants
|
||||||
import ShiggyXposed.xposed.Constants
|
import GoonXposed.xposed.Module
|
||||||
import ShiggyXposed.xposed.Module
|
import GoonXposed.xposed.Utils
|
||||||
import ShiggyXposed.xposed.Utils
|
import GoonXposed.xposed.Utils.Companion.JSON
|
||||||
import ShiggyXposed.xposed.Utils.Companion.JSON
|
import GoonXposed.xposed.Utils.Companion.reloadApp
|
||||||
|
import GoonXposed.xposed.Utils.Log
|
||||||
import ShiggyXposed.xposed.Utils.Log
|
|
||||||
import ShiggyXposed.xposed.modules.bridge.BridgeModule
|
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.*
|
import io.ktor.client.call.*
|
||||||
import io.ktor.client.engine.cio.*
|
import io.ktor.client.engine.cio.*
|
||||||
|
|
@ -22,41 +22,49 @@ import io.ktor.client.statement.*
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class CustomLoadUrl(val enabled: Boolean = false, val url: String = "")
|
data class CustomLoadUrl(
|
||||||
|
val enabled: Boolean = false, val url: String = ""
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class LoaderConfig(val customLoadUrl: CustomLoadUrl = CustomLoadUrl(), val disableInjection: Boolean = false)
|
data class LoaderConfig(
|
||||||
|
val customLoadUrl: CustomLoadUrl = CustomLoadUrl()
|
||||||
@Serializable
|
)
|
||||||
data class EndpointInfo(val paths: ArrayList<String>, val hash: String? = null, val version: String)
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module that updates the JS bundle by downloading it from a remote URL.
|
||||||
|
*
|
||||||
|
* Shows dialogs when failed allowing retry.
|
||||||
|
*/
|
||||||
object UpdaterModule : Module() {
|
object UpdaterModule : Module() {
|
||||||
private lateinit var config: LoaderConfig
|
private lateinit var config: LoaderConfig
|
||||||
val isCustomUrlEnabled: Boolean
|
|
||||||
get() = config.customLoadUrl.enabled
|
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
private var lastActivity: WeakReference<Activity>? = null
|
private var lastActivity: WeakReference<Activity>? = null
|
||||||
|
|
||||||
private lateinit var cacheDir: File
|
private lateinit var cacheDir: File
|
||||||
private lateinit var bundle: File
|
private lateinit var bundle: File
|
||||||
private lateinit var etag: 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 TIMEOUT_CACHED = 5000L
|
||||||
private const val MIN_BYTECODE_SIZE = 512
|
private const val TIMEOUT = 10000L
|
||||||
private const val ETAG_FILE = "etag.txt"
|
private const val ETAG_FILE = "etag.txt"
|
||||||
private const val CONFIG_FILE = "loader.json"
|
private const val CONFIG_FILE = "loader.json"
|
||||||
|
|
||||||
private const val DEFAULT_BASE_URL = "https://github.com/kmmiio99o/ShiggyCord/releases/latest/download/"
|
val isCustomUrlEnabled: Boolean
|
||||||
private const val FALLBACK_BASE_URL = "https://bundle.shiggycord.dev/"
|
get() = if (::config.isInitialized) config.customLoadUrl.enabled else false
|
||||||
private const val DEFAULT_BUNDLE_NAME = "shiggycord.min.js"
|
|
||||||
|
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) {
|
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() }
|
cacheDir = File(appInfo.dataDir, Constants.CACHE_DIR).apply { mkdirs() }
|
||||||
val filesDir = File(appInfo.dataDir, Constants.FILES_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)
|
etag = File(cacheDir, ETAG_FILE)
|
||||||
|
|
||||||
val configFile = File(filesDir, CONFIG_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") {
|
config = runCatching {
|
||||||
if (bundle.exists()) bundle.delete()
|
if (configFile.exists()) {
|
||||||
if (etag.exists()) etag.delete()
|
JSON.decodeFromString<LoaderConfig>(configFile.readText())
|
||||||
null
|
} else LoaderConfig()
|
||||||
}
|
}.getOrDefault(LoaderConfig())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun downloadScript(activity: Activity? = null, showUpdateDialog: Boolean = true): Job = scope.launch {
|
fun downloadScript(activity: Activity? = null, showUpdateDialog: Boolean = true): Job = scope.launch {
|
||||||
try {
|
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) {
|
HttpClient(CIO) {
|
||||||
expectSuccess = false
|
expectSuccess = false
|
||||||
install(HttpTimeout) {
|
|
||||||
requestTimeoutMillis = TIMEOUT_STRICT
|
|
||||||
connectTimeoutMillis = 5000L
|
|
||||||
socketTimeoutMillis = TIMEOUT_STRICT
|
|
||||||
}
|
|
||||||
install(UserAgent) { agent = Constants.USER_AGENT }
|
install(UserAgent) { agent = Constants.USER_AGENT }
|
||||||
install(HttpRedirect) { checkHttpMethod = false }
|
install(HttpRedirect) {}
|
||||||
}.use { client ->
|
}.use { client ->
|
||||||
val targetUrl = resolveTargetUrl(client)
|
val url = config.customLoadUrl.takeIf { it.enabled }?.url ?: DEFAULT_BUNDLE_URL
|
||||||
Log.i("Fetching bundle: $targetUrl")
|
Log.i("Fetching JS bundle from: $url")
|
||||||
|
|
||||||
val response: HttpResponse = client.get(targetUrl) {
|
val response: HttpResponse = client.get(url) {
|
||||||
headers {
|
headers {
|
||||||
if (etag.exists() && bundle.exists()) {
|
if (etag.exists() && bundle.exists()) {
|
||||||
append(HttpHeaders.IfNoneMatch, etag.readText())
|
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) {
|
when (response.status) {
|
||||||
HttpStatusCode.OK -> {
|
HttpStatusCode.OK -> {
|
||||||
val bytes: ByteArray = response.body()
|
val bytes: ByteArray = response.body()
|
||||||
|
AtomicFile(bundle).writeBytes(bytes)
|
||||||
|
|
||||||
if (bytes.size < MIN_BYTECODE_SIZE) {
|
val newTag = response.headers[HttpHeaders.ETag]
|
||||||
throw Exception("Payload too small (${bytes.size} bytes). Possible corrupt build.")
|
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()
|
HttpStatusCode.NotModified -> {
|
||||||
Log.i("Bundle updated: ${bytes.size} bytes")
|
Log.i("Server responded with 304, no changes")
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpStatusCode.NotModified -> Log.i("Bundle is up to date (304)")
|
else -> {
|
||||||
else -> throw ResponseException(response, "HTTP ${response.status}")
|
throw ResponseException(response, "Received status: ${response.status}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.e("Updater Error", e)
|
Log.e("Failed to download script", e)
|
||||||
}
|
showErrorDialog(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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,16 +179,16 @@ object UpdaterModule : Module() {
|
||||||
lastActivity = WeakReference(activity)
|
lastActivity = WeakReference(activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setDisableInjection(context: Context, disabled: Boolean) {
|
fun showErrorDialog(e: Throwable) {
|
||||||
val filesDir = File(context.dataDir, Constants.FILES_DIR).apply { mkdirs() }
|
val activity = lastActivity?.get() ?: return
|
||||||
val configFile = File(filesDir, CONFIG_FILE)
|
|
||||||
val newCfg = config.copy(disableInjection = disabled)
|
activity.runOnUiThread {
|
||||||
configFile.writeText(JSON.encodeToString(newCfg))
|
}
|
||||||
config = newCfg
|
|
||||||
Toast.makeText(context, "Injection ${if (disabled) "disabled" else "enabled"}", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isInjectionDisabled(context: Context? = null): Boolean {
|
fun resetLoaderConfig(context: Context) {
|
||||||
return if (::config.isInitialized) config.disableInjection else false
|
val filesDir = File(context.dataDir, Constants.FILES_DIR)
|
||||||
|
val config = File(filesDir, CONFIG_FILE)
|
||||||
|
if (config.exists()) config.delete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package ShiggyXposed.xposed.modules.appearance
|
package GoonXposed.xposed.modules.appearance
|
||||||
|
|
||||||
import android.content.res.AssetManager
|
import android.content.res.AssetManager
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
|
|
@ -9,10 +9,12 @@ import android.os.Build
|
||||||
import de.robv.android.xposed.XC_MethodReplacement
|
import de.robv.android.xposed.XC_MethodReplacement
|
||||||
import de.robv.android.xposed.XposedHelpers
|
import de.robv.android.xposed.XposedHelpers
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import ShiggyXposed.xposed.Constants
|
import GoonXposed.xposed.Constants
|
||||||
import ShiggyXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import ShiggyXposed.xposed.Utils.Companion.JSON
|
import GoonXposed.xposed.Utils.Companion.JSON
|
||||||
import ShiggyXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
|
import GoonXposed.xposed.asDir
|
||||||
|
import GoonXposed.xposed.asFile
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.*
|
import io.ktor.client.call.*
|
||||||
import io.ktor.client.engine.cio.*
|
import io.ktor.client.engine.cio.*
|
||||||
|
|
@ -40,6 +42,11 @@ object FontsModule : Module() {
|
||||||
private val FILE_EXTENSIONS = arrayOf(".ttf", ".otf")
|
private val FILE_EXTENSIONS = arrayOf(".ttf", ".otf")
|
||||||
private const val FONTS_ASSET_PATH = "fonts/"
|
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 fontsDir: File
|
||||||
private lateinit var fontsDownloadsDir: File
|
private lateinit var fontsDownloadsDir: File
|
||||||
private var fontsAbsPath: String? = null
|
private var fontsAbsPath: String? = null
|
||||||
|
|
@ -51,21 +58,33 @@ object FontsModule : Module() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
||||||
XposedHelpers.findAndHookMethod(
|
val replacement = object : XC_MethodReplacement() {
|
||||||
"com.facebook.react.common.assets.ReactFontManager\$Companion",
|
|
||||||
classLoader,
|
|
||||||
"createAssetTypeface",
|
|
||||||
String::class.java,
|
|
||||||
Int::class.java,
|
|
||||||
"android.content.res.AssetManager",
|
|
||||||
object : XC_MethodReplacement() {
|
|
||||||
override fun replaceHookedMethod(param: MethodHookParam): Typeface? {
|
override fun replaceHookedMethod(param: MethodHookParam): Typeface? {
|
||||||
val fontFamilyName: String = param.args[0].toString()
|
val fontFamilyName: String = param.args[0].toString()
|
||||||
val style: Int = param.args[1] as Int
|
val style: Int = param.args[1] as Int
|
||||||
val assetManager: AssetManager = param.args[2] as AssetManager
|
val assetManager: AssetManager = param.args[2] as AssetManager
|
||||||
return createAssetTypeface(fontFamilyName, style, 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() }
|
val fontDefFile = File(appInfo.dataDir, "${Constants.FILES_DIR}/fonts.json").apply { asFile() }
|
||||||
if (!fontDefFile.exists()) return@with
|
if (!fontDefFile.exists()) return@with
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
package ShiggyXposed.xposed.modules.appearance
|
package GoonXposed.xposed.modules.appearance
|
||||||
|
|
||||||
import android.R.color
|
import android.R.color
|
||||||
import android.app.AndroidAppHelper
|
import android.app.AndroidAppHelper
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import ShiggyXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
package ShiggyXposed.xposed.modules.appearance
|
package GoonXposed.xposed.modules.appearance
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import androidx.core.graphics.toColorInt
|
import androidx.core.graphics.toColorInt
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import ShiggyXposed.xposed.Constants
|
import GoonXposed.xposed.Constants
|
||||||
import ShiggyXposed.xposed.Module
|
import GoonXposed.xposed.MethodHookBuilder
|
||||||
import ShiggyXposed.xposed.Utils.Companion.JSON
|
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.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.JsonObjectBuilder
|
import kotlinx.serialization.json.JsonObjectBuilder
|
||||||
|
|
@ -42,7 +46,7 @@ object ThemesModule : Module() {
|
||||||
|
|
||||||
private const val THEME_FILE = "current-theme.json"
|
private const val THEME_FILE = "current-theme.json"
|
||||||
|
|
||||||
|
@Deprecated("This method is deprecated in the parent class")
|
||||||
@ExperimentalSerializationApi
|
@ExperimentalSerializationApi
|
||||||
override fun buildPayload(builder: JsonObjectBuilder) {
|
override fun buildPayload(builder: JsonObjectBuilder) {
|
||||||
builder.apply {
|
builder.apply {
|
||||||
|
|
@ -81,10 +85,22 @@ object ThemesModule : Module() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hookTheme() {
|
fun hookTheme() {
|
||||||
val themeManager = param.classLoader.loadClass("com.discord.theme.utils.ColorUtilsKt")
|
try {
|
||||||
val darkTheme = param.classLoader.loadClass("com.discord.theme.DarkerTheme")
|
val themeManager = runCatching { param.classLoader.loadClass("com.discord.theme.utils.ColorUtilsKt") }.getOrNull()
|
||||||
val lightTheme = param.classLoader.loadClass("com.discord.theme.LightTheme")
|
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
|
val theme = this.theme ?: return
|
||||||
|
|
||||||
// Apply rawColors
|
// Apply rawColors
|
||||||
|
|
@ -97,51 +113,72 @@ object ThemesModule : Module() {
|
||||||
// TEXT_NORMAL -> getTextNormal
|
// TEXT_NORMAL -> getTextNormal
|
||||||
val methodName = "get${key.fromScreamingSnakeToCamelCase()}"
|
val methodName = "get${key.fromScreamingSnakeToCamelCase()}"
|
||||||
value.forEachIndexed { index, v ->
|
value.forEachIndexed { index, v ->
|
||||||
|
val colorInt = hexStringToColorInt(v)
|
||||||
when (index) {
|
when (index) {
|
||||||
0 -> hookThemeMethod(darkTheme, methodName, hexStringToColorInt(v))
|
0 -> {
|
||||||
1 -> hookThemeMethod(lightTheme, methodName, hexStringToColorInt(v))
|
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 there's any rawColors value, hook the color getter
|
||||||
if (!theme.data.rawColors.isNullOrEmpty()) {
|
if (!theme.data.rawColors.isNullOrEmpty() && themeManager != null) {
|
||||||
val getColorCompat = themeManager.getDeclaredMethod(
|
try {
|
||||||
|
val getColorCompat = runCatching {
|
||||||
|
themeManager.getDeclaredMethod(
|
||||||
"getColorCompat",
|
"getColorCompat",
|
||||||
Resources::class.java,
|
Resources::class.java,
|
||||||
Int::class.javaPrimitiveType,
|
Int::class.javaPrimitiveType,
|
||||||
Resources.Theme::class.java,
|
Resources.Theme::class.java,
|
||||||
)
|
)
|
||||||
|
}.getOrNull()
|
||||||
|
|
||||||
val getColorCompatLegacy = themeManager.getDeclaredMethod(
|
val getColorCompatLegacy = runCatching {
|
||||||
|
themeManager.getDeclaredMethod(
|
||||||
"getColorCompat", Context::class.java, Int::class.javaPrimitiveType
|
"getColorCompat", Context::class.java, Int::class.javaPrimitiveType
|
||||||
)
|
)
|
||||||
|
}.getOrNull()
|
||||||
|
|
||||||
val patch = MethodHookBuilder().run {
|
val patch = MethodHookBuilder().run {
|
||||||
before {
|
before {
|
||||||
|
try {
|
||||||
val arg1 = args[0]
|
val arg1 = args[0]
|
||||||
val resources = if (arg1 is Context) arg1.resources else (arg1 as Resources)
|
val resources = if (arg1 is Context) arg1.resources else (arg1 as Resources)
|
||||||
val name = resources.getResourceEntryName(args[1] as Int)
|
val name = resources.getResourceEntryName(args[1] as Int)
|
||||||
|
|
||||||
if (rawColorMap[name] != null) result = rawColorMap[name]
|
if (rawColorMap[name] != null) result = rawColorMap[name]
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
build()
|
build()
|
||||||
}
|
}
|
||||||
|
|
||||||
getColorCompat.hook(patch)
|
getColorCompat?.hook(patch)
|
||||||
getColorCompatLegacy.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"
|
// Parse HEX colour string to INT. Takes "#RRGGBBAA" or "#RRGGBB"
|
||||||
private fun hexStringToColorInt(hexString: String): Int {
|
private fun hexStringToColorInt(hexString: String): Int {
|
||||||
return if (hexString.length == 9) {
|
return try {
|
||||||
|
if (hexString.length == 9) {
|
||||||
// Rearrange RRGGBBAA -> AARRGGBB so parseColor() is happy
|
// Rearrange RRGGBBAA -> AARRGGBB so parseColor() is happy
|
||||||
val alpha = hexString.substring(7, 9)
|
val alpha = hexString.substring(7, 9)
|
||||||
val rrggbb = hexString.substring(1, 7)
|
val rrggbb = hexString.substring(1, 7)
|
||||||
"#$alpha$rrggbb".toColorInt()
|
"#$alpha$rrggbb".toColorInt()
|
||||||
} else hexString.toColorInt()
|
} else hexString.toColorInt()
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hookThemeMethod(themeClass: Class<*>, methodName: String, themeValue: Int) {
|
private fun hookThemeMethod(themeClass: Class<*>, methodName: String, themeValue: Int) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package ShiggyXposed.xposed.modules.bridge
|
package GoonXposed.xposed.modules.bridge
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
|
|
@ -7,9 +7,9 @@ import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import ShiggyXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import ShiggyXposed.xposed.Utils
|
import GoonXposed.xposed.Utils
|
||||||
import ShiggyXposed.xposed.Utils.Companion.reloadApp
|
import GoonXposed.xposed.getAppInfo
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
object AdditionalBridgeMethodsModule : Module() {
|
object AdditionalBridgeMethodsModule : Module() {
|
||||||
|
|
@ -48,11 +48,6 @@ object AdditionalBridgeMethodsModule : Module() {
|
||||||
|
|
||||||
file.writeText(contents as String)
|
file.writeText(contents as String)
|
||||||
}
|
}
|
||||||
|
|
||||||
BridgeModule.registerMethod("Shiggy.app.reload") {
|
|
||||||
reloadApp()
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivity(activity: Activity) = with(activity) {
|
override fun onActivity(activity: Activity) = with(activity) {
|
||||||
|
|
@ -65,10 +60,10 @@ object AdditionalBridgeMethodsModule : Module() {
|
||||||
val clip = ClipData.newPlainText("Stack Trace", errorString)
|
val clip = ClipData.newPlainText("Stack Trace", errorString)
|
||||||
|
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
.setTitle("ShiggyCord Error")
|
.setTitle("GoonCord Error")
|
||||||
.setMessage(
|
.setMessage(
|
||||||
"""
|
"""
|
||||||
ShiggyCord: $version
|
GoonCord: $version
|
||||||
${app.name}: ${app.version} (${app.versionCode})
|
${app.name}: ${app.version} (${app.versionCode})
|
||||||
Device: ${Build.MANUFACTURER} ${Build.MODEL}
|
Device: ${Build.MANUFACTURER} ${Build.MODEL}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
package ShiggyXposed.xposed.modules.bridge
|
package GoonXposed.xposed.modules.bridge
|
||||||
|
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import ShiggyXposed.xposed.BuildConfig
|
import GoonXposed.xposed.BuildConfig
|
||||||
import ShiggyXposed.xposed.Constants
|
import GoonXposed.xposed.Constants
|
||||||
import ShiggyXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import ShiggyXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
|
import GoonXposed.xposed.hookMethod
|
||||||
|
import GoonXposed.xposed.method
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
package ShiggyXposed.xposed.modules.no_track
|
package GoonXposed.xposed.modules.no_track
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import ShiggyXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import ShiggyXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
|
import GoonXposed.xposed.hookMethod
|
||||||
|
import GoonXposed.xposed.safeLoadClass
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hooks Discord's crash reporting to disable Sentry initialization.
|
* Hooks Discord's crash reporting to disable Sentry initialization.
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
package ShiggyXposed.xposed.modules.no_track
|
package GoonXposed.xposed.modules.no_track
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import ShiggyXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import ShiggyXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
|
import GoonXposed.xposed.hookMethod
|
||||||
|
import GoonXposed.xposed.safeLoadClass
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hooks Discord's deep links tracking to disable AppsFlyer initialization.
|
* Hooks Discord's deep links tracking to disable AppsFlyer initialization.
|
||||||
|
|
|
||||||
9
app/src/main/res/values/arrays.xml
Normal file
9
app/src/main/res/values/arrays.xml
Normal 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>
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">ShiggyXposed</string>
|
<string name="app_name">GoonXposed</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue