Compare commits

..

367 commits

Author SHA1 Message Date
OTKUSteyler
a707e6fd05
Update UpdaterModule.kt
Some checks failed
Build APK / Build and push (push) Failing after 6m51s
2026-09-12 15:55:10 +02:00
OTKUSteyler
d55ed5221c
Update LogBoxModule.kt 2026-09-12 12:52:36 +02:00
OTKUSteyler
a5af0bddb8
Update FontsModule.kt 2026-09-12 12:41:28 +02:00
OTKUSteyler
9611038048
Update FontsModule.kt 2026-09-12 12:33:34 +02:00
OTKUSteyler
06dde39cfd
Update LogBoxModule.kt 2026-09-12 12:25:15 +02:00
OTKUSteyler
69496c1f3c Revert "Fix: Add missing LogBoxModule and onActivity hook - fixes blank plugin/theme tabs"
This reverts commit 1a9fd61ec3.
2026-09-12 11:27:48 +02:00
OTKUSteyler
ce6f201fcd
Update FontsModule.kt 2026-09-12 10:13:49 +02:00
OTKUSteyler
73a93a29bb
Update Main.kt 2026-09-12 10:10:19 +02:00
OTKUSteyler
ee7e30e67c
Update FontsModule.kt 2026-09-12 10:08:55 +02:00
OTKUSteyler
57ebd3daed
Update LogBoxModule.kt 2026-09-11 22:12:32 +02:00
OTKUSteyler
82214b6ed0
Update FontsModule.kt 2026-09-11 21:48:46 +02:00
OTKUSteyler
2b9956d97b
Update LogBoxModule.kt 2026-09-10 17:47:17 +02:00
OTKUSteyler
b2c7e23a57
Update FontsModule.kt 2026-09-10 17:15:27 +02:00
OTKUSteyler
468311bbba
Update LogBoxModule.kt 2026-09-10 17:15:05 +02:00
OTKUSteyler
7f90a61ca5
Delete app/src/main/kotlin/GoonXposed/xposed/xposed/a 2026-09-10 16:58:53 +02:00
OTKUSteyler
17e1b9220f
Create a 2026-09-10 16:57:51 +02:00
OTKUSteyler
34b4a9f30c
Delete app/src/main/kotlin/GoonXposed/xposed/xposed/a 2026-09-10 16:56:10 +02:00
OTKUSteyler
abb1c6ddd8
Create a 2026-09-10 16:55:55 +02:00
OTKUSteyler
32421df002
Update Main.kt 2026-09-10 15:20:01 +02:00
OTKUSteyler
f16af054be
Update Utils.kt 2026-09-10 15:11:45 +02:00
OTKUSteyler
3a6abc9a79
Update FontsModule.kt 2026-09-10 13:54:51 +02:00
OTKUSteyler
a836fd1e73
Update FontsModule.kt 2026-09-10 12:49:20 +02:00
OTKUSteyler
0190e2d468
Update Main.kt 2026-09-10 12:18:30 +02:00
OTKUSteyler
ea21c20d18
Update Main.kt 2026-09-10 12:09:38 +02:00
OTKUSteyler
ad10c8a2a1
Update build.gradle.kts 2026-09-10 08:05:34 +02:00
OTKUSteyler
65d6ae239a
Update build.gradle.kts 2026-09-09 21:16:48 +02:00
OTKUSteyler
415f0066d7
Update build.gradle.kts 2026-09-09 21:13:44 +02:00
OTKUSteyler
4f3c599c3b
Update build.gradle.kts 2026-09-09 20:25:56 +02:00
OTKUSteyler
5e8e4538f8
Update build.gradle.kts 2026-09-09 20:17:37 +02:00
OTKUSteyler
64f83dcd5c
Update FontsModule.kt 2026-09-09 19:52:56 +02:00
OTKUSteyler
cd418c2d79
Update LogBoxActions.kt 2026-09-09 19:51:20 +02:00
OTKUSteyler
0267d095e2
Update FontsModule.kt 2026-09-09 19:42:01 +02:00
OTKUSteyler
2d62630e30
Update Main.kt 2026-09-09 17:45:20 +02:00
OTKUSteyler
750a4cd5b0
Update Main.kt 2026-09-05 20:37:19 +02:00
OTKUSteyler
baacefef54
Update FontsModule.kt 2026-09-05 19:46:48 +02:00
OTKUSteyler
f96d53db9a
Update Main.kt 2026-09-05 13:43:27 +02:00
OTKUSteyler
720e84487e
Update FontsModule.kt 2026-09-05 13:26:30 +02:00
OTKUSteyler
8616f04995
Update ThemesModule.kt 2026-09-05 13:25:51 +02:00
OTKUSteyler
7152b06265
Update Main.kt 2026-09-05 13:25:17 +02:00
OTKUSteyler
2b3510f55d
Update Utils.kt 2026-09-05 12:53:22 +02:00
OTKUSteyler
d1334d0ec0
Update FixResourcesModule.kt 2026-09-05 12:49:09 +02:00
OTKUSteyler
853564ebe9
Update FontsModule.kt 2026-09-05 12:48:11 +02:00
OTKUSteyler
3bf2f76c56
Update RecoveryMenu.kt 2026-09-05 12:47:19 +02:00
OTKUSteyler
6f707af677
Update OptionsMenu.kt 2026-09-05 12:45:27 +02:00
OTKUSteyler
1abaaff7d1
Update PayloadGlobalModule.kt 2026-09-05 12:44:49 +02:00
OTKUSteyler
3aa888bc26
Update HookScriptLoaderModule.kt 2026-09-05 12:44:22 +02:00
OTKUSteyler
d57081364b
Update SysColorsModule.kt 2026-09-05 12:43:35 +02:00
OTKUSteyler
4599c2d96f
Update Utils.kt 2026-09-05 12:43:04 +02:00
OTKUSteyler
14a8067aea
Update Module.kt 2026-09-05 12:42:17 +02:00
OTKUSteyler
d719e15cf8
Add files via upload 2026-09-05 12:25:16 +02:00
OTKUSteyler
7814cd0e82
Add files via upload 2026-09-05 12:24:51 +02:00
OTKUSteyler
469cdfb95e
Add files via upload 2026-09-05 12:24:24 +02:00
OTKUSteyler
e90c20bfc5
Add files via upload 2026-09-05 12:23:41 +02:00
OTKUSteyler
fc9dd1ee9b
Add files via upload 2026-09-05 12:22:25 +02:00
OTKUSteyler
c85761f92e
Add files via upload 2026-09-05 12:21:42 +02:00
OTKUSteyler
c342b478c1
Update gradle.properties 2026-09-05 12:16:21 +02:00
OTKUSteyler
fc6772a124
Update gradle.properties 2026-09-05 12:13:46 +02:00
OTKUSteyler
743b6283c0
Update build.gradle.kts 2026-09-04 06:25:39 +02:00
OTKUSteyler
3562b0e730
Update FontsModule.kt 2026-09-03 21:06:47 +02:00
OTKUSteyler
26a356a158
Update xposed_init 2026-09-03 20:15:40 +02:00
OTKUSteyler
a3ebf0259e
Update xposed_init 2026-09-03 20:12:29 +02:00
OTKUSteyler
df097bfc81
Update build.gradle.kts 2026-09-03 19:35:10 +02:00
OTKUSteyler
765c546c20
Update Module.kt 2026-09-03 18:29:43 +02:00
OTKUSteyler
13bdcdad03
Update FontsModule.kt 2026-09-03 18:27:03 +02:00
OTKUSteyler
b083a66afd
Update FontsModule.kt
Some checks failed
Build APK / Build and push (push) Failing after 3m49s
2026-09-03 17:45:01 +02:00
OTKUSteyler
4115c3d4df
Update build.gradle.kts
Some checks failed
Build APK / Build and push (push) Failing after 3m58s
2026-09-03 15:36:30 +02:00
OTKUSteyler
c63eaa5a12
Update FontsModule.kt 2026-09-03 14:58:44 +02:00
OTKUSteyler
531cb9281c
Update FontsModule.kt 2026-09-03 13:00:51 +02:00
OTKUSteyler
bff796d574
Update FontsModule.kt 2026-09-03 12:45:07 +02:00
OTKUSteyler
f7d1b2ea1d
Update FontsModule.kt 2026-09-03 05:58:15 +02:00
OTKUSteyler
9a620545b4
Update FontsModule.kt 2026-09-02 16:17:06 +02:00
OTKUSteyler
6e7f5302e3
Update build.gradle.kts
Some checks failed
Build APK / Build and push (push) Failing after 4m49s
2026-09-02 15:27:31 +02:00
OTKUSteyler
ec787e7cf5
Update build.gradle.kts 2026-09-02 15:27:08 +02:00
OTKUSteyler
8ef48f3608
Update FontsModule.kt 2026-09-02 14:43:48 +02:00
OTKUSteyler
2bddedb96c
Update FontsModule.kt 2026-09-02 14:41:44 +02:00
OTKUSteyler
5d39748a28 Fix overload resolution ambiguity by replacing wildcard import with specific LogBoxModule import 2026-08-26 16:46:01 +02:00
OTKUSteyler
1a9fd61ec3 Fix: Add missing LogBoxModule and onActivity hook - fixes blank plugin/theme tabs 2026-08-26 16:41:59 +02:00
OTKUSteyler
a9a1dc4ad3 chore: Bump version to 1.3.6 2026-08-26 16:29:21 +02:00
OTKUSteyler
aab9fdc101 Fix: Add @Deprecated annotation to buildPayload override 2026-08-26 16:22:22 +02:00
OTKUSteyler
21029f5f72 Fix: Add @Deprecated annotation to buildPayload override 2026-08-26 16:22:10 +02:00
OTKUSteyler
64ffdf4f32 Fix: Add @Deprecated annotation to buildPayload override 2026-08-26 16:21:48 +02:00
OTKUSteyler
68c85dd812 Fix: Skip release signing when keystore not found and suppress deprecated warnings 2026-08-26 16:21:40 +02:00
OTKUSteyler
fd84dd0320 Fix: Remove invalid method parameters from UpdaterModule calls
- Line 86: Remove `isCustomUrlEnabled` reference (doesn't exist in UpdaterModule)
- Line 92: Remove `showUpdateDialog = false` parameter from `downloadScript()` call
- Line 96: Remove `showUpdateDialog = true` parameter from `downloadScript()` call

The `UpdaterModule.downloadScript()` method only accepts an optional `activity` parameter, not `showUpdateDialog`.
The logic has been simplified to check only if the main script exists.
2026-08-26 16:15:12 +02:00
OTKUSteyler
7df631b799
Update README.md 2026-08-26 16:09:02 +02:00
OTKUSteyler
3d45a8d70f
Merge branch 'kmmiio99o:master' into master 2026-08-26 16:05:36 +02:00
kmmiio99o
af387dc48a
fix: fonts and themes pages not showing up on latest alpha 2026-08-26 15:28:26 +02:00
OTKUSteyler
2978e3b8d1
Merge branch 'kmmiio99o:master' into master 2026-08-26 06:16:01 +02:00
kmmiio99o
32794a134c
chore: bump version to 1.3.5 and manify release 2026-08-25 22:29:27 +02:00
kmmiio99o
13459ef238
fix: hook createAssetTypeface with fallback for older Discord versions 2026-08-25 22:29:16 +02:00
OTKUSteyler
ffcc29796c
Update LogBoxModule.kt 2026-08-18 10:38:53 +02:00
OTKUSteyler
79298ddf6c
Update LogBoxMenu.kt 2026-08-18 10:37:38 +02:00
OTKUSteyler
e0917110e1
Update LogBoxModule.kt 2026-08-18 10:29:36 +02:00
OTKUSteyler
1de8af79f0
Rename package from ShiggyXposed to GoonXposed 2026-08-18 10:20:54 +02:00
OTKUSteyler
5fe307a852
Update and rename Module.kt.txt to Module.kt 2026-08-18 10:19:28 +02:00
OTKUSteyler
08bfa8199d
Add files via upload 2026-08-18 10:18:42 +02:00
OTKUSteyler
2c94757947
Update CacheModule.kt 2026-08-18 10:09:31 +02:00
OTKUSteyler
85837531de
Delete app/src/main/kotlin/GoonXposed/xposed/xposed directory 2026-08-17 09:07:40 +02:00
OTKUSteyler
3360ee33f1
Rename Module.kt to GoonXposed directory 2026-08-17 09:07:18 +02:00
OTKUSteyler
d882007611
Delete app/src/main/kotlin/GoonXposed/xposed/xposed/Utils.kt 2026-08-17 09:06:16 +02:00
OTKUSteyler
bd9116e1cf
Rename Utils.kt to Utils.kt 2026-08-17 09:05:44 +02:00
OTKUSteyler
5e54c7bf61
Update LogBoxModule.kt 2026-08-17 08:58:41 +02:00
OTKUSteyler
bf6f3c0691
Refactor LogBoxModule to use urlEnabled instead of isEnabled 2026-08-17 08:45:26 +02:00
OTKUSteyler
7eb893bcc9
Update build.gradle.kts 2026-08-17 08:35:12 +02:00
OTKUSteyler
52f05764f4 Merge remote-tracking branch 'upstream/master' 2026-08-17 08:25:42 +02:00
kmmiio99o
4900b9533c
fix: put button in the right recovery options 😭 2026-08-16 18:10:12 +02:00
kmmiio99o
ced4c1ce1f
chore: bump to 1.3.4 2026-08-16 17:46:15 +02:00
kmmiio99o
05d6f5004d
feat: add option to delete Devtools.js 2026-08-16 17:46:09 +02:00
OTKUSteyler
cdbbb3a957
Update README.md 2026-05-22 21:51:39 +02:00
OTKUSteyler
457af73f88
Update OptionsMenu.kt 2026-05-18 01:10:23 +02:00
OTKUSteyler
6be3d89176
Update LogBoxUtils.kt 2026-05-18 01:10:02 +02:00
OTKUSteyler
9267cb3918
Update LogBoxTheme.kt 2026-05-18 01:09:28 +02:00
OTKUSteyler
f55d1a4a62
Rename package from ShiggyXposed to GoonXposed 2026-05-18 01:09:06 +02:00
OTKUSteyler
cc0ffe6fb2
Update LogBoxMenu.kt 2026-05-18 01:08:46 +02:00
OTKUSteyler
acca6bfe47
Rename package from ShiggyXposed to GoonXposed 2026-05-18 01:08:09 +02:00
OTKUSteyler
8148f860e5
Update LogBoxConstants.kt 2026-05-18 01:07:46 +02:00
OTKUSteyler
f0e9503354
Update LogBoxComponents.kt 2026-05-18 01:07:24 +02:00
OTKUSteyler
4f779c9af0
Update RecoveryMenu.kt 2026-05-18 01:07:00 +02:00
OTKUSteyler
202152bbf0
Update LogBoxActions.kt 2026-05-18 01:05:58 +02:00
OTKUSteyler
d55b9dcda0
Rename package from ShiggyXposed to GoonXposed 2026-05-18 01:05:28 +02:00
OTKUSteyler
6f6f2e4abc
Update CustomBundleDialog.kt 2026-05-18 01:05:06 +02:00
OTKUSteyler
c3c21a26e0 Merge remote-tracking branch 'upstream/master' 2026-05-18 01:01:03 +02:00
kmmiio99o
b62f7b4daa
chore: bump to 1.3.3 2026-05-15 22:08:37 +02:00
kmmiio99o
1e61dd9832
feat: fallback to https://bundle.shiggycord.dev
also add siging release apk's
2026-05-15 22:05:42 +02:00
OTKUSteyler
5e2fd369fe Merge remote-tracking branch 'upstream/master' 2026-02-14 21:00:53 +01:00
kmmiio99o
a9bac3f1cb
chore: bump to 1.3.2 2026-02-14 16:44:29 +01:00
kmmiio99o
f3cc5b4e2c
Merge remote-tracking branch 'upstream/master' 2026-02-14 16:34:45 +01:00
cocobo1
7f2d4991c5 Merge branch 'main' of https://github.com/revenge-mod/revenge-xposed 2026-02-10 14:43:19 +02:00
GitHub Actions
c6667c2307 chore: bump version to 1.4.10 (1410) 2026-02-01 11:11:17 +00:00
Palm
a386e1907f
perf(loader): don't block startup on bundle update if cached (#19) 2026-02-01 17:55:54 +07:00
kmmiio99o
8ddd3a336e
chore: bump to 1.3.1 2026-01-31 21:49:01 +01:00
kmmiio99o
2a223671b9
fix: not being able to enable bundle injection 2026-01-31 21:48:43 +01:00
kmmiio99o
1f8ddf4176
chore: bump to 1.3.0 2026-01-31 20:28:57 +01:00
kmmiio99o
82ea78edbb feat(xposed/modules/LogBox/LogBoxMenu.kt): Reload app after
Disabling/Enabling bundle injection
2026-01-31 20:26:27 +01:00
kmmiio99o
a85f399b29 Merge remote-tracking branch 'upstream/master' 2026-01-31 20:23:09 +01:00
kmmiio99o
be86071f4f
feat: refactor LogBoxModule file stracture 2026-01-31 20:15:11 +01:00
Purple_Ξye™
d9603ffdb5 fix(modules/HookScriptLoaderModule): don't use cache when custom bundle URL is enabled 2026-01-30 05:09:47 +01:00
Purple_Ξye™
87d4c78c67 feat(modules/UpdaterModule): show update dialog when bundle is updated in background 2026-01-29 08:39:24 +01:00
Purple_Ξye™
da337dfd98 feat(modules/UpdaterModule): add methods to force bundle update 2026-01-29 08:38:57 +01:00
Purple_Ξye™
e0c75e30af perf(modules/HookScriptLoaderModule): don't block startup on bundle update if cached 2026-01-28 22:00:59 +01:00
cocobo1
bf0389fe99 feat: hermes bytecode support 2026-01-11 16:34:38 +02:00
OTKUSteyler
7635ac1d90
Merge branch 'kmmiio99o:master' into master 2026-01-10 16:32:15 +00:00
kmmiio99o
179db56473
Merge remote-tracking branch 'upstream/master' 2026-01-10 17:29:00 +01:00
cocobo1
bc4cfa0265 chore: bump version 2026-01-10 17:47:50 +02:00
cocobo1
3ba04c366c feat: Add option to disable bundle injection (#2) 2026-01-06 17:12:55 +01:00
kmmiio99o
52bcb9912b
feat: Add option to disable bundle injection
why? People can disable bundle injection when Kettu gets stuck
2026-01-06 13:33:41 +01:00
OTKUSteyler
5769fe2211
Update CacheModule.kt 2026-01-03 00:24:07 +00:00
OTKUSteyler
04a799e7f6
Update BridgeModule.kt 2026-01-03 00:23:02 +00:00
OTKUSteyler
ed8d5fb7a8
Update BridgeModule.kt 2026-01-03 00:06:52 +00:00
OTKUSteyler
8038be3023
Update gradle.properties 2026-01-03 00:05:33 +00:00
OTKUSteyler
511fd1df4e
Update PerfPatchesModule.kt 2026-01-03 00:01:21 +00:00
OTKUSteyler
0b63dbbc9a
Update CacheModule.kt 2026-01-02 23:58:25 +00:00
OTKUSteyler
85290a619e Merge remote-tracking branch 'upstream/master' 2026-01-03 00:53:48 +01:00
kmmiio99o
04a68d623d
suppress deprecated java version 2026-01-02 15:16:18 +01:00
kmmiio99o
0805a7314d
add some more patches for faster start up 2026-01-02 15:08:43 +01:00
kmmiio99o
7726840d75
Merge remote-tracking branch 'upstream/master' 2026-01-02 15:06:47 +01:00
kmmiio99o
9895188f24
Merge branch 'master' of https://github.com/ShiggyCord-Team/ShiggyXposed 2025-12-22 17:04:13 +01:00
kmmiio99o
fdca98a994
bump to 1.2.4 2025-12-22 17:04:10 +01:00
kmmiio99o
86994eefb9 bump to 1.2.4 2025-12-20 21:28:30 +01:00
kmmiio99o
59b5d75f6f Patch some things on discord startup to make the app even faster 2025-12-20 20:41:59 +01:00
cocobo1
27e4917c34 Merge pull request 'Make Xposed Recovery Menu look better :P' (#1) from kmmiio99o/KettuXposed:master into master
Reviewed-on: https://codeberg.org/cocobo1/KettuXposed/pulls/1
2025-12-19 23:05:36 +01:00
kmmiio99o
85795c69b9 Make Xposed Recovery look better :P 2025-12-19 22:51:44 +01:00
kmmiio99o
de57766e21 rename forgotten things 2025-12-11 18:46:05 +01:00
kmmiio99o
f8fe4f03a9 Update README file
I don't know, just better documentation
2025-12-11 18:45:08 +01:00
OTKUSteyler
d1796e4c68 Merge remote-tracking branch 'upstream/master' 2025-12-11 18:29:19 +01:00
kmmiio99o
6eef736615 Merge branch 'master' of https://github.com/ShiggyCord-Team/ShiggyXposed 2025-12-11 18:18:39 +01:00
kmmiio99o
8a134d7ee1 Try to fix size of the Recovery menu first page 2025-12-11 18:18:05 +01:00
kmmiio99o
8bc7c1671d Fix width in recovery menu and remove some buttons 2025-12-11 18:03:04 +01:00
kmmiio99o
2b1a7919a3 feat: Add Themes submenu and segmented appearance selector
Introduce Themes submenu with segmented appearance selector and flavor
picker for LogBox menu. Move Refetch and Clear Cache from Recovery into
Options. Apply menu colors in-place via currentMenuContainer
2025-12-11 17:33:31 +01:00
kmmiio99o
035f8aa570 Fix: fix disabling and enabling bundle injection 2025-12-11 16:40:57 +01:00
kmmiio99o
5876b7da10 Add options menu in LogBox module and bump to 1.2.3
Add options menu in xposed settings to disabling injecting bundle,
change Xposed menu flavor, and choose theme from dark to light mode and
system detection
2025-12-11 16:30:57 +01:00
OTKUSteyler
f92e36e7d4
Merge branch 'kmmiio99o:master' into master 2025-12-10 07:01:53 +01:00
OTKUSteyler
cbf96e8b45
Update README.md 2025-12-07 23:38:33 +01:00
OTKUSteyler
4537e7d56c
Update README.md 2025-12-07 23:37:35 +01:00
kmmiio99o
33faa383b0 A DOT 2025-12-07 23:36:32 +01:00
OTKUSteyler
782c40b5d3
Merge branch 'kmmiio99o:master' into master 2025-12-07 23:36:03 +01:00
kmmiio99o
224ca9997a
Add readme
Add nice readme with useful info for Xposed Module and about it
2025-12-07 23:33:41 +01:00
OTKUSteyler
21effc7f21
Update build.gradle.kts
Some checks failed
Build APK / Build and push (push) Failing after 4m47s
2025-11-30 22:47:37 +01:00
OTKUSteyler
cc1f7fe682
Update LogBoxModule.kt 2025-11-30 22:30:33 +01:00
OTKUSteyler
f619b8702c
Update build.gradle.kts 2025-11-29 11:58:34 +01:00
OTKUSteyler
b4c17583b6
Update Utils.kt 2025-11-28 21:46:14 +01:00
OTKUSteyler
ff308a190c
Update LogBoxModule.kt 2025-11-28 18:54:02 +01:00
OTKUSteyler
c5f3b83cdf
Update build.gradle.kts 2025-11-28 18:45:48 +01:00
OTKUSteyler
03c81d8cc9
Merge branch 'kmmiio99o:master' into master 2025-11-28 18:37:39 +01:00
OTKUSteyler
3c4c9ca555
Update build.yml 2025-11-28 18:33:04 +01:00
OTKUSteyler
19bade53a0 Merge remote-tracking branch 'upstream/master' 2025-11-28 18:28:20 +01:00
kmmiio99o
e3e6f484ea bump to 1.2.2 2025-11-28 18:27:41 +01:00
kmmiio99o
74adbf4790 Fix linting V2 2025-11-28 18:09:13 +01:00
kmmiio99o
65c175f977 Fix linting error
Happened without a reason really, but should be working now I think
2025-11-28 17:59:18 +01:00
OTKUSteyler
351086110c
Lower minSdk and targetSdk to 28 2025-11-28 17:55:38 +01:00
OTKUSteyler
5bc83206b2
Update build.gradle.kts 2025-11-28 16:05:08 +01:00
OTKUSteyler
8aa226d6ca
Update versionCode and versionName to 1.3.2
Some checks failed
Build APK / Build and push (push) Failing after 4m40s
2025-11-28 15:53:59 +01:00
OTKUSteyler
02310df32f
Update LogBoxModule.kt 2025-11-28 15:53:26 +01:00
OTKUSteyler
ffdd65f74d
Update Utils.kt 2025-11-28 12:26:02 +01:00
OTKUSteyler
90b346b75a
Update build.gradle.kts 2025-11-28 11:56:47 +01:00
OTKUSteyler
3619dafd47
Update build.gradle.kts 2025-11-28 11:42:28 +01:00
OTKUSteyler
12491b2235
Update build.gradle.kts 2025-11-28 11:37:47 +01:00
OTKUSteyler
2dd8151138
Update build.gradle.kts 2025-11-28 11:13:01 +01:00
OTKUSteyler
8c033fe878
Update AndroidManifest.xml 2025-11-28 09:57:37 +01:00
OTKUSteyler
f6d2384fa5
Update AndroidManifest.xml 2025-11-28 09:53:05 +01:00
OTKUSteyler
2621359a61
Update AndroidManifest.xml 2025-11-28 09:45:37 +01:00
OTKUSteyler
9f8c521659
Update build.yml 2025-11-26 18:47:57 +01:00
OTKUSteyler
2ffe2f510d
Update build.yml 2025-11-26 18:35:10 +01:00
OTKUSteyler
f4d48fdf90
Delete .github/workflows/release.yml 2025-11-26 18:33:59 +01:00
OTKUSteyler
66bb7fce6a
Update build.yml 2025-11-26 17:27:56 +01:00
OTKUSteyler
42636b29e8
Update build.yml 2025-11-26 16:30:37 +01:00
OTKUSteyler
e0cb50aaea
Update build.yml 2025-11-26 16:24:32 +01:00
kmmiio99o
bd9fe79f2a Fix Fix 2025-11-26 16:20:02 +01:00
kmmiio99o
b90c9e3afa Fix signing apks 2025-11-26 16:14:29 +01:00
OTKUSteyler
f2371d1c7e
Update build.gradle.kts 2025-11-24 17:59:36 +01:00
OTKUSteyler
01f52c1e2f
Delete app/app directory 2025-11-24 17:52:21 +01:00
OTKUSteyler
8d1d6303d3
Add files via upload 2025-11-24 17:50:15 +01:00
OTKUSteyler
bf05c67966
Delete app/src/main/kotlin/GoonXposed/a 2025-11-24 17:48:25 +01:00
OTKUSteyler
4102b2298d
Create a 2025-11-24 17:48:14 +01:00
OTKUSteyler
2171fc59eb
Delete app/src/main/kotlin/io/github/revenge/xposed directory 2025-11-24 17:38:08 +01:00
OTKUSteyler
8389d19545
Add files via upload 2025-11-24 17:37:47 +01:00
OTKUSteyler
5fe672884e
Delete app/src/main/kotlin/ShiggyXposed/xposed/xposed directory 2025-11-24 17:29:20 +01:00
OTKUSteyler
9481d80e5f Update build.gradle.kts 2025-11-24 17:14:07 +01:00
OTKUSteyler
3f309f58bb
Update UpdaterModule.kt 2025-11-24 16:58:04 +01:00
OTKUSteyler
004bba80ae
Update UpdaterModule.kt 2025-11-24 16:57:36 +01:00
OTKUSteyler
0fc969cf00
Update BridgeModule.kt 2025-11-24 16:21:39 +01:00
GitHub Actions
5c29b1072b chore: bump version to 1.4.9 (1409) 2025-11-23 22:03:50 +00:00
OTKUSteyler
691db71d12
Update recovery alert title from ShiggyCord to GoonCord 2025-11-23 18:50:47 +01:00
OTKUSteyler
07265446cf
Update ThemesModule.kt 2025-11-23 18:46:38 +01:00
OTKUSteyler
86c9658318
Update SysColorsModule.kt 2025-11-23 18:46:07 +01:00
OTKUSteyler
8c584d7cfc
Update FontsModule.kt 2025-11-23 18:45:44 +01:00
OTKUSteyler
f42f53b375
Rename package from ShiggyXposed to GoonXposed 2025-11-23 18:45:03 +01:00
OTKUSteyler
8c6c43429d
Update BridgeModule.kt 2025-11-23 18:44:06 +01:00
OTKUSteyler
a4c6e4871e
Update BlockCrashReportingModule.kt 2025-11-23 18:42:57 +01:00
OTKUSteyler
f17cdc8fd0
Update BlockDeepLinksTrackingModule.kt 2025-11-23 18:42:29 +01:00
OTKUSteyler
3506cfc97d
Update CacheModule.kt 2025-11-23 18:41:56 +01:00
OTKUSteyler
7d8a71eaf6
Update FixResourcesModule.kt 2025-11-23 18:41:18 +01:00
OTKUSteyler
eeac21e2aa
Update HookScriptLoaderModule.kt 2025-11-23 18:40:46 +01:00
OTKUSteyler
eb58fdfa00
Update LogBoxModule.kt 2025-11-23 18:39:46 +01:00
OTKUSteyler
44288a202d
Update Constants.kt 2025-11-23 18:38:22 +01:00
OTKUSteyler
3196f8a0a4
Update PayloadGlobalModule.kt 2025-11-23 18:11:04 +01:00
OTKUSteyler
63c12c7984
Change package name in PluginsModule.kt
Updated package name from ShiggyXposed to GoonXposed.
2025-11-23 18:10:01 +01:00
OTKUSteyler
9a716efd37
Update Utils.kt 2025-11-23 18:09:07 +01:00
OTKUSteyler
136b6f7c46
Update Main.kt 2025-11-23 18:08:30 +01:00
OTKUSteyler
63d8e86725
Update UpdaterModule.kt 2025-11-23 16:42:24 +01:00
OTKUSteyler
6cfb92faf1
Update UpdaterModule.kt 2025-11-23 16:39:40 +01:00
OTKUSteyler
f47d6d1d93 Merge remote-tracking branch 'upstream/master' 2025-11-22 22:04:25 +01:00
kmmiio99o
556126191d bump to 1.2.2 2025-11-22 19:56:00 +01:00
kmmiio99o
054eb5fd6c Merge remote-tracking branch 'upstream/master' 2025-11-22 19:54:13 +01:00
kmmiio99o
2f2db32e8c feat: Merge KettuXposed
- Merge KettuXposed Github - Make better Recovery Menu - Fix 307204+
issues
2025-11-22 19:49:10 +01:00
cocobo1
c9ab6316c7 reAdd license 2025-11-22 16:25:00 +02:00
cocobo1
7a3ba1a85a fix(shake shake): make devmennu override 2025-11-22 16:15:05 +02:00
PalmDevs
8a413f279a fix(modules/no_track/BlockCrashReportingModule): support 307204+
Discord pushed an update that changed some logic for the setTag method. Previously it only asserted initialization if isDisabled returns false, now it asserts initialization regardless.

The init method sets the initialization latch and returns if crash reporting is disabled, so we don't need to hook that anymore.
2025-11-22 12:20:03 +02:00
PalmDevs
9a323a663e fix(modules/no_track/BlockCrashReportingModule): make backwards compatible 2025-11-22 12:19:30 +02:00
cocobo1
bb37f8e7ef merge revengeXposed 2025-11-22 12:19:12 +02:00
PalmDevs
623804ade5 fix(modules/no_track/BlockCrashReportingModule): also hook isDisabled to true
Prevents Discord from crashing when trying to set a Sentry tag.
2025-11-22 12:18:27 +02:00
PalmDevs
38c7082760
fix(modules/no_track/BlockCrashReportingModule): support 307204+
Discord pushed an update that changed some logic for the setTag method. Previously it only asserted initialization if isDisabled returns false, now it asserts initialization regardless.

The init method sets the initialization latch and returns if crash reporting is disabled, so we don't need to hook that anymore.
2025-11-22 14:49:17 +07:00
OTKUSteyler
2eb7cbf84e
Update LogBoxModule.kt 2025-11-22 00:30:46 +01:00
OTKUSteyler
3d6f61bb56
Update FontsModule.kt 2025-11-22 00:26:34 +01:00
OTKUSteyler
01fdbe19d7
Update UpdaterModule.kt 2025-11-22 00:23:41 +01:00
OTKUSteyler
4ecfdfce27
Update Main.kt 2025-11-22 00:18:09 +01:00
OTKUSteyler
e198c43ce2
Update ThemesModule.kt 2025-11-22 00:13:17 +01:00
OTKUSteyler
ee7c27d280
Update SysColorsModule.kt 2025-11-22 00:12:52 +01:00
OTKUSteyler
9a6886ad71
Update FontsModule.kt 2025-11-22 00:12:24 +01:00
OTKUSteyler
019075d927
Update LogBoxModule.kt 2025-11-22 00:11:39 +01:00
OTKUSteyler
6131c74016
Update PayloadGlobalModule.kt 2025-11-22 00:10:53 +01:00
OTKUSteyler
52e10aca4c
Update PluginsModule.kt 2025-11-22 00:10:35 +01:00
OTKUSteyler
4b7192edfa
Update AdditionalBridgeMethodsModule.kt 2025-11-22 00:09:44 +01:00
OTKUSteyler
ac5d0e551b
Update BridgeModule.kt 2025-11-22 00:09:14 +01:00
OTKUSteyler
d04d1d608d
Update BlockCrashReportingModule.kt 2025-11-22 00:08:32 +01:00
OTKUSteyler
46e37bfe7e
Update BlockDeepLinksTrackingModule.kt 2025-11-22 00:07:56 +01:00
OTKUSteyler
e9a2a8c51e
Update CacheModule.kt 2025-11-22 00:07:28 +01:00
OTKUSteyler
e453be092b
Update FixResourcesModule.kt 2025-11-22 00:06:49 +01:00
OTKUSteyler
ced4d26d20
Update FixResourcesModule.kt 2025-11-22 00:06:40 +01:00
OTKUSteyler
683137451b
Update HookScriptLoaderModule.kt 2025-11-22 00:06:17 +01:00
OTKUSteyler
61af37117d
Update LogBoxModule.kt 2025-11-22 00:05:06 +01:00
OTKUSteyler
fb8a6119f9
Update PayloadGlobalModule.kt 2025-11-22 00:03:52 +01:00
OTKUSteyler
62513e7a04
Update UpdaterModule.kt 2025-11-22 00:02:59 +01:00
OTKUSteyler
007a6f8a59
Add files via upload 2025-11-21 23:54:09 +01:00
OTKUSteyler
aa0970913c
Update PayloadGlobalModule.kt 2025-11-21 23:48:46 +01:00
OTKUSteyler
4ae471a7b9
Update Main.kt 2025-11-21 23:47:28 +01:00
OTKUSteyler
f19d654e87
Update build.gradle.kts 2025-11-21 23:45:51 +01:00
kmmiio99o
056d6d81d9 merge changes from KettuXposed and bump to 1.2.1
only issue is not working recovery menu, but who cares?
2025-11-21 20:58:00 +01:00
GitHub Actions
c20cf01f6b chore: bump version to 1.4.8 (1408) 2025-11-21 09:44:30 +00:00
PalmDevs
4a8df8954e
fix(modules/no_track/BlockCrashReportingModule): make backwards compatible 2025-11-21 16:36:14 +07:00
GitHub Actions
f49ea13da5 chore: bump version to 1.4.7 (1407) 2025-11-21 06:17:17 +00:00
PalmDevs
06d6778c45
fix(modules/no_track/BlockCrashReportingModule): also hook isDisabled to true
Prevents Discord from crashing when trying to set a Sentry tag.
2025-11-21 13:04:36 +07:00
OTKUSteyler
7348da9aff
Update LogBoxModule.kt
Some checks failed
Build APK / Build and push (push) Failing after 4m44s
2025-11-19 21:50:01 +01:00
OTKUSteyler
9a6d328b47
Update build.gradle.kts 2025-11-19 21:45:50 +01:00
cocobo1
0eb28c094d feat(shake menu): add more options 2025-11-18 17:46:58 +02:00
cocobo1
c9ecf551bf fix(rebase) 2025-11-18 17:21:29 +02:00
cocobo1
d0a809c8a2 rebase 2025-11-18 15:39:00 +02:00
cocobo1
c2ecba402b Merge remote-tracking branch 'upstream' 2025-11-18 15:06:21 +02:00
GitHub Actions
9f1941e974 chore: bump version to 1.4.6 (1406) 2025-11-18 13:02:47 +00:00
PalmDevs
113cb05e91 fix: support 307201+
Discord bumped React Native to 0.81.4 and script loader classes are now named differently.
2025-11-18 15:01:58 +02:00
PalmDevs
ebcf8eaf4d
fix: support 307201+
Discord bumped React Native to 0.81.4 and script loader classes are now named differently.
2025-11-18 19:51:28 +07:00
kmmiio99o
5b510a9552 bump to 1.2.0 and make recovery look better
- Added a "Clear Cache & Reset" option - Improved UI of the recovery
menu - Added more descriptive dialogs - Fixed custom bundle not
downloading - Fixed text styling in the menu - Fixed file pathing -
Added more null safety - Upgraded gradle
2025-11-16 23:02:53 +01:00
OTKUSteyler
afdda092bc
Update build.gradle.kts 2025-11-10 23:57:59 +01:00
kmmiio99o
4283bc237a Make Recovery Menu dark and Fix Custom URL Loader
- Make the full Recovery Menu in dark mode - Fix Custom URL Loader menu
to actually load, remove and add URL's into it
2025-11-10 20:26:38 +01:00
PalmDevs
9b6474983d
fix: IllegalAccessException on Main.<init> 2025-11-09 23:03:38 +07:00
PalmDevs
c1f037c15d
chore: convert most Modules to objects 2025-11-08 19:05:47 +07:00
kmmiio99o
5f26b75039 Pull KettuXposed and rename it fully 2025-11-07 22:19:26 +01:00
cocobo1
a6c9e7ed0a chore: bump version 2025-11-06 18:07:33 +02:00
cocobo1
bfa2757156 Merge remote-tracking branch 'upstream' 2025-11-05 19:28:24 +02:00
PalmDevs
3c0204af03 feat: block tracking 2025-10-19 12:53:51 +03:00
PalmDevs
4e9116f3f9 feat: show whole stack trace in strings 2025-10-19 12:51:47 +03:00
PalmDevs
9ef9156d7e fix(modules/LogBoxModule): initialize packageParam regardless of build type 2025-10-19 12:50:55 +03:00
PalmDevs
9828f9d267
feat: show whole stack trace in strings 2025-10-16 01:41:34 +07:00
PalmDevs
536e9ef21e
fix(modules/LogBoxModule): initialize packageParam regardless of build type 2025-10-15 18:55:32 +07:00
PalmDevs
8e1e9a104b
ci: fix workflows 2025-10-15 01:48:25 +07:00
PalmDevs
2ca240625d
fix(modules/bridge/AdditionalBridgeMethodsModule): use Activity to alert instead 2025-10-15 01:23:57 +07:00
PalmDevs
4e33eeaf85
ci: update workflows 2025-10-15 01:10:27 +07:00
PalmDevs
f40c9518c9
fix(modules/LogBoxModule): only enable on debug builds 2025-10-15 01:10:26 +07:00
PalmDevs
09b99fe4a4
feat(modules/bridge/AdditionalBridgeMethodsModule): add method to show Recovery alert 2025-10-15 01:10:25 +07:00
PalmDevs
0f0fc1645a
chore: move showRecoveryAlert to Utils 2025-10-15 01:10:24 +07:00
PalmDevs
c5279ae484
feat: block tracking 2025-10-13 14:32:12 +07:00
PalmDevs
614f9ced6f
feat: add Reset Loader Config in Recovery dialog, show Recovery option in Update Failed dialog 2025-10-13 03:49:58 +07:00
PalmDevs
12e35d8d04
fix(modules/LogBoxModule): hook bridgeless devsupport 2025-10-13 03:48:06 +07:00
PalmDevs
8f703cd03b
chore: bump version 2025-10-13 02:53:01 +07:00
PalmDevs
3ec9b9a4f7
fix: run Module.onContext properly for recreated processes 2025-10-13 02:52:19 +07:00
PalmDevs
4de1a97144
chore: bump version 2025-10-12 03:00:24 +07:00
PalmDevs
90ecc5fd2e
fix(modules/UpdaterModule): actually fix memory leak correctly 2025-10-12 03:00:15 +07:00
PalmDevs
b0fd423863
fix(modules/HookScriptLoaderModule): delay script execution until hooks completed 2025-10-12 02:05:19 +07:00
PalmDevs
4f583a86f2
fix(modules): move fs bridge methods to AdditionalBridgeMethodsModule 2025-10-12 02:04:09 +07:00
PalmDevs
fcdb4ec546
fix(modules/UpdaterModule): memory leak 2025-10-12 02:02:45 +07:00
Palm
874da44a79
chore: bump version 2025-10-01 18:15:37 +07:00
Purple_Ξye™
b138891642
fix: defer script download until loadScript* is called (#16)
Co-authored-by: Palm <git@palmdevs.me>
2025-10-01 18:14:25 +07:00
cocobo1
431f821039 fix(popups): removed pesky popups + bump so people actually update 2025-09-28 13:29:23 +03:00
cocobo1
766c0dd608 fix(gitignore): fixed gitignore 2025-09-25 22:42:14 +03:00
cocobo1
9970bc5751 rebase(xposed): rebase onto revengeXposed 2025-09-24 19:06:50 +03:00
PalmDevs
f153c143e3
chore: bump dependencies 2025-09-17 00:37:41 +07:00
PalmDevs
188d69ad49
chore: bump version 2025-09-17 00:37:11 +07:00
PalmDevs
22290545fd
chore(modules/appearance): ThemeModule -> ThemesModule 2025-09-17 00:35:30 +07:00
PalmDevs
53970fe277
fix(modules/PayloadGlobalModule): also hook loadScriptFromFile 2025-09-17 00:33:54 +07:00
PalmDevs
0e77f3cd91
chore: update project configuration 2025-09-16 02:36:33 +07:00
PalmDevs
7297122fc5
chore: bump version 2025-09-16 02:36:13 +07:00
PalmDevs
eb6c2e72a9
fix(modules/HookScriptLoaderModule): also hook loadScriptFromFile 2025-09-16 02:36:00 +07:00
PalmDevs
0ac3673f03
fix: update Xposed module description 2025-09-16 01:11:49 +07:00
Palm
0c662d8b66
feat: more native bridge methods (#12) 2025-09-16 00:21:36 +07:00
PalmDevs
d21d130c9b
fix(modules/UpdaterModule): mark job as Volatile and private set 2025-09-15 23:19:49 +07:00
PalmDevs
c0045b2e1e
fix(modules): register all native methods in onContext 2025-09-15 23:19:04 +07:00
PalmDevs
f015246e94
chore: update project configuration 2025-09-15 23:12:18 +07:00
PalmDevs
85fd27fc48
chore: update dependencies and sdk ints 2025-09-15 23:12:06 +07:00
PalmDevs
656cb361c8
fix(modules): load caches and plugin states lazily 2025-09-15 23:10:05 +07:00
PalmDevs
0edc47cf65
fix: editor warnings 2025-09-15 23:09:33 +07:00
PalmDevs
d54e3dd25a
feat(modules): add PluginsModule
Only adding saving states for now.
2025-09-15 23:09:32 +07:00
PalmDevs
3bba1e9d6a
feat(modules): add CacheModule 2025-09-15 23:09:30 +07:00
PalmDevs
21d693290a
feat: more native bridge methods 2025-09-15 23:06:57 +07:00
PalmDevs
06878b3135
chore: refactor 2025-09-15 21:43:01 +07:00
PalmDevs
b12b6ab996
feat(Module): allow modules to attach own Activity.onCreate callbacks 2025-08-19 22:23:37 +07:00
PalmDevs
f8f77e88a1
feat(modules/NativeBridgeModule): map Unit return to null 2025-08-19 22:18:29 +07:00
PalmDevs
d9c6281862
fix: use ArrayList for bridge method arguments 2025-08-17 03:46:02 +07:00
PalmDevs
c5cff14c2f
feat: ability to call custom native callbacks 2025-08-17 00:01:10 +07:00
PalmDevs
56ce7f674b
feat: refactor 2025-08-16 23:49:47 +07:00
PalmDevs
8ce3e63a58
fix: fetch bundle only once 2025-08-09 20:33:09 +07:00
PalmDevs
57886feaab
feat: add back non-bridgeless compatibility + refactor 2025-08-09 19:01:51 +07:00
PalmDevs
2ef6807ac0
fix: get correct class for bridgeless 2025-08-02 18:02:50 +07:00
PalmDevs
fe80ca398b
chore: bump version 2025-05-06 18:55:36 +07:00
PalmDevs
fc6aa66b4a
fix(FontsModule): handle correct exceptions for backwards compat 2025-05-06 18:55:27 +07:00
PalmDevs
290be34b28
chore: bump dependencies and version 2025-05-06 15:21:21 +07:00
PalmDevs
163c3a5eb6
fix(FontsModule): hook correct class on 280201+ 2025-05-06 15:20:50 +07:00
PalmDevs
bf39d6c7ef
ci(release): bump actions/setup-java to v3 2025-05-04 17:24:38 +07:00
PalmDevs
930e96b649
fix: merge fail 2025-05-04 17:16:48 +07:00
PalmDevs
cfb4833373
chore: merge upstream/master into main 2025-05-04 17:06:07 +07:00
Palm
e4b2449880
ci(release): don't upload duplicate apk 2024-10-22 23:27:18 +07:00
Palm
750cbac9db
chore: build version 2024-10-22 23:18:08 +07:00
Palm
7a7dcefe74
fix: increase fetch timeout when bundled is cached 2024-10-22 23:16:39 +07:00
Palm
5f3d7be45c
fix: rename loader name 2024-10-21 02:43:27 +07:00
Palm
249f37fd15 chore: bump version 2024-10-20 18:25:11 +00:00
Gabriel.W.Jackson
a5257fad86
feat: Rebrand to Revenge (#2)
Co-authored-by: Palm <git@palmdevs.me>
Co-authored-by: pylixonly <82711525+pylixonly@users.noreply.github.com>
2024-10-21 01:21:18 +07:00
Palm
72af6df31f
fix: revert toasts change to fix unloading issue 2024-10-04 19:12:35 +00:00
Pun Butrach
de0a2aca32 build: Remove JCenter as repository source 2024-10-04 19:11:11 +00:00
oSumAtrIX
a29048358f
feat: Download Revenge JS 2024-09-14 14:58:15 +02:00
54 changed files with 5650 additions and 2117 deletions

View file

@ -1,25 +1,27 @@
name: Build APK
on: [push]
jobs:
build:
name: Build and push
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-java@v3
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"
- name: "Grant execution permission"
- name: Make Gradle executable
run: chmod +x ./gradlew
- name: "Build APKs"
- name: Build APKs
run: |
# Retry the Gradle build up to 3 times to mitigate transient cache/service errors
ATTEMPT=0
@ -35,30 +37,30 @@ jobs:
exit 1
fi
- name: "Setup directories"
run: |
- name: Setup directories
run:
mkdir -p /tmp/ShiggyXposedbuild/apks
mkdir -p /tmp/ShiggyXposedbuild/tools
- name: "Move debug APK"
- name: Move debug APK
run: mv ./app/build/outputs/apk/debug/app-debug.apk /tmp/ShiggyXposedbuild/apks
- name: "Download uber-apk-signer"
- name: Download uber-apk-signer
run: wget -nv "https://github.com/patrickfav/uber-apk-signer/releases/download/v1.2.1/uber-apk-signer-1.2.1.jar" -O /tmp/ShiggyXposedbuild/tools/uber-apk-signer.jar
- name: "Sign release APK"
- name: Sign release APK
run: java -jar /tmp/ShiggyXposedbuild/tools/uber-apk-signer.jar --apks ./app/build/outputs/apk/release/app-release-unsigned.apk --out /tmp/ShiggyXposedbuild/apks/
- name: "Rename release APK"
- name: Rename release APK
run: mv /tmp/ShiggyXposedbuild/apks/app-release-aligned-debugSigned.apk /tmp/ShiggyXposedbuild/apks/app-release.apk
- name: "Upload debug APK"
- name: Upload debug APK
uses: actions/upload-artifact@v4
with:
name: app-debug
path: /tmp/ShiggyXposedbuild/apks/app-debug.apk
- name: "Upload release APK"
- name: Upload All APKs
uses: actions/upload-artifact@v4
with:
name: app-release

View file

@ -1,128 +0,0 @@
name: Build Release
on:
workflow_dispatch:
inputs:
versionName:
required: true
description: This releases version name
default: "1.0.0"
versionCode:
required: true
description: This releases version code
default: "1000"
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup JDK 17
uses: actions/setup-java@v3
with:
distribution: "temurin"
java-version: 17
- name: Setup Android SDK
uses: android-actions/setup-android@v2.0.10
- name: Set Version
uses: chkfung/android-version-actions@v1.1
with:
gradlePath: app/build.gradle.kts
versionCode: ${{github.event.inputs.versionCode}}
versionName: ${{github.event.inputs.versionName}}
- name: Bump Version
continue-on-error: true
run: |
git config --local user.email "actions@github.com"
git config --local user.name "GitHub Actions"
git add app/build.gradle.kts
if ! git diff --quiet app/build.gradle.kts; then
git commit -m "chore: Bump version to ${{github.event.inputs.versionName}} (${{github.event.inputs.versionCode}})"
git push
else
echo "No changes to version file, skipping commit"
fi
- name: Build Signed APK
run: |
# Ensure keystore secret is present
if [ -z "${{ secrets.keystore }}" ]; then
echo "Keystore secret missing, aborting"
exit 1
fi
echo "${{ secrets.keystore }}" | base64 -d > $GITHUB_WORKSPACE/signing-key.jks
chmod +x ./gradlew
# Retry gradle up to 3 times to mitigate transient network/cache service issues
ATTEMPT=0
until [ $ATTEMPT -ge 3 ]
do
./gradlew packageReleaseUniversalApk \
-Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/signing-key.jks \
-Pandroid.injected.signing.store.password=${{ secrets.keystore_password }} \
-Pandroid.injected.signing.key.alias=${{ secrets.key_alias }} \
-Pandroid.injected.signing.key.password=${{ secrets.key_password }} --no-daemon --stacktrace && break
ATTEMPT=$((ATTEMPT+1))
echo "Gradle build failed, retrying ($ATTEMPT/3)..."
sleep 5
done
if [ $ATTEMPT -ge 3 ]; then
echo "Gradle build failed after 3 attempts"
exit 1
fi
- name: Release
run: |
set -e
echo "Locating built APK(s) under app/build/outputs..."
# Find APK files produced by the build in common output locations
apk_paths=$(find app/build/outputs -type f -name "*.apk" 2>/dev/null || true)
if [ -z "$apk_paths" ]; then
echo "ERROR: No APK files found under app/build/outputs"
echo "Diagnostics: top-level listing of app/build/outputs:"
ls -la app/build/outputs || true
echo "Diagnostics: recursive listing (files only):"
find app/build/outputs -type f -maxdepth 6 -print || true
echo "Failing the job so you can inspect the build logs and diagnostics above."
exit 1
fi
# Prepare a directory to collect APKs for the release
mkdir -p release-apks
echo "Copying found APK(s) to release-apks/ for upload:"
for apk in $apk_paths; do
echo " - $apk"
cp "$apk" "release-apks/$(basename "$apk")"
done
# Keep backward compatibility: if exactly one APK found, copy it to the root as app-release.apk
apk_count=$(echo "$apk_paths" | wc -w)
if [ "$apk_count" -eq 1 ]; then
single_apk=$(echo "$apk_paths")
cp "$single_apk" ./app-release.apk
echo "Single APK copied to ./app-release.apk"
else
echo "Multiple APKs found ($apk_count). All copied to release-apks/"
fi
git config --local user.email "actions@github.com"
git config --local user.name "GitHub Actions"
tag="${{ github.event.inputs.versionCode }}"
git tag "$tag"
git push origin "$tag"
# Create the GitHub release including all APK(s) we collected (debug + release)
gh release create "$tag" \
--title "${{ github.event.inputs.versionName }}" \
--generate-notes \
release-apks/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View file

@ -15,3 +15,4 @@ build
local.properties
app/release/release/
release
release.keystore

3
.idea/.gitignore generated vendored Normal file
View file

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

40
README.md Normal file
View file

@ -0,0 +1,40 @@
# GoonXposed
GoonXposed is a root Xposed module that injects the GoonCord Discord mobile client modifications into the official Discord application.
## Info
* Injects GoonCord client into the Discord mobile app.
* Built in Kotlin.
* Material 3 like Shake menu with some discord popup accents
## Prerequisites
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.
The project is built with Kotlin and inherits its foundational structure from KettuXposed.
# Troubleshooting
- 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.
# Thanks & Credits
This project was made possible thanks to the work of:
· 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
![OTKUSteyler](https://count.getloli.com/@OTKUSteyler?name=OTKUSteyler&theme=miku&padding=7&offset=0&align=top&scale=1&pixelated=1&darkmode=1)

View file

@ -12,8 +12,20 @@ android {
applicationId = "GoonXposed.xposed"
minSdk = 24
targetSdk = 36
versionCode = 1100
versionName = "1.2.0"
versionCode = 1358
versionName = "1.3.8"
}
signingConfigs {
create("release") {
val keystoreFile = file("../release.keystore")
if (keystoreFile.exists()) {
storeFile = keystoreFile
storePassword = "goonxposed"
keyAlias = "goonxposed"
keyPassword = "goonxposed"
}
}
}
buildTypes {
@ -24,35 +36,38 @@ android {
}
release {
isDebuggable = false
isMinifyEnabled = false
isMinifyEnabled = true
val keystoreFile = file("../release.keystore")
if (keystoreFile.exists()) {
signingConfig = signingConfigs.getByName("release")
}
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
buildConfig = true
}
kotlinOptions { jvmTarget = "17" }
buildFeatures { buildConfig = true }
}
repositories {
mavenCentral()
google()
maven("https://jitpack.io")
tasks.withType<org.gradle.api.tasks.compile.JavaCompile>().configureEach {
options.compilerArgs.add("-Xlint:-options")
}
dependencies {
compileOnly("de.robv.android.xposed:api:82")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1")
implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.0")
implementation("io.ktor:ktor-client-core:3.3.0")
implementation("io.ktor:ktor-client-cio:3.3.0")
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
implementation("androidx.core:core-ktx:1.17.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2")
implementation("io.ktor:ktor-client-core:3.3.1")
implementation("io.ktor:ktor-client-cio:3.3.1")
implementation("com.google.android.material:material:1.12.0")
implementation("dev.rikka.shizuku:api:13.1.5")
implementation("dev.rikka.shizuku:provider:13.1.5")
}

View file

@ -1,8 +1,9 @@
<?xml version="1.0" encoding="utf-8" ?>
<manifest
package="GoonXposed.xposed"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
xmlns:tools="http://schemas.android.com/tools"
>
<application
android:allowBackup="true"
android:label="@string/app_name"

View file

@ -9,7 +9,8 @@ class Constants {
const val CACHE_DIR = "cache/pyoncord"
const val MAIN_SCRIPT_FILE = "bundle.js"
const val LOG_TAG = "GoonXposed"
const val LOG_TAG = "Goon"
const val LOADER_NAME = "GoonXposed"

View file

@ -0,0 +1,108 @@
package GoonXposed.xposed
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.os.Bundle
import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.IXposedHookZygoteInit
import de.robv.android.xposed.callbacks.XC_LoadPackage
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.modules.*
import GoonXposed.xposed.modules.appearance.FontsModule
import GoonXposed.xposed.modules.appearance.SysColorsModule
import GoonXposed.xposed.modules.appearance.ThemesModule
import GoonXposed.xposed.modules.bridge.AdditionalBridgeMethodsModule
import GoonXposed.xposed.modules.bridge.BridgeModule
import GoonXposed.xposed.modules.no_track.BlockCrashReportingModule
import GoonXposed.xposed.modules.no_track.BlockDeepLinksTrackingModule
import kotlinx.coroutines.CompletableDeferred
object HookStateHolder {
/**
* Whether all hooks are completed, and we are ready to load the JS bundle.
*/
val readyDeferred = CompletableDeferred<Unit>()
/**
* Whether we have successfully received a [Context] yet.
* Sometimes the app process is recreated and Xposed hooks way too late for us to get [Context] from [ContextWrapper.attachBaseContext].
* But since Xposed hooks before [Activity.onCreate], we can still get it from there and still initialize properly.
*/
@Volatile
var gotContext = false
}
class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
private var hooked = false
private val modules = mutableListOf(
HookScriptLoaderModule,
BridgeModule,
AdditionalBridgeMethodsModule,
PluginsModule(),
UpdaterModule,
FixResourcesModule,
BlockDeepLinksTrackingModule,
BlockCrashReportingModule,
LogBoxModule,
CacheModule,
PerfPatchesModule,
FontsModule,
ThemesModule,
SysColorsModule
)
init {
modules += PayloadGlobalModule(modules)
}
override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) {
for (module in modules) module.onInit(startupParam)
}
override fun handleLoadPackage(param: XC_LoadPackage.LoadPackageParam) = with(param) {
if (hooked) return
val reactActivity = classLoader.loadClass(Constants.TARGET_ACTIVITY)
ContextWrapper::class.java.hookMethod("attachBaseContext", Context::class.java) {
after {
val ctx = args[0] as Context
HookStateHolder.gotContext = true
Log.i("Received Context")
this@Main.onContext(ctx)
}
}
reactActivity.hookMethod("onCreate", Bundle::class.java) {
after {
val act = thisObject as Activity
Log.i("Received Activity")
if (!HookStateHolder.gotContext) {
Log.w("Activity created before we got Context, process may have been recreated!")
this@Main.onContext(act.applicationContext)
}
this@Main.onActivity(act)
HookStateHolder.readyDeferred.complete(Unit)
}
}
this@Main.onLoad(param)
hooked = true
}
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {
for (module in modules) module.onLoad(packageParam)
}
override fun onContext(context: Context) {
for (module in modules) module.onContext(context)
}
override fun onActivity(activity: Activity) {
for (module in modules) module.onActivity(activity)
}
}

View file

@ -0,0 +1,161 @@
package GoonXposed.xposed
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
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 kotlinx.serialization.json.JsonObjectBuilder
import java.io.File
import java.lang.reflect.Method
/**
* 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 {
open fun onInit(startupParam: IXposedHookZygoteInit.StartupParam) {}
open fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {}
open fun onContext(context: Context) {}
open fun onActivity(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) {
val thisObject: Any?
get() = param.thisObject
val args: Array<Any?>
get() = param.args
var result: Any?
get() = param.result
set(value) {
param.result = value
}
var throwable: Throwable?
get() = param.throwable
set(value) {
param.throwable = value
}
}
/**
* 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)
beforeCallbacks.forEach { scope.run(it) }
}
override fun afterHookedMethod(param: MethodHookParam) {
val scope = HookScope(param)
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(block: MethodHookBuilder.() -> Unit) {
val builder = MethodHookBuilder()
builder.block()
XposedBridge.hookMethod(this, builder.build())
}
fun Method.hook(hook: XC_MethodHook) {
XposedBridge.hookMethod(this, hook)
}
fun Class<*>.hookMethod(
name: String,
vararg parameterTypes: Class<*>,
block: MethodHookBuilder.() -> Unit
): Method {
val method = method(name, *parameterTypes)
method.hook(block)
return method
}
fun File.asDir(): File {
if (!exists() || !isDirectory) mkdirs()
return this
}
fun File.asFile(): File {
parentFile?.mkdirs()
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 {
packageManager.getPackageInfo(packageName, 0)
} catch (e: PackageManager.NameNotFoundException) {
null
}
return AppInfo(
name = label,
packageName = packageName,
version = packageInfo?.versionName ?: "",
versionCode = packageInfo?.versionCode ?: 0
)
}

View file

@ -0,0 +1,56 @@
package GoonXposed.xposed
import android.app.AlertDialog
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
import kotlin.system.exitProcess
class Utils {
companion object {
val JSON = Json { ignoreUnknownKeys = true }
fun reloadApp() {
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("GoonCord Recovery Options")
.setItems(arrayOf("Reload", "Delete Script", "Reset Loader Config")) { _, which ->
when (which) {
0 -> {
reloadApp()
}
1 -> {
val bundleFile = File(
context.dataDir, "${Constants.CACHE_DIR}/${Constants.MAIN_SCRIPT_FILE}"
)
if (bundleFile.exists()) bundleFile.delete()
reloadApp()
}
}
}.show()
}
}
object Log {
fun e(msg: String) = android.util.Log.e(Constants.LOG_TAG, msg)
fun e(msg: String, throwable: Throwable) = android.util.Log.e(Constants.LOG_TAG, msg, throwable)
fun i(msg: String) = android.util.Log.i(Constants.LOG_TAG, msg)
fun i(msg: String, throwable: Throwable) = android.util.Log.i(Constants.LOG_TAG, msg, throwable)
fun w(msg: String) = android.util.Log.w(Constants.LOG_TAG, msg)
fun w(msg: String, throwable: Throwable) = android.util.Log.w(Constants.LOG_TAG, msg, throwable)
}
}

View file

@ -0,0 +1,273 @@
package GoonXposed.xposed.modules
import android.content.Context
import android.util.AtomicFile
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.asDir
import GoonXposed.xposed.asFile
import GoonXposed.xposed.getAppInfo
import GoonXposed.xposed.modules.bridge.BridgeModule
import java.io.*
/**
* A module for caching versioned modules finds and blacklist, and assets mappings.
*
* See [ModulesCache] and [AssetsCache] for the cache structure.
*
* ## Methods
*
* ### Modules cache
*
* - `Shiggy.caches.modules.read() : { blacklist: number[], finds: { [filter: string]: { [id: string]: number } | null }, version: number } | null`
* - Reads the modules cache. Returns `null` if the cache file does not exist or is invalid.
*
* - `Shiggy.caches.modules.write(blacklist: number[], finds: { [filter: string]: { [id: string]: number } | null }) : void`
* - Writes the modules cache.
*
* ### Assets cache
*
* - `Shiggy.caches.assets.read() : { data: { [name: string]: { [type: string]: number } }, version: number } | null`
* - Reads the assets cache. Returns `null` if the cache file does not exist or is invalid.
*
* - `Shiggy.caches.assets.write(data: { [name: string]: { [type: string]: number } }) : void`
* - Writes the assets cache.
*
* The caches are versioned and tied to the app version code. When the app is updated,
* the caches are invalidated and new cache files are created.
*/
object CacheModule : Module() {
private const val CACHE_DIR = "Goon"
private const val MODULES_CACHE_PREFIX = "modules"
private const val ASSETS_CACHE_PREFIX = "assets"
private lateinit var modulesCache: ModulesCache
private lateinit var assetsCache: AssetsCache
override fun onContext(context: Context) = with(context) {
val ShiggyCacheDir = File(cacheDir, CACHE_DIR).apply { asDir() }
val (_, _, _, versionCode) = getAppInfo()
val modulesCacheFile = File(
ShiggyCacheDir, "$MODULES_CACHE_PREFIX.$versionCode"
).apply { asFile() }
val assetsCacheFile = File(
ShiggyCacheDir, "$ASSETS_CACHE_PREFIX.$versionCode"
).apply { asFile() }
BridgeModule.registerMethod("Shiggy.caches.modules.read") {
if (::modulesCache.isInitialized) modulesCache.toMap() else ModulesCache.loadFromFileOrNull(modulesCacheFile)
?.let {
modulesCache = it
it.toMap()
}
}
BridgeModule.registerMethod("Shiggy.caches.modules.write") {
val (blacklist, finds) = it
@Suppress("UNCHECKED_CAST")
modulesCache = ModulesCache(
blacklist as ArrayList<Double>, finds as HashMap<String, HashMap<String, Double>?>
).apply { saveToFile(modulesCacheFile) }
Log.i("Modules cache saved: ${modulesCacheFile.absolutePath} (blacklisted: ${blacklist.size}, finds: ${finds.size})")
}
BridgeModule.registerMethod("Shiggy.caches.assets.read") {
if (::assetsCache.isInitialized) assetsCache.toMap() else AssetsCache.loadFromFileOrNull(assetsCacheFile)
?.let {
assetsCache = it
it.toMap()
}
}
BridgeModule.registerMethod("Shiggy.caches.assets.write") {
val (data) = it
@Suppress("UNCHECKED_CAST")
assetsCache =
AssetsCache(data as HashMap<String, HashMap<String, Double>>).apply { saveToFile(assetsCacheFile) }
Log.i("Assets cache saved: ${assetsCacheFile.absolutePath} (count: ${data.size})")
}
}
}
private class CacheVersionMismatchException(expected: Int, actual: Int) :
Throwable("Expected cache version: $expected, but got version: $actual")
data class ModulesCache(
val blacklist: List<Double>, val finds: Map<String, Map<String, Double>?>
) {
fun saveToFile(file: File) {
val atomic = AtomicFile(file)
var fos: FileOutputStream? = null
try {
fos = atomic.startWrite()
val out = DataOutputStream(BufferedOutputStream(fos))
out.writeInt(VERSION)
// Write blacklist
out.writeInt(blacklist.size)
for (num in blacklist) out.writeInt(num.toInt())
// Write finds
out.writeInt(finds.size)
for ((filter, matches) in finds) {
out.writeUTF(filter)
if (matches == null) {
out.writeBoolean(false) // null marker
} else {
out.writeBoolean(true) // not null
out.writeInt(matches.size)
for ((id, flag) in matches) {
out.writeInt(id.toInt())
out.writeInt(flag.toInt())
}
}
}
out.flush()
atomic.finishWrite(fos)
} catch (t: Throwable) {
if (fos != null) atomic.failWrite(fos)
throw t
}
}
fun toMap(): Map<String, Any> {
val findsMap = finds.mapValues { (_, matches) -> matches?.map { (k, v) -> k to v }?.toMap() }
return mapOf("blacklist" to blacklist, "finds" to findsMap, "version" to VERSION)
}
companion object {
const val VERSION = 3
fun loadFromFileOrNull(file: File): ModulesCache? {
if (!file.exists() || file.length() <= 0L) return null
val atomic = AtomicFile(file)
try {
DataInputStream(BufferedInputStream(atomic.openRead())).use { input ->
val version = input.readInt()
if (version != VERSION) throw CacheVersionMismatchException(VERSION, version)
val blacklistSize = input.readInt()
val blacklist = MutableList(blacklistSize) { input.readInt().toDouble() }
val findsSize = input.readInt()
val finds = mutableMapOf<String, Map<String, Double>?>()
repeat(findsSize) {
val filter = input.readUTF()
val hasMatches = input.readBoolean()
if (!hasMatches) {
finds[filter] = null
} else {
val mapSize = input.readInt()
val matches = mutableMapOf<String, Double>()
repeat(mapSize) {
val id = input.readInt().toString()
val flag = input.readInt().toDouble()
matches[id] = flag
}
finds[filter] = matches
}
}
return ModulesCache(blacklist, finds)
}
} catch (e: CacheVersionMismatchException) {
Log.i("Modules cache version mismatch: ${e.message}")
} catch (e: EOFException) {
Log.e("Modules cache corrupt: ${e.message}")
} catch (e: IOException) {
Log.e("Failed to read modules cache: ${e.message}")
}
runCatching { file.delete() }
return null
}
}
}
data class AssetsCache(
val data: Map<String, Map<String, Double>>
) {
fun saveToFile(file: File) {
val atomic = AtomicFile(file)
var fos: FileOutputStream? = null
try {
fos = atomic.startWrite()
val out = DataOutputStream(BufferedOutputStream(fos))
out.writeInt(VERSION)
out.writeInt(data.size)
for ((name, mappings) in data) {
out.writeUTF(name)
out.writeInt(mappings.size)
for ((type, id) in mappings) {
out.writeUTF(type)
out.writeInt(id.toInt())
}
}
out.flush()
atomic.finishWrite(fos)
} catch (t: Throwable) {
if (fos != null) atomic.failWrite(fos)
throw t
}
}
fun toMap(): Map<String, Any> {
return mapOf(
"data" to data.mapValues { (_, mappings) ->
mappings.map { (type, id) -> type to id }.toMap()
}, "version" to VERSION
)
}
companion object {
const val VERSION = 2
fun loadFromFileOrNull(file: File): AssetsCache? {
if (!file.exists() || file.length() <= 0L) return null
val atomic = AtomicFile(file)
try {
DataInputStream(BufferedInputStream(atomic.openRead())).use { input ->
val version = input.readInt()
if (version != VERSION) throw CacheVersionMismatchException(VERSION, version)
val dataSize = input.readInt()
val data = mutableMapOf<String, Map<String, Double>>()
repeat(dataSize) {
val name = input.readUTF()
val mappingsSize = input.readInt()
val mappings = mutableMapOf<String, Double>()
repeat(mappingsSize) {
val type = input.readUTF()
val id = input.readInt().toDouble()
mappings[type] = id
}
data[name] = mappings
}
return AssetsCache(data)
}
} catch (e: CacheVersionMismatchException) {
Log.i("Assets cache version mismatch: ${e.message}")
} catch (e: EOFException) {
Log.e("Assets cache corrupt: ${e.message}")
} catch (e: IOException) {
Log.e("Failed to read assets cache: ${e.message}")
}
runCatching { file.delete() }
return null
}
}
}

View file

@ -0,0 +1,25 @@
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
import GoonXposed.xposed.Module
import GoonXposed.xposed.hookMethod
/**
* Hooks [Resources.getIdentifier] to fix resource package name mismatch.
*/
object FixResourcesModule : Module() {
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
if (packageName != TARGET_PACKAGE) {
Resources::class.java.hookMethod(
"getIdentifier", String::class.java, String::class.java, String::class.java
) {
before {
if (args[2] == packageName) args[2] = TARGET_PACKAGE
}
}
}
}
}

View file

@ -0,0 +1,140 @@
package GoonXposed.xposed.modules
import android.content.res.AssetManager
import android.content.res.XModuleResources
import de.robv.android.xposed.IXposedHookZygoteInit
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.callbacks.XC_LoadPackage
import GoonXposed.xposed.Constants
import GoonXposed.xposed.HookScope
import GoonXposed.xposed.HookStateHolder
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.asDir
import GoonXposed.xposed.asFile
import GoonXposed.xposed.hook
import GoonXposed.xposed.method
import GoonXposed.xposed.safeLoadClass
import GoonXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import java.io.File
import java.lang.reflect.Method
/**
* Hooks React Native's script loading methods to load custom scripts and bundles.
*
* Preload scripts should be placed in the [PRELOADS_DIR] directory inside the module's files directory.
*
* The main bundle should be placed in the [Constants.CACHE_DIR] directory named [Constants.MAIN_SCRIPT_FILE].
* If the bundle file does not exist, it will attempt to load `assets://Shiggy.bundle` from the module's assets.
*/
object HookScriptLoaderModule : Module() {
private lateinit var preloadsDir: File
private lateinit var mainScript: File
private lateinit var modulePath: String
private lateinit var resources: XModuleResources
const val PRELOADS_DIR = "preloads"
override fun onInit(startupParam: IXposedHookZygoteInit.StartupParam) {
this@HookScriptLoaderModule.modulePath = startupParam.modulePath
}
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
val cacheDir = File(appInfo.dataDir, Constants.CACHE_DIR).apply { asDir() }
val filesDir = File(appInfo.dataDir, Constants.FILES_DIR).apply { asDir() }
preloadsDir = File(filesDir, PRELOADS_DIR).apply { asDir() }
mainScript = File(cacheDir, Constants.MAIN_SCRIPT_FILE).apply { asFile() }
listOf(
"com.facebook.react.runtime.ReactInstance\$loadJSBundle$1",
"com.facebook.react.runtime.ReactInstance$1",
// TODO: Remove once Discord fully switches to Bridgeless
"com.facebook.react.bridge.CatalystInstanceImpl"
).mapNotNull { classLoader.safeLoadClass(it) }.forEach { hook(it) }
}
private fun hook(instance: Class<*>) = runCatching {
val loadScriptFromAssets = instance.method(
"loadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType!!
)
val loadScriptFromFile = instance.method(
"loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType!!
)
loadScriptFromAssets.hook {
before {
Log.i("Received call to loadScriptFromAssets: ${args[1]} (sync: ${args[2]})")
runCustomScripts(loadScriptFromFile, loadScriptFromAssets)
}
}
loadScriptFromFile.hook {
before {
Log.i("Received call to loadScriptFromFile: ${args[0]} (sync: ${args[2]})")
runCustomScripts(loadScriptFromFile, loadScriptFromAssets)
}
}
}.onFailure {
Log.e("Failed to hook script loading methods in ${instance.name}:", it)
}
private fun HookScope.runCustomScripts(loadScriptFromFile: Method, loadScriptFromAssets: Method) {
Log.i("Running custom scripts...")
runBlocking {
val ready = async { HookStateHolder.readyDeferred.join() }
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()
ready.await()
}
}
val loadSynchronously = args[2]
val runScriptFile = { file: File ->
Log.i("Loading script: ${file.absolutePath}")
XposedBridge.invokeOriginalMethod(
loadScriptFromFile, thisObject, arrayOf(file.absolutePath, file.absolutePath, loadSynchronously)
)
Unit
}
try {
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 {
preloadsDir.walk().filter { it.isFile }.forEach(runScriptFile)
if (mainScript.exists()) {
runScriptFile(mainScript)
} else {
Log.i("Main script does not exist, falling back")
if (!::resources.isInitialized) resources = XModuleResources.createInstance(modulePath, null)
XposedBridge.invokeOriginalMethod(
loadScriptFromAssets,
thisObject,
arrayOf(resources.assets, "assets://Shiggy.bundle", loadSynchronously)
)
}
}
} catch (e: Throwable) {
Log.e("Unable to run scripts:", e)
}
}
}

View file

@ -0,0 +1,170 @@
package GoonXposed.xposed.modules.LogBox
import android.app.AlertDialog
import android.content.Context
import android.text.InputType
import android.widget.*
import org.json.JSONObject
import java.io.File
object CustomBundleDialog {
fun showCustomBundleDialog(context: Context) {
val colors = LogBoxTheme.getM3Colors(context)
val filesDir = LogBoxUtils.getPyoncordDirectory(context)
val configFile = File(filesDir, "loader.json")
var currentUrl: String? = null
var isEnabled = false
if (configFile.exists()) {
try {
val json = JSONObject(configFile.readText())
val customLoadUrl = json.optJSONObject("customLoadUrl")
if (customLoadUrl != null) {
isEnabled = customLoadUrl.optBoolean("enabled", false)
currentUrl = customLoadUrl.optString("url", "")
}
} catch (_: Exception) {
}
}
val container = LogBoxComponents.createMenuContainer(context, colors)
container.addView(LogBoxComponents.createTitle(context, colors, "Custom Bundle URL"))
val toggleContainer = LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
gravity = android.view.Gravity.CENTER_VERTICAL
val params = LinearLayout.LayoutParams(
android.view.ViewGroup.LayoutParams.MATCH_PARENT,
android.view.ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
setMargins(0, 0, 0, LogBoxUtils.dpToPx(context, 16))
}
layoutParams = params
}
val toggleLabel = TextView(context).apply {
text = "Enable Custom URL"
textSize = 14f
setTextColor(colors.onSurface)
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
layoutParams = LinearLayout.LayoutParams(
0,
android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
1f
)
}
val toggleSwitch = LogBoxComponents.createM3Switch(context, colors, isEnabled)
toggleContainer.addView(toggleLabel)
toggleContainer.addView(toggleSwitch)
container.addView(toggleContainer)
val urlInput = EditText(context).apply {
hint = "http://localhost:4040/gooncord.js"
setTextColor(colors.onSurface)
setHintTextColor(colors.onSurfaceVariant)
background = LogBoxUtils.createM3Background(context, colors.surfaceVariant, 12f)
inputType = InputType.TYPE_TEXT_VARIATION_URI
setText(currentUrl ?: "")
textSize = 14f
setPadding(
LogBoxUtils.dpToPx(context, 16),
LogBoxUtils.dpToPx(context, 12),
LogBoxUtils.dpToPx(context, 16),
LogBoxUtils.dpToPx(context, 12)
)
alpha = if (isEnabled) 1f else 0.5f
val params = LinearLayout.LayoutParams(
android.view.ViewGroup.LayoutParams.MATCH_PARENT,
android.view.ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
setMargins(0, 0, 0, LogBoxUtils.dpToPx(context, 24))
}
layoutParams = params
}
container.addView(urlInput)
toggleSwitch.setOnClickListener {
val currentState = toggleSwitch.tag as Boolean
val newState = !currentState
toggleSwitch.tag = newState
LogBoxComponents.updateSwitchAppearance(context, toggleSwitch, colors, newState)
urlInput.animate()
.alpha(if (newState) 1f else 0.5f)
.setDuration(250)
.withEndAction {
urlInput.isEnabled = newState
}
.start()
}
val buttonContainer = LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
gravity = android.view.Gravity.END
}
val cancelButton = TextView(context).apply {
text = "Cancel"
setTextColor(colors.primary)
textSize = 14f
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
setPadding(
LogBoxUtils.dpToPx(context, 16),
LogBoxUtils.dpToPx(context, 10),
LogBoxUtils.dpToPx(context, 16),
LogBoxUtils.dpToPx(context, 10)
)
}
val saveButton = TextView(context).apply {
text = "Save"
setTextColor(colors.primary)
textSize = 14f
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
setPadding(
LogBoxUtils.dpToPx(context, 16),
LogBoxUtils.dpToPx(context, 10),
LogBoxUtils.dpToPx(context, 16),
LogBoxUtils.dpToPx(context, 10)
)
}
val dialog = LogBoxDialogs.createDialog(context, container)
cancelButton.setOnClickListener { dialog.dismiss() }
saveButton.setOnClickListener {
try {
val url = urlInput.text?.toString()?.trim() ?: ""
val enabled = toggleSwitch.tag as Boolean
if (enabled && url.isNotEmpty()) {
LogBoxActions.setCustomBundleURL(context, url, true)
dialog.dismiss()
} else if (!enabled) {
LogBoxActions.setCustomBundleURL(
context,
url.ifEmpty { "http://localhost:4040/shiggycord.js" },
false
)
dialog.dismiss()
} else {
Toast.makeText(context, "Please enter a valid URL", Toast.LENGTH_SHORT).show()
}
} catch (e: Exception) {
val err = e.message ?: "unknown error"
Toast.makeText(context, "Failed to set custom bundle: $err", Toast.LENGTH_LONG).show()
}
}
buttonContainer.addView(cancelButton)
buttonContainer.addView(saveButton)
container.addView(buttonContainer)
LogBoxDialogs.currentMenuContainer = container
dialog.show()
LogBoxDialogs.setDialogWindowWidth(dialog, context)
}
}

View file

@ -0,0 +1,35 @@
package GoonXposed.xposed.modules.LogBox
import android.app.AlertDialog
import android.content.Context
object FlavorSelectionMenu {
fun showFlavorSelection(context: Context) {
val colors = LogBoxTheme.getM3Colors(context)
lateinit var dialog: AlertDialog
val container = LogBoxComponents.createMenuContainer(context, colors, 20)
container.addView(LogBoxComponents.createTitle(context, colors, "Select Menu Flavor", center = true))
LogBoxConstants.FLAVORS.forEach { flavor ->
container.addView(
LogBoxComponents.createM3Button(
context,
flavor.replaceFirstChar { it.uppercase() },
colors
) {
try {
dialog.dismiss()
} catch (_: Exception) {
}
LogBoxUtils.saveMenuFlavor(context, flavor)
})
}
dialog = LogBoxDialogs.createDialog(context, container)
LogBoxDialogs.currentMenuContainer = container
dialog.show()
LogBoxDialogs.setDialogWindowWidth(dialog, context)
}
}

View file

@ -0,0 +1,211 @@
package GoonXposed.xposed.modules.LogBox
import android.content.Context
import android.widget.Toast
import org.json.JSONObject
import java.io.File
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.Utils.Companion.reloadApp
import GoonXposed.xposed.Constants
import kotlinx.coroutines.*
object LogBoxActions {
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
fun toggleSafeMode(context: Context) {
try {
val settingsFile = File(context.filesDir, "logbox/LOGBOX_SETTINGS")
val themeFile = File(context.filesDir, "logbox/LOGBOX_THEMES")
settingsFile.parentFile?.mkdirs()
val settings = if (settingsFile.exists()) {
JSONObject(settingsFile.readText())
} else {
JSONObject()
}
val safeMode = settings.optJSONObject("safeMode") ?: JSONObject()
val currentState = safeMode.optBoolean("enabled", false)
val newState = !currentState
safeMode.put("enabled", newState)
if (newState && themeFile.exists()) {
val theme = JSONObject(themeFile.readText())
val themeId = theme.optString("id")
if (themeId.isNotEmpty()) {
safeMode.put("currentThemeId", themeId)
themeFile.delete()
}
}
settings.put("safeMode", safeMode)
settingsFile.writeText(settings.toString())
Toast.makeText(context, "Safe Mode ${if (newState) "Enabled" else "Disabled"}", Toast.LENGTH_SHORT).show()
reloadApp()
} catch (e: Exception) {
Log.e("Error toggling safe mode: ${e.message}")
showError(context, "Failed to toggle safe mode", e.message)
}
}
fun toggleBundleInjection(context: Context) {
try {
val cacheDir = File(context.dataDir, Constants.CACHE_DIR)
cacheDir.mkdirs()
val bundle = File(cacheDir, Constants.MAIN_SCRIPT_FILE)
val disabled = File(cacheDir, "${Constants.MAIN_SCRIPT_FILE}.disabled")
val settingsFile = File(context.filesDir, "logbox/LOGBOX_SETTINGS")
settingsFile.parentFile?.mkdirs()
val settings = if (settingsFile.exists()) JSONObject(settingsFile.readText()) else JSONObject()
val currentlyDisabled = LogBoxUtils.isBundleInjectionDisabled(context)
val newState = !currentlyDisabled
if (bundle.exists() && disabled.exists()) {
bundle.delete()
}
if (newState) {
// Disable injection
if (disabled.exists()) {
disabled.delete()
}
if (bundle.exists()) {
bundle.renameTo(disabled)
} else {
disabled.writeText("")
}
settings.put("bundleInjectionDisabled", true)
Toast.makeText(context, "Bundle injection disabled", Toast.LENGTH_SHORT).show()
} else {
// Enable injection
if (disabled.exists()) {
disabled.renameTo(bundle)
} else if (!bundle.exists()) {
bundle.writeText("")
}
settings.put("bundleInjectionDisabled", false)
Toast.makeText(context, "Bundle injection enabled", Toast.LENGTH_SHORT).show()
}
settingsFile.writeText(settings.toString())
} catch (e: Exception) {
Log.e("Error toggling bundle injection: ${e.message}")
showError(context, "Failed to toggle bundle injection", e.message)
}
}
fun refetchBundle(context: Context) {
try {
val pyoncordDir = LogBoxUtils.getPyoncordDirectory(context)
val bundleFile = File(pyoncordDir, "bundle.js")
val backupFile = File(pyoncordDir, "bundle.js.backup")
if (bundleFile.exists()) {
backupFile.delete()
bundleFile.renameTo(backupFile)
Log.e("Bundle moved to backup")
}
reloadApp()
} catch (e: Exception) {
Log.e("Error refetching bundle: ${e.message}")
showError(context, "Failed to refetch bundle", e.message)
}
}
fun clearCacheAndReset(context: Context) {
scope.launch(Dispatchers.IO) {
try {
val pyoncordDir = LogBoxUtils.getPyoncordDirectory(context)
val bundleFile = File(pyoncordDir, "bundle.js")
val configFile = File(pyoncordDir, "loader.json")
bundleFile.delete()
if (configFile.exists()) {
val config = JSONObject(configFile.readText())
val customLoadUrl = config.optJSONObject("customLoadUrl") ?: JSONObject()
customLoadUrl.put("enabled", false)
customLoadUrl.put("url", "http://localhost:4040/gooncord.js")
config.put("customLoadUrl", customLoadUrl)
configFile.writeText(config.toString())
}
withContext(Dispatchers.Main) {
Toast.makeText(context, "Cache cleared successfully", Toast.LENGTH_SHORT).show()
reloadApp()
}
} catch (e: Exception) {
Log.e("Error clearing cache: ${e.message}")
withContext(Dispatchers.Main) {
Toast.makeText(context, "Failed to clear cache", Toast.LENGTH_SHORT).show()
}
}
}
}
fun setCustomBundleURL(context: Context, url: String, enabled: Boolean) {
try {
val pyoncordDir = LogBoxUtils.getPyoncordDirectory(context)
val configFile = File(pyoncordDir, "loader.json")
val config = if (configFile.exists()) {
JSONObject(configFile.readText())
} else {
JSONObject().apply {
put("loadReactDevTools", false)
}
}
val customLoadUrl = config.optJSONObject("customLoadUrl") ?: JSONObject()
customLoadUrl.put("enabled", enabled)
customLoadUrl.put("url", url)
config.put("customLoadUrl", customLoadUrl)
configFile.writeText(config.toString())
if (enabled) {
File(pyoncordDir, "bundle.js").delete()
Toast.makeText(context, "Custom bundle enabled", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Custom bundle disabled", Toast.LENGTH_SHORT).show()
}
reloadApp()
} catch (e: Exception) {
Log.e("Error setting custom bundle URL: ${e.message}")
showError(context, "Failed to save configuration", e.message)
}
}
fun deleteDevtools(context: Context) {
try {
val devtoolsFile = File(LogBoxUtils.getPyoncordDirectory(context), "preloads/reactDevtools.js")
if (devtoolsFile.exists()) {
if (devtoolsFile.delete()) {
Toast.makeText(context, "Devtools deleted", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Failed to delete Devtools", Toast.LENGTH_SHORT).show()
}
} else {
Toast.makeText(context, "Devtools not found", Toast.LENGTH_SHORT).show()
}
} catch (e: Exception) {
Log.e("Error deleting Devtools: ${e.message}")
showError(context, "Failed to delete Devtools", e.message)
}
}
fun showError(context: Context, title: String, message: String?) {
LogBoxMenu.showErrorDialog(context, title, message)
}
}

View file

@ -0,0 +1,257 @@
package GoonXposed.xposed.modules.LogBox
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import android.animation.*
import android.view.animation.DecelerateInterpolator
object LogBoxComponents {
fun createM3Button(context: Context, text: String, colors: M3Colors, onClick: () -> Unit): View {
return LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER
background = LogBoxUtils.createM3Background(context, colors.primaryContainer, 20f)
setPadding(
LogBoxUtils.dpToPx(context, 24),
LogBoxUtils.dpToPx(context, 10),
LogBoxUtils.dpToPx(context, 24),
LogBoxUtils.dpToPx(context, 10)
)
clipToOutline = true
isClickable = true
isFocusable = true
val params = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
LogBoxUtils.dpToPx(context, 40)
).apply {
setMargins(0, LogBoxUtils.dpToPx(context, 8), 0, 0)
}
layoutParams = params
addView(TextView(context).apply {
this.text = text
setTextColor(colors.onPrimaryContainer)
textSize = 14f
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
gravity = Gravity.CENTER
})
val rippleDrawable = android.graphics.drawable.RippleDrawable(
ColorStateList.valueOf(Color.argb(30, 255, 255, 255)),
LogBoxUtils.createM3Background(context, colors.primaryContainer, 20f),
null
)
background = rippleDrawable
setOnClickListener { onClick() }
}
}
fun createTitle(context: Context, colors: M3Colors, text: String, center: Boolean = false): TextView {
return TextView(context).apply {
this.text = text
textSize = if (center) 20f else 22f
setTextColor(colors.onSurface)
typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD)
setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, if (center) 12 else 16))
gravity = if (center) Gravity.CENTER else Gravity.START
}
}
fun createSubtitle(context: Context, colors: M3Colors, text: String): TextView {
return TextView(context).apply {
this.text = text
textSize = 14f
setTextColor(colors.onSurfaceVariant)
typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.NORMAL)
setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, 24))
gravity = Gravity.CENTER
}
}
fun createMenuContainer(context: Context, colors: M3Colors, padding: Int = 24): LinearLayout {
return LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setPadding(
LogBoxUtils.dpToPx(context, padding),
LogBoxUtils.dpToPx(context, padding),
LogBoxUtils.dpToPx(context, padding),
LogBoxUtils.dpToPx(context, padding)
)
background = LogBoxUtils.createM3Background(context, colors.surface, if (padding == 24) 28f else 24f)
}
}
fun createM3Switch(context: Context, colors: M3Colors, isChecked: Boolean): View {
val switchView = LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
tag = isChecked
layoutParams = LinearLayout.LayoutParams(
LogBoxUtils.dpToPx(context, 52),
LogBoxUtils.dpToPx(context, 32)
)
}
updateSwitchAppearance(context, switchView, colors, isChecked)
return switchView
}
fun updateSwitchAppearance(context: Context, switchView: View, colors: M3Colors, isChecked: Boolean) {
val trackColor = if (isChecked) colors.primary else colors.surfaceVariant
val thumbColor = if (isChecked) colors.onPrimary else colors.onSurfaceVariant
val currentTrackColor = if (switchView.background is GradientDrawable) {
(switchView.background as GradientDrawable).color?.defaultColor
?: (if (!isChecked) colors.primary else colors.surfaceVariant)
} else {
if (!isChecked) colors.primary else colors.surfaceVariant
}
val trackColorAnimator = ValueAnimator.ofObject(ArgbEvaluator(), currentTrackColor, trackColor)
trackColorAnimator.duration = 250
trackColorAnimator.interpolator = DecelerateInterpolator()
trackColorAnimator.addUpdateListener { animator ->
val animatedColor = animator.animatedValue as Int
val track = GradientDrawable().apply {
setColor(animatedColor)
setCornerRadius(LogBoxUtils.dpToPx(context, 16).toFloat())
}
switchView.background = track
}
if (switchView is LinearLayout) {
val thumb = if (switchView.childCount > 0) {
switchView.getChildAt(0)
} else {
View(context).apply {
val size = LogBoxUtils.dpToPx(context, 24)
layoutParams = LinearLayout.LayoutParams(size, size).apply {
marginStart = LogBoxUtils.dpToPx(context, 4)
marginEnd = LogBoxUtils.dpToPx(context, 24)
}
switchView.addView(this)
}
}
val currentParams = thumb.layoutParams as LinearLayout.LayoutParams
val targetMarginStart = if (isChecked) LogBoxUtils.dpToPx(context, 24) else LogBoxUtils.dpToPx(context, 4)
val marginAnimator = ValueAnimator.ofInt(currentParams.marginStart, targetMarginStart)
marginAnimator.duration = 250
marginAnimator.interpolator = DecelerateInterpolator()
marginAnimator.addUpdateListener { animator ->
val animatedMargin = animator.animatedValue as Int
val params = thumb.layoutParams as LinearLayout.LayoutParams
params.marginStart = animatedMargin
params.marginEnd = LogBoxUtils.dpToPx(context, 28) - animatedMargin
thumb.layoutParams = params
}
val currentThumbColor = if (thumb.background is GradientDrawable) {
(thumb.background as GradientDrawable).color?.defaultColor
?: (if (!isChecked) colors.onPrimary else colors.onSurfaceVariant)
} else {
if (!isChecked) colors.onPrimary else colors.onSurfaceVariant
}
val thumbColorAnimator = ValueAnimator.ofObject(ArgbEvaluator(), currentThumbColor, thumbColor)
thumbColorAnimator.duration = 250
thumbColorAnimator.interpolator = DecelerateInterpolator()
thumbColorAnimator.addUpdateListener { animator ->
val animatedColor = animator.animatedValue as Int
thumb.background = GradientDrawable().apply {
setColor(animatedColor)
shape = GradientDrawable.OVAL
}
}
val animatorSet = AnimatorSet()
animatorSet.playTogether(trackColorAnimator, marginAnimator, thumbColorAnimator)
animatorSet.start()
}
}
fun createAppearanceSelector(context: Context, colors: M3Colors): View {
val currentMode = LogBoxUtils.getAppearanceMode(context)
val options = listOf("System", "Light", "Dark")
val keys = listOf("system", "light", "dark")
val wrapper = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
val params = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
layoutParams = params
setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, 8))
}
val label = TextView(context).apply {
text = "Appearance"
textSize = 14f
setTextColor(colors.onSurface)
setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, 8))
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
}
wrapper.addView(label)
val row = LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
val params = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
LogBoxUtils.dpToPx(context, 40)
)
layoutParams = params
}
options.forEachIndexed { idx, title ->
val key = keys[idx]
val seg = TextView(context).apply {
text = title
gravity = Gravity.CENTER
textSize = 14f
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
val segParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)
if (idx > 0) segParams.setMargins(LogBoxUtils.dpToPx(context, 8), 0, 0, 0)
layoutParams = segParams
val isSelected = currentMode == key
background = LogBoxUtils.createM3Background(
context,
if (isSelected) colors.primaryContainer else colors.surfaceVariant,
12f
)
setTextColor(if (isSelected) colors.onPrimaryContainer else colors.onSurface)
setPadding(0, 0, 0, 0)
setOnClickListener {
LogBoxUtils.setAppearanceMode(context, key)
for (i in 0 until row.childCount) {
val child = row.getChildAt(i) as TextView
val sel = keys[i] == key
child.background = LogBoxUtils.createM3Background(
context,
if (sel) colors.primaryContainer else colors.surfaceVariant,
12f
)
child.setTextColor(if (sel) colors.onPrimaryContainer else colors.onSurface)
}
}
}
row.addView(seg)
}
wrapper.addView(row)
return wrapper
}
}

View file

@ -0,0 +1,35 @@
package GoonXposed.xposed.modules.LogBox
import android.graphics.Color
data class M3Colors(
val surface: Int,
val surfaceVariant: Int,
val onSurface: Int,
val onSurfaceVariant: Int,
val primary: Int,
val onPrimary: Int,
val primaryContainer: Int,
val onPrimaryContainer: Int,
val secondaryContainer: Int,
val onSecondaryContainer: Int,
val error: Int,
val onError: Int
)
object LogBoxConstants {
const val TAG = "LogBoxModule"
val FLAVOR_COLORS = mapOf(
"blue" to Pair("#0D47A1", "#82B1FF"),
"green" to Pair("#1B5E20", "#A5D6A7"),
"mocha" to Pair("#3E2723", "#BCAAA4"),
"vanilla" to Pair("#F9A825", "#FFF59D"),
"purple" to Pair("#6A1B9A", "#E1BEE7"),
"amber" to Pair("#FF6F00", "#FFE0B2"),
"teal" to Pair("#004D40", "#80CBC4")
)
val APPEARANCE_MODES = listOf("system", "light", "dark")
val FLAVORS = listOf("blue", "green", "mocha", "vanilla", "purple", "amber", "teal")
}

View file

@ -0,0 +1,74 @@
package GoonXposed.xposed.modules.LogBox
import android.app.AlertDialog
import android.content.Context
import android.graphics.Color
import android.view.ViewGroup
import android.widget.LinearLayout
import GoonXposed.xposed.Utils.Log
object LogBoxDialogs {
var currentMenuContainer: LinearLayout? = null
fun createDialog(context: Context, container: LinearLayout): AlertDialog {
val dialog = AlertDialog.Builder(context)
.setView(container)
.create()
dialog.window?.setBackgroundDrawable(
LogBoxUtils.createM3Background(context, Color.TRANSPARENT, 28f)
)
constrainContainerWidth(context, container)
return dialog
}
fun constrainContainerWidth(context: Context, container: LinearLayout) {
try {
val maxW = (context.resources.displayMetrics.widthPixels * 0.9).toInt()
val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
container.layoutParams = lp
try {
container.minimumWidth = maxW
} catch (_: Exception) {
}
} catch (_: Exception) {
}
}
fun setDialogWindowWidth(dialog: AlertDialog, context: Context) {
try {
val screenW = context.resources.displayMetrics.widthPixels
val maxByPercent = (screenW * 0.9).toInt()
val maxByDp = LogBoxUtils.dpToPx(context, 420)
val targetW = if (maxByPercent < maxByDp) maxByPercent else maxByDp
dialog.window?.setLayout(targetW, ViewGroup.LayoutParams.WRAP_CONTENT)
try {
currentMenuContainer?.let { container ->
val lp = container.layoutParams ?: LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
lp.width = ViewGroup.LayoutParams.MATCH_PARENT
container.layoutParams = lp
try {
container.minimumWidth = targetW
} catch (_: Exception) {
}
container.post {
try {
container.requestLayout()
container.invalidate()
} catch (_: Exception) {
}
}
}
} catch (_: Exception) {
}
} catch (_: Exception) {
}
}
}

View file

@ -0,0 +1,795 @@
package GoonXposed.xposed.modules.LogBox
import android.app.AlertDialog
import android.content.Context
import android.graphics.Color
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.*
import android.animation.*
import android.graphics.drawable.GradientDrawable
import android.text.InputType
import android.content.res.ColorStateList
import android.view.animation.DecelerateInterpolator
import org.json.JSONObject
import java.io.File
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.Utils
object LogBoxMenu {
private var currentMenuContainer: LinearLayout? = null
fun showRecoveryMenu(context: Context) {
Log.e("showRecoveryMenu called with context: $context")
try {
val colors = LogBoxTheme.getM3Colors(context)
lateinit var dialog: AlertDialog
val container = createMenuContainer(context, colors)
// Title
container.addView(createTitle(context, colors, "GoonXposed Recovery", center = true))
// Subtitle
container.addView(createSubtitle(context, colors, "Select an option to continue"))
// Safe Mode Button
val safeModeText = if (LogBoxUtils.isSafeModeEnabled(context)) "Disable Safe Mode" else "Enable Safe Mode"
container.addView(createM3Button(context, safeModeText, colors) {
dialog.dismiss()
handleMenuSelection(context, 0)
})
// Load Custom Bundle Button
container.addView(createM3Button(context, "Load Custom Bundle", colors) {
dialog.dismiss()
handleMenuSelection(context, 2)
})
// Reload App Button
container.addView(createM3Button(context, "Reload App", colors) {
dialog.dismiss()
handleMenuSelection(context, 3)
})
// Options Button
val optionsBtn = createM3Button(context, "Options", colors) {
dialog.dismiss()
handleMenuSelection(context, 5)
}
(optionsBtn.layoutParams as? LinearLayout.LayoutParams)?.let { lp ->
lp.setMargins(lp.leftMargin, LogBoxUtils.dpToPx(context, 16), lp.rightMargin, lp.bottomMargin)
optionsBtn.layoutParams = lp
}
container.addView(optionsBtn)
dialog = createDialog(context, container)
currentMenuContainer = container
dialog.show()
setDialogWindowWidth(dialog, context)
Log.e("Recovery menu shown successfully")
} catch (e: Exception) {
Log.e("Error showing recovery menu: ${e.message}", e)
throw e
}
}
fun showOptionsMenu(context: Context) {
val colors = LogBoxTheme.getM3Colors(context)
lateinit var dialog: AlertDialog
val container = createMenuContainer(context, colors, 20)
container.addView(createTitle(context, colors, "Options", center = true))
// Themes button -> opens Themes submenu
container.addView(createM3Button(context, "Themes", colors) {
dialog.dismiss()
showThemesMenu(context)
})
// Disable/Enable bundle injection
val injectionDisabled = LogBoxUtils.isBundleInjectionDisabled(context)
val injectionText = if (injectionDisabled) "Enable Bundle Injection" else "Disable Bundle Injection"
container.addView(createM3Button(context, injectionText, colors) {
dialog.dismiss()
LogBoxActions.toggleBundleInjection(context)
Utils.reloadApp()
})
container.addView(createM3Button(context, "Refetch Bundle", colors) {
dialog.dismiss()
showConfirmAction(context, "Refetch Bundle", "This will download the latest bundle from Github.") {
LogBoxActions.refetchBundle(context)
}
})
container.addView(createM3Button(context, "Clear Cache & Reset", colors) {
dialog.dismiss()
showConfirmAction(
context,
"Clear Cache & Reset",
"This will clear all cached bundles and reset to default settings."
) {
LogBoxActions.clearCacheAndReset(context)
}
})
container.addView(createM3Button(context, "Delete Devtools", colors) {
dialog.dismiss()
showConfirmAction(
context,
"Delete Devtools",
"This will delete the reactDevtools.js preload file if it exists."
) {
LogBoxActions.deleteDevtools(context)
}
})
container.addView(createM3Button(context, "Back", colors) {
dialog.dismiss()
showRecoveryMenu(context)
})
dialog = createDialog(context, container)
currentMenuContainer = container
dialog.show()
setDialogWindowWidth(dialog, context)
}
fun showThemesMenu(context: Context) {
val colors = LogBoxTheme.getM3Colors(context)
lateinit var dialog: AlertDialog
val container = createMenuContainer(context, colors, 20)
container.addView(createTitle(context, colors, "Themes", center = true))
// Segmented appearance selector
container.addView(createAppearanceSelector(context, colors))
// Flavor selection
container.addView(createM3Button(context, "Menu Color Flavor", colors) {
dialog.dismiss()
showFlavorSelection(context)
})
// Back button (return to Options)
container.addView(createM3Button(context, "Back", colors) {
dialog.dismiss()
showOptionsMenu(context)
})
dialog = createDialog(context, container)
currentMenuContainer = container
dialog.show()
setDialogWindowWidth(dialog, context)
}
fun showFlavorSelection(context: Context) {
val colors = LogBoxTheme.getM3Colors(context)
lateinit var dialog: AlertDialog
val container = createMenuContainer(context, colors, 20)
container.addView(createTitle(context, colors, "Select Menu Flavor", center = true))
LogBoxConstants.FLAVORS.forEach { flavor ->
container.addView(createM3Button(context, flavor.replaceFirstChar { it.uppercase() }, colors) {
try {
dialog.dismiss()
} catch (_: Exception) {
}
LogBoxUtils.saveMenuFlavor(context, flavor)
showThemesMenu(context)
})
}
dialog = createDialog(context, container)
currentMenuContainer = container
dialog.show()
setDialogWindowWidth(dialog, context)
}
fun showConfirmAction(context: Context, title: String, message: String, action: () -> Unit) {
val colors = LogBoxTheme.getM3Colors(context)
val container = createMenuContainer(context, colors)
container.addView(createTitle(context, colors, title))
val messageView = TextView(context).apply {
text = message
textSize = 14f
setTextColor(colors.onSurfaceVariant)
setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, 24))
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
lineHeight = LogBoxUtils.dpToPx(context, 20)
}
}
container.addView(messageView)
val buttonContainer = LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.END
}
val cancelButton = TextView(context).apply {
text = "Cancel"
setTextColor(colors.primary)
textSize = 14f
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
setPadding(
LogBoxUtils.dpToPx(context, 16),
LogBoxUtils.dpToPx(context, 10),
LogBoxUtils.dpToPx(context, 16),
LogBoxUtils.dpToPx(context, 10)
)
}
val confirmButton = TextView(context).apply {
text = "Confirm"
setTextColor(colors.primary)
textSize = 14f
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
setPadding(
LogBoxUtils.dpToPx(context, 16),
LogBoxUtils.dpToPx(context, 10),
LogBoxUtils.dpToPx(context, 16),
LogBoxUtils.dpToPx(context, 10)
)
}
val dialog = createDialog(context, container)
cancelButton.setOnClickListener { dialog.dismiss() }
confirmButton.setOnClickListener {
dialog.dismiss()
action()
}
buttonContainer.addView(cancelButton)
buttonContainer.addView(confirmButton)
container.addView(buttonContainer)
currentMenuContainer = container
dialog.show()
setDialogWindowWidth(dialog, context)
}
fun showCustomBundleDialog(context: Context) {
val colors = LogBoxTheme.getM3Colors(context)
val filesDir = LogBoxUtils.getPyoncordDirectory(context)
val configFile = File(filesDir, "loader.json")
var currentUrl: String? = null
var isEnabled = false
if (configFile.exists()) {
try {
val json = JSONObject(configFile.readText())
val customLoadUrl = json.optJSONObject("customLoadUrl")
if (customLoadUrl != null) {
isEnabled = customLoadUrl.optBoolean("enabled", false)
currentUrl = customLoadUrl.optString("url", "")
}
} catch (_: Exception) {
}
}
val container = createMenuContainer(context, colors)
container.addView(createTitle(context, colors, "Custom Bundle URL"))
// Toggle switch container
val toggleContainer = LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
val params = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
setMargins(0, 0, 0, LogBoxUtils.dpToPx(context, 16))
}
layoutParams = params
}
val toggleLabel = TextView(context).apply {
text = "Enable Custom URL"
textSize = 14f
setTextColor(colors.onSurface)
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
layoutParams = LinearLayout.LayoutParams(
0,
ViewGroup.LayoutParams.WRAP_CONTENT,
1f
)
}
val toggleSwitch = createM3Switch(context, colors, isEnabled)
toggleContainer.addView(toggleLabel)
toggleContainer.addView(toggleSwitch)
container.addView(toggleContainer)
val urlInput = EditText(context).apply {
hint = "http://localhost:4040/shiggycord.js"
setTextColor(colors.onSurface)
setHintTextColor(colors.onSurfaceVariant)
background = LogBoxUtils.createM3Background(context, colors.surfaceVariant, 12f)
inputType = InputType.TYPE_TEXT_VARIATION_URI
setText(currentUrl ?: "")
textSize = 14f
setPadding(
LogBoxUtils.dpToPx(context, 16),
LogBoxUtils.dpToPx(context, 12),
LogBoxUtils.dpToPx(context, 16),
LogBoxUtils.dpToPx(context, 12)
)
alpha = if (isEnabled) 1f else 0.5f
val params = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
setMargins(0, 0, 0, LogBoxUtils.dpToPx(context, 24))
}
layoutParams = params
}
container.addView(urlInput)
toggleSwitch.setOnClickListener {
val currentState = toggleSwitch.tag as Boolean
val newState = !currentState
toggleSwitch.tag = newState
updateSwitchAppearance(context, toggleSwitch, colors, newState)
urlInput.animate()
.alpha(if (newState) 1f else 0.5f)
.setDuration(250)
.withEndAction {
urlInput.isEnabled = newState
}
.start()
}
val buttonContainer = LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.END
}
val cancelButton = TextView(context).apply {
text = "Cancel"
setTextColor(colors.primary)
textSize = 14f
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
setPadding(
LogBoxUtils.dpToPx(context, 16),
LogBoxUtils.dpToPx(context, 10),
LogBoxUtils.dpToPx(context, 16),
LogBoxUtils.dpToPx(context, 10)
)
}
val saveButton = TextView(context).apply {
text = "Save"
setTextColor(colors.primary)
textSize = 14f
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
setPadding(
LogBoxUtils.dpToPx(context, 16),
LogBoxUtils.dpToPx(context, 10),
LogBoxUtils.dpToPx(context, 16),
LogBoxUtils.dpToPx(context, 10)
)
}
val dialog = createDialog(context, container)
cancelButton.setOnClickListener { dialog.dismiss() }
saveButton.setOnClickListener {
try {
val url = urlInput.text?.toString()?.trim() ?: ""
val enabled = toggleSwitch.tag as Boolean
if (enabled && url.isNotEmpty()) {
LogBoxActions.setCustomBundleURL(context, url, true)
dialog.dismiss()
} else if (!enabled) {
LogBoxActions.setCustomBundleURL(
context,
url.ifEmpty { "http://localhost:4040/shiggycord.js" },
false
)
dialog.dismiss()
} else {
Toast.makeText(context, "Please enter a valid URL", Toast.LENGTH_SHORT).show()
}
} catch (e: Exception) {
val err = e.message ?: "unknown error"
Toast.makeText(context, "Failed to set custom bundle: $err", Toast.LENGTH_LONG).show()
}
}
buttonContainer.addView(cancelButton)
buttonContainer.addView(saveButton)
container.addView(buttonContainer)
currentMenuContainer = container
dialog.show()
setDialogWindowWidth(dialog, context)
}
fun showErrorDialog(context: Context, title: String, message: String?) {
val colors = LogBoxTheme.getM3Colors(context)
val container = createMenuContainer(context, colors)
val titleView = TextView(context).apply {
text = title
textSize = 20f
setTextColor(colors.error)
typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD)
setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, 16))
}
container.addView(titleView)
val messageView = TextView(context).apply {
text = message ?: "An unknown error occurred"
textSize = 14f
setTextColor(colors.onSurfaceVariant)
setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, 24))
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
lineHeight = LogBoxUtils.dpToPx(context, 20)
}
}
container.addView(messageView)
val okButton = TextView(context).apply {
text = "OK"
setTextColor(colors.primary)
textSize = 14f
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
gravity = Gravity.END
setPadding(
LogBoxUtils.dpToPx(context, 16),
LogBoxUtils.dpToPx(context, 10),
LogBoxUtils.dpToPx(context, 16),
LogBoxUtils.dpToPx(context, 10)
)
}
val dialog = createDialog(context, container)
okButton.setOnClickListener { dialog.dismiss() }
container.addView(okButton)
currentMenuContainer = container
dialog.show()
setDialogWindowWidth(dialog, context)
}
// Helper functions
private fun createMenuContainer(context: Context, colors: M3Colors, padding: Int = 24): LinearLayout {
return LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setPadding(
LogBoxUtils.dpToPx(context, padding),
LogBoxUtils.dpToPx(context, padding),
LogBoxUtils.dpToPx(context, padding),
LogBoxUtils.dpToPx(context, padding)
)
background = LogBoxUtils.createM3Background(context, colors.surface, if (padding == 24) 28f else 24f)
}
}
private fun createTitle(context: Context, colors: M3Colors, text: String, center: Boolean = false): TextView {
return TextView(context).apply {
this.text = text
textSize = if (center) 20f else 22f
setTextColor(colors.onSurface)
typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD)
setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, if (center) 12 else 16))
gravity = if (center) Gravity.CENTER else Gravity.START
}
}
private fun createSubtitle(context: Context, colors: M3Colors, text: String): TextView {
return TextView(context).apply {
this.text = text
textSize = 14f
setTextColor(colors.onSurfaceVariant)
typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.NORMAL)
setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, 24))
gravity = Gravity.CENTER
}
}
fun createM3Button(context: Context, text: String, colors: M3Colors, onClick: () -> Unit): View {
return LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER
background = LogBoxUtils.createM3Background(context, colors.primaryContainer, 20f)
setPadding(
LogBoxUtils.dpToPx(context, 24),
LogBoxUtils.dpToPx(context, 10),
LogBoxUtils.dpToPx(context, 24),
LogBoxUtils.dpToPx(context, 10)
)
clipToOutline = true
isClickable = true
isFocusable = true
val params = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
LogBoxUtils.dpToPx(context, 40)
).apply {
setMargins(0, LogBoxUtils.dpToPx(context, 8), 0, 0)
}
layoutParams = params
addView(TextView(context).apply {
this.text = text
setTextColor(colors.onPrimaryContainer)
textSize = 14f
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
gravity = Gravity.CENTER
})
val rippleDrawable = android.graphics.drawable.RippleDrawable(
ColorStateList.valueOf(Color.argb(30, 255, 255, 255)),
LogBoxUtils.createM3Background(context, colors.primaryContainer, 20f),
null
)
background = rippleDrawable
setOnClickListener { onClick() }
}
}
private fun createAppearanceSelector(context: Context, colors: M3Colors): View {
val currentMode = LogBoxUtils.getAppearanceMode(context)
val options = listOf("System", "Light", "Dark")
val keys = listOf("system", "light", "dark")
val wrapper = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
val params = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
layoutParams = params
setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, 8))
}
val label = TextView(context).apply {
text = "Appearance"
textSize = 14f
setTextColor(colors.onSurface)
setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, 8))
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
}
wrapper.addView(label)
val row = LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
val params = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
LogBoxUtils.dpToPx(context, 40)
)
layoutParams = params
}
options.forEachIndexed { idx, title ->
val key = keys[idx]
val seg = TextView(context).apply {
text = title
gravity = Gravity.CENTER
textSize = 14f
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
val segParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)
if (idx > 0) segParams.setMargins(LogBoxUtils.dpToPx(context, 8), 0, 0, 0)
layoutParams = segParams
val isSelected = currentMode == key
background = LogBoxUtils.createM3Background(
context,
if (isSelected) colors.primaryContainer else colors.surfaceVariant,
12f
)
setTextColor(if (isSelected) colors.onPrimaryContainer else colors.onSurface)
setPadding(0, 0, 0, 0)
setOnClickListener {
LogBoxUtils.setAppearanceMode(context, key)
for (i in 0 until row.childCount) {
val child = row.getChildAt(i) as TextView
val sel = keys[i] == key
child.background = LogBoxUtils.createM3Background(
context,
if (sel) colors.primaryContainer else colors.surfaceVariant,
12f
)
child.setTextColor(if (sel) colors.onPrimaryContainer else colors.onSurface)
}
}
}
row.addView(seg)
}
wrapper.addView(row)
return wrapper
}
private fun createM3Switch(context: Context, colors: M3Colors, isChecked: Boolean): View {
val switchView = LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
tag = isChecked
layoutParams = LinearLayout.LayoutParams(
LogBoxUtils.dpToPx(context, 52),
LogBoxUtils.dpToPx(context, 32)
)
}
updateSwitchAppearance(context, switchView, colors, isChecked)
return switchView
}
private fun updateSwitchAppearance(context: Context, switchView: View, colors: M3Colors, isChecked: Boolean) {
val trackColor = if (isChecked) colors.primary else colors.surfaceVariant
val thumbColor = if (isChecked) colors.onPrimary else colors.onSurfaceVariant
val currentTrackColor = if (switchView.background is GradientDrawable) {
(switchView.background as GradientDrawable).color?.defaultColor
?: (if (!isChecked) colors.primary else colors.surfaceVariant)
} else {
if (!isChecked) colors.primary else colors.surfaceVariant
}
val trackColorAnimator = ValueAnimator.ofObject(ArgbEvaluator(), currentTrackColor, trackColor)
trackColorAnimator.duration = 250
trackColorAnimator.interpolator = DecelerateInterpolator()
trackColorAnimator.addUpdateListener { animator ->
val animatedColor = animator.animatedValue as Int
val track = GradientDrawable().apply {
setColor(animatedColor)
setCornerRadius(LogBoxUtils.dpToPx(context, 16).toFloat())
}
switchView.background = track
}
if (switchView is LinearLayout) {
val thumb = if (switchView.childCount > 0) {
switchView.getChildAt(0)
} else {
View(context).apply {
val size = LogBoxUtils.dpToPx(context, 24)
layoutParams = LinearLayout.LayoutParams(size, size).apply {
marginStart = LogBoxUtils.dpToPx(context, 4)
marginEnd = LogBoxUtils.dpToPx(context, 24)
}
switchView.addView(this)
}
}
val currentParams = thumb.layoutParams as LinearLayout.LayoutParams
val targetMarginStart = if (isChecked) LogBoxUtils.dpToPx(context, 24) else LogBoxUtils.dpToPx(context, 4)
val marginAnimator = ValueAnimator.ofInt(currentParams.marginStart, targetMarginStart)
marginAnimator.duration = 250
marginAnimator.interpolator = DecelerateInterpolator()
marginAnimator.addUpdateListener { animator ->
val animatedMargin = animator.animatedValue as Int
val params = thumb.layoutParams as LinearLayout.LayoutParams
params.marginStart = animatedMargin
params.marginEnd = LogBoxUtils.dpToPx(context, 28) - animatedMargin
thumb.layoutParams = params
}
val currentThumbColor = if (thumb.background is GradientDrawable) {
(thumb.background as GradientDrawable).color?.defaultColor
?: (if (!isChecked) colors.onPrimary else colors.onSurfaceVariant)
} else {
if (!isChecked) colors.onPrimary else colors.onSurfaceVariant
}
val thumbColorAnimator = ValueAnimator.ofObject(ArgbEvaluator(), currentThumbColor, thumbColor)
thumbColorAnimator.duration = 250
thumbColorAnimator.interpolator = DecelerateInterpolator()
thumbColorAnimator.addUpdateListener { animator ->
val animatedColor = animator.animatedValue as Int
thumb.background = GradientDrawable().apply {
setColor(animatedColor)
shape = GradientDrawable.OVAL
}
}
val animatorSet = AnimatorSet()
animatorSet.playTogether(trackColorAnimator, marginAnimator, thumbColorAnimator)
animatorSet.start()
}
}
private fun createDialog(context: Context, container: LinearLayout): AlertDialog {
val dialog = AlertDialog.Builder(context)
.setView(container)
.create()
dialog.window?.setBackgroundDrawable(
LogBoxUtils.createM3Background(context, Color.TRANSPARENT, 28f)
)
constrainContainerWidth(context, container)
return dialog
}
private fun constrainContainerWidth(context: Context, container: LinearLayout) {
try {
val maxW = (context.resources.displayMetrics.widthPixels * 0.9).toInt()
val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
container.layoutParams = lp
try {
container.minimumWidth = maxW
} catch (_: Exception) {
}
} catch (_: Exception) {
}
}
private fun setDialogWindowWidth(dialog: AlertDialog, context: Context) {
try {
val screenW = context.resources.displayMetrics.widthPixels
val maxByPercent = (screenW * 0.9).toInt()
val maxByDp = LogBoxUtils.dpToPx(context, 420)
val targetW = if (maxByPercent < maxByDp) maxByPercent else maxByDp
dialog.window?.setLayout(targetW, ViewGroup.LayoutParams.WRAP_CONTENT)
try {
currentMenuContainer?.let { container ->
val lp = container.layoutParams ?: ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
lp.width = ViewGroup.LayoutParams.MATCH_PARENT
container.layoutParams = lp
try {
container.minimumWidth = targetW
} catch (_: Exception) {
}
container.post {
try {
container.requestLayout()
container.invalidate()
} catch (_: Exception) {
}
}
}
} catch (_: Exception) {
}
} catch (_: Exception) {
}
}
private fun handleMenuSelection(context: Context, index: Int) {
when (index) {
0 -> LogBoxActions.toggleSafeMode(context)
1 -> showConfirmAction(
context, "Refetch Bundle",
"This will download the latest bundle from Github."
) { LogBoxActions.refetchBundle(context) }
2 -> showCustomBundleDialog(context)
3 -> Utils.reloadApp()
4 -> showConfirmAction(
context, "Clear Cache & Reset",
"This will clear all cached bundles and reset to default settings."
) { LogBoxActions.clearCacheAndReset(context) }
// Options submenu
5 -> showOptionsMenu(context)
}
}
}

View file

@ -0,0 +1,205 @@
package GoonXposed.xposed.modules.LogBox
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.Utils.Companion.reloadApp
import android.content.Context
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XC_MethodReplacement
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage
import kotlinx.coroutines.*
object LogBoxModule : Module() {
lateinit var packageParam: XC_LoadPackage.LoadPackageParam
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
var contextForMenu: Context? = null
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
this@LogBoxModule.packageParam = packageParam
try {
val dcdReactNativeHostClass = classLoader.loadClass("com.discord.bridge.DCDReactNativeHost")
val getUseDeveloperSupportMethod =
dcdReactNativeHostClass.methods.first { it.name == "getUseDeveloperSupport" }
// 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
}
override fun onContext(context: Context) {
try {
Log.e("onContext called with context: $context")
contextForMenu = context
val possibleClasses = listOf(
"com.facebook.react.devsupport.BridgeDevSupportManager",
"com.facebook.react.devsupport.BridgelessDevSupportManager",
"com.facebook.react.devsupport.DevSupportManagerImpl",
"com.facebook.react.devsupport.DevSupportManagerBase",
"com.facebook.react.devsupport.DefaultDevSupportManager"
)
var foundAny = false
possibleClasses.forEach { className ->
try {
val clazz = packageParam.classLoader.loadClass(className)
Log.e("Found class: $className")
hookDevSupportManager(clazz, context)
foundAny = true
} catch (e: Exception) {
Log.e("Class not found: $className - ${e.message}")
}
}
if (!foundAny) {
tryFindDevSupportClasses(context)
}
} catch (e: Exception) {
// Handle exception
}
}
private fun tryFindDevSupportClasses(context: Context) {
try {
val dexFile = packageParam.classLoader.javaClass.getDeclaredField("pathList")
dexFile.isAccessible = true
Log.e("Searching for DevSupport classes in classloader...")
} catch (e: Exception) {
Log.e("Could not search for classes: ${e.message}")
}
}
private fun hookDevSupportManager(clazz: Class<*>, context: Context) {
Log.e("Attempting to hook ${clazz.name}")
Log.e("Available methods in ${clazz.simpleName}:")
clazz.methods.forEach { method ->
if (method.name.contains("Dev") || method.name.contains("Reload") || method.name.contains("Options")) {
Log.e(" - ${method.name}")
}
}
try {
try {
val handleReloadJSMethod = clazz.methods.firstOrNull { it.name == "handleReloadJS" }
if (handleReloadJSMethod != null) {
XposedBridge.hookMethod(handleReloadJSMethod, object : XC_MethodReplacement() {
override fun replaceHookedMethod(param: MethodHookParam): Any? {
Log.e("handleReloadJS called - reloading app")
reloadApp()
return null
}
})
}
} catch (e: Exception) {
Log.e("Failed to hook handleReloadJS: ${e.message}")
}
try {
val showDevOptionsDialogMethod = clazz.methods.firstOrNull { it.name == "showDevOptionsDialog" }
if (showDevOptionsDialogMethod != null) {
XposedBridge.hookMethod(showDevOptionsDialogMethod, object : XC_MethodReplacement() {
override fun replaceHookedMethod(param: MethodHookParam): Any? {
try {
var activityContext: Context? = null
try {
activityContext = getContextFromDevSupport(clazz, param.thisObject)
if (activityContext != null) {
Log.e("Successfully got context from DevSupport")
}
} catch (e: Exception) {
Log.e("Failed to get context from DevSupport (non-fatal): ${e.message}")
}
val finalContext = activityContext ?: contextForMenu ?: context
Log.e("Using context: $finalContext (type: ${finalContext.javaClass.name})")
LogBoxNavigation.showRecoveryMenu(finalContext)
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
})
}
} catch (e: Exception) {
// Handle exception
}
} catch (e: Exception) {
// Handle exception
}
}
private fun getContextFromDevSupport(clazz: Class<*>, instance: Any?): Context? {
if (instance == null) {
Log.e("getContextFromDevSupport: instance is null")
return null
}
return try {
val helpers = listOf(
"mReactInstanceDevHelper",
"reactInstanceDevHelper",
"mReactInstanceManager",
"mApplicationContext"
)
for (helperName in helpers) {
try {
Log.e("Trying field: $helperName")
val helperField = XposedHelpers.findFieldIfExists(clazz, helperName)
if (helperField == null) {
Log.e("Field $helperName not found, skipping")
continue
}
val helper = helperField.get(instance)
if (helper == null) {
Log.e("Field $helperName is null, skipping")
continue
}
if (helper is Context) {
Log.e("Field $helperName is a Context, returning it")
return helper
}
val getCurrentActivityMethod = helper.javaClass.methods.firstOrNull {
it.name == "getCurrentActivity"
}
if (getCurrentActivityMethod != null) {
val ctx = getCurrentActivityMethod.invoke(helper) as? Context
if (ctx != null) {
Log.e("Got context from $helperName.getCurrentActivity()")
return ctx
}
}
} catch (e: Exception) {
Log.e("Error trying $helperName: ${e.message}")
}
}
Log.e("Could not get context from DevSupport object using any method")
null
} catch (e: Exception) {
Log.e("Failed to get context (outer catch): ${e.message}")
null
}
}
}

View file

@ -0,0 +1,9 @@
package GoonXposed.xposed.modules.LogBox
import android.content.Context
object LogBoxNavigation {
fun showRecoveryMenu(context: Context) {
LogBoxMenu.showRecoveryMenu(context)
}
}

View file

@ -0,0 +1,95 @@
package GoonXposed.xposed.modules.LogBox
import android.content.Context
import android.graphics.Color
import org.json.JSONObject
import java.io.File
import GoonXposed.xposed.Utils.Log
object LogBoxTheme {
fun isDarkMode(context: Context): Boolean {
try {
val settingsFile = File(context.filesDir, "logbox/LOGBOX_SETTINGS")
if (settingsFile.exists()) {
val json = JSONObject(settingsFile.readText())
val mode = json.optString("appearanceMode", "")
if (mode == "light") return false
if (mode == "dark") return true
}
} catch (e: Exception) {
Log.e("Error reading LogBox appearance settings: ${e.message}")
}
return (context.resources.configuration.uiMode and
android.content.res.Configuration.UI_MODE_NIGHT_MASK) ==
android.content.res.Configuration.UI_MODE_NIGHT_YES
}
fun getM3Colors(context: Context): M3Colors {
val isDark = isDarkMode(context)
val base = if (isDark) {
M3Colors(
surface = Color.parseColor("#1C1B1F"),
surfaceVariant = Color.parseColor("#49454F"),
onSurface = Color.parseColor("#E6E1E5"),
onSurfaceVariant = Color.parseColor("#CAC4D0"),
primary = Color.parseColor("#D0BCFF"),
onPrimary = Color.parseColor("#381E72"),
primaryContainer = Color.parseColor("#4F378B"),
onPrimaryContainer = Color.parseColor("#EADDFF"),
secondaryContainer = Color.parseColor("#4A4458"),
onSecondaryContainer = Color.parseColor("#E8DEF8"),
error = Color.parseColor("#F2B8B5"),
onError = Color.parseColor("#601410")
)
} else {
M3Colors(
surface = Color.parseColor("#FFFBFE"),
surfaceVariant = Color.parseColor("#E7E0EC"),
onSurface = Color.parseColor("#1C1B1F"),
onSurfaceVariant = Color.parseColor("#49454F"),
primary = Color.parseColor("#6750A4"),
onPrimary = Color.parseColor("#FFFFFF"),
primaryContainer = Color.parseColor("#EADDFF"),
onPrimaryContainer = Color.parseColor("#21005D"),
secondaryContainer = Color.parseColor("#E8DEF8"),
onSecondaryContainer = Color.parseColor("#1D192B"),
error = Color.parseColor("#B3261E"),
onError = Color.parseColor("#FFFFFF")
)
}
var flavor = "mocha"
try {
val themeFile = File(context.filesDir, "logbox/LOGBOX_THEMES")
if (themeFile.exists()) {
val json = JSONObject(themeFile.readText())
val f = json.optString("menuFlavor", json.optString("flavor", json.optString("id", ""))).lowercase()
if (f.isNotEmpty()) flavor = f
}
if (flavor.isNotEmpty()) {
val pair = LogBoxConstants.FLAVOR_COLORS[flavor]
if (pair != null) {
val primary = Color.parseColor(pair.first)
val primaryContainer = Color.parseColor(pair.second)
val onPrimary = if (isDark) Color.WHITE else Color.WHITE
val onPrimaryContainer = if (isDark) Color.BLACK else Color.BLACK
return base.copy(
primary = primary,
primaryContainer = primaryContainer,
onPrimary = onPrimary,
onPrimaryContainer = onPrimaryContainer
)
}
}
} catch (e: Exception) {
Log.e("Error applying LogBox flavor: ${e.message}")
}
return base
}
}

View file

@ -0,0 +1,121 @@
package GoonXposed.xposed.modules.LogBox
import android.content.Context
import android.util.TypedValue
import android.graphics.drawable.GradientDrawable
import android.graphics.Color
import android.content.res.ColorStateList
import android.view.View
import android.widget.Toast
import org.json.JSONObject
import java.io.File
import GoonXposed.xposed.Constants
import GoonXposed.xposed.Utils.Log
object LogBoxUtils {
fun dpToPx(context: Context, dp: Int): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dp.toFloat(),
context.resources.displayMetrics
).toInt()
}
fun createM3Background(context: Context, color: Int, cornerRadius: Float): GradientDrawable {
return GradientDrawable().apply {
setColor(color)
setCornerRadius(dpToPx(context, cornerRadius.toInt()).toFloat())
}
}
fun getPyoncordDirectory(context: Context): File {
val dir = File(context.filesDir, "pyoncord")
if (!dir.exists()) {
dir.mkdirs()
}
return dir
}
fun showM3Toast(context: Context, message: String) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
fun getAppearanceMode(context: Context): String {
return try {
val settingsFile = File(context.filesDir, "logbox/LOGBOX_SETTINGS")
if (!settingsFile.exists()) return "system"
val json = JSONObject(settingsFile.readText())
json.optString("appearanceMode", "system")
} catch (e: Exception) {
Log.e("Error reading appearance mode: ${e.message}")
"system"
}
}
fun setAppearanceMode(context: Context, mode: String) {
try {
val settingsFile = File(context.filesDir, "logbox/LOGBOX_SETTINGS")
settingsFile.parentFile?.mkdirs()
val settings = if (settingsFile.exists()) JSONObject(settingsFile.readText()) else JSONObject()
settings.put("appearanceMode", mode)
settingsFile.writeText(settings.toString())
} catch (e: Exception) {
LogBoxActions.showError(context, "Failed to set appearance", e.message)
}
}
fun saveMenuFlavor(context: Context, flavor: String) {
try {
val themeFile = File(context.filesDir, "logbox/LOGBOX_THEMES")
themeFile.parentFile?.mkdirs()
val themeJson = JSONObject().apply {
put("menuFlavor", flavor)
}
themeFile.writeText(themeJson.toString())
} catch (e: Exception) {
Log.e("Error saving menu flavor: ${e.message}")
LogBoxActions.showError(context, "Failed to save flavor", e.message)
}
}
fun isBundleInjectionDisabled(context: Context): Boolean {
return try {
val cacheDir = File(context.dataDir, Constants.CACHE_DIR)
val bundle = File(cacheDir, Constants.MAIN_SCRIPT_FILE)
val disabled = File(cacheDir, "${Constants.MAIN_SCRIPT_FILE}.disabled")
if (disabled.exists()) {
if (bundle.exists()) {
bundle.delete()
}
return true
}
if (bundle.exists()) return false
// If neither file exists, check settings
val settingsFile = File(context.filesDir, "logbox/LOGBOX_SETTINGS")
if (!settingsFile.exists()) return false
val json = JSONObject(settingsFile.readText())
json.optBoolean("bundleInjectionDisabled", false)
} catch (e: Exception) {
Log.e("Error reading injection setting: ${e.message}")
false
}
}
fun isSafeModeEnabled(context: Context): Boolean {
return try {
val settingsFile = File(context.filesDir, "vd_mmkv/VENDETTA_SETTINGS")
if (!settingsFile.exists()) return false
val json = JSONObject(settingsFile.readText())
json.optJSONObject("safeMode")?.optBoolean("enabled", false) ?: false
} catch (e: Exception) {
Log.e("Error checking safe mode: ${e.message}")
false
}
}
}

View file

@ -0,0 +1,57 @@
package GoonXposed.xposed.modules.LogBox
import android.app.AlertDialog
import android.content.Context
object OptionsMenu {
fun showOptionsMenu(context: Context) {
val colors = LogBoxTheme.getM3Colors(context)
lateinit var dialog: AlertDialog
val container = LogBoxComponents.createMenuContainer(context, colors, 20)
container.addView(LogBoxComponents.createTitle(context, colors, "Options", center = true))
val injectionDisabled = LogBoxUtils.isBundleInjectionDisabled(context)
val injectionText = if (injectionDisabled) "Enable Bundle Injection" else "Disable Bundle Injection"
container.addView(LogBoxComponents.createM3Button(context, injectionText, colors) {
dialog.dismiss()
LogBoxActions.toggleBundleInjection(context)
GoonXposed.xposed.Utils.Companion.reloadApp()
})
container.addView(LogBoxComponents.createM3Button(context, "Refetch Bundle", colors) {
dialog.dismiss()
RecoveryMenu.showConfirmAction(
context,
"Refetch Bundle",
"This will download the latest bundle from Github."
) {
LogBoxActions.refetchBundle(context)
}
})
container.addView(LogBoxComponents.createM3Button(context, "Clear Cache & Reset", colors) {
dialog.dismiss()
RecoveryMenu.showConfirmAction(
context,
"Clear Cache & Reset",
"This will clear all cached bundles and reset to default settings."
) {
LogBoxActions.clearCacheAndReset(context)
}
})
container.addView(LogBoxComponents.createM3Button(context, "Back", colors) {
dialog.dismiss()
RecoveryMenu.showRecoveryMenu(context)
})
dialog = LogBoxDialogs.createDialog(context, container)
LogBoxDialogs.currentMenuContainer = container
dialog.show()
LogBoxDialogs.setDialogWindowWidth(dialog, context)
}
}

View file

@ -0,0 +1,143 @@
package GoonXposed.xposed.modules.LogBox
import android.app.AlertDialog
import android.content.Context
import android.widget.LinearLayout
import GoonXposed.xposed.Utils.Log
object RecoveryMenu {
fun showRecoveryMenu(context: Context) {
Log.e("showRecoveryMenu called with context: $context")
try {
val colors = LogBoxTheme.getM3Colors(context)
lateinit var dialog: AlertDialog
val container = LogBoxComponents.createMenuContainer(context, colors)
container.addView(LogBoxComponents.createTitle(context, colors, "GoonXposed Recovery", center = true))
container.addView(LogBoxComponents.createSubtitle(context, colors, "Select an option to continue"))
val safeModeText = if (LogBoxUtils.isSafeModeEnabled(context)) "Disable Safe Mode" else "Enable Safe Mode"
container.addView(LogBoxComponents.createM3Button(context, safeModeText, colors) {
dialog.dismiss()
handleMenuSelection(context, 0)
})
container.addView(LogBoxComponents.createM3Button(context, "Load Custom Bundle", colors) {
dialog.dismiss()
handleMenuSelection(context, 2)
})
container.addView(LogBoxComponents.createM3Button(context, "Reload App", colors) {
dialog.dismiss()
handleMenuSelection(context, 3)
})
val optionsBtn = LogBoxComponents.createM3Button(context, "Options", colors) {
dialog.dismiss()
handleMenuSelection(context, 5)
}
(optionsBtn.layoutParams as? LinearLayout.LayoutParams)?.let { lp ->
lp.setMargins(lp.leftMargin, LogBoxUtils.dpToPx(context, 16), lp.rightMargin, lp.bottomMargin)
optionsBtn.layoutParams = lp
}
container.addView(optionsBtn)
dialog = LogBoxDialogs.createDialog(context, container)
LogBoxDialogs.currentMenuContainer = container
dialog.show()
LogBoxDialogs.setDialogWindowWidth(dialog, context)
Log.e("Recovery menu shown successfully")
} catch (e: Exception) {
Log.e("Error showing recovery menu: ${e.message}", e)
throw e
}
}
private fun handleMenuSelection(context: Context, index: Int) {
when (index) {
0 -> LogBoxActions.toggleSafeMode(context)
1 -> showConfirmAction(context, "Refetch Bundle", "This will download the latest bundle from Github.") {
LogBoxActions.refetchBundle(context)
}
2 -> CustomBundleDialog.showCustomBundleDialog(context)
3 -> GoonXposed.xposed.Utils.Companion.reloadApp()
4 -> showConfirmAction(
context,
"Clear Cache & Reset",
"This will clear all cached bundles and reset to default settings."
) {
LogBoxActions.clearCacheAndReset(context)
}
5 -> OptionsMenu.showOptionsMenu(context)
}
}
fun showConfirmAction(context: Context, title: String, message: String, action: () -> Unit) {
val colors = LogBoxTheme.getM3Colors(context)
val container = LogBoxComponents.createMenuContainer(context, colors)
container.addView(LogBoxComponents.createTitle(context, colors, title))
val messageView = android.widget.TextView(context).apply {
text = message
textSize = 14f
setTextColor(colors.onSurfaceVariant)
setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, 24))
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
lineHeight = LogBoxUtils.dpToPx(context, 20)
}
}
container.addView(messageView)
val buttonContainer = LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
gravity = android.view.Gravity.END
}
val cancelButton = android.widget.TextView(context).apply {
text = "Cancel"
setTextColor(colors.primary)
textSize = 14f
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
setPadding(
LogBoxUtils.dpToPx(context, 16),
LogBoxUtils.dpToPx(context, 10),
LogBoxUtils.dpToPx(context, 16),
LogBoxUtils.dpToPx(context, 10)
)
}
val confirmButton = android.widget.TextView(context).apply {
text = "Confirm"
setTextColor(colors.primary)
textSize = 14f
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
setPadding(
LogBoxUtils.dpToPx(context, 16),
LogBoxUtils.dpToPx(context, 10),
LogBoxUtils.dpToPx(context, 16),
LogBoxUtils.dpToPx(context, 10)
)
}
val dialog = LogBoxDialogs.createDialog(context, container)
cancelButton.setOnClickListener { dialog.dismiss() }
confirmButton.setOnClickListener {
dialog.dismiss()
action()
}
buttonContainer.addView(cancelButton)
buttonContainer.addView(confirmButton)
container.addView(buttonContainer)
LogBoxDialogs.currentMenuContainer = container
dialog.show()
LogBoxDialogs.setDialogWindowWidth(dialog, context)
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,84 @@
package GoonXposed.xposed.modules
import android.content.res.AssetManager
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.callbacks.XC_LoadPackage
import GoonXposed.xposed.BuildConfig
import GoonXposed.xposed.Constants
import GoonXposed.xposed.MethodHookBuilder
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Companion.JSON
import GoonXposed.xposed.hook
import GoonXposed.xposed.method
import GoonXposed.xposed.safeLoadClass
import GoonXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import java.io.File
// TODO: Remove this redundant module and merge its functionality into AdditionalBridgeMethodsModule if needed
class PayloadGlobalModule(private val modules: List<Module>) : Module() {
private companion object {
const val GLOBAL_NAME = "__PYON_LOADER__"
}
private fun getPayloadString(): String = JSON.encodeToString(
buildJsonObject {
put("loaderName", Constants.LOADER_NAME)
put("loaderVersion", BuildConfig.VERSION_NAME)
@Suppress("DEPRECATION") for (module in modules) module.buildPayload(this)
})
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
val catalystInstance = classLoader.safeLoadClass("com.facebook.react.bridge.CatalystInstanceImpl")
val scriptLoader = classLoader.safeLoadClass("com.facebook.react.runtime.ReactInstance\$loadJSBundle$1")
?: classLoader.safeLoadClass("com.facebook.react.runtime.ReactInstance$1")
val setGlobalVariable: (XC_MethodHook.MethodHookParam, String, String) -> Unit = { param, key, json ->
runCatching {
// Attempt old CatalystInstanceImpl method
catalystInstance!!.method(
"setGlobalVariable", String::class.java, String::class.java
).invoke(param.thisObject, key, json)
}.onFailure {
// Failed (likely because it's a stub), use a workaround
val preloadsDir = File("${appInfo.dataDir}/${Constants.FILES_DIR}", PRELOADS_DIR)
File(preloadsDir, "rv_globals_$key.js").apply {
writeText("this[${JSON.encodeToString(key)}]=$json")
XposedBridge.invokeOriginalMethod(
scriptLoader!!.method(
"loadScriptFromFile",
String::class.java,
String::class.java,
Boolean::class.javaPrimitiveType!!
), param.thisObject, arrayOf(absolutePath, absolutePath, param.args[2])
)
delete()
}
}
}
val hook = MethodHookBuilder().run {
before {
setGlobalVariable(param, GLOBAL_NAME, getPayloadString())
}
build()
}
listOf(catalystInstance, scriptLoader).forEach { if (it != null) hook(it, hook) }
}
private fun hook(instance: Class<*>, hook: XC_MethodHook) {
instance.method(
"loadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType!!,
).hook(hook)
instance.method(
"loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType!!
).hook(hook)
}
}

View file

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

View file

@ -0,0 +1,125 @@
package GoonXposed.xposed.modules
import android.content.Context
import android.util.AtomicFile
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.asFile
import GoonXposed.xposed.modules.bridge.BridgeModule
import java.io.*
/**
* Manages the states of plugins, allowing reading and writing of plugin states to a file.
*
* ## Methods
*
* - `Shiggy.plugins.states.read(): { flags: { [pluginId: string]: number } }`
* - Reads the current plugin states from the file and returns them as a map.
*
* - `Shiggy.plugins.states.write(flags: { [pluginId: string]: number }): void`
* - Writes the provided plugin states to the file.
*/
class PluginsModule : Module() {
private companion object {
const val DATA_DIR = "Shiggy/plugins"
const val STATES_FILE = "states"
}
private lateinit var states: PluginStates
override fun onContext(context: Context) = with(context) {
val dataDir = File(filesDir, DATA_DIR)
val statesFile = File(
dataDir, STATES_FILE
).apply { asFile() }
BridgeModule.registerMethod("Shiggy.plugins.states.read") {
if (::states.isInitialized) states.toMap() else
PluginStates.loadFromFileOrNull(statesFile)?.let {
states = it
it.toMap()
}
}
BridgeModule.registerMethod("Shiggy.plugins.states.write") {
val (flags) = it
@Suppress("UNCHECKED_CAST")
states = PluginStates(
flags as Map<String, Double>
).apply { saveToFile(statesFile) }
Log.i("Plugin states saved: ${statesFile.absolutePath}")
}
}
}
private class UnsupportedPluginStatesVersionException(version: Int) :
Throwable("Unsupported plugin states version: $version")
data class PluginStates(
val flags: Map<String, Double>
) {
fun saveToFile(file: File) {
val atomic = AtomicFile(file)
var fos: FileOutputStream? = null
try {
fos = atomic.startWrite()
val out = DataOutputStream(BufferedOutputStream(fos))
out.writeInt(CURRENT_VERSION)
// Write flags
out.writeInt(flags.size)
for ((id, flags) in flags) {
out.writeUTF(id)
out.writeInt(flags.toInt())
}
out.flush()
atomic.finishWrite(fos)
} catch (t: Throwable) {
if (fos != null) atomic.failWrite(fos)
throw t
}
}
fun toMap(): Map<String, Any> = mapOf("flags" to flags)
companion object {
const val CURRENT_VERSION = 1
fun loadFromFileOrNull(file: File): PluginStates? {
if (!file.exists() || file.length() <= 0L) return null
val atomic = AtomicFile(file)
try {
DataInputStream(BufferedInputStream(atomic.openRead())).use { input ->
when (val version = input.readInt()) {
1 -> return loadV1FromFileOrNull(input)
else -> throw UnsupportedPluginStatesVersionException(version)
}
}
} catch (e: UnsupportedPluginStatesVersionException) {
Log.i(e.message!!)
} catch (e: EOFException) {
Log.e("Plugin states corrupt: ${e.message}")
} catch (e: IOException) {
Log.e("Failed to read plugin states: ${e.message}")
}
runCatching { file.delete() }
return null
}
private fun loadV1FromFileOrNull(input: DataInputStream): PluginStates {
val flagsSize = input.readInt()
val flags = mutableMapOf<String, Double>()
repeat(flagsSize) {
flags[input.readUTF()] = input.readInt().toDouble()
}
return PluginStates(flags)
}
}
}

View file

@ -0,0 +1,176 @@
package GoonXposed.xposed.modules
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.util.AtomicFile
import android.widget.Toast
import androidx.core.util.writeBytes
import de.robv.android.xposed.callbacks.XC_LoadPackage
import GoonXposed.xposed.Constants
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils
import GoonXposed.xposed.Utils.Companion.JSON
import GoonXposed.xposed.Utils.Companion.reloadApp
import GoonXposed.xposed.Utils.Log
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.*
import kotlinx.serialization.Serializable
import java.io.File
import java.lang.ref.WeakReference
@Serializable
data class CustomLoadUrl(
val enabled: Boolean = false, val url: String = ""
)
@Serializable
data class LoaderConfig(
val customLoadUrl: CustomLoadUrl = CustomLoadUrl()
)
/**
* Module that updates the JS bundle by downloading it from a remote URL.
*
* Shows dialogs when failed allowing retry.
*/
object UpdaterModule : Module() {
private lateinit var config: LoaderConfig
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var lastActivity: WeakReference<Activity>? = null
private lateinit var cacheDir: File
private lateinit var bundle: File
private lateinit var etag: File
// Path to the application's data directory so we can read LogBox settings
private lateinit var appDataDir: File
private const val TIMEOUT_CACHED = 5000L
private const val TIMEOUT = 10000L
private const val ETAG_FILE = "etag.txt"
private const val CONFIG_FILE = "loader.json"
private const val DEFAULT_BUNDLE_URL =
"https://github.com/OTKUSteyler/GoonCord/releases/latest/download/gooncord.js"
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
// store app data dir for later checks (LogBox settings live under files/logbox)
appDataDir = File(appInfo.dataDir)
cacheDir = File(appInfo.dataDir, Constants.CACHE_DIR).apply { mkdirs() }
val filesDir = File(appInfo.dataDir, Constants.FILES_DIR).apply { mkdirs() }
bundle = File(cacheDir, Constants.MAIN_SCRIPT_FILE)
etag = File(cacheDir, ETAG_FILE)
val configFile = File(filesDir, CONFIG_FILE)
config = runCatching {
if (configFile.exists()) {
JSON.decodeFromString<LoaderConfig>(configFile.readText())
} else LoaderConfig()
}.getOrDefault(LoaderConfig())
}
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,
// skip attempting to download/update the bundle.
try {
val settingsFile = File(appDataDir, "files/logbox/LOGBOX_SETTINGS")
if (settingsFile.exists()) {
val txt = settingsFile.readText()
// look for a simple true marker; this avoids introducing a new JSON dependency here
if (txt.contains("\"bundleInjectionDisabled\"") && txt.contains("true")) {
Log.i("UpdaterModule: bundle injection disabled by LogBox settings — skipping download")
return@launch
}
}
} catch (ignore: Exception) {
// If anything goes wrong reading the setting, proceed with normal behaviour.
}
HttpClient(CIO) {
expectSuccess = false
install(UserAgent) { agent = Constants.USER_AGENT }
install(HttpRedirect) {}
}.use { client ->
val url = config.customLoadUrl.takeIf { it.enabled }?.url ?: DEFAULT_BUNDLE_URL
Log.i("Fetching JS bundle from: $url")
val response: HttpResponse = client.get(url) {
headers {
if (etag.exists() && bundle.exists()) {
append(HttpHeaders.IfNoneMatch, etag.readText())
}
}
// Retries don't need timeout
if (activity == null) {
timeout {
requestTimeoutMillis = if (!bundle.exists()) TIMEOUT else TIMEOUT_CACHED
}
}
}
when (response.status) {
HttpStatusCode.OK -> {
val bytes: ByteArray = response.body()
AtomicFile(bundle).writeBytes(bytes)
val newTag = response.headers[HttpHeaders.ETag]
if (!newTag.isNullOrEmpty()) etag.writeText(newTag) else etag.delete()
Log.i("Bundle updated (${bytes.size} bytes)")
// This is a retry, so we show a dialog
if (activity != null) {
withContext(Dispatchers.Main) {
AlertDialog.Builder(activity).setTitle("GoonCord Update Successful")
.setMessage("A reload is required for changes to take effect.")
.setPositiveButton("Reload") { dialog, _ ->
reloadApp()
dialog.dismiss()
}.setCancelable(false).show()
}
}
}
HttpStatusCode.NotModified -> {
Log.i("Server responded with 304, no changes")
}
else -> {
throw ResponseException(response, "Received status: ${response.status}")
}
}
}
} catch (e: Throwable) {
Log.e("Failed to download script", e)
showErrorDialog(e)
}
}
override fun onActivity(activity: Activity) {
lastActivity = WeakReference(activity)
}
fun showErrorDialog(e: Throwable) {
val activity = lastActivity?.get() ?: return
activity.runOnUiThread {
}
}
fun resetLoaderConfig(context: Context) {
val filesDir = File(context.dataDir, Constants.FILES_DIR)
val config = File(filesDir, CONFIG_FILE)
if (config.exists()) config.delete()
}
}

View file

@ -0,0 +1,250 @@
@file:Suppress("DEPRECATION")
package GoonXposed.xposed.modules.appearance
import android.content.res.AssetManager
import android.graphics.Typeface
import android.graphics.Typeface.CustomFallbackBuilder
import android.graphics.fonts.Font
import android.graphics.fonts.FontFamily
import android.os.Build
import de.robv.android.xposed.XC_MethodReplacement
import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage
import GoonXposed.xposed.Constants
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Companion.JSON
import GoonXposed.xposed.Utils.Log
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObjectBuilder
import kotlinx.serialization.json.put
import java.io.File
import java.io.IOException
// 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,
val description: String? = null,
val spec: Int? = null,
val main: Map<String, String>,
)
object FontsModule : Module() {
private val EXTENSIONS = arrayOf("", "_bold", "_italic", "_bold_italic")
private val FILE_EXTENSIONS = arrayOf(".ttf", ".otf")
private const val FONTS_ASSET_PATH = "fonts/"
private lateinit var fontsDir: File
private lateinit var fontsDownloadsDir: File
private var fontsAbsPath: String? = null
override fun buildPayload(builder: JsonObjectBuilder) {
builder.apply {
put("fontPatch", 2)
}
}
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
XposedHelpers.findAndHookMethod(
"com.facebook.react.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
val fontDef = try {
JSON.decodeFromString<FontDefinition>(fontDefFile.readText())
} catch (_: Throwable) {
return@with
}
fontsDownloadsDir = File(appInfo.dataDir, "${Constants.FILES_DIR}/downloads/fonts").apply { asDir() }
fontsDir = File(fontsDownloadsDir, fontDef.name!!).apply { asDir() }
fontsAbsPath = fontsDir.absolutePath + "/"
fontsDir.listFiles()?.forEach { file ->
val fileName = file.name
if (!fileName.startsWith(".")) {
val fontName = fileName.split('.')[0]
if (fontDef.main.keys.none { it == fontName }) {
Log.i("Deleting font file: $fileName")
file.delete()
}
}
}
// These files should be downloaded by the JS side, but oh well
CoroutineScope(Dispatchers.IO).launch {
val client = HttpClient(CIO) {
install(UserAgent) { agent = Constants.USER_AGENT }
install(HttpTimeout) {
requestTimeoutMillis = 15_000
connectTimeoutMillis = 10_000
socketTimeoutMillis = 15_000
}
}
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()
}
}
}
private fun createAssetTypefaceWithFallbacks(
fontFamilyNames: Array<String>, style: Int, assetManager: AssetManager
): Typeface? {
val fontFamilies: MutableList<FontFamily> = ArrayList()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Iterate over the list of fontFamilyNames, constructing new FontFamily objects
// for use in the CustomFallbackBuilder below.
for (fontFamilyName in fontFamilyNames) {
try {
for (fileExtension in FILE_EXTENSIONS) {
val (customName, refName) = fontFamilyName.split(":")
val file = File(fontsDownloadsDir, "$customName/$refName.$fileExtension").apply { asFile() }
val font = Font.Builder(file).build()
val family = FontFamily.Builder(font).build()
fontFamilies.add(family)
}
} catch (_: Throwable) {
}
for (fontRootPath in arrayOf(fontsAbsPath, FONTS_ASSET_PATH).filterNotNull()) {
for (fileExtension in FILE_EXTENSIONS) {
val fileName =
StringBuilder().append(fontRootPath).append(fontFamilyName).append(fileExtension).toString()
try {
val builder = if (fileName[0] == '/') Font.Builder(File(fileName)) else Font.Builder(
assetManager, fileName
)
val font = builder.build()
val family = FontFamily.Builder(font).build()
fontFamilies.add(family)
} catch (_: RuntimeException) {
// If the typeface asset does not exist, try another extension.
continue
} catch (_: IOException) {
// If the font asset does not exist, try another extension.
continue
}
}
}
}
// If there's some problem constructing fonts, fall back to the default behavior.
if (fontFamilies.isEmpty()) return createAssetTypeface(fontFamilyNames[0], style, assetManager)
val fallbackBuilder = CustomFallbackBuilder(fontFamilies[0])
for (i in 1 until fontFamilies.size) {
fallbackBuilder.addCustomFallback(fontFamilies[i])
}
return fallbackBuilder.build()
}
return null
}
private fun createAssetTypeface(
fontFamilyName: String, style: Int, assetManager: AssetManager
): Typeface? {
// This logic attempts to safely check if the frontend code is attempting to use
// fallback fonts, and if it is, to use the fallback typeface creation logic.
var fontFamilyName: String = fontFamilyName
val fontFamilyNames = fontFamilyName.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
for (i in fontFamilyNames.indices) {
fontFamilyNames[i] = fontFamilyNames[i].trim()
}
// If there are multiple font family names:
// For newer versions of Android, construct a Typeface with fallbacks
// For older versions of Android, ignore all the fallbacks and just use the first font family
if (fontFamilyNames.size > 1) {
fontFamilyName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
return createAssetTypefaceWithFallbacks(fontFamilyNames, style, assetManager)
} else {
fontFamilyNames[0]
}
}
val extension = EXTENSIONS[style]
try {
for (fileExtension in FILE_EXTENSIONS) {
val (customName, refName) = fontFamilyName.split(":")
val file = File(fontsDownloadsDir, "$customName/$refName.$fileExtension").apply { asFile() }
if (!file.exists()) throw Exception()
return Typeface.createFromFile(file.absolutePath)
}
} catch (_: Throwable) {
}
// Lastly, after all those checks above, this is the original RN logic for
// getting the typeface.
for (fontRootPath in arrayOf(fontsAbsPath, FONTS_ASSET_PATH).filterNotNull()) {
for (fileExtension in FILE_EXTENSIONS) {
val fileName =
StringBuilder().append(fontRootPath).append(fontFamilyName).append(extension).append(fileExtension)
.toString()
return try {
if (fileName[0] == '/') Typeface.createFromFile(fileName)
else Typeface.createFromAsset(assetManager, fileName)
} catch (_: RuntimeException) {
// If the typeface asset does not exist, try another extension.
continue
}
}
}
return Typeface.create(fontFamilyName, style)
}
}

View file

@ -0,0 +1,49 @@
package GoonXposed.xposed.modules.appearance
import android.R.color
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
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(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)
builder.apply {
put("isSysColorsSupported", isSupported())
if (isSupported()) putJsonObject("sysColors") {
for (accent in accents) putJsonArray(accent) {
for (shade in shades) {
val colorName = "system_" + accent + "_" + shade
val colorResourceId = runCatching {
color::class.java.getField(colorName).getInt(null)
}.getOrElse { 0 }
add(convertToColor(colorResourceId))
}
}
}
}
}
private fun convertToColor(id: Int): String {
val ctx = context.get()
val clr = if (isSupported() && ctx != null) ContextCompat.getColor(ctx, id) else 0
return String.format("#%06X", 0xFFFFFF and clr)
}
}

View file

@ -0,0 +1,184 @@
package GoonXposed.xposed.modules.appearance
import android.content.Context
import android.content.res.Resources
import androidx.core.graphics.toColorInt
import de.robv.android.xposed.callbacks.XC_LoadPackage
import GoonXposed.xposed.Constants
import GoonXposed.xposed.MethodHookBuilder
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Companion.JSON
import GoonXposed.xposed.asFile
import GoonXposed.xposed.hook
import GoonXposed.xposed.method
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObjectBuilder
import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.json.put
import java.io.File
@Serializable
data class Author(
val name: String, val id: String? = null
)
@Serializable
data class ThemeData(
val name: String,
val description: String? = null,
val authors: List<Author>? = null,
val spec: Int,
val semanticColors: Map<String, List<String>>? = null,
val rawColors: Map<String, String>? = null
)
@Serializable
data class Theme(
val id: String, val selected: Boolean, val data: ThemeData
)
object ThemesModule : Module() {
private lateinit var param: XC_LoadPackage.LoadPackageParam
private var theme: Theme? = null
private val rawColorMap = mutableMapOf<String, Int>()
private const val THEME_FILE = "current-theme.json"
@Deprecated("This method is deprecated in the parent class")
@ExperimentalSerializationApi
override fun buildPayload(builder: JsonObjectBuilder) {
builder.apply {
put("hasThemeSupport", true)
if (theme != null) put("storedTheme", JSON.encodeToJsonElement<Theme>(theme!!))
else put("storedTheme", null)
}
}
private fun String.fromScreamingSnakeToCamelCase() =
this.split("_").joinToString("") { it -> it.lowercase().replaceFirstChar { it.uppercase() } }
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {
param = packageParam
theme = getTheme()
hookTheme()
}
private fun File.isValidThemeFile(): Boolean {
if (!this.exists()) return false
val text = this.readText()
return text.isNotBlank() && text != "{}" && text != "null"
}
private fun getTheme(): Theme? {
val themeFile = File(param.appInfo.dataDir, "${Constants.FILES_DIR}/${THEME_FILE}").apply { asFile() }
if (!themeFile.isValidThemeFile()) return null
return try {
val themeText = themeFile.readText()
JSON.decodeFromString<Theme>(themeText)
} catch (_: Exception) {
null
}
}
fun hookTheme() {
try {
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, lightTheme)
} catch (e: Throwable) {
GoonXposed.xposed.Utils.Log.e("Failed to hook theme: ${e.message}")
}
}
private fun hookThemeInternal(
themeManager: Class<*>, darkTheme: Class<*>, lightTheme: Class<*>
) {
val theme = this.theme ?: return
// Apply rawColors
theme.data.rawColors?.forEach { (key, value) ->
rawColorMap[key.lowercase()] = hexStringToColorInt(value)
}
// Apply semanticColors
theme.data.semanticColors?.forEach { (key, value) ->
// TEXT_NORMAL -> getTextNormal
val methodName = "get${key.fromScreamingSnakeToCamelCase()}"
value.forEachIndexed { index, v ->
when (index) {
0 -> hookThemeMethod(darkTheme, methodName, hexStringToColorInt(v))
1 -> hookThemeMethod(lightTheme, methodName, hexStringToColorInt(v))
}
}
}
// If there's any rawColors value, hook the color getter
if (!theme.data.rawColors.isNullOrEmpty()) {
try {
val getColorCompat = themeManager.getDeclaredMethod(
"getColorCompat",
Resources::class.java,
Int::class.javaPrimitiveType,
Resources.Theme::class.java,
)
val getColorCompatLegacy = themeManager.getDeclaredMethod(
"getColorCompat", Context::class.java, Int::class.javaPrimitiveType
)
val patch = MethodHookBuilder().run {
before {
try {
val arg1 = args[0]
val resources = if (arg1 is Context) arg1.resources else (arg1 as Resources)
val name = resources.getResourceEntryName(args[1] as Int)
if (rawColorMap[name] != null) result = rawColorMap[name]
} catch (_: Throwable) {
}
}
build()
}
getColorCompat.hook(patch)
getColorCompatLegacy.hook(patch)
} catch (e: Throwable) {
GoonXposed.xposed.Utils.Log.e("Failed to hook getColorCompat: ${e.message}")
}
}
}
// Parse HEX colour string to INT. Takes "#RRGGBBAA" or "#RRGGBB"
private fun hexStringToColorInt(hexString: String): Int {
return try {
if (hexString.length == 9) {
// Rearrange RRGGBBAA -> AARRGGBB so parseColor() is happy
val alpha = hexString.substring(7, 9)
val rrggbb = hexString.substring(1, 7)
"#$alpha$rrggbb".toColorInt()
} else hexString.toColorInt()
} catch (_: Throwable) {
0
}
}
private fun hookThemeMethod(themeClass: Class<*>, methodName: String, themeValue: Int) {
try {
themeClass.getDeclaredMethod(methodName).let { method ->
method.hook {
before {
result = themeValue
}
}
}
} catch (_: NoSuchMethodException) {
}
}
}

View file

@ -0,0 +1,95 @@
package GoonXposed.xposed.modules.bridge
import android.app.Activity
import android.app.AlertDialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import android.widget.Toast
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils
import GoonXposed.xposed.getAppInfo
import java.io.File
object AdditionalBridgeMethodsModule : Module() {
override fun onContext(context: Context) = with(context) {
BridgeModule.registerMethod("Shiggy.fs.getConstants") {
mapOf(
"data" to dataDir.absolutePath,
"files" to filesDir.absolutePath,
"cache" to cacheDir.absolutePath,
)
}
BridgeModule.registerMethod("Shiggy.fs.delete") {
val (path) = it
File(path as String).run {
if (this.isDirectory) this.deleteRecursively()
else this.delete()
}
}
BridgeModule.registerMethod("Shiggy.fs.exists") {
val (path) = it
File(path as String).exists()
}
BridgeModule.registerMethod("Shiggy.fs.read") { it ->
val (path) = it
val file = File(path as String).apply { openFileGuarded() }
file.bufferedReader().use { it.readText() }
}
BridgeModule.registerMethod("Shiggy.fs.write") {
val (path, contents) = it
val file = File(path as String).apply { openFileGuarded() }
file.writeText(contents as String)
}
}
override fun onActivity(activity: Activity) = with(activity) {
BridgeModule.registerMethod("Shiggy.alertError") {
val (error, version) = it
val app = getAppInfo()
val errorString = "$error"
val clipboard = applicationContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Stack Trace", errorString)
AlertDialog.Builder(this)
.setTitle("GoonCord Error")
.setMessage(
"""
GoonCord: $version
${app.name}: ${app.version} (${app.versionCode})
Device: ${Build.MANUFACTURER} ${Build.MODEL}
""".trimIndent() + errorString
)
.setPositiveButton("OK") { dialog, _ ->
dialog.dismiss()
}
.setNeutralButton("Copy") { dialog, _ ->
clipboard.setPrimaryClip(clip)
Toast.makeText(applicationContext, "Copied stack trace", Toast.LENGTH_SHORT).show()
dialog.dismiss()
}
.show()
null
}
BridgeModule.registerMethod("Shiggy.showRecoveryAlert") {
Utils.showRecoveryAlert(this)
}
}
private fun File.openFileGuarded() {
if (!this.exists()) throw Error("Path does not exist: $path")
if (!this.isFile) throw Error("Path is not a file: $path")
}
}

View file

@ -0,0 +1,159 @@
package GoonXposed.xposed.modules.bridge
import de.robv.android.xposed.callbacks.XC_LoadPackage
import GoonXposed.xposed.BuildConfig
import GoonXposed.xposed.Constants
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.hookMethod
import GoonXposed.xposed.method
import java.lang.reflect.Method
/**
* See for possible return types:
* https://github.com/facebook/react-native/blob/c23e84ae9/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/Arguments.kt#L19
*
* You may return a [Unit] and the resulting value will be `null`.
*/
typealias BridgeMethodCallback = (args: BridgeMethodArgs) -> Any?
typealias BridgeMethodArgs = ArrayList<Any>
/**
* A module that exposes a bridge for calling methods from JavaScript.
*
* To call a method, pass an object with the following structure to a hooked method:
* ```js
* {
* Shiggy: {
* method: "method.name",
* args: [arg1, arg2, ...]
* }
* }
* ```
*
* For methods that accept additional arguments, only the first object argument is used for the bridge call.
*
* The result will be an object with either a `result` or `error` key:
* ```js
* {
* result: ... // The return value of the method
* }
* ```
* or
* ```js
* {
* error: "Error message"
* }
* ```
*/
object BridgeModule : Module() {
private lateinit var readableMapGetString: Method
private lateinit var readableMapToHashMap: Method
private lateinit var argumentsMakeNative: Method
private const val CALL_DATA_KEY = "Shiggy"
private const val METHOD_NAME_KEY = "method"
private const val METHOD_ARGS_KEY = "args"
private val methods: MutableMap<String, BridgeMethodCallback> = mutableMapOf()
/**
* Registers a bridge method that can be called from JavaScript.
*
* If the method is already registered, it will be overwritten.
*
* @param name The name of the method to register.
* @param callback The callback to invoke when the method is called.
*/
fun registerMethod(name: String, callback: BridgeMethodCallback) {
if (methods.containsKey(name)) Log.w("Bridge method already exists and will be overridden: $name")
methods[name] = callback
}
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
val arguments = classLoader.loadClass("com.facebook.react.bridge.Arguments")
val readableMap = classLoader.loadClass("com.facebook.react.bridge.ReadableMap")
val promise = classLoader.loadClass("com.facebook.react.bridge.Promise")
val promiseResolve = promise.method("resolve", Object::class.java)
argumentsMakeNative = arguments.method("makeNativeObject", Object::class.java)
readableMapGetString = readableMap.method("getString", String::class.java)
readableMapToHashMap = readableMap.method("toHashMap")
classLoader.loadClass("com.horcrux.svg.RNSVGRenderableManager")
.hookMethod("getBBox", Double::class.javaObjectType, readableMap) {
before {
callBridgeMethod(readableMapToHashMap(args[1]!!))?.let { result = it.toNativeObject() }
}
}
classLoader.loadClass("com.facebook.react.modules.blob.FileReaderModule")
.hookMethod("readAsDataURL", readableMap, promise) {
before {
val (readableMap, promise) = args
callBridgeMethod(readableMapToHashMap(readableMap!!))?.let {
promiseResolve.invoke(promise, it.toNativeObject())
result = null
}
}
}
// Clear methods every package load. Plugins should re-register on each load.
methods.clear()
registerDefaultMethods()
return@with
}
private fun registerDefaultMethods() {
methods["Shiggy.info"] = {
mapOf(
"name" to Constants.LOADER_NAME, "version" to BuildConfig.VERSION_CODE
)
}
methods["Shiggy.test"] = {
mapOf(
"string" to "string",
"number" to 7256,
"array" to listOf("testing", 527737, listOf(true)),
"object" to mapOf("nested" to true),
"boolean" to false,
"args" to it,
)
}
}
private fun Any?.toNativeObject(): Any? = argumentsMakeNative.invoke(
null, when (this) {
Unit -> null
else -> this
}
)
private fun readableMapToHashMap(map: Any): HashMap<String, Any?> {
@Suppress("UNCHECKED_CAST") return readableMapToHashMap.invoke(map) as HashMap<String, Any?>
}
private fun callBridgeMethod(hashMap: HashMap<String, Any?>): Map<String, Any?>? = try {
val (method, args) = getBridgeCallData(hashMap) ?: return null
val ret = method(args).toNativeObject()
mapOf("result" to ret)
} catch (e: Throwable) {
mapOf("error" to e.stackTraceToString())
}
private fun getBridgeCallData(hashMap: HashMap<String, Any?>): Pair<BridgeMethodCallback, BridgeMethodArgs>? {
@Suppress("UNCHECKED_CAST") val data = hashMap[CALL_DATA_KEY] as HashMap<String, Any?>?
data ?: return null
@Suppress("UNCHECKED_CAST") val name = data[METHOD_NAME_KEY] as String
val method = methods[name]
method ?: throw Error("Method not registered: $name")
@Suppress("UNCHECKED_CAST") val args = data[METHOD_ARGS_KEY] as BridgeMethodArgs
return Pair(method, args)
}
}

View file

@ -0,0 +1,52 @@
package GoonXposed.xposed.modules.no_track
import android.content.Context
import de.robv.android.xposed.callbacks.XC_LoadPackage
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.hookMethod
import GoonXposed.xposed.safeLoadClass
/**
* Hooks Discord's crash reporting to disable Sentry initialization.
* While Discord doesn't have Sentry auto-initialization on by default, we still hook the content provider just to be safe.
*/
object BlockCrashReportingModule : Module() {
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
val crashReportingClass = classLoader.safeLoadClass("com.discord.crash_reporting.CrashReporting")
crashReportingClass?.apply {
// This only exists on 30720x and above
runCatching {
hookMethod("isDisabled") {
before {
Log.i("Forced CrashReporting.isDisabled() to true")
result = true
}
}
}.onFailure {
// In older versions, this hook works fine.
// Hooking this on 30720x will result in a crash after a few seconds,
// since Discord asserts initialization when setting a Sentry tag.
hookMethod(
"init", Context::class.java, String::class.java
) {
before {
Log.i("Blocked CrashReporting initialization")
result = null
}
}
}
}
val sentryInitProviderClass = classLoader.safeLoadClass("io.sentry.android.core.SentryInitProvider")
sentryInitProviderClass?.hookMethod("onCreate") {
before {
Log.i("Blocked SentryInitProvider initialization")
result = true
}
}
return@with
}
}

View file

@ -0,0 +1,27 @@
package GoonXposed.xposed.modules.no_track
import android.content.Context
import de.robv.android.xposed.callbacks.XC_LoadPackage
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.hookMethod
import GoonXposed.xposed.safeLoadClass
/**
* Hooks Discord's deep links tracking to disable AppsFlyer initialization.
*/
object BlockDeepLinksTrackingModule : Module() {
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
val deepLinksClass = classLoader.safeLoadClass("com.discord.deep_link.DeepLinks")
deepLinksClass?.hookMethod(
"init", Context::class.java
) {
before {
Log.i("Blocked DeepLinks tracking initialization")
result = null
}
}
return@with
}
}

View file

@ -1,70 +0,0 @@
package GoonXposed.xposed
import GoonXposed.xposed.modules.*
import GoonXposed.xposed.module.appearance.ThemesModule
import GoonXposed.xposed.modules.appearance.FontsModule
import GoonXposed.xposed.modules.appearance.SysColorsModule
import GoonXposed.xposed.modules.bridge.AdditionalBridgeMethodsModule
import GoonXposed.xposed.modules.bridge.BridgeModule
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.os.Bundle
import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.IXposedHookZygoteInit
import de.robv.android.xposed.callbacks.XC_LoadPackage
class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
private var hooked = false
private val modules =
mutableListOf(
HookScriptLoaderModule(),
BridgeModule(),
AdditionalBridgeMethodsModule(),
UpdaterModule(),
FixResourcesModule(),
LogBoxModule(),
FontsModule(),
SysColorsModule(),
ThemesModule(),
)
init {
modules.add(PayloadGlobalModule(modules))
}
override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) {
for (module in modules) module.onInit(startupParam)
}
override fun handleLoadPackage(param: XC_LoadPackage.LoadPackageParam) =
with(param) {
if (hooked) return
val reactActivity = classLoader.loadClass(Constants.TARGET_ACTIVITY)
ContextWrapper::class.java.hookMethod("attachBaseContext", Context::class.java) {
after { this@Main.onContext(args[0] as Context) }
}
reactActivity.hookMethod("onCreate", Bundle::class.java) {
after { this@Main.onActivity(thisObject as Activity) }
}
this@Main.onLoad(param)
hooked = true
}
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {
for (module in modules) module.onLoad(packageParam)
}
override fun onContext(context: Context) {
for (module in modules) module.onContext(context)
}
override fun onActivity(activity: Activity) {
for (module in modules) module.onActivity(activity)
}
}

View file

@ -1,192 +0,0 @@
package GoonXposed.xposed
import android.app.Activity
import android.content.Context
import android.os.Build
import de.robv.android.xposed.IXposedHookZygoteInit
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.callbacks.XC_LoadPackage
import java.io.File
import java.lang.reflect.Method
import kotlinx.serialization.json.JsonObjectBuilder
data class AppInfo(
val name: String,
val packageName: String,
val version: String,
val versionCode: Long,
)
abstract class Module {
/** Builds a JSON payload to be injected into the JavaScript context. */
@Deprecated(
"This will be removed in future versions. Payloads can be replaced via synchronous bridge methods."
)
open fun buildPayload(builder: JsonObjectBuilder) {}
/** Called during Zygote initialization. */
open fun onInit(startupParam: IXposedHookZygoteInit.StartupParam) {}
/**
* Called when a package is loaded.
*
* Hooks should typically be set up here.
*
* Bridge methods that do not require [Context] can also be registered here.
*/
open fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {}
/**
* Called after [Context] has been attached to a [android.content.ContextWrapper] via
* [android.content.ContextWrapper.attachBaseContext].
*
* This may be called multiple times for different [Context]s, e.g. when the app is killed by
* the system.
*
* For bridge methods that require access to [Context], register them here.
*/
open fun onContext(context: Context) {}
/**
* Called after an [Activity.onCreate] method is executed.
*
* UI code that requires an [Activity] context can be placed here.
*
* Bridge methods must not be registered here, as it can be too late and cause crashes.
*
* If you need to register bridge methods that require an [Activity], do it in [onContext] and
* make it async.
*
* Once an [Activity] is available, execute the queued calls. Then from JS, call the method
* asynchronously.
*/
open fun onActivity(activity: Activity) {}
protected fun Context.getAppInfo(): AppInfo {
val pInfo = packageManager.getPackageInfo(packageName, 0)
val versionCode =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) pInfo.longVersionCode
else @Suppress("DEPRECATION") pInfo.versionCode.toLong()
return AppInfo(
packageManager.getApplicationLabel(applicationInfo).toString(),
packageName,
pInfo.versionName ?: versionCode.toString(),
versionCode,
)
}
protected fun File.asDir() {
if (!this.isDirectory) this.delete()
this.mkdirs()
}
protected fun File.asFile() {
if (!this.isFile) this.deleteRecursively()
}
protected fun Class<*>.method(name: String, vararg params: Class<*>?): Method =
getDeclaredMethod(name, *params).apply { isAccessible = true }
protected fun ClassLoader.safeLoadClass(name: String): Class<*>? =
runCatching { loadClass(name) }.getOrNull()
protected fun Method.hook(hook: XC_MethodHook): XC_MethodHook.Unhook =
XposedBridge.hookMethod(this, hook)
protected fun Method.hook(block: MethodHookBuilder.() -> Unit): XC_MethodHook.Unhook =
hook(MethodHookBuilder().apply(block).build())
protected fun Class<*>.hookMethod(
name: String,
vararg params: Class<*>?,
block: MethodHookBuilder.() -> Unit
): XC_MethodHook.Unhook = method(name, *params).hook(MethodHookBuilder().apply(block).build())
protected class MethodHookBuilder {
private var beforeBlock: (HookScope.() -> Unit)? = null
private var afterBlock: (HookScope.() -> Unit)? = null
fun before(block: HookScope.() -> Unit) {
beforeBlock = block
}
fun after(block: HookScope.() -> Unit) {
afterBlock = block
}
fun build(): XC_MethodHook =
object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) {
val b = beforeBlock
if (b != null) {
val scope =
HookScope(
param = param,
proceed = { p -> super.beforeHookedMethod(p) }
)
scope.b()
} else {
super.beforeHookedMethod(param)
}
}
override fun afterHookedMethod(param: MethodHookParam) {
val a = afterBlock
if (a != null) {
val scope =
HookScope(
param = param,
proceed = { p -> super.afterHookedMethod(p) }
)
scope.a()
} else {
super.afterHookedMethod(param)
}
}
}
}
/**
* Scope object passed to before/after hook blocks.
*
* Provides:
* - Access to the [param] object
* - [proceed] to call the original XC_MethodHook super method
* - Accessors for `thisObject`, `args`, `result`, and `throwable`
*
* @property param The [XC_MethodHook.MethodHookParam] for the current hook.
* @property proceed Function that calls the super method.
*/
protected class HookScope
internal constructor(
val param: XC_MethodHook.MethodHookParam,
private val proceed: (XC_MethodHook.MethodHookParam) -> Unit
) {
/**
* Continues with the default XC_MethodHook super behavior. Equivalent to calling
* `super.beforeHookedMethod(param)` or `super.afterHookedMethod(param)` depending on the
* phase.
*/
fun proceed() = proceed(param)
val thisObject: Any?
get() = param.thisObject
val args: Array<Any?>
get() = param.args
var result: Any?
get() = param.result
set(value) {
param.result = value
}
var throwable: Throwable?
get() = param.throwable
set(value) {
param.throwable = value
}
}
}

View file

@ -1,32 +0,0 @@
package GoonXposed.xposed
import android.app.AndroidAppHelper
import android.content.Intent
import kotlin.system.exitProcess
import kotlinx.serialization.json.Json
class Utils {
companion object {
val JSON = Json { ignoreUnknownKeys = true }
fun reloadApp() {
val application = AndroidAppHelper.currentApplication()
val intent =
application.packageManager.getLaunchIntentForPackage(application.packageName)
application.startActivity(Intent.makeRestartActivityTask(intent!!.component))
exitProcess(0)
}
}
object Log {
fun e(msg: String) = android.util.Log.e(Constants.LOG_TAG, msg)
fun e(msg: String, throwable: Throwable) =
android.util.Log.e(Constants.LOG_TAG, msg, throwable)
fun i(msg: String) = android.util.Log.i(Constants.LOG_TAG, msg)
fun i(msg: String, throwable: Throwable) =
android.util.Log.i(Constants.LOG_TAG, msg, throwable)
fun w(msg: String) = android.util.Log.w(Constants.LOG_TAG, msg)
fun w(msg: String, throwable: Throwable) =
android.util.Log.w(Constants.LOG_TAG, msg, throwable)
}
}

View file

@ -1,21 +0,0 @@
package GoonXposed.xposed.modules
import GoonXposed.xposed.Constants.Companion.TARGET_PACKAGE
import GoonXposed.xposed.Module
import android.content.res.Resources
import de.robv.android.xposed.callbacks.XC_LoadPackage
/** Hooks [Resources.getIdentifier] to fix resource package name mismatch. */
class FixResourcesModule : Module() {
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) =
with(packageParam) {
if (packageName != TARGET_PACKAGE) {
Resources::class.java.hookMethod(
"getIdentifier",
String::class.java,
String::class.java,
String::class.java
) { before { if (args[2] == packageName) args[2] = TARGET_PACKAGE } }
}
}
}

View file

@ -1,133 +0,0 @@
package GoonXposed.xposed.modules
import GoonXposed.xposed.Constants
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.modules.HookScriptLoaderModule.Companion.PRELOADS_DIR
import android.content.res.AssetManager
import android.content.res.XModuleResources
import de.robv.android.xposed.IXposedHookZygoteInit
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.callbacks.XC_LoadPackage
import java.io.File
import java.lang.reflect.Method
import kotlinx.coroutines.runBlocking
/**
* Hooks React Native's script loading methods to load custom scripts and bundles.
*
* Preload scripts should be placed in the [PRELOADS_DIR] directory inside the module's files
* directory.
*
* The main bundle should be placed in the [Constants.CACHE_DIR] directory named
* [Constants.MAIN_SCRIPT_FILE]. If the bundle file does not exist, it will attempt to load
* `assets://revenge.bundle` from the module's assets.
*/
class HookScriptLoaderModule : Module() {
private lateinit var preloadsDir: File
private lateinit var mainScript: File
private lateinit var modulePath: String
private lateinit var resources: XModuleResources
companion object {
const val PRELOADS_DIR = "preloads"
}
override fun onInit(startupParam: IXposedHookZygoteInit.StartupParam) {
this@HookScriptLoaderModule.modulePath = startupParam.modulePath
}
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) =
with(packageParam) {
val cacheDir = File(appInfo.dataDir, Constants.CACHE_DIR).apply { asDir() }
val filesDir = File(appInfo.dataDir, Constants.FILES_DIR).apply { asDir() }
preloadsDir = File(filesDir, PRELOADS_DIR).apply { asDir() }
mainScript = File(cacheDir, Constants.MAIN_SCRIPT_FILE).apply { asFile() }
listOf(
"com.facebook.react.runtime.ReactInstance$1",
// TODO: Remove once Discord fully switches to Bridgeless
"com.facebook.react.bridge.CatalystInstanceImpl"
)
.mapNotNull { classLoader.safeLoadClass(it) }
.forEach { hook(it) }
}
private fun hook(instance: Class<*>) {
val loadScriptFromAssets =
instance.method(
"loadScriptFromAssets",
AssetManager::class.java,
String::class.java,
Boolean::class.javaPrimitiveType
)
val loadScriptFromFile =
instance.method(
"loadScriptFromFile",
String::class.java,
String::class.java,
Boolean::class.javaPrimitiveType
)
loadScriptFromAssets.hook {
before {
Log.i("Received call to loadScriptFromAssets: ${args[1]} (sync: ${args[2]})")
runCustomScripts(loadScriptFromFile, loadScriptFromAssets)
}
}
loadScriptFromFile.hook {
before {
Log.i("Received call to loadScriptFromFile: ${args[0]} (sync: ${args[2]})")
runCustomScripts(loadScriptFromFile, loadScriptFromAssets)
}
}
}
private fun HookScope.runCustomScripts(
loadScriptFromFile: Method,
loadScriptFromAssets: Method
) {
Log.i("Running custom scripts...")
// TODO: Is there a better way to do this?
runBlocking { UpdaterModule.job?.join() }
val loadSynchronously = args[2]
val runScriptFile = { file: File ->
Log.i("Loading script: ${file.absolutePath}")
XposedBridge.invokeOriginalMethod(
loadScriptFromFile,
thisObject,
arrayOf(file.absolutePath, file.absolutePath, loadSynchronously)
)
Unit
}
try {
preloadsDir.walk().filter { it.isFile }.forEach(runScriptFile)
if (mainScript.exists()) runScriptFile(mainScript)
else {
Log.i("Main script does not exist, falling back")
if (!::resources.isInitialized)
resources = XModuleResources.createInstance(modulePath, null)
XposedBridge.invokeOriginalMethod(
loadScriptFromAssets,
thisObject,
arrayOf(resources.assets, "assets://bundle.js", loadSynchronously)
)
}
} catch (e: Throwable) {
Log.e("Unable to run scripts:", e)
}
}
}

View file

@ -1,510 +0,0 @@
package GoonXposed.xposed.modules
import GoonXposed.xposed.Constants
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils
import GoonXposed.xposed.Utils.Companion.reloadApp
import GoonXposed.xposed.Utils.Log
import android.app.AlertDialog
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.Switch
import android.widget.TextView
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 java.io.File
import kotlinx.serialization.json.*
/**
* Replacement for the old recovery/dev dialog. Shows a dark-themed dialog on the main thread with:
* - Load from custom URL toggle + URL input (persists to files/pyoncord/loader.json)
* - Save & Reload, Delete bundle, Close actions
*
* This simplified version removes the Safe Mode toggle and the per-addon list. Save only persists
* the custom URL configuration.
*/
class LogBoxModule : Module() {
lateinit var packageParam: XC_LoadPackage.LoadPackageParam
lateinit var bridgeDevSupportManagerClass: Class<*>
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) =
with(packageParam) {
this@LogBoxModule.packageParam = packageParam
try {
val dcdReactNativeHostClass =
classLoader.loadClass("com.discord.bridge.DCDReactNativeHost")
bridgeDevSupportManagerClass =
classLoader.loadClass(
"com.facebook.react.devsupport.BridgeDevSupportManager"
)
val getUseDeveloperSupportMethod =
dcdReactNativeHostClass.methods.first {
it.name == "getUseDeveloperSupport"
}
val handleReloadJSMethod =
bridgeDevSupportManagerClass.methods.first {
it.name == "handleReloadJS"
}
val showDevOptionsDialogMethod =
bridgeDevSupportManagerClass.methods.first {
it.name == "showDevOptionsDialog"
}
val mReactInstanceDevHelperField =
XposedHelpers.findField(
bridgeDevSupportManagerClass,
"mReactInstanceDevHelper"
)
// Enable developer support (so shake/dev menu becomes available)
XposedBridge.hookMethod(
getUseDeveloperSupportMethod,
object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) {
param.result = true
}
}
)
// Replace reload with app restart to avoid remote dev server dependency
XposedBridge.hookMethod(
handleReloadJSMethod,
object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) {
reloadApp()
param.result = null
}
}
)
// Intercept showDevOptionsDialog and replace with our dialog
XposedBridge.hookMethod(
showDevOptionsDialogMethod,
object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) {
try {
val mReactInstanceDevHelper =
mReactInstanceDevHelperField.get(param.thisObject)
val getCurrentActivityMethod =
mReactInstanceDevHelper.javaClass.methods.first {
it.name == "getCurrentActivity"
}
val context =
getCurrentActivityMethod.invoke(
mReactInstanceDevHelper
) as
Context
showRecoveryAlert(context)
} catch (ex: Exception) {
Log.e(
"Failed to show dev options dialog: ${ex.message}",
ex
)
XposedBridge.log(
"GoonXposed: Failed to show dev options dialog: ${android.util.Log.getStackTraceString(ex)}"
)
}
// Prevent original menu from showing
param.result = null
}
}
)
} catch (e: Exception) {
Log.e("Error in LogBoxModule.onLoad: ${e.message}", e)
XposedBridge.log(
"GoonXposed: LogBoxModule.onLoad error: ${android.util.Log.getStackTraceString(e)}"
)
}
return@with
}
private fun showRecoveryAlert(context: Context) {
// Always perform UI operations on the main thread to avoid runtime exceptions
Handler(Looper.getMainLooper()).post {
try {
val themed = context
// Prepare paths
val filesDir =
File(packageParam.appInfo.dataDir, Constants.FILES_DIR).apply { mkdirs() }
val configFile = File(filesDir, "loader.json")
val preloadsDir =
File(filesDir, HookScriptLoaderModule.PRELOADS_DIR).apply { mkdirs() }
// Read existing loader.json (guarded) for custom URL
var customEnabled = false
var customUrl = ""
try {
if (configFile.exists()) {
val je = Utils.JSON.parseToJsonElement(configFile.readText())
val custom = je.jsonObject["customLoadUrl"]?.jsonObject
if (custom != null) {
customEnabled = custom["enabled"]?.jsonPrimitive?.booleanOrNull ?: false
customUrl = custom["url"]?.jsonPrimitive?.contentOrNull ?: ""
}
}
} catch (t: Throwable) {
Log.e("Failed to parse loader.json: ${t.message}", t)
XposedBridge.log(
"ShiggyXposed: loader.json parse failed: ${android.util.Log.getStackTraceString(t)}"
)
}
// Build the dialog UI programmatically.
val padding = (themed.resources.displayMetrics.density * 16).toInt()
val container =
LinearLayout(themed).apply {
orientation = LinearLayout.VERTICAL
setPadding(padding, padding, padding, padding)
setBackgroundColor(0xFF121212.toInt())
}
// Helper to create a labeled switch row
fun addSwitchRow(labelText: String, checked: Boolean): Pair<TextView, Switch> {
val rowLp =
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
val row =
LinearLayout(themed).apply {
orientation = LinearLayout.HORIZONTAL
layoutParams = rowLp
}
val tvLp =
LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
val tv =
TextView(themed).apply {
text = labelText
setTextColor(0xFFFFFFFF.toInt())
textSize = 15f
layoutParams = tvLp
}
val swLp =
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
val sw =
Switch(themed).apply {
isChecked = checked
layoutParams = swLp
}
row.addView(tv)
row.addView(sw)
container.addView(row)
return Pair(tv, sw)
}
// Create rows: custom URL toggle
val customRow = addSwitchRow("Load from custom URL", false)
val customSwitch = customRow.second
// Confirmation modal helper (keeps previous messaging/behavior)
fun showReloadConfirmModal(parentCtx: Context, turningOn: Boolean) {
try {
val dialogContainer =
LinearLayout(parentCtx).apply {
orientation = LinearLayout.VERTICAL
val pad =
(parentCtx.resources.displayMetrics.density * 16)
.toInt()
setPadding(pad, pad, pad, pad)
setBackgroundColor(0xFF121212.toInt())
}
val message =
TextView(parentCtx).apply {
text =
if (turningOn) {
"All add-ons will be temporarily disabled upon reload."
} else {
"All add-ons will load normally."
}
setTextColor(0xFFFFFFFF.toInt())
textSize = 14f
setPadding(0, 0, 0, 8)
}
dialogContainer.addView(message)
val buttonsRow =
LinearLayout(parentCtx).apply {
orientation = LinearLayout.HORIZONTAL
val lp =
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
layoutParams = lp
}
val reloadNowBtn =
Button(parentCtx).apply {
text = "Reload Now"
setTextColor(0xFF000000.toInt())
setBackgroundColor(0xFFBB86FC.toInt())
val wlp =
LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1f
)
wlp.marginEnd = 8
layoutParams = wlp
setOnClickListener {
try {
reloadApp()
} catch (_: Throwable) {}
}
}
val laterBtn =
Button(parentCtx).apply {
text = "Later"
setTextColor(0xFFFFFFFF.toInt())
val wlp =
LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1f
)
layoutParams = wlp
}
buttonsRow.addView(
reloadNowBtn,
LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1f
)
.apply { marginEnd = 8 }
)
buttonsRow.addView(
laterBtn,
LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1f
)
)
dialogContainer.addView(buttonsRow)
val dlg = AlertDialog.Builder(parentCtx).setView(dialogContainer).create()
laterBtn.setOnClickListener { dlg.dismiss() }
dlg.show()
} catch (t: Throwable) {
Log.e("Failed to show reload confirm modal: ${t.message}", t)
XposedBridge.log(
"GoonXposed: Reload confirm modal failed: ${android.util.Log.getStackTraceString(t)}"
)
try {
AlertDialog.Builder(parentCtx)
.setTitle("Reload now?")
.setMessage(
if (turningOn)
"All add-ons will be temporarily disabled upon reload."
else "All add-ons will load normally."
)
.setPositiveButton("Reload Now") { _, _ -> reloadApp() }
.setNegativeButton("Later", null)
.show()
} catch (_: Throwable) {}
}
}
// Safe mode toggle removed; no-op.
// URL label + input
val lblUrl =
TextView(themed).apply {
text = "Custom bundle URL"
setTextColor(0xFFFFFFFF.toInt())
textSize = 13f
val lp =
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
lp.topMargin = (themed.resources.displayMetrics.density * 8).toInt()
layoutParams = lp
}
val urlInput =
EditText(themed).apply {
hint = "https://example.com/bundle.js"
setTextColor(0xFFFFFFFF.toInt())
setHintTextColor(0xFFBDBDBD.toInt())
setBackgroundColor(0xFF1E1E1E.toInt())
val lp =
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
lp.topMargin = (themed.resources.displayMetrics.density * 6).toInt()
layoutParams = lp
visibility = View.GONE
}
container.addView(lblUrl)
container.addView(urlInput)
// Buttons row - weighted buttons for consistent spacing
val buttonsRow =
LinearLayout(themed).apply {
orientation = LinearLayout.HORIZONTAL
val lp =
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
lp.topMargin = (themed.resources.displayMetrics.density * 12).toInt()
layoutParams = lp
}
fun createButton(text: String): Button {
return Button(themed).apply {
this.text = text
setAllCaps(false)
setTextColor(0xFFFFFFFF.toInt())
val bLp =
LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1f
)
bLp.marginEnd = (themed.resources.displayMetrics.density * 8).toInt()
layoutParams = bLp
minHeight = (themed.resources.displayMetrics.density * 36).toInt()
setBackgroundColor(0x00000000)
}
}
val deleteBtn = createButton("Delete bundle")
val closeBtn = createButton("Close")
val saveBtn =
createButton("Save & Reload").apply {
setBackgroundColor(0xFFBB86FC.toInt())
setTextColor(0xFF000000.toInt())
}
// Add Delete and Close to the top row (equal width)
buttonsRow.addView(deleteBtn)
buttonsRow.addView(closeBtn)
container.addView(buttonsRow)
// Save button in its own row (full-width look)
val saveRow =
LinearLayout(themed).apply {
orientation = LinearLayout.HORIZONTAL
val lp =
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
lp.topMargin = (themed.resources.displayMetrics.density * 8).toInt()
layoutParams = lp
}
val saveLp =
LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
saveBtn.layoutParams = saveLp
saveBtn.minHeight = (themed.resources.displayMetrics.density * 40).toInt()
saveRow.addView(saveBtn)
container.addView(saveRow)
// Initialize UI state
customSwitch.isChecked = customEnabled
urlInput.setText(customUrl)
urlInput.visibility = if (customEnabled) View.VISIBLE else View.GONE
// Toggle URL input visibility when switching custom URL on/off
customSwitch.setOnCheckedChangeListener { _, checked ->
urlInput.visibility = if (checked) View.VISIBLE else View.GONE
}
// Save button: only persist custom URL config and then reload
saveBtn.setOnClickListener {
try {
val enabled = customSwitch.isChecked
val url = urlInput.text?.toString() ?: ""
val escapedUrl = url.replace("\\", "\\\\").replace("\"", "\\\"")
val json =
"{\"customLoadUrl\":{\"enabled\":$enabled,\"url\":\"$escapedUrl\"}}"
runCatching { configFile.writeText(json) }.onFailure { t ->
Log.e("Failed to write loader.json: ${t.message}", t)
XposedBridge.log(
"GoonXposed: write loader.json failed: ${android.util.Log.getStackTraceString(t)}"
)
}
} catch (t: Throwable) {
Log.e("Failed to save loader.json: ${t.message}", t)
}
reloadApp()
}
// Delete bundle
deleteBtn.setOnClickListener {
try {
val bundleFile =
File(
packageParam.appInfo.dataDir,
"${Constants.CACHE_DIR}/${Constants.MAIN_SCRIPT_FILE}"
)
if (bundleFile.exists()) bundleFile.delete()
} catch (t: Throwable) {
Log.e("Failed to delete bundle: ${t.message}", t)
}
reloadApp()
}
// Close button dismisses the created dialog
val dlg = AlertDialog.Builder(themed).setView(container).create()
dlg.show()
closeBtn.setOnClickListener { dlg.dismiss() }
} catch (e: Throwable) {
// Log and fallback to old simple dialog
Log.e("Dialog failed, falling back: ${e.message}", e)
XposedBridge.log(
"GoonXposed: Dialog failed: ${android.util.Log.getStackTraceString(e)}"
)
try {
AlertDialog.Builder(context)
.setTitle("GoonXposed Recovery Options")
.setItems(arrayOf("Reload", "Delete bundle.js")) { _, which ->
when (which) {
0 -> reloadApp()
1 -> {
val bundleFile =
File(
packageParam.appInfo.dataDir,
"${Constants.CACHE_DIR}/${Constants.MAIN_SCRIPT_FILE}"
)
if (bundleFile.exists()) bundleFile.delete()
reloadApp()
}
}
}
.show()
} catch (t: Throwable) {
Log.e("Fallback dialog also failed: ${t.message}", t)
XposedBridge.log(
"GoonXposed: Fallback dialog failed: ${android.util.Log.getStackTraceString(t)}"
)
}
}
}
}
}

View file

@ -1,108 +0,0 @@
package GoonXposed.xposed.modules
import GoonXposed.xposed.BuildConfig
import GoonXposed.xposed.Constants
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Companion.JSON
import GoonXposed.xposed.modules.HookScriptLoaderModule.Companion.PRELOADS_DIR
import android.content.res.AssetManager
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.callbacks.XC_LoadPackage
import java.io.File
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
// TODO: Remove this redundant module and merge its functionality into AdditionalBridgeMethodsModule
// if needed
class PayloadGlobalModule(private val modules: List<Module>) : Module() {
private companion object {
const val GLOBAL_NAME = "__PYON_LOADER__"
}
private fun getPayloadString(): String =
JSON.encodeToString(
buildJsonObject {
put("loaderName", Constants.LOADER_NAME)
put("loaderVersion", BuildConfig.VERSION_NAME)
@Suppress("DEPRECATION") for (module in modules) module.buildPayload(this)
}
)
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) =
with(packageParam) {
val catalystInstance =
classLoader.safeLoadClass("com.facebook.react.bridge.CatalystInstanceImpl")
val reactInstance =
classLoader.safeLoadClass("com.facebook.react.runtime.ReactInstance$1")
val setGlobalVariable: (XC_MethodHook.MethodHookParam, String, String) -> Unit =
{ param, key, json ->
runCatching {
// Attempt old CatalystInstanceImpl method
catalystInstance!!
.method(
"setGlobalVariable",
String::class.java,
String::class.java
)
.invoke(param.thisObject, key, json)
}
.onFailure {
// Failed (likely because it's a stub), use a workaround
val preloadsDir =
File(
"${appInfo.dataDir}/${Constants.FILES_DIR}",
PRELOADS_DIR
)
File(preloadsDir, "rv_globals_$key.js").apply {
writeText("this[${JSON.encodeToString(key)}]=$json")
XposedBridge.invokeOriginalMethod(
reactInstance!!.method(
"loadScriptFromFile",
String::class.java,
String::class.java,
Boolean::class.javaPrimitiveType
),
param.thisObject,
arrayOf(
absolutePath,
absolutePath,
param.args[2]
)
)
delete()
}
}
}
val hook =
MethodHookBuilder().run {
before { setGlobalVariable(param, GLOBAL_NAME, getPayloadString()) }
build()
}
listOf(catalystInstance, reactInstance).forEach { if (it != null) hook(it, hook) }
}
private fun hook(instance: Class<*>, hook: XC_MethodHook) {
instance.method(
"loadScriptFromAssets",
AssetManager::class.java,
String::class.java,
Boolean::class.javaPrimitiveType,
)
.hook(hook)
instance.method(
"loadScriptFromFile",
String::class.java,
String::class.java,
Boolean::class.javaPrimitiveType
)
.hook(hook)
}
}

View file

@ -1,148 +0,0 @@
package GoonXposed.xposed.modules
import GoonXposed.xposed.Constants
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Companion.JSON
import GoonXposed.xposed.Utils.Log
import android.app.Activity
import android.util.AtomicFile
import androidx.core.util.writeBytes
import de.robv.android.xposed.callbacks.XC_LoadPackage
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import java.io.File
import kotlinx.coroutines.*
import kotlinx.serialization.Serializable
@Serializable data class CustomLoadUrl(val enabled: Boolean = false, val url: String = "")
@Serializable data class LoaderConfig(val customLoadUrl: CustomLoadUrl = CustomLoadUrl())
/**
* Module that updates the JS bundle by downloading it from a remote URL.
*
* Shows dialogs when failed allowing retry.
*/
class UpdaterModule : Module() {
private lateinit var config: LoaderConfig
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var error: Throwable? = null
private lateinit var cacheDir: File
private lateinit var bundle: File
private lateinit var etag: File
companion object {
var job: Job? = null
private const val TIMEOUT_CACHED = 5000L
private const val TIMEOUT = 50000L
private const val ETAG_FILE = "etag.txt"
private const val CONFIG_FILE = "loader.json"
private const val DEFAULT_BUNDLE_URL =
"https://github.com/OTKUSteyler/GoonCord/releases/download/1.1.5/goooncord.js"
}
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) =
with(packageParam) {
cacheDir = File(appInfo.dataDir, Constants.CACHE_DIR).apply { mkdirs() }
val filesDir = File(appInfo.dataDir, Constants.FILES_DIR).apply { mkdirs() }
bundle = File(cacheDir, Constants.MAIN_SCRIPT_FILE)
etag = File(cacheDir, ETAG_FILE)
val configFile = File(filesDir, CONFIG_FILE)
config =
runCatching {
if (configFile.exists()) {
JSON.decodeFromString<LoaderConfig>(configFile.readText())
} else LoaderConfig()
}
.getOrDefault(LoaderConfig())
downloadScript()
}
private fun downloadScript(activity: Activity? = null) {
job =
scope.launch {
try {
HttpClient(CIO) {
expectSuccess = false
install(UserAgent) { agent = Constants.USER_AGENT }
install(HttpRedirect) {}
}
.use { client ->
val url =
config.customLoadUrl.takeIf { it.enabled }?.url
?: DEFAULT_BUNDLE_URL
Log.i("Fetching JS bundle from: $url")
val response: HttpResponse =
client.get(url) {
headers {
if (etag.exists() && bundle.exists()) {
append(
HttpHeaders.IfNoneMatch,
etag.readText()
)
}
}
// Retries don't need timeout
if (activity != null) {
var timeoutMs = TIMEOUT_CACHED
if (!bundle.exists()) {
timeoutMs = TIMEOUT
}
timeout { requestTimeoutMillis = timeoutMs }
}
}
when (response.status) {
HttpStatusCode.OK -> {
val bytes: ByteArray = response.body()
AtomicFile(bundle).writeBytes(bytes)
val newTag = response.headers[HttpHeaders.ETag]
if (!newTag.isNullOrEmpty()) etag.writeText(newTag)
else etag.delete()
Log.i("Bundle updated (${bytes.size} bytes)")
// This is a retry, so we show a dialog
if (activity != null) {
withContext(Dispatchers.Main) {}
}
}
HttpStatusCode.NotModified -> {
Log.i("Server responded with 304, no changes")
}
else -> {
throw ResponseException(
response,
"Received status: ${response.status}"
)
}
}
}
} catch (e: Throwable) {
Log.e("Failed to download script", e)
error = e
}
}
}
override fun onActivity(activity: Activity) {
error ?: return
error = null
downloadScript(activity)
}
}

View file

@ -1,271 +0,0 @@
package GoonXposed.xposed.modules.appearance
import GoonXposed.xposed.Constants
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Companion.JSON
import GoonXposed.xposed.Utils.Log
import android.content.res.AssetManager
import android.graphics.Typeface
import android.graphics.Typeface.CustomFallbackBuilder
import android.graphics.fonts.Font
import android.graphics.fonts.FontFamily
import android.os.Build
import de.robv.android.xposed.XC_MethodReplacement
import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import java.io.File
import java.io.IOException
import kotlinx.coroutines.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObjectBuilder
import kotlinx.serialization.json.put
@Serializable
data class FontDefinition(
val name: String? = null,
val description: String? = null,
val spec: Int? = null,
val main: Map<String, String>,
)
class FontsModule : Module() {
private companion object {
val EXTENSIONS = arrayOf("", "_bold", "_italic", "_bold_italic")
val FILE_EXTENSIONS = arrayOf(".ttf", ".otf")
const val FONTS_ASSET_PATH = "fonts/"
}
private lateinit var fontsDir: File
private lateinit var fontsDownloadsDir: File
private var fontsAbsPath: String? = null
override fun buildPayload(builder: JsonObjectBuilder) {
builder.apply { put("fontPatch", 2) }
}
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) =
with(packageParam) {
XposedHelpers.findAndHookMethod(
"com.facebook.react.views.text.ReactFontManager\$Companion",
classLoader,
"createAssetTypeface",
String::class.java,
Int::class.java,
"android.content.res.AssetManager",
object : XC_MethodReplacement() {
override fun replaceHookedMethod(param: MethodHookParam): Typeface? {
val fontFamilyName: String = param.args[0].toString()
val style: Int = param.args[1] as Int
val assetManager: AssetManager = param.args[2] as AssetManager
return createAssetTypeface(fontFamilyName, style, assetManager)
}
}
)
val fontDefFile =
File(appInfo.dataDir, "${Constants.FILES_DIR}/fonts.json").apply {
asFile()
}
if (!fontDefFile.exists()) return@with
val fontDef =
try {
JSON.decodeFromString<FontDefinition>(fontDefFile.readText())
} catch (_: Throwable) {
return@with
}
fontsDownloadsDir =
File(appInfo.dataDir, "${Constants.FILES_DIR}/downloads/fonts").apply {
asDir()
}
fontsDir = File(fontsDownloadsDir, fontDef.name!!).apply { asDir() }
fontsAbsPath = fontsDir.absolutePath + "/"
fontsDir.listFiles()?.forEach { file ->
val fileName = file.name
if (!fileName.startsWith(".")) {
val fontName = fileName.split('.')[0]
if (fontDef.main.keys.none { it == fontName }) {
Log.i("Deleting font file: $fileName")
file.delete()
}
}
}
// These files should be downloaded by the JS side, but oh well
CoroutineScope(Dispatchers.IO).launch {
fontDef.main
.keys
.map { name ->
async {
val url = fontDef.main.getValue(name)
try {
Log.i("Downloading $name from $url")
val file =
File(
fontsDir,
"$name${FILE_EXTENSIONS.first { url.endsWith(it) }}"
)
.apply { asFile() }
if (file.exists()) return@async
val client =
HttpClient(CIO) {
install(UserAgent) {
agent = Constants.USER_AGENT
}
}
val response: HttpResponse = client.get(url)
if (response.status == HttpStatusCode.OK)
file.writeBytes(response.body())
return@async
} catch (e: Throwable) {
Log.e("Failed to download fonts ($name from $url)", e)
}
}
}
.awaitAll()
}
}
private fun createAssetTypefaceWithFallbacks(
fontFamilyNames: Array<String>,
style: Int,
assetManager: AssetManager
): Typeface? {
val fontFamilies: MutableList<FontFamily> = ArrayList()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Iterate over the list of fontFamilyNames, constructing new FontFamily objects
// for use in the CustomFallbackBuilder below.
for (fontFamilyName in fontFamilyNames) {
try {
for (fileExtension in FILE_EXTENSIONS) {
val (customName, refName) = fontFamilyName.split(":")
val file =
File(fontsDownloadsDir, "$customName/$refName.$fileExtension")
.apply { asFile() }
val font = Font.Builder(file).build()
val family = FontFamily.Builder(font).build()
fontFamilies.add(family)
}
} catch (_: Throwable) {}
for (fontRootPath in arrayOf(fontsAbsPath, FONTS_ASSET_PATH).filterNotNull()) {
for (fileExtension in FILE_EXTENSIONS) {
val fileName =
StringBuilder()
.append(fontRootPath)
.append(fontFamilyName)
.append(fileExtension)
.toString()
try {
val builder =
if (fileName[0] == '/') Font.Builder(File(fileName))
else Font.Builder(assetManager, fileName)
val font = builder.build()
val family = FontFamily.Builder(font).build()
fontFamilies.add(family)
} catch (_: RuntimeException) {
// If the typeface asset does not exist, try another extension.
continue
} catch (_: IOException) {
// If the font asset does not exist, try another extension.
continue
}
}
}
}
// If there's some problem constructing fonts, fall back to the default behavior.
if (fontFamilies.isEmpty())
return createAssetTypeface(fontFamilyNames[0], style, assetManager)
val fallbackBuilder = CustomFallbackBuilder(fontFamilies[0])
for (i in 1 until fontFamilies.size) {
fallbackBuilder.addCustomFallback(fontFamilies[i])
}
return fallbackBuilder.build()
}
return null
}
private fun createAssetTypeface(
fontFamilyName: String,
style: Int,
assetManager: AssetManager
): Typeface? {
// This logic attempts to safely check if the frontend code is attempting to use
// fallback fonts, and if it is, to use the fallback typeface creation logic.
var fontFamilyName: String = fontFamilyName
val fontFamilyNames =
fontFamilyName.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
for (i in fontFamilyNames.indices) {
fontFamilyNames[i] = fontFamilyNames[i].trim()
}
// If there are multiple font family names:
// For newer versions of Android, construct a Typeface with fallbacks
// For older versions of Android, ignore all the fallbacks and just use the first font
// family
if (fontFamilyNames.size > 1) {
fontFamilyName =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
return createAssetTypefaceWithFallbacks(
fontFamilyNames,
style,
assetManager
)
} else {
fontFamilyNames[0]
}
}
val extension = EXTENSIONS[style]
try {
for (fileExtension in FILE_EXTENSIONS) {
val (customName, refName) = fontFamilyName.split(":")
val file =
File(fontsDownloadsDir, "$customName/$refName.$fileExtension").apply {
asFile()
}
if (!file.exists()) throw Exception()
return Typeface.createFromFile(file.absolutePath)
}
} catch (_: Throwable) {}
// Lastly, after all those checks above, this is the original RN logic for
// getting the typeface.
for (fontRootPath in arrayOf(fontsAbsPath, FONTS_ASSET_PATH).filterNotNull()) {
for (fileExtension in FILE_EXTENSIONS) {
val fileName =
StringBuilder()
.append(fontRootPath)
.append(fontFamilyName)
.append(extension)
.append(fileExtension)
.toString()
return try {
if (fileName[0] == '/') Typeface.createFromFile(fileName)
else Typeface.createFromAsset(assetManager, fileName)
} catch (_: RuntimeException) {
// If the typeface asset does not exist, try another extension.
continue
}
}
}
return Typeface.create(fontFamilyName, style)
}
}

View file

@ -1,45 +0,0 @@
package GoonXposed.xposed.modules.appearance
import GoonXposed.xposed.Module
import android.R.color
import android.app.AndroidAppHelper
import android.content.Context
import android.os.Build
import androidx.core.content.ContextCompat
import kotlinx.serialization.json.*
class SysColorsModule : Module() {
private lateinit var context: Context
private fun isSupported() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
override fun buildPayload(builder: JsonObjectBuilder) {
context = AndroidAppHelper.currentApplication()
val accents = arrayOf("accent1", "accent2", "accent3", "neutral1", "neutral2")
val shades = arrayOf(0, 10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000)
builder.apply {
put("isSysColorsSupported", isSupported())
if (isSupported())
putJsonObject("sysColors") {
for (accent in accents) putJsonArray(accent) {
for (shade in shades) {
val colorName = "system_" + accent + "_" + shade
val colorResourceId =
runCatching {
color::class.java.getField(colorName).getInt(null)
}
.getOrElse { 0 }
add(convertToColor(colorResourceId))
}
}
}
}
}
private fun convertToColor(id: Int): String {
val clr = if (isSupported()) ContextCompat.getColor(context, id) else 0
return String.format("#%06X", 0xFFFFFF and clr)
}
}

View file

@ -1,160 +0,0 @@
package GoonXposed.xposed.module.appearance
import GoonXposed.xposed.Constants
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Companion.JSON
import android.content.Context
import android.content.res.Resources
import androidx.core.graphics.toColorInt
import de.robv.android.xposed.callbacks.XC_LoadPackage
import java.io.File
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObjectBuilder
import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.json.put
@Serializable data class Author(val name: String, val id: String? = null)
@Serializable
data class ThemeData(
val name: String,
val description: String? = null,
val authors: List<Author>? = null,
val spec: Int,
val semanticColors: Map<String, List<String>>? = null,
val rawColors: Map<String, String>? = null
)
@Serializable data class Theme(val id: String, val selected: Boolean, val data: ThemeData)
class ThemesModule : Module() {
private lateinit var param: XC_LoadPackage.LoadPackageParam
private var theme: Theme? = null
private val rawColorMap = mutableMapOf<String, Int>()
private companion object {
const val THEME_FILE = "current-theme.json"
}
@ExperimentalSerializationApi
override fun buildPayload(builder: JsonObjectBuilder) {
builder.apply {
put("hasThemeSupport", true)
if (theme != null) put("storedTheme", JSON.encodeToJsonElement<Theme>(theme!!))
else put("storedTheme", null)
}
}
private fun String.fromScreamingSnakeToCamelCase() =
this.split("_").joinToString("") { it ->
it.lowercase().replaceFirstChar { it.uppercase() }
}
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {
param = packageParam
theme = getTheme()
hookTheme()
}
private fun File.isValidish(): Boolean {
if (!this.exists()) return false
val text = this.readText()
return text.isNotBlank() && text != "{}" && text != "null"
}
private fun getTheme(): Theme? {
val themeFile =
File(param.appInfo.dataDir, "${Constants.FILES_DIR}/${THEME_FILE}").apply {
asFile()
}
if (!themeFile.isValidish()) return null
return try {
val themeText = themeFile.readText()
JSON.decodeFromString<Theme>(themeText)
} catch (_: Exception) {
null
}
}
fun hookTheme() {
val themeManager = param.classLoader.loadClass("com.discord.theme.utils.ColorUtilsKt")
val darkTheme = param.classLoader.loadClass("com.discord.theme.DarkerTheme")
val lightTheme = param.classLoader.loadClass("com.discord.theme.LightTheme")
val theme = this.theme ?: return
// Apply rawColors
theme.data.rawColors?.forEach { (key, value) ->
rawColorMap[key.lowercase()] = hexStringToColorInt(value)
}
// Apply semanticColors
theme.data.semanticColors?.forEach { (key, value) ->
// TEXT_NORMAL -> getTextNormal
val methodName = "get${key.fromScreamingSnakeToCamelCase()}"
value.forEachIndexed { index, v ->
when (index) {
0 -> hookThemeMethod(darkTheme, methodName, hexStringToColorInt(v))
1 -> hookThemeMethod(lightTheme, methodName, hexStringToColorInt(v))
}
}
}
// If there's any rawColors value, hook the color getter
if (!theme.data.rawColors.isNullOrEmpty()) {
val getColorCompat =
themeManager.getDeclaredMethod(
"getColorCompat",
Resources::class.java,
Int::class.javaPrimitiveType,
Resources.Theme::class.java,
)
val getColorCompatLegacy =
themeManager.getDeclaredMethod(
"getColorCompat",
Context::class.java,
Int::class.javaPrimitiveType
)
val patch =
MethodHookBuilder().run {
before {
val arg1 = args[0]
val resources =
if (arg1 is Context) arg1.resources else (arg1 as Resources)
val name = resources.getResourceEntryName(args[1] as Int)
if (rawColorMap[name] != null) result = rawColorMap[name]
}
build()
}
getColorCompat.hook(patch)
getColorCompatLegacy.hook(patch)
}
}
// Parse HEX colour string to INT. Takes "#RRGGBBAA" or "#RRGGBB"
private fun hexStringToColorInt(hexString: String): Int {
return if (hexString.length == 9) {
// Rearrange RRGGBBAA -> AARRGGBB so parseColor() is happy
val alpha = hexString.substring(7, 9)
val rrggbb = hexString.substring(1, 7)
"#$alpha$rrggbb".toColorInt()
} else hexString.toColorInt()
}
private fun hookThemeMethod(themeClass: Class<*>, methodName: String, themeValue: Int) {
try {
themeClass.getDeclaredMethod(methodName).let { method ->
method.hook { before { result = themeValue } }
}
} catch (_: NoSuchMethodException) {}
}
}

View file

@ -1,52 +0,0 @@
package GoonXposed.xposed.modules.bridge
import GoonXposed.xposed.Module
import android.app.AlertDialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import android.widget.Toast
class AdditionalBridgeMethodsModule : Module() {
override fun onContext(context: Context) =
with(context) {
BridgeModule.registerMethod("revenge.alertError") {
val (error, version) = it
val app = getAppInfo()
val errorString = "$error"
val clipboard =
applicationContext.getSystemService(Context.CLIPBOARD_SERVICE) as
ClipboardManager
val clip = ClipData.newPlainText("Stack Trace", errorString)
AlertDialog.Builder(this)
.setTitle("GoonXposed Error")
.setMessage(
"""
GoonXposed: $version
${app.name}: ${app.version} (${app.versionCode})
Device: ${Build.MANUFACTURER} ${Build.MODEL}
""".trimIndent() +
errorString
)
.setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
.setNeutralButton("Copy") { dialog, _ ->
clipboard.setPrimaryClip(clip)
Toast.makeText(
applicationContext,
"Copied stack trace",
Toast.LENGTH_SHORT
)
.show()
dialog.dismiss()
}
.show()
null
}
}
}

View file

@ -1,208 +0,0 @@
package GoonXposed.xposed.modules.bridge
import GoonXposed.xposed.BuildConfig
import GoonXposed.xposed.Constants
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import de.robv.android.xposed.callbacks.XC_LoadPackage
import java.io.File
import java.lang.reflect.Method
/**
* See for possible return types:
* https://github.com/facebook/react-native/blob/c23e84ae9/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/Arguments.kt#L19
*
* You may return a [Unit] and the resulting value will be `null`.
*/
typealias BridgeMethodCallback = (args: BridgeMethodArgs) -> Any?
typealias BridgeMethodArgs = ArrayList<Any>
/**
* A module that exposes a bridge for calling methods from JavaScript.
*
* To call a method, pass an object with the following structure to a hooked method:
* ```js
* {
* revenge: {
* method: "method.name",
* args: [arg1, arg2, ...]
* }
* }
* ```
*
* For methods that accept additional arguments, only the first object argument is used for the
* bridge call.
*
* The result will be an object with either a `result` or `error` key:
* ```js
* {
* result: ... // The return value of the method
* }
* ```
* or
* ```js
* {
* error: "Error message"
* }
* ```
*/
class BridgeModule : Module() {
private lateinit var readableMapGetString: Method
private lateinit var readableMapToHashMapMethod: Method
private lateinit var argumentsMakeNative: Method
companion object {
private const val CALL_DATA_KEY = "revenge"
private const val METHOD_NAME_KEY = "method"
private const val METHOD_ARGS_KEY = "args"
private fun File.openFileGuarded() {
if (!this.exists()) throw Error("Path does not exist: $path")
if (!this.isFile) throw Error("Path is not a file: $path")
}
private val methods: MutableMap<String, BridgeMethodCallback> =
mutableMapOf(
"revenge.info" to
{
mapOf(
"name" to Constants.LOADER_NAME,
"version" to BuildConfig.VERSION_CODE
)
},
"revenge.test" to
{
mapOf(
"string" to "string",
"number" to 7256,
"array" to listOf("testing", 527737, listOf(true)),
"object" to mapOf("nested" to true),
"boolean" to false,
"args" to it,
)
},
"revenge.fs.delete" to
{
val (path) = it
File(path as String).run {
if (this.isDirectory) this.deleteRecursively()
else this.delete()
}
},
"revenge.fs.exists" to
{
val (path) = it
File(path as String).exists()
},
"revenge.fs.read" to
{ it ->
val (path) = it
val file = File(path as String).apply { openFileGuarded() }
file.bufferedReader().use { it.readText() }
},
"revenge.fs.write" to
{ it ->
val (path, contents) = it
val file = File(path as String).apply { openFileGuarded() }
file.writeText(contents as String)
}
)
/**
* Registers a bridge method that can be called from JavaScript.
*
* If the method is already registered, it will be overwritten.
*
* @param name The name of the method to register.
* @param callback The callback to invoke when the method is called.
*/
fun registerMethod(name: String, callback: BridgeMethodCallback) {
if (methods.containsKey(name))
Log.w("Bridge method already exists and will be overridden: $name")
methods[name] = callback
}
}
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {
with(packageParam) {
val arguments = classLoader.loadClass("com.facebook.react.bridge.Arguments")
val readableMap = classLoader.loadClass("com.facebook.react.bridge.ReadableMap")
val promise = classLoader.loadClass("com.facebook.react.bridge.Promise")
val promiseResolve = promise.method("resolve", Object::class.java)
argumentsMakeNative = arguments.method("makeNativeObject", Object::class.java)
readableMapGetString = readableMap.method("getString", String::class.java)
readableMapToHashMapMethod = readableMap.method("toHashMap")
classLoader.loadClass("com.horcrux.svg.RNSVGRenderableManager").hookMethod(
"getBBox",
Double::class.javaObjectType,
readableMap
) {
before {
callBridgeMethod(readableMapToHashMapConverter(args[1]!!))?.let {
result = it.toNativeObject()
}
}
}
classLoader.loadClass("com.facebook.react.modules.blob.FileReaderModule").hookMethod(
"readAsDataURL",
readableMap,
promise
) {
before {
// Avoid shadowing the outer `readableMap` and `promise` variables
val (readableMapArg, promiseArg) = args
callBridgeMethod(readableMapToHashMapConverter(readableMapArg!!))?.let {
promiseResolve.invoke(promiseArg, it.toNativeObject())
result = null
}
}
}
}
}
private fun Any?.toNativeObject(): Any? =
argumentsMakeNative.invoke(
null,
when (this) {
Unit -> null
else -> this
}
)
private fun readableMapToHashMapConverter(map: Any): HashMap<String, Any?> {
@Suppress("UNCHECKED_CAST")
return readableMapToHashMapMethod.invoke(map) as HashMap<String, Any?>
}
private fun callBridgeMethod(hashMap: HashMap<String, Any?>): Map<String, Any?>? {
try {
val pair = getBridgeCallData(hashMap) ?: return null
val (method, args) = pair
val ret = method(args).toNativeObject()
return mapOf("result" to ret)
} catch (e: Throwable) {
return mapOf("error" to e.toString())
}
}
private fun getBridgeCallData(
hashMap: HashMap<String, Any?>
): Pair<BridgeMethodCallback, BridgeMethodArgs>? {
@Suppress("UNCHECKED_CAST") val data = hashMap[CALL_DATA_KEY] as HashMap<String, Any?>?
data ?: return null
@Suppress("UNCHECKED_CAST") val name = data[METHOD_NAME_KEY] as String
val method = methods[name]
method ?: throw Error("Method not registered: $name")
@Suppress("UNCHECKED_CAST") val args = data[METHOD_ARGS_KEY] as BridgeMethodArgs
return Pair(method, args)
}
}

View file

@ -20,4 +20,6 @@ kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.nonTransitiveRClass=true
# Suppress deprecation warning when building with a newer JDK (e.g., Java 21)
android.javaCompile.suppressSourceTargetDeprecationWarning=true

View file

@ -1,2 +1,2 @@
rootProject.name = "KettuXposed"
rootProject.name = "ShiggyXposed"
include(":app")