Compare commits
260 commits
fix/ci-bui
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a707e6fd05 | ||
|
|
d55ed5221c | ||
|
|
a5af0bddb8 | ||
|
|
9611038048 | ||
|
|
06dde39cfd | ||
|
|
69496c1f3c | ||
|
|
ce6f201fcd | ||
|
|
73a93a29bb | ||
|
|
ee7e30e67c | ||
|
|
57ebd3daed | ||
|
|
82214b6ed0 | ||
|
|
2b9956d97b | ||
|
|
b2c7e23a57 | ||
|
|
468311bbba | ||
|
|
7f90a61ca5 | ||
|
|
17e1b9220f | ||
|
|
34b4a9f30c | ||
|
|
abb1c6ddd8 | ||
|
|
32421df002 | ||
|
|
f16af054be | ||
|
|
3a6abc9a79 | ||
|
|
a836fd1e73 | ||
|
|
0190e2d468 | ||
|
|
ea21c20d18 | ||
|
|
ad10c8a2a1 | ||
|
|
65d6ae239a | ||
|
|
415f0066d7 | ||
|
|
4f3c599c3b | ||
|
|
5e8e4538f8 | ||
|
|
64f83dcd5c | ||
|
|
cd418c2d79 | ||
|
|
0267d095e2 | ||
|
|
2d62630e30 | ||
|
|
750a4cd5b0 | ||
|
|
baacefef54 | ||
|
|
f96d53db9a | ||
|
|
720e84487e | ||
|
|
8616f04995 | ||
|
|
7152b06265 | ||
|
|
2b3510f55d | ||
|
|
d1334d0ec0 | ||
|
|
853564ebe9 | ||
|
|
3bf2f76c56 | ||
|
|
6f707af677 | ||
|
|
1abaaff7d1 | ||
|
|
3aa888bc26 | ||
|
|
d57081364b | ||
|
|
4599c2d96f | ||
|
|
14a8067aea | ||
|
|
d719e15cf8 | ||
|
|
7814cd0e82 | ||
|
|
469cdfb95e | ||
|
|
e90c20bfc5 | ||
|
|
fc9dd1ee9b | ||
|
|
c85761f92e | ||
|
|
c342b478c1 | ||
|
|
fc6772a124 | ||
|
|
743b6283c0 | ||
|
|
3562b0e730 | ||
|
|
26a356a158 | ||
|
|
a3ebf0259e | ||
|
|
df097bfc81 | ||
|
|
765c546c20 | ||
|
|
13bdcdad03 | ||
|
|
b083a66afd | ||
|
|
4115c3d4df | ||
|
|
c63eaa5a12 | ||
|
|
531cb9281c | ||
|
|
bff796d574 | ||
|
|
f7d1b2ea1d | ||
|
|
9a620545b4 | ||
|
|
6e7f5302e3 | ||
|
|
ec787e7cf5 | ||
|
|
8ef48f3608 | ||
|
|
2bddedb96c | ||
|
|
5d39748a28 | ||
|
|
1a9fd61ec3 | ||
|
|
a9a1dc4ad3 | ||
|
|
aab9fdc101 | ||
|
|
21029f5f72 | ||
|
|
64ffdf4f32 | ||
|
|
68c85dd812 | ||
|
|
fd84dd0320 | ||
|
|
7df631b799 | ||
|
|
3d45a8d70f | ||
|
|
2978e3b8d1 | ||
|
|
ffcc29796c | ||
|
|
79298ddf6c | ||
|
|
e0917110e1 | ||
|
|
1de8af79f0 | ||
|
|
5fe307a852 | ||
|
|
08bfa8199d | ||
|
|
2c94757947 | ||
|
|
85837531de | ||
|
|
3360ee33f1 | ||
|
|
d882007611 | ||
|
|
bd9116e1cf | ||
|
|
5e54c7bf61 | ||
|
|
bf6f3c0691 | ||
|
|
7eb893bcc9 | ||
|
|
52f05764f4 | ||
|
|
cdbbb3a957 | ||
|
|
457af73f88 | ||
|
|
6be3d89176 | ||
|
|
9267cb3918 | ||
|
|
f55d1a4a62 | ||
|
|
cc0ffe6fb2 | ||
|
|
acca6bfe47 | ||
|
|
8148f860e5 | ||
|
|
f0e9503354 | ||
|
|
4f779c9af0 | ||
|
|
202152bbf0 | ||
|
|
d55b9dcda0 | ||
|
|
6f6f2e4abc | ||
|
|
c3c21a26e0 | ||
|
|
5e2fd369fe | ||
|
|
7635ac1d90 | ||
|
|
5769fe2211 | ||
|
|
04a799e7f6 | ||
|
|
ed8d5fb7a8 | ||
|
|
8038be3023 | ||
|
|
511fd1df4e | ||
|
|
0b63dbbc9a | ||
|
|
85290a619e | ||
|
|
d1796e4c68 | ||
|
|
f92e36e7d4 | ||
|
|
cbf96e8b45 | ||
|
|
4537e7d56c | ||
|
|
782c40b5d3 | ||
|
|
21effc7f21 | ||
|
|
cc1f7fe682 | ||
|
|
f619b8702c | ||
|
|
b4c17583b6 | ||
|
|
ff308a190c | ||
|
|
c5f3b83cdf | ||
|
|
03c81d8cc9 | ||
|
|
3c4c9ca555 | ||
|
|
19bade53a0 | ||
|
|
351086110c | ||
|
|
5bc83206b2 | ||
|
|
8aa226d6ca | ||
|
|
02310df32f | ||
|
|
ffdd65f74d | ||
|
|
90b346b75a | ||
|
|
3619dafd47 | ||
|
|
12491b2235 | ||
|
|
2dd8151138 | ||
|
|
8c033fe878 | ||
|
|
f6d2384fa5 | ||
|
|
2621359a61 | ||
|
|
9f8c521659 | ||
|
|
2ffe2f510d | ||
|
|
f4d48fdf90 | ||
|
|
66bb7fce6a | ||
|
|
42636b29e8 | ||
|
|
e0cb50aaea | ||
|
|
f2371d1c7e | ||
|
|
01f52c1e2f | ||
|
|
8d1d6303d3 | ||
|
|
bf05c67966 | ||
|
|
4102b2298d | ||
|
|
2171fc59eb | ||
|
|
8389d19545 | ||
|
|
5fe672884e | ||
|
|
9481d80e5f | ||
|
|
3f309f58bb | ||
|
|
004bba80ae | ||
|
|
0fc969cf00 | ||
|
|
691db71d12 | ||
|
|
07265446cf | ||
|
|
86c9658318 | ||
|
|
8c584d7cfc | ||
|
|
f42f53b375 | ||
|
|
8c6c43429d | ||
|
|
a4c6e4871e | ||
|
|
f17cdc8fd0 | ||
|
|
3506cfc97d | ||
|
|
7d8a71eaf6 | ||
|
|
eeac21e2aa | ||
|
|
eb58fdfa00 | ||
|
|
44288a202d | ||
|
|
3196f8a0a4 | ||
|
|
63c12c7984 | ||
|
|
9a716efd37 | ||
|
|
136b6f7c46 | ||
|
|
63d8e86725 | ||
|
|
6cfb92faf1 | ||
|
|
f47d6d1d93 | ||
|
|
2eb7cbf84e | ||
|
|
3d6f61bb56 | ||
|
|
01fdbe19d7 | ||
|
|
4ecfdfce27 | ||
|
|
e198c43ce2 | ||
|
|
ee7c27d280 | ||
|
|
9a6886ad71 | ||
|
|
019075d927 | ||
|
|
6131c74016 | ||
|
|
52e10aca4c | ||
|
|
4b7192edfa | ||
|
|
ac5d0e551b | ||
|
|
d04d1d608d | ||
|
|
46e37bfe7e | ||
|
|
e9a2a8c51e | ||
|
|
e453be092b | ||
|
|
ced4d26d20 | ||
|
|
683137451b | ||
|
|
61af37117d | ||
|
|
fb8a6119f9 | ||
|
|
62513e7a04 | ||
|
|
007a6f8a59 | ||
|
|
aa0970913c | ||
|
|
4ae471a7b9 | ||
|
|
f19d654e87 | ||
|
|
7348da9aff | ||
|
|
9a6d328b47 | ||
|
|
afdda092bc | ||
|
|
6858113d54 | ||
|
|
adeba43dca | ||
|
|
660ee94361 | ||
|
|
ae7ab4fd21 | ||
|
|
cf531f247a | ||
|
|
c1e73bc602 | ||
|
|
ccd0a1f066 | ||
|
|
f210de66f2 | ||
|
|
1d0c42680f | ||
|
|
6e58a231e0 | ||
|
|
4d94dccdfc | ||
|
|
4ab61ffcc5 | ||
|
|
5eede3f588 | ||
|
|
28d9fbabc8 | ||
|
|
651e647f29 | ||
|
|
0a712869d5 | ||
|
|
3d4e9d052b | ||
|
|
a05d9a9b38 | ||
|
|
d0160e829f | ||
|
|
fb8756b371 | ||
|
|
2ddfdd3bfa | ||
|
|
078604ef25 | ||
|
|
0bacc54ac4 | ||
|
|
af9bd39358 | ||
|
|
67317dde13 | ||
|
|
eccadcdeff | ||
|
|
a854f81dda | ||
|
|
eeec556c9f | ||
|
|
48d1cd7807 | ||
|
|
be25ad7213 | ||
|
|
7a90e92892 | ||
|
|
c06ab2754d | ||
|
|
02d6302a8c | ||
|
|
76f8a3cd01 | ||
|
|
3905a1327d | ||
|
|
63892f2a8f | ||
|
|
62336a53a9 | ||
|
|
052edd13fa | ||
|
|
f9e29656fa | ||
|
|
f035c217e6 | ||
|
|
c4734b8b90 | ||
|
|
50994e9278 | ||
|
|
ed704eb7f8 | ||
|
|
f195d82414 |
23 changed files with 1549 additions and 759 deletions
29
.github/workflows/build.yml
vendored
29
.github/workflows/build.yml
vendored
|
|
@ -1,13 +1,6 @@
|
|||
name: Build APK
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
tags: [ "v*", "V*" ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
@ -72,23 +65,3 @@ jobs:
|
|||
with:
|
||||
name: app-release
|
||||
path: /tmp/ShiggyXposedbuild/apks/app-release.apk
|
||||
|
||||
- name: Publish APKs to GitHub Release
|
||||
if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/master'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
||||
TAG_NAME="${GITHUB_REF#refs/tags/}"
|
||||
else
|
||||
TAG_NAME=$(gh release view --json tagName -q .tagName 2>/dev/null || echo "")
|
||||
fi
|
||||
if [ -n "$TAG_NAME" ]; then
|
||||
echo "Uploading APKs to release $TAG_NAME..."
|
||||
gh release upload "$TAG_NAME" \
|
||||
/tmp/ShiggyXposedbuild/apks/app-release.apk \
|
||||
/tmp/ShiggyXposedbuild/apks/app-debug.apk \
|
||||
--clobber
|
||||
else
|
||||
echo "No release tag found to upload assets to."
|
||||
fi
|
||||
2
.idea/.name
generated
2
.idea/.name
generated
|
|
@ -1 +1 @@
|
|||
ShiggyXposed
|
||||
GoonXposed
|
||||
|
|
|
|||
203
README.md
203
README.md
|
|
@ -1,193 +1,40 @@
|
|||
# GoonXposed
|
||||
|
||||
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.
|
||||
GoonXposed is a root Xposed module that injects the GoonCord Discord mobile client modifications into the official Discord application.
|
||||
|
||||
This README explains prerequisites, installation, usage, troubleshooting, and credits. If you want help, open an issue on the repository or join the project's support server (links below).
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [What is this](#what-is-this)
|
||||
- [Features](#features)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Installation](#installation)
|
||||
- [Using the Module](#using-the-module)
|
||||
- [Recovery / Options / Themes menu](#recovery--options--themes-menu)
|
||||
- [Bundle injection / Custom bundle URL](#bundle-injection--custom-bundle-url)
|
||||
- [Development / Building](#development--building)
|
||||
- [Troubleshooting & FAQ](#troubleshooting--faq)
|
||||
- [Contributing](#contributing)
|
||||
- [Credits](#credits)
|
||||
- [License](#license)
|
||||
- [Contact & Links](#contact--links)
|
||||
|
||||
---
|
||||
|
||||
## What is this
|
||||
|
||||
GoonXposed injects a patched GoonCord bundle into the official Discord application by using the Xposed framework. It's intended for users who run a rooted Android device and want to apply client-side modifications to Discord.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- Inject a custom GoonCord bundle into Discord.
|
||||
- Toggle bundle injection on/off (marker-based).
|
||||
- Load a custom bundle URL and toggle it on/off.
|
||||
- Recovery/LogBox action sheet for reload, safe-mode, refetch/reset, and options.
|
||||
- Options to change the LogBox menu appearance (System / Light / Dark) and a set of menu color "flavors".
|
||||
- In-place UI theming for the recovery/options menus.
|
||||
- Helpers for refetching or backing up the currently loaded bundle.
|
||||
|
||||
---
|
||||
## Info
|
||||
* Injects GoonCord client into the Discord mobile app.
|
||||
* Built in Kotlin.
|
||||
* Material 3 like Shake menu with some discord popup accents
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before installing and using GoonXposed:
|
||||
|
||||
- A rooted Android device. Supported root solutions you might use:
|
||||
- Magisk, KernelSU, KernelSUNext, or Sukisu Ultra.
|
||||
- Xposed framework installed. The project targets LSPosed or forks because of compatibility with modern Android and Discord:
|
||||
- Recommended: JingMatrix Fork of [LSPosed](https://github.com/JingMatrix/LSPosed)
|
||||
- The official Discord Android application (stable/beta/alpha) installed and included in the module scope.
|
||||
|
||||
---
|
||||
To use this module, your device must meet the following requirements:
|
||||
* **Root Access**: Provided by Magisk, KernelSU, KernelSUNext, or Sukisu Ultra.
|
||||
* **Xposed Framework**: Use JingMatrix Fork of [LSPosed](https://github.com/JingMatrix/LSPosed).
|
||||
* **Target Application**: An official Discord application (stable, beta, or alpha version).
|
||||
|
||||
## Installation
|
||||
1. Download the latest version of the module from the [Releases](https://github.com/OTKUSteyler/GoonXposed/releases) page.
|
||||
2. Install the downloaded APK file on your device.
|
||||
3. Open the LSPosed manager application.
|
||||
4. Go to the **Modules** section and enable **GoonXposed**.
|
||||
5. In the module's settings, set the scope to include the **Discord** application.
|
||||
6. Reboot your device for the changes to take effect.
|
||||
|
||||
1. Download the latest APK from the repository Releases:
|
||||
- https://github.com/OTKUSteyler/GoonXposed/releases
|
||||
2. Install the APK on your device (standard package install).
|
||||
3. Open your LSPosed manager (or chosen Xposed manager).
|
||||
4. Enable the 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.
|
||||
The project is built with Kotlin and inherits its foundational structure from KettuXposed.
|
||||
|
||||
---
|
||||
# Troubleshooting
|
||||
|
||||
## Using the Module
|
||||
- Module not working: Ensure the module is enabled in LSPosed and that Discord is correctly selected in its scope. if still nothing, head to our [discord support server](https://discord.gg/nQykFF9Ud6)
|
||||
- Any other issues: Same server as above, listing them all is not worth it.
|
||||
|
||||
Once installed and active, you can open the bundled Recovery/LogBox menu (depending on the hooked RN dev dialog) to access all tools.
|
||||
# Thanks & Credits
|
||||
|
||||
Recovery → Options → Themes:
|
||||
- Recovery (Action Sheet) includes:
|
||||
- Safe Mode toggle (toggle persisted safe-mode setting + optional theme handling)
|
||||
- Load Custom Bundle
|
||||
- Reload App
|
||||
- Options (opens the Options action sheet)
|
||||
- Options includes:
|
||||
- Themes (open appearance/flavor controls)
|
||||
- Toggle bundle injection (rename/add marker file in cache to prevent injection)
|
||||
- Refetch Bundle (moves or resets current bundle and triggers a reload)
|
||||
- Clear Cache & Reset (clears cached bundle and resets loader config)
|
||||
- Back (return to Recovery)
|
||||
This project was made possible thanks to the work of:
|
||||
|
||||
Themes:
|
||||
- Pick appearance: System / Light / Dark
|
||||
- Choose Menu Color Flavor: blue, green, mocha, vanilla, purple, amber, teal
|
||||
- Theme & flavor settings persist under the app's files directory (`files/logbox/LOGBOX_SETTINGS` and `files/logbox/LOGBOX_THEMES`)
|
||||
|
||||
Bundle injection:
|
||||
- When you disable injection, the module will create/rename a marker file in the cache directory so other components (loader) skip injection.
|
||||
- Toggling injection is handled by `toggleBundleInjection(context)` and is persisted to `LOGBOX_SETTINGS`.
|
||||
|
||||
Custom bundle URL:
|
||||
- The custom bundle loader configuration is stored in `files/pyoncord/loader.json`.
|
||||
- You can enable/disable the custom URL and provide a remote URL (the module exposes a dialog for this).
|
||||
|
||||
---
|
||||
|
||||
## Development / Building
|
||||
|
||||
If you want to build the project locally:
|
||||
|
||||
- Prerequisites: JDK, Android SDK, and Gradle (the project uses the Gradle wrapper).
|
||||
- From the repository root:
|
||||
|
||||
- Build debug APK:
|
||||
```
|
||||
./gradlew :app:assembleDebug
|
||||
```
|
||||
|
||||
- Install to connected device (adb required):
|
||||
```
|
||||
adb install -r app/build/outputs/apk/debug/app-debug.apk
|
||||
```
|
||||
|
||||
- Run and test on a rooted device/emulator with Xposed/LSPosed installed and Discord in scope.
|
||||
|
||||
Notes:
|
||||
- The project is written in Kotlin and uses Android UI primitives to construct the recovery/LogBox UI. The dev menu is created in-place to avoid modifying Discord views.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting & FAQ
|
||||
|
||||
1. Module not working or not showing in Discord?
|
||||
- Ensure GoonXposed is enabled in LSPosed and Discord is included in the module's scope.
|
||||
- Reboot after enabling the module.
|
||||
- Confirm you have the compatible LSPosed fork installed (JingMatrix recommended).
|
||||
|
||||
2. Recovery menu shows visual glitches when changing themes?
|
||||
- The menu attempts to apply colors in-place. If you see transient visual artifacts, try closing and reopening the menu — the settings are persisted and will apply on reopen.
|
||||
- If an area does not update, please report which dialog/control (and the device/Android version) so it can be targeted.
|
||||
|
||||
3. Bundle injection not toggling?
|
||||
- Toggling injection manipulates files under the app's cache directory. If that fails, check file permissions or whether the cache path changed on your build of Discord.
|
||||
- You can also manually inspect `data/data/<discord.package>/cache/...` to see `.disabled` marker files (depending on your device and build).
|
||||
|
||||
4. I accidentally broke Discord — how do I recover?
|
||||
- Use the Recovery → Options → Clear Cache & Reset option to remove cached bundles and restore default loader settings, then reload Discord.
|
||||
- If Discord fails to start, reboot the device to make sure changes are applied cleanly.
|
||||
|
||||
If you still have issues, open an issue on GitHub or join the support server (link below). Please include:
|
||||
- Device model and Android version
|
||||
- Discord app version
|
||||
- A clear description of the problem and steps to reproduce
|
||||
- Any relevant logs or screenshots (redact personal data)
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome. If you want to help:
|
||||
|
||||
- Open an issue to discuss a bug or feature.
|
||||
- Send pull requests with focused, well-documented changes.
|
||||
- When working on UI or theme changes, test on multiple screen sizes and Android versions if possible.
|
||||
|
||||
Guidelines:
|
||||
- Keep commits small and descriptive.
|
||||
- Include code comments for complex logic (hooks, file-system operations).
|
||||
- Respect user privacy — do not add telemetry or data exfiltration.
|
||||
|
||||
---
|
||||
|
||||
## Credits
|
||||
|
||||
This project stands on the shoulders of many open-source projects and contributors:
|
||||
|
||||
- [LSPosed Team](https://github.com/LSPosed) — the Xposed framework implementation used to run modules.
|
||||
- [JingMatrix](https://github.com/JingMatrix) — author of the LSPosed fork referenced by many users.
|
||||
- [cocobo1](https://codeberg.org/cocobo1) — for adapting some features to Xposed which [Revenge Team](https://github.com/revenge-mod) didn't.
|
||||
- [Revenge Team](https://github.com/revenge-mod) — provided earlier Xposed module work that informed parts of this project.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
Please refer to the `LICENSE` file in the repository root for the project license (if present). If no license file exists, the repository's default (copyright) status applies — check the repo settings or ask the maintainer for clarification.
|
||||
|
||||
---
|
||||
|
||||
## Contact & Links
|
||||
|
||||
- Releases: https://github.com/OTKUSteyler/GoonXposed/releases
|
||||
- Repository: https://github.com/OTKUSteyler/GoonXposed
|
||||
- Support server: https://discord.gg/nQykFF9Ud6
|
||||
|
||||
---
|
||||
|
||||
Thank you for using GoonXposed.
|
||||
· The LSPosed Team for the framework.
|
||||
· JingMatrix for their LSPosed fork.
|
||||
· [cocobo1](https://codeberg.org/cocobo1) for adjusting the Xposed module for Kettu client mod.
|
||||
· [Revenge Team](https://github.com/revenge-mod) for making a really good Xposed Module to base of
|
||||
|
||||

|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ android {
|
|||
}
|
||||
release {
|
||||
isDebuggable = false
|
||||
isMinifyEnabled = false
|
||||
isMinifyEnabled = true
|
||||
val keystoreFile = file("../release.keystore")
|
||||
if (keystoreFile.exists()) {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
|
|
@ -68,5 +68,6 @@ dependencies {
|
|||
implementation("io.ktor:ktor-client-core:3.3.1")
|
||||
implementation("io.ktor:ktor-client-cio:3.3.1")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
implementation("dev.rushii:libunbound:1.0.0")
|
||||
implementation("dev.rikka.shizuku:api:13.1.5")
|
||||
implementation("dev.rikka.shizuku:provider:13.1.5")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,6 @@
|
|||
android:value="Xposed module to inject GoonCord into discord"
|
||||
/>
|
||||
<meta-data android:name="xposedminversion" android:value="82" />
|
||||
<meta-data
|
||||
android:name="xposedscope"
|
||||
android:resource="@array/xposed_scope"
|
||||
/>
|
||||
<meta-data android:name="xposedscope" android:value="com.discord" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -16,7 +16,6 @@ import GoonXposed.xposed.modules.bridge.AdditionalBridgeMethodsModule
|
|||
import GoonXposed.xposed.modules.bridge.BridgeModule
|
||||
import GoonXposed.xposed.modules.no_track.BlockCrashReportingModule
|
||||
import GoonXposed.xposed.modules.no_track.BlockDeepLinksTrackingModule
|
||||
import GoonXposed.xposed.modules.LogBox.*
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
|
||||
object HookStateHolder {
|
||||
|
|
@ -50,8 +49,7 @@ class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
|
|||
PerfPatchesModule,
|
||||
FontsModule,
|
||||
ThemesModule,
|
||||
SysColorsModule,
|
||||
GmsCompatModule
|
||||
SysColorsModule
|
||||
)
|
||||
|
||||
init {
|
||||
|
|
@ -59,41 +57,13 @@ class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
|
|||
}
|
||||
|
||||
override fun initZygote(startupParam: IXposedHookZygoteInit.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")
|
||||
for (module in modules) module.onInit(startupParam)
|
||||
}
|
||||
|
||||
override fun handleLoadPackage(param: XC_LoadPackage.LoadPackageParam) = with(param) {
|
||||
if (!isTargetPackage(packageName)) return
|
||||
if (processName.contains(":")) return
|
||||
if (hooked) return
|
||||
|
||||
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")
|
||||
}
|
||||
val reactActivity = classLoader.loadClass(Constants.TARGET_ACTIVITY)
|
||||
|
||||
ContextWrapper::class.java.hookMethod("attachBaseContext", Context::class.java) {
|
||||
after {
|
||||
|
|
@ -104,18 +74,10 @@ class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
|
|||
}
|
||||
}
|
||||
|
||||
val targetActivityClass = reactActivity ?: Activity::class.java
|
||||
targetActivityClass.hookMethod("onCreate", Bundle::class.java) {
|
||||
reactActivity.hookMethod("onCreate", Bundle::class.java) {
|
||||
after {
|
||||
val act = thisObject as 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}")
|
||||
Log.i("Received Activity")
|
||||
|
||||
if (!HookStateHolder.gotContext) {
|
||||
Log.w("Activity created before we got Context, process may have been recreated!")
|
||||
|
|
@ -127,53 +89,20 @@ class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
|
|||
}
|
||||
}
|
||||
|
||||
targetActivityClass.hookMethod("onResume") {
|
||||
after {
|
||||
val act = thisObject as Activity
|
||||
this@Main.onResume(act)
|
||||
}
|
||||
}
|
||||
|
||||
targetActivityClass.hookMethod("onPause") {
|
||||
after {
|
||||
val act = thisObject as Activity
|
||||
this@Main.onPause(act)
|
||||
}
|
||||
}
|
||||
|
||||
this@Main.onLoad(param)
|
||||
|
||||
hooked = true
|
||||
}
|
||||
|
||||
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {
|
||||
dispatch("onLoad") { module -> module.onLoad(packageParam) }
|
||||
for (module in modules) module.onLoad(packageParam)
|
||||
}
|
||||
|
||||
override fun onContext(context: Context) {
|
||||
dispatch("onContext") { module -> module.onContext(context) }
|
||||
for (module in modules) module.onContext(context)
|
||||
}
|
||||
|
||||
override fun onActivity(activity: 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)
|
||||
}
|
||||
}
|
||||
for (module in modules) module.onActivity(activity)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,24 +3,15 @@ package GoonXposed.xposed
|
|||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import de.robv.android.xposed.IXposedHookZygoteInit
|
||||
import de.robv.android.xposed.XC_MethodHook
|
||||
import de.robv.android.xposed.XposedBridge
|
||||
import de.robv.android.xposed.XposedHelpers
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||
import GoonXposed.xposed.Utils.Log
|
||||
import kotlinx.serialization.json.JsonObjectBuilder
|
||||
import java.io.File
|
||||
import java.lang.reflect.Method
|
||||
|
||||
data class AppInfo(
|
||||
val name: String,
|
||||
val packageName: String,
|
||||
val version: String,
|
||||
val versionCode: Long,
|
||||
)
|
||||
|
||||
/**
|
||||
* Base class for every Xposed side module.
|
||||
*
|
||||
|
|
@ -28,66 +19,23 @@ data class AppInfo(
|
|||
* registered [Module] instance. Modules may override any subset of the hooks below.
|
||||
*/
|
||||
open class Module {
|
||||
/**
|
||||
* Builds a JSON payload to be injected into the JavaScript context.
|
||||
*/
|
||||
@Deprecated("This will be removed in future versions. Payloads can be replaced via synchronous bridge methods.")
|
||||
open fun buildPayload(builder: JsonObjectBuilder) {}
|
||||
|
||||
/**
|
||||
* Called during Zygote initialization.
|
||||
*/
|
||||
open fun onInit(startupParam: IXposedHookZygoteInit.StartupParam) {}
|
||||
|
||||
/**
|
||||
* Called when a package is loaded.
|
||||
*
|
||||
* Hooks should typically be set up here.
|
||||
*
|
||||
* Bridge methods that do not require [Context] can also be registered here.
|
||||
*/
|
||||
open fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {}
|
||||
|
||||
/**
|
||||
* Called after [Context] has been attached to a [android.content.ContextWrapper]
|
||||
* via [android.content.ContextWrapper.attachBaseContext].
|
||||
*
|
||||
* This may be called multiple times for different [Context]s, e.g. when the app is killed by the system.
|
||||
*
|
||||
* For bridge methods that require access to [Context], register them here.
|
||||
*/
|
||||
open fun onContext(context: Context) {}
|
||||
|
||||
/**
|
||||
* Called after an [Activity.onCreate] method is executed.
|
||||
*
|
||||
* UI code that requires an [Activity] context can be placed here.
|
||||
*
|
||||
* Bridge methods must not be registered here, as it can be too late and cause crashes.
|
||||
*
|
||||
* If you need to register bridge methods that require an [Activity], do it in [onContext] and make it async.
|
||||
*
|
||||
* Once an [Activity] is available, execute the queued calls. Then from JS, call the method asynchronously.
|
||||
*/
|
||||
open fun onActivity(activity: Activity) {}
|
||||
|
||||
open fun onResume(activity: Activity) {}
|
||||
|
||||
open fun onPause(activity: Activity) {}
|
||||
@Deprecated("This method is deprecated in the parent class")
|
||||
open fun buildPayload(builder: JsonObjectBuilder) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime wrapper around an [XC_MethodHook.MethodHookParam] used as the receiver of every
|
||||
* `before`/`after` hook block, exposing [thisObject], [args], [result] and [param] directly.
|
||||
*/
|
||||
class HookScope(
|
||||
val param: XC_MethodHook.MethodHookParam,
|
||||
private val proceed: ((XC_MethodHook.MethodHookParam) -> Unit)? = null
|
||||
) {
|
||||
fun proceed() {
|
||||
proceed?.invoke(param)
|
||||
}
|
||||
|
||||
class HookScope(val param: XC_MethodHook.MethodHookParam) {
|
||||
val thisObject: Any?
|
||||
get() = param.thisObject
|
||||
|
||||
|
|
@ -132,12 +80,12 @@ class MethodHookBuilder {
|
|||
|
||||
fun build(): XC_MethodHook = object : XC_MethodHook() {
|
||||
override fun beforeHookedMethod(param: MethodHookParam) {
|
||||
val scope = HookScope(param = param, proceed = { p -> super.beforeHookedMethod(p) })
|
||||
val scope = HookScope(param)
|
||||
beforeCallbacks.forEach { scope.run(it) }
|
||||
}
|
||||
|
||||
override fun afterHookedMethod(param: MethodHookParam) {
|
||||
val scope = HookScope(param = param, proceed = { p -> super.afterHookedMethod(p) })
|
||||
val scope = HookScope(param)
|
||||
afterCallbacks.forEach { scope.run(it) }
|
||||
}
|
||||
}
|
||||
|
|
@ -156,27 +104,28 @@ fun ClassLoader.safeLoadClass(name: String): Class<*>? = try {
|
|||
null
|
||||
}
|
||||
|
||||
fun Class<*>.method(name: String, vararg parameterTypes: Class<*>?): Method =
|
||||
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 {
|
||||
fun Method.hook(block: MethodHookBuilder.() -> Unit) {
|
||||
val builder = MethodHookBuilder()
|
||||
builder.block()
|
||||
return XposedBridge.hookMethod(this, builder.build())
|
||||
XposedBridge.hookMethod(this, builder.build())
|
||||
}
|
||||
|
||||
fun Method.hook(hook: XC_MethodHook) {
|
||||
XposedBridge.hookMethod(this, hook)
|
||||
}
|
||||
|
||||
fun Class<*>.hookMethod(
|
||||
name: String,
|
||||
vararg parameterTypes: Class<*>?,
|
||||
vararg parameterTypes: Class<*>,
|
||||
block: MethodHookBuilder.() -> Unit
|
||||
): XC_MethodHook.Unhook? = runCatching {
|
||||
): Method {
|
||||
val method = method(name, *parameterTypes)
|
||||
method.hook(block)
|
||||
}.onFailure {
|
||||
Log.w("Failed to hook method $name on ${this.name}: ${it.message}")
|
||||
}.getOrNull()
|
||||
return method
|
||||
}
|
||||
|
||||
fun File.asDir(): File {
|
||||
if (!exists() || !isDirectory) mkdirs()
|
||||
|
|
@ -188,6 +137,13 @@ fun File.asFile(): File {
|
|||
return this
|
||||
}
|
||||
|
||||
data class AppInfo(
|
||||
val name: String,
|
||||
val packageName: String,
|
||||
val version: String,
|
||||
val versionCode: Int
|
||||
)
|
||||
|
||||
fun Context.getAppInfo(): AppInfo {
|
||||
val label = applicationInfo.loadLabel(packageManager).toString()
|
||||
val packageInfo = try {
|
||||
|
|
@ -196,15 +152,10 @@ fun Context.getAppInfo(): AppInfo {
|
|||
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
|
||||
versionCode = packageInfo?.versionCode ?: 0
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
package GoonXposed.xposed
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.AndroidAppHelper
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import de.robv.android.xposed.XposedHelpers
|
||||
import GoonXposed.xposed.modules.UpdaterModule
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
|
|
@ -14,14 +15,16 @@ class Utils {
|
|||
val JSON = Json { ignoreUnknownKeys = true }
|
||||
|
||||
fun reloadApp() {
|
||||
val application = AndroidAppHelper.currentApplication()
|
||||
val activityThread = XposedHelpers.findClass("android.app.ActivityThread", null)
|
||||
val application =
|
||||
XposedHelpers.callStaticMethod(activityThread, "currentApplication") as? Application ?: return
|
||||
val intent = application.packageManager.getLaunchIntentForPackage(application.packageName)
|
||||
application.startActivity(Intent.makeRestartActivityTask(intent!!.component))
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
fun showRecoveryAlert(context: Context) {
|
||||
AlertDialog.Builder(context).setTitle("ShiggyCord Recovery Options")
|
||||
AlertDialog.Builder(context).setTitle("GoonCord Recovery Options")
|
||||
.setItems(arrayOf("Reload", "Delete Script", "Reset Loader Config")) { _, which ->
|
||||
when (which) {
|
||||
0 -> {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package GoonXposed.xposed.modules
|
||||
|
||||
import GoonXposed.xposed.hookMethod
|
||||
import android.content.res.Resources
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||
import GoonXposed.xposed.Constants.Companion.TARGET_PACKAGE
|
||||
|
|
@ -16,13 +17,7 @@ object FixResourcesModule : Module() {
|
|||
"getIdentifier", String::class.java, String::class.java, String::class.java
|
||||
) {
|
||||
before {
|
||||
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:")
|
||||
}
|
||||
if (args[2] == packageName) args[2] = TARGET_PACKAGE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,6 @@ import GoonXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
|
|||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import java.io.File
|
||||
import java.lang.reflect.Method
|
||||
|
||||
|
|
@ -59,53 +58,46 @@ object HookScriptLoaderModule : Module() {
|
|||
).mapNotNull { classLoader.safeLoadClass(it) }.forEach { hook(it) }
|
||||
}
|
||||
|
||||
private fun hook(instance: Class<*>) {
|
||||
val loadScriptFromAssets = runCatching {
|
||||
instance.method(
|
||||
"loadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType
|
||||
)
|
||||
}.getOrNull()
|
||||
private fun hook(instance: Class<*>) = runCatching {
|
||||
val loadScriptFromAssets = instance.method(
|
||||
"loadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType!!
|
||||
)
|
||||
|
||||
val loadScriptFromFile = runCatching {
|
||||
instance.method(
|
||||
"loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType
|
||||
)
|
||||
}.getOrNull()
|
||||
val loadScriptFromFile = instance.method(
|
||||
"loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType!!
|
||||
)
|
||||
|
||||
loadScriptFromAssets?.hook {
|
||||
loadScriptFromAssets.hook {
|
||||
before {
|
||||
Log.i("Received call to loadScriptFromAssets: ${args[1]} (sync: ${args[2]})")
|
||||
runCustomScripts(loadScriptFromFile, loadScriptFromAssets)
|
||||
}
|
||||
}
|
||||
|
||||
loadScriptFromFile?.hook {
|
||||
loadScriptFromFile.hook {
|
||||
before {
|
||||
Log.i("Received call to loadScriptFromFile: ${args[0]} (sync: ${args[2]})")
|
||||
runCustomScripts(loadScriptFromFile, loadScriptFromAssets)
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
Log.e("Failed to hook script loading methods in ${instance.name}:", it)
|
||||
}
|
||||
|
||||
private fun HookScope.runCustomScripts(loadScriptFromFile: Method?, loadScriptFromAssets: Method?) {
|
||||
private fun HookScope.runCustomScripts(loadScriptFromFile: Method, loadScriptFromAssets: Method) {
|
||||
Log.i("Running custom scripts...")
|
||||
|
||||
runBlocking {
|
||||
val isCustomUrl = UpdaterModule.isCustomUrlEnabled
|
||||
val ready = async { HookStateHolder.readyDeferred.join() }
|
||||
|
||||
if (!mainScript.exists() || isCustomUrl) {
|
||||
val reason = if (isCustomUrl) "Custom URL enabled" else "Main script does not exist"
|
||||
Log.i("$reason, downloading before load...")
|
||||
try {
|
||||
withTimeout(10000L) {
|
||||
UpdaterModule.downloadScript(showUpdateDialog = false).join()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.w("Bundle download did not complete in time", e)
|
||||
}
|
||||
if (!mainScript.exists()) {
|
||||
Log.i("Main script does not exist, downloading before load...")
|
||||
val download = async { UpdaterModule.downloadScript().join() }
|
||||
awaitAll(ready, download)
|
||||
} else {
|
||||
Log.i("Main script exists, updating in background...")
|
||||
UpdaterModule.downloadScript(showUpdateDialog = true)
|
||||
UpdaterModule.downloadScript()
|
||||
ready.await()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -113,48 +105,32 @@ object HookScriptLoaderModule : Module() {
|
|||
val runScriptFile = { file: File ->
|
||||
Log.i("Loading script: ${file.absolutePath}")
|
||||
|
||||
if (loadScriptFromFile != null) {
|
||||
XposedBridge.invokeOriginalMethod(
|
||||
loadScriptFromFile, thisObject, arrayOf(file.absolutePath, file.absolutePath, loadSynchronously)
|
||||
)
|
||||
}
|
||||
XposedBridge.invokeOriginalMethod(
|
||||
loadScriptFromFile, thisObject, arrayOf(file.absolutePath, file.absolutePath, loadSynchronously)
|
||||
)
|
||||
|
||||
Unit
|
||||
}
|
||||
|
||||
try {
|
||||
// If a disabled marker exists next to the cached bundle, treat this as a global
|
||||
// disable for the entire script-loading step. In that case, we skip preloads,
|
||||
// cached main script and the assets fallback so nothing is injected.
|
||||
val disabledMarker = File(mainScript.parentFile, "${Constants.MAIN_SCRIPT_FILE}.disabled")
|
||||
if (disabledMarker.exists()) {
|
||||
Log.i("Script loading disabled by marker; skipping preloads, cached bundle and fallback")
|
||||
} else {
|
||||
// Normal behaviour: run preloads then cached main script or fallback to bundled asset
|
||||
preloadsDir.walk().filter { it.isFile }.forEach(runScriptFile)
|
||||
|
||||
if (mainScript.exists()) {
|
||||
runScriptFile(mainScript)
|
||||
} else {
|
||||
Log.i("Main script does not exist, checking for fallback asset")
|
||||
Log.i("Main script does not exist, falling back")
|
||||
|
||||
if (!::resources.isInitialized) resources = XModuleResources.createInstance(modulePath, null)
|
||||
|
||||
var hasFallback = false
|
||||
try {
|
||||
resources.assets.open("Shiggy.bundle").use { hasFallback = true }
|
||||
} catch (_: Throwable) {}
|
||||
|
||||
if (hasFallback && loadScriptFromAssets != null) {
|
||||
Log.i("Loading fallback assets://Shiggy.bundle")
|
||||
XposedBridge.invokeOriginalMethod(
|
||||
loadScriptFromAssets,
|
||||
thisObject,
|
||||
arrayOf(resources.assets, "assets://Shiggy.bundle", loadSynchronously)
|
||||
)
|
||||
} else {
|
||||
Log.w("No fallback bundle asset found; letting host app load default JS")
|
||||
}
|
||||
XposedBridge.invokeOriginalMethod(
|
||||
loadScriptFromAssets,
|
||||
thisObject,
|
||||
arrayOf(resources.assets, "assets://Shiggy.bundle", loadSynchronously)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ object LogBoxActions {
|
|||
val config = JSONObject(configFile.readText())
|
||||
val customLoadUrl = config.optJSONObject("customLoadUrl") ?: JSONObject()
|
||||
customLoadUrl.put("enabled", false)
|
||||
customLoadUrl.put("url", "http://localhost:4040/shiggycord.js")
|
||||
customLoadUrl.put("url", "http://localhost:4040/gooncord.js")
|
||||
config.put("customLoadUrl", customLoadUrl)
|
||||
configFile.writeText(config.toString())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package GoonXposed.xposed.modules.LogBox
|
|||
import GoonXposed.xposed.Module
|
||||
import GoonXposed.xposed.Utils.Log
|
||||
import GoonXposed.xposed.Utils.Companion.reloadApp
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import de.robv.android.xposed.XC_MethodHook
|
||||
import de.robv.android.xposed.XC_MethodReplacement
|
||||
|
|
@ -11,57 +10,33 @@ import de.robv.android.xposed.XposedBridge
|
|||
import de.robv.android.xposed.XposedHelpers
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||
import kotlinx.coroutines.*
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
object LogBoxModule : Module() {
|
||||
lateinit var packageParam: XC_LoadPackage.LoadPackageParam
|
||||
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
var contextForMenu: Context? = null
|
||||
private var currentActivity: WeakReference<Activity>? = null
|
||||
|
||||
private val shakeDetector = ShakeDetector {
|
||||
val act = currentActivity?.get() ?: return@ShakeDetector
|
||||
act.runOnUiThread {
|
||||
try {
|
||||
Log.i("Shake detected! Opening Recovery Menu...")
|
||||
LogBoxNavigation.showRecoveryMenu(act)
|
||||
} catch (e: Throwable) {
|
||||
Log.e("Failed to show recovery menu on shake: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivity(activity: Activity) {
|
||||
currentActivity = WeakReference(activity)
|
||||
contextForMenu = activity
|
||||
}
|
||||
|
||||
override fun onResume(activity: Activity) {
|
||||
currentActivity = WeakReference(activity)
|
||||
contextForMenu = activity
|
||||
try {
|
||||
shakeDetector.start(activity)
|
||||
Log.i("ShakeDetector started for activity: ${activity.javaClass.simpleName}")
|
||||
} catch (e: Throwable) {
|
||||
Log.e("Failed to start ShakeDetector: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause(activity: Activity) {
|
||||
try {
|
||||
shakeDetector.stop()
|
||||
Log.i("ShakeDetector stopped")
|
||||
} catch (e: Throwable) {
|
||||
Log.e("Failed to stop ShakeDetector: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
||||
this@LogBoxModule.packageParam = packageParam
|
||||
|
||||
// Do not force getUseDeveloperSupport = true, as it causes React Native to loop
|
||||
// attempting to connect to a Metro bundler on localhost:8081 and hang the app loading screen.
|
||||
// Shake detector and recovery menu work independently without it.
|
||||
try {
|
||||
val dcdReactNativeHostClass = classLoader.loadClass("com.discord.bridge.DCDReactNativeHost")
|
||||
val getUseDeveloperSupportMethod =
|
||||
dcdReactNativeHostClass.methods.first { it.name == "getUseDeveloperSupport" }
|
||||
|
||||
// NOTE: This used to call a custom `.hook { before { result = true } }` DSL that
|
||||
// relied on a shared extensions file which broke (unresolved references).
|
||||
// Rewritten using the standard XposedBridge/XC_MethodHook API, which is already
|
||||
// used elsewhere in this file, to force this method to always return true.
|
||||
XposedBridge.hookMethod(getUseDeveloperSupportMethod, object : XC_MethodHook() {
|
||||
override fun beforeHookedMethod(param: MethodHookParam) {
|
||||
param.result = true
|
||||
}
|
||||
})
|
||||
Log.e("Successfully hooked DCDReactNativeHost")
|
||||
} catch (e: Exception) {
|
||||
Log.e("Failed to hook DCDReactNativeHost: ${e.message}")
|
||||
}
|
||||
|
||||
return@with
|
||||
}
|
||||
|
|
@ -151,7 +126,7 @@ object LogBoxModule : Module() {
|
|||
Log.e("Failed to get context from DevSupport (non-fatal): ${e.message}")
|
||||
}
|
||||
|
||||
val finalContext = currentActivity?.get() ?: activityContext ?: contextForMenu ?: context
|
||||
val finalContext = activityContext ?: contextForMenu ?: context
|
||||
Log.e("Using context: $finalContext (type: ${finalContext.javaClass.name})")
|
||||
|
||||
LogBoxNavigation.showRecoveryMenu(finalContext)
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -9,7 +9,6 @@ import GoonXposed.xposed.Constants
|
|||
import GoonXposed.xposed.MethodHookBuilder
|
||||
import GoonXposed.xposed.Module
|
||||
import GoonXposed.xposed.Utils.Companion.JSON
|
||||
import GoonXposed.xposed.Utils.Log
|
||||
import GoonXposed.xposed.hook
|
||||
import GoonXposed.xposed.method
|
||||
import GoonXposed.xposed.safeLoadClass
|
||||
|
|
@ -28,11 +27,7 @@ class PayloadGlobalModule(private val modules: List<Module>) : Module() {
|
|||
buildJsonObject {
|
||||
put("loaderName", Constants.LOADER_NAME)
|
||||
put("loaderVersion", BuildConfig.VERSION_NAME)
|
||||
@Suppress("DEPRECATION") for (module in modules) runCatching {
|
||||
module.buildPayload(this)
|
||||
}.onFailure {
|
||||
Log.e("Module ${module.javaClass.simpleName} failed while building payload", it)
|
||||
}
|
||||
@Suppress("DEPRECATION") for (module in modules) module.buildPayload(this)
|
||||
})
|
||||
|
||||
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
||||
|
|
@ -57,7 +52,7 @@ class PayloadGlobalModule(private val modules: List<Module>) : Module() {
|
|||
"loadScriptFromFile",
|
||||
String::class.java,
|
||||
String::class.java,
|
||||
Boolean::class.javaPrimitiveType
|
||||
Boolean::class.javaPrimitiveType!!
|
||||
), param.thisObject, arrayOf(absolutePath, absolutePath, param.args[2])
|
||||
)
|
||||
|
||||
|
|
@ -79,11 +74,11 @@ class PayloadGlobalModule(private val modules: List<Module>) : Module() {
|
|||
|
||||
private fun hook(instance: Class<*>, hook: XC_MethodHook) {
|
||||
instance.method(
|
||||
"loadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType,
|
||||
"loadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType!!,
|
||||
).hook(hook)
|
||||
|
||||
instance.method(
|
||||
"loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType
|
||||
"loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType!!
|
||||
).hook(hook)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,20 +25,293 @@ object PerfPatchesModule : Module() {
|
|||
|
||||
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {
|
||||
try {
|
||||
val filesDir = File(packageParam.appInfo.dataDir, Constants.FILES_DIR)
|
||||
val preloadsDir = File(filesDir, HookScriptLoaderModule.PRELOADS_DIR)
|
||||
// Build preloads path inside the app data files directory
|
||||
val filesDir = File(packageParam.appInfo.dataDir, Constants.FILES_DIR).apply { mkdirs() }
|
||||
val preloadsDir = File(filesDir, HookScriptLoaderModule.PRELOADS_DIR).apply { mkdirs() }
|
||||
val out = File(preloadsDir, FILE_NAME)
|
||||
if (out.exists()) {
|
||||
out.delete()
|
||||
Log.i("PerfPatchesModule: deleted legacy perf_patches.js")
|
||||
|
||||
// Defensive JS runtime patch. Avoid template literals with ${} to prevent Kotlin interpolation.
|
||||
val js = """
|
||||
(function(){
|
||||
try {
|
||||
if (!globalThis.__SHIGGY_PERF_PATCHES__) {
|
||||
globalThis.__SHIGGY_PERF_PATCHES__ = { installedAt: Date.now(), enabled: true };
|
||||
}
|
||||
|
||||
function safe(fn) {
|
||||
return function() {
|
||||
try { return fn.apply(this, arguments); }
|
||||
catch (e) { try { console && console.warn && console.warn('shiggy patch error', e); } catch(_){} }
|
||||
};
|
||||
}
|
||||
|
||||
function memoize(fn, maxEntries) {
|
||||
if (typeof fn !== 'function') return fn;
|
||||
var cache = new Map();
|
||||
maxEntries = Number(maxEntries) || 128;
|
||||
return function() {
|
||||
var key;
|
||||
try { key = JSON.stringify(Array.prototype.slice.call(arguments)); }
|
||||
catch (e) { key = String(arguments.length) + ':' + String(arguments[0]); }
|
||||
if (cache.has(key)) return cache.get(key);
|
||||
var res = fn.apply(this, arguments);
|
||||
cache.set(key, res);
|
||||
if (cache.size > maxEntries) {
|
||||
var it = cache.keys().next();
|
||||
if (!it.done) cache.delete(it.value);
|
||||
}
|
||||
return res;
|
||||
};
|
||||
}
|
||||
|
||||
function createBatched(fn, intervalMs) {
|
||||
if (typeof fn !== 'function') return fn;
|
||||
var buf = [];
|
||||
var timer = null;
|
||||
intervalMs = Number(intervalMs) || 2000;
|
||||
function flush() {
|
||||
var batch = buf.splice(0, buf.length);
|
||||
if (!batch.length) return;
|
||||
try { fn.call(null, batch); }
|
||||
catch (e) {
|
||||
// Fallback: call per event asynchronously
|
||||
try {
|
||||
batch.forEach(function(ev){
|
||||
setTimeout(function(){ try { fn(ev); } catch (_) {} }, 0);
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
return function(event) {
|
||||
try {
|
||||
buf.push(event);
|
||||
if (!timer) timer = setTimeout(function(){ timer = null; flush(); }, intervalMs);
|
||||
} catch (e) {}
|
||||
};
|
||||
}
|
||||
|
||||
function patchIfExists(path, wrapper) {
|
||||
try {
|
||||
var parts = path.split('.');
|
||||
var obj = globalThis;
|
||||
for (var i = 0; i < parts.length - 1; i++) {
|
||||
obj = obj && obj[parts[i]];
|
||||
}
|
||||
if (!obj) return false;
|
||||
var key = parts[parts.length - 1];
|
||||
if (typeof obj[key] !== 'function') return false;
|
||||
obj[key] = wrapper(obj[key]);
|
||||
return true;
|
||||
} catch (e) { return false; }
|
||||
}
|
||||
|
||||
// Try memoizing a few well-known functions
|
||||
['computeHappeningNowState', 'computeNowState'].forEach(function(name){
|
||||
try { patchIfExists(name, function(orig){ return memoize(orig, 128); }); } catch(_) {}
|
||||
});
|
||||
|
||||
// Try caching RowGenerator.prototype.generate by instance signature
|
||||
try {
|
||||
var RG = globalThis && globalThis.RowGenerator;
|
||||
if (RG && RG.prototype && typeof RG.prototype.generate === 'function') {
|
||||
(function(){
|
||||
var orig = RG.prototype.generate;
|
||||
RG.prototype.generate = function() {
|
||||
try {
|
||||
var sig;
|
||||
if (arguments && arguments.length) {
|
||||
var a0 = arguments[0];
|
||||
if (Array.isArray(a0)) {
|
||||
sig = 'arr:' + a0.length + ':' + a0.slice(0,8).map(function(x){ return x && x.id ? x.id : ''; }).join(',');
|
||||
} else if (a0 && a0.id) {
|
||||
sig = 'id:' + a0.id;
|
||||
} else {
|
||||
try { sig = JSON.stringify(a0).slice(0,200); } catch(e){ sig = String(a0); }
|
||||
}
|
||||
} else sig = 'noargs';
|
||||
|
||||
if (this.__shiggy_sig === sig && typeof this.__shiggy_cache !== 'undefined') {
|
||||
return this.__shiggy_cache;
|
||||
}
|
||||
var res = orig.apply(this, arguments);
|
||||
this.__shiggy_sig = sig;
|
||||
this.__shiggy_cache = res;
|
||||
return res;
|
||||
} catch (e) { return orig.apply(this, arguments); }
|
||||
};
|
||||
})();
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// Batch analytics-like methods (best-effort)
|
||||
['app_ui_viewed', 'logEvent', 'Analytics.log', 'Analytics.track', 'analytics.trackEvent'].forEach(function(name){
|
||||
try {
|
||||
var parts = name.split('.');
|
||||
var obj = globalThis;
|
||||
for (var i = 0; i < parts.length - 1; i++) obj = obj && obj[parts[i]];
|
||||
if (!obj) return;
|
||||
var key = parts[parts.length - 1];
|
||||
if (typeof obj[key] === 'function') obj[key] = createBatched(obj[key], 2000);
|
||||
} catch (e) {}
|
||||
});
|
||||
|
||||
// Make console methods async to avoid blocking hot-paths during startup
|
||||
(function(){
|
||||
try {
|
||||
if (typeof console !== 'undefined') {
|
||||
['log','info','warn','error','debug','trace'].forEach(function(m){
|
||||
try {
|
||||
if (typeof console[m] === 'function') {
|
||||
var orig = console[m];
|
||||
console[m] = function() {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
setTimeout(function(){ try { orig.apply(console, args); } catch(_){} }, 0);
|
||||
};
|
||||
}
|
||||
} catch(e){}
|
||||
});
|
||||
}
|
||||
} catch(e){}
|
||||
})();
|
||||
|
||||
// Deduplicate and slightly delay repeated kv calls to reduce IO contention on startup
|
||||
(function(){
|
||||
try {
|
||||
var pending = new Map();
|
||||
// Lightweight stats for dedupe wrapper to help telemetry/debugging in-case we need to tune delays.
|
||||
try {
|
||||
globalThis.__SHIGGY_PERF_DEDUPE_STATS__ = globalThis.__SHIGGY_PERF_DEDUPE_STATS__ || {
|
||||
methods: {},
|
||||
totalRequests: 0,
|
||||
totalHits: 0,
|
||||
totalMisses: 0,
|
||||
pending: 0,
|
||||
lastUpdated: Date.now()
|
||||
};
|
||||
} catch(e) {}
|
||||
|
||||
function _incStats(name, key) {
|
||||
try {
|
||||
var s = globalThis.__SHIGGY_PERF_DEDUPE_STATS__;
|
||||
if (!s) return;
|
||||
s.lastUpdated = Date.now();
|
||||
var m = s.methods[name] || (s.methods[name] = { requests: 0, hits: 0, misses: 0, pending: 0 });
|
||||
if (key === 'requests') { m.requests++; s.totalRequests++; }
|
||||
else if (key === 'hits') { m.hits++; s.totalHits++; }
|
||||
else if (key === 'misses') { m.misses++; s.totalMisses++; }
|
||||
else if (key === 'pending_inc') { m.pending++; s.pending++; }
|
||||
else if (key === 'pending_dec') { m.pending = Math.max(0, m.pending - 1); s.pending = Math.max(0, s.pending - 1); }
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
function wrapAsyncDedupe(kvObj, methodName, delayMs){
|
||||
try {
|
||||
var orig = kvObj[methodName];
|
||||
if (typeof orig !== 'function') return;
|
||||
kvObj[methodName] = function() {
|
||||
var argsArr = Array.prototype.slice.call(arguments);
|
||||
var key;
|
||||
try { key = JSON.stringify([methodName, argsArr]); } catch(e){ key = methodName + ':' + String(argsArr[0]); }
|
||||
_incStats(methodName, 'requests');
|
||||
if (pending.has(key)) { _incStats(methodName, 'hits'); return pending.get(key); }
|
||||
_incStats(methodName, 'misses');
|
||||
var self = this;
|
||||
var p = new Promise(function(resolve, reject){
|
||||
setTimeout(function(){
|
||||
try {
|
||||
var res = orig.apply(self, argsArr);
|
||||
if (res && typeof res.then === 'function') res.then(resolve, reject);
|
||||
else resolve(res);
|
||||
} catch (e) { reject(e); }
|
||||
}, Number(delayMs) || 20);
|
||||
});
|
||||
pending.set(key, p);
|
||||
_incStats(methodName, 'pending_inc');
|
||||
var cleanup = function(){ pending.delete(key); _incStats(methodName, 'pending_dec'); };
|
||||
p.then(cleanup, cleanup);
|
||||
return p;
|
||||
};
|
||||
} catch(e){}
|
||||
}
|
||||
if (globalThis.kv) {
|
||||
if (globalThis.kv.get_many) wrapAsyncDedupe(globalThis.kv, 'get_many', 20);
|
||||
if (globalThis.kv.get_kv_entries) wrapAsyncDedupe(globalThis.kv, 'get_kv_entries', 20);
|
||||
}
|
||||
} catch(e){}
|
||||
})();
|
||||
|
||||
// Defer heavy filesystem/storage calls slightly to prioritize UI rendering
|
||||
(function(){
|
||||
try {
|
||||
function makeDeferredIfAsync(obj, name, delayMs) {
|
||||
try {
|
||||
var orig = obj[name];
|
||||
if (typeof orig !== 'function') return;
|
||||
var isAsync = orig.constructor && orig.constructor.name === 'AsyncFunction';
|
||||
var src = String(orig);
|
||||
if (!isAsync && src.indexOf('return new Promise') === -1 && src.indexOf('.then(') === -1) {
|
||||
// Likely synchronous - skip deferral to avoid changing semantics
|
||||
return;
|
||||
}
|
||||
obj[name] = function() {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
var self = this;
|
||||
return new Promise(function(resolve, reject){
|
||||
setTimeout(function(){
|
||||
try {
|
||||
var res = orig.apply(self, args);
|
||||
if (res && typeof res.then === 'function') res.then(resolve, reject);
|
||||
else resolve(res);
|
||||
} catch (e) { reject(e); }
|
||||
}, Number(delayMs) || 100);
|
||||
});
|
||||
};
|
||||
} catch(e){}
|
||||
}
|
||||
if (globalThis.db && typeof globalThis.db.fs_info === 'function') {
|
||||
makeDeferredIfAsync(globalThis.db, 'fs_info', 100);
|
||||
}
|
||||
if (globalThis.Storage && typeof globalThis.Storage.refresh === 'function') {
|
||||
makeDeferredIfAsync(globalThis.Storage, 'refresh', 50);
|
||||
}
|
||||
} catch(e){}
|
||||
})();
|
||||
|
||||
// Expose lightweight API
|
||||
try {
|
||||
if (!globalThis.__SHIGGY_PERF_API__) {
|
||||
globalThis.__SHIGGY_PERF_API__ = {
|
||||
installedAt: Date.now(),
|
||||
enabled: true,
|
||||
uninstall: function() { try { globalThis.__SHIGGY_PERF_API__.enabled = false; } catch(e) {} }
|
||||
};
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
} catch (e) {
|
||||
try { console && console.warn && console.warn('PerfPatches: error', e); } catch(_) {}
|
||||
}
|
||||
})();
|
||||
""".trimIndent()
|
||||
|
||||
// Write file if absent or changed
|
||||
try {
|
||||
if (!out.exists() || out.readText() != js) {
|
||||
out.writeText(js)
|
||||
Log.i("PerfPatchesModule: wrote patch to " + out.absolutePath)
|
||||
} else {
|
||||
Log.i("PerfPatchesModule: perf patch already up-to-date")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e("PerfPatchesModule: failed writing patch", e)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e("PerfPatchesModule: cleanup failed", e)
|
||||
Log.e("PerfPatchesModule:onLoad failed", e)
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Overrides deprecated Module.buildPayload")
|
||||
override fun buildPayload(builder: JsonObjectBuilder) {
|
||||
builder.put("perfPatches", false)
|
||||
builder.put("perfPatches", true)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,9 +56,6 @@ object UpdaterModule : Module() {
|
|||
private const val ETAG_FILE = "etag.txt"
|
||||
private const val CONFIG_FILE = "loader.json"
|
||||
|
||||
val isCustomUrlEnabled: Boolean
|
||||
get() = if (::config.isInitialized) config.customLoadUrl.enabled else false
|
||||
|
||||
private const val DEFAULT_BUNDLE_URL =
|
||||
"https://github.com/OTKUSteyler/GoonCord/releases/latest/download/gooncord.js"
|
||||
|
||||
|
|
@ -80,7 +77,7 @@ object UpdaterModule : Module() {
|
|||
}.getOrDefault(LoaderConfig())
|
||||
}
|
||||
|
||||
fun downloadScript(activity: Activity? = null, showUpdateDialog: Boolean = true): Job = scope.launch {
|
||||
fun downloadScript(activity: Activity? = null): Job = scope.launch {
|
||||
try {
|
||||
// Respect LogBox setting to disable bundle injections.
|
||||
// If the file "files/logbox/LOGBOX_SETTINGS" contains the flag bundleInjectionDisabled=true,
|
||||
|
|
@ -132,6 +129,7 @@ object UpdaterModule : Module() {
|
|||
|
||||
Log.i("Bundle updated (${bytes.size} bytes)")
|
||||
|
||||
// This is a retry, so we show a dialog
|
||||
if (activity != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
AlertDialog.Builder(activity).setTitle("GoonCord Update Successful")
|
||||
|
|
@ -141,22 +139,6 @@ object UpdaterModule : Module() {
|
|||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package GoonXposed.xposed.modules.appearance
|
||||
|
||||
import android.content.res.AssetManager
|
||||
|
|
@ -13,8 +15,6 @@ import GoonXposed.xposed.Constants
|
|||
import GoonXposed.xposed.Module
|
||||
import GoonXposed.xposed.Utils.Companion.JSON
|
||||
import GoonXposed.xposed.Utils.Log
|
||||
import GoonXposed.xposed.asDir
|
||||
import GoonXposed.xposed.asFile
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.engine.cio.*
|
||||
|
|
@ -29,6 +29,14 @@ import kotlinx.serialization.json.put
|
|||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
// NOTE: These extensions used to be resolved from a shared Utils/Extensions file,
|
||||
// but that reference broke (renamed/removed/relocated), causing "Unresolved reference"
|
||||
// build failures. Defining them locally here removes the dependency on that file.
|
||||
// asFile(): ensures the parent directory of a file exists.
|
||||
// asDir(): ensures the directory itself exists.
|
||||
private fun File.asFile(): File = apply { parentFile?.mkdirs() }
|
||||
private fun File.asDir(): File = apply { mkdirs() }
|
||||
|
||||
@Serializable
|
||||
data class FontDefinition(
|
||||
val name: String? = null,
|
||||
|
|
@ -42,11 +50,6 @@ object FontsModule : Module() {
|
|||
private val FILE_EXTENSIONS = arrayOf(".ttf", ".otf")
|
||||
private const val FONTS_ASSET_PATH = "fonts/"
|
||||
|
||||
private val FONT_MANAGER_CLASSES = arrayOf(
|
||||
"com.facebook.react.views.text.ReactFontManager\$Companion",
|
||||
"com.facebook.react.common.assets.ReactFontManager\$Companion",
|
||||
)
|
||||
|
||||
private lateinit var fontsDir: File
|
||||
private lateinit var fontsDownloadsDir: File
|
||||
private var fontsAbsPath: String? = null
|
||||
|
|
@ -58,33 +61,21 @@ object FontsModule : Module() {
|
|||
}
|
||||
|
||||
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
||||
val replacement = object : XC_MethodReplacement() {
|
||||
override fun replaceHookedMethod(param: MethodHookParam): Typeface? {
|
||||
val fontFamilyName: String = param.args[0].toString()
|
||||
val style: Int = param.args[1] as Int
|
||||
val assetManager: AssetManager = param.args[2] as AssetManager
|
||||
return createAssetTypeface(fontFamilyName, style, assetManager)
|
||||
}
|
||||
}
|
||||
|
||||
// React Native moved ReactFontManager between releases, so try every known package.
|
||||
// Never let this throw: an uncaught error here aborts the whole module chain.
|
||||
val hooked = FONT_MANAGER_CLASSES.any { className ->
|
||||
runCatching {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
className,
|
||||
classLoader,
|
||||
"createAssetTypeface",
|
||||
String::class.java,
|
||||
Int::class.javaPrimitiveType ?: java.lang.Integer.TYPE,
|
||||
"android.content.res.AssetManager",
|
||||
replacement
|
||||
)
|
||||
}.onFailure { Log.e("Failed to hook createAssetTypeface on $className", it) }
|
||||
.isSuccess
|
||||
}
|
||||
|
||||
if (!hooked) Log.e("Failed to hook createAssetTypeface on all known ReactFontManager classes")
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"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? {
|
||||
val fontFamilyName: String = param.args[0].toString()
|
||||
val style: Int = param.args[1] as Int
|
||||
val assetManager: AssetManager = param.args[2] as AssetManager
|
||||
return createAssetTypeface(fontFamilyName, style, assetManager)
|
||||
}
|
||||
})
|
||||
|
||||
val fontDefFile = File(appInfo.dataDir, "${Constants.FILES_DIR}/fonts.json").apply { asFile() }
|
||||
if (!fontDefFile.exists()) return@with
|
||||
|
|
@ -112,28 +103,40 @@ object FontsModule : Module() {
|
|||
|
||||
// These files should be downloaded by the JS side, but oh well
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
fontDef.main.keys.map { name ->
|
||||
async {
|
||||
val url = fontDef.main.getValue(name)
|
||||
try {
|
||||
Log.i("Downloading $name from $url")
|
||||
val file =
|
||||
File(fontsDir, "$name${FILE_EXTENSIONS.first { url.endsWith(it) }}").apply { asFile() }
|
||||
if (file.exists()) return@async
|
||||
|
||||
val client = HttpClient(CIO) {
|
||||
install(UserAgent) { agent = Constants.USER_AGENT }
|
||||
}
|
||||
|
||||
val response: HttpResponse = client.get(url)
|
||||
if (response.status == HttpStatusCode.OK) file.writeBytes(response.body())
|
||||
|
||||
return@async
|
||||
} catch (e: Throwable) {
|
||||
Log.e("Failed to download fonts ($name from $url)", e)
|
||||
}
|
||||
val client = HttpClient(CIO) {
|
||||
install(UserAgent) { agent = Constants.USER_AGENT }
|
||||
install(HttpTimeout) {
|
||||
requestTimeoutMillis = 15_000
|
||||
connectTimeoutMillis = 10_000
|
||||
socketTimeoutMillis = 15_000
|
||||
}
|
||||
}.awaitAll()
|
||||
}
|
||||
|
||||
try {
|
||||
fontDef.main.keys.map { name ->
|
||||
async {
|
||||
val url = fontDef.main.getValue(name)
|
||||
try {
|
||||
val extension = FILE_EXTENSIONS.firstOrNull { url.substringBefore('?').endsWith(it) }
|
||||
?: FILE_EXTENSIONS[0]
|
||||
val file = File(fontsDir, "$name$extension").apply { asFile() }
|
||||
if (file.exists()) return@async
|
||||
|
||||
Log.i("Downloading $name from $url")
|
||||
val response: HttpResponse = client.get(url)
|
||||
if (response.status == HttpStatusCode.OK) {
|
||||
file.writeBytes(response.body())
|
||||
} else {
|
||||
Log.e("Failed to download fonts ($name from $url): HTTP ${response.status}")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e("Failed to download fonts ($name from $url)", e)
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
} finally {
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package GoonXposed.xposed.modules.appearance
|
||||
|
||||
import android.R.color
|
||||
import android.app.AndroidAppHelper
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import de.robv.android.xposed.XposedHelpers
|
||||
import GoonXposed.xposed.Module
|
||||
import kotlinx.serialization.json.*
|
||||
import java.lang.ref.WeakReference
|
||||
|
|
@ -13,8 +13,13 @@ object SysColorsModule : Module() {
|
|||
private lateinit var context: WeakReference<Context>
|
||||
private fun isSupported() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
|
||||
@Deprecated("This method is deprecated in the parent class")
|
||||
override fun buildPayload(builder: JsonObjectBuilder) {
|
||||
context = WeakReference(AndroidAppHelper.currentApplication())
|
||||
context = WeakReference(runCatching {
|
||||
XposedHelpers.callStaticMethod(
|
||||
XposedHelpers.findClass("android.app.ActivityThread", null), "currentApplication"
|
||||
) as Context
|
||||
}.getOrNull())
|
||||
val accents = arrayOf("accent1", "accent2", "accent3", "neutral1", "neutral2")
|
||||
val shades = arrayOf(0, 10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000)
|
||||
|
||||
|
|
@ -37,7 +42,8 @@ object SysColorsModule : Module() {
|
|||
}
|
||||
|
||||
private fun convertToColor(id: Int): String {
|
||||
val clr = if (isSupported()) ContextCompat.getColor(context.get()!!, id) else 0
|
||||
val ctx = context.get()
|
||||
val clr = if (isSupported() && ctx != null) ContextCompat.getColor(ctx, id) else 0
|
||||
return String.format("#%06X", 0xFFFFFF and clr)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,20 +86,18 @@ object ThemesModule : Module() {
|
|||
|
||||
fun hookTheme() {
|
||||
try {
|
||||
val themeManager = runCatching { param.classLoader.loadClass("com.discord.theme.utils.ColorUtilsKt") }.getOrNull()
|
||||
val darkTheme = runCatching { param.classLoader.loadClass("com.discord.theme.DarkTheme") }.getOrNull()
|
||||
?: runCatching { param.classLoader.loadClass("com.discord.theme.DarkerTheme") }.getOrNull()
|
||||
val onyxTheme = runCatching { param.classLoader.loadClass("com.discord.theme.OnyxTheme") }.getOrNull()
|
||||
val lightTheme = runCatching { param.classLoader.loadClass("com.discord.theme.LightTheme") }.getOrNull()
|
||||
val themeManager = param.classLoader.loadClass("com.discord.theme.utils.ColorUtilsKt")
|
||||
val darkTheme = param.classLoader.loadClass("com.discord.theme.DarkerTheme")
|
||||
val lightTheme = param.classLoader.loadClass("com.discord.theme.LightTheme")
|
||||
|
||||
hookThemeInternal(themeManager, darkTheme, onyxTheme, lightTheme)
|
||||
hookThemeInternal(themeManager, darkTheme, 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<*>?
|
||||
themeManager: Class<*>, darkTheme: Class<*>, lightTheme: Class<*>
|
||||
) {
|
||||
val theme = this.theme ?: return
|
||||
|
||||
|
|
@ -113,36 +111,26 @@ object ThemesModule : Module() {
|
|||
// TEXT_NORMAL -> getTextNormal
|
||||
val methodName = "get${key.fromScreamingSnakeToCamelCase()}"
|
||||
value.forEachIndexed { index, v ->
|
||||
val colorInt = hexStringToColorInt(v)
|
||||
when (index) {
|
||||
0 -> {
|
||||
if (darkTheme != null) hookThemeMethod(darkTheme, methodName, colorInt)
|
||||
if (onyxTheme != null) hookThemeMethod(onyxTheme, methodName, colorInt)
|
||||
}
|
||||
1 -> {
|
||||
if (lightTheme != null) hookThemeMethod(lightTheme, methodName, colorInt)
|
||||
}
|
||||
0 -> hookThemeMethod(darkTheme, methodName, hexStringToColorInt(v))
|
||||
1 -> hookThemeMethod(lightTheme, methodName, hexStringToColorInt(v))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there's any rawColors value, hook the color getter
|
||||
if (!theme.data.rawColors.isNullOrEmpty() && themeManager != null) {
|
||||
if (!theme.data.rawColors.isNullOrEmpty()) {
|
||||
try {
|
||||
val getColorCompat = runCatching {
|
||||
themeManager.getDeclaredMethod(
|
||||
"getColorCompat",
|
||||
Resources::class.java,
|
||||
Int::class.javaPrimitiveType,
|
||||
Resources.Theme::class.java,
|
||||
)
|
||||
}.getOrNull()
|
||||
val getColorCompat = themeManager.getDeclaredMethod(
|
||||
"getColorCompat",
|
||||
Resources::class.java,
|
||||
Int::class.javaPrimitiveType,
|
||||
Resources.Theme::class.java,
|
||||
)
|
||||
|
||||
val getColorCompatLegacy = runCatching {
|
||||
themeManager.getDeclaredMethod(
|
||||
"getColorCompat", Context::class.java, Int::class.javaPrimitiveType
|
||||
)
|
||||
}.getOrNull()
|
||||
val getColorCompatLegacy = themeManager.getDeclaredMethod(
|
||||
"getColorCompat", Context::class.java, Int::class.javaPrimitiveType
|
||||
)
|
||||
|
||||
val patch = MethodHookBuilder().run {
|
||||
before {
|
||||
|
|
@ -159,8 +147,8 @@ object ThemesModule : Module() {
|
|||
build()
|
||||
}
|
||||
|
||||
getColorCompat?.hook(patch)
|
||||
getColorCompatLegacy?.hook(patch)
|
||||
getColorCompat.hook(patch)
|
||||
getColorCompatLegacy.hook(patch)
|
||||
} catch (e: Throwable) {
|
||||
GoonXposed.xposed.Utils.Log.e("Failed to hook getColorCompat: ${e.message}")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
<?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>
|
||||
Loading…
Add table
Reference in a new issue