Compare commits

...

384 Commits

Author SHA1 Message Date
Pun Butrach
f6194edde9 fix: Specify Build tools for API 2025-07-22 20:43:15 +07:00
Ax333l
fc96137567 fix: remove unused function preventing compilation 2025-07-18 16:21:47 +02:00
Ax333l
244674a603 chore: remove unused dependency 2025-07-15 17:56:24 +02:00
oSumAtrIX
47e4ed8336 feat: Rename "Patch bundle" to "Patches" (#2541)
Co-authored-by: Ax333l <main@axelen.xyz>
2025-07-15 17:36:34 +02:00
brosssh
486ed5967f fix: Show selection warning also on patch option (#2643) 2025-07-15 15:32:49 +02:00
Ax333l
789f9ec867 feat: allow bundles to use classes from other bundles (#1951) 2025-07-15 14:28:40 +02:00
Pun Butrach
b51d1ee47a fix: Transparent status on fullscreen dialog (#2654) 2025-07-14 15:35:27 +02:00
oSumAtrIX
7148ee66f8 feat: Rename strings 2025-07-10 22:29:25 +02:00
brosssh
f7a4ae5791 fix: Add missing header for "Updates" settings (#2642) 2025-07-08 21:57:56 +02:00
brosssh
cb2dbbee24 feat: Improve bundle info screen design (#2548) 2025-07-08 20:23:03 +02:00
Pun Butrach
578dcce9b6 chore: Merge branch 'dev' into compose-dev 2025-07-08 22:24:17 +07:00
brosssh
8c6c0f3c76 fix: Patch selection screen padding (#2533) 2025-07-08 17:20:44 +02:00
Pun Butrach
979a2dc410 fix: Playback Switch's Haptic Feedback (#2639)
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
2025-07-08 18:11:45 +07:00
kitadai31
baa9122a88 fix: Improve background running notification (#2614) 2025-07-07 22:38:41 +02:00
brosssh
b70fc03bc7 fix: Allow different app version when downloading via plugin if setting is off (#2579)
Co-authored-by: Ax333l <main@axelen.xyz>
2025-07-05 18:44:11 +02:00
Ax333l
81a4ebd327 fix: display version from manifest (#2634) 2025-07-04 18:58:11 +02:00
aAbed
5fc54eb53b fix: Correct preference description (#2619) 2025-07-02 13:49:59 +02:00
brosssh
88b0b8c078 feat: Set app ownership when installing apps (#2558) 2025-06-16 21:55:32 +02:00
brosssh
7959c36e71 fix: Selected patch count (#2559) 2025-06-10 16:43:50 +02:00
brosssh
e9542c6cf0 fix: CI flows (#2598) 2025-06-10 16:38:37 +02:00
brosssh
0992e63c28 feat: Add confirmation dialog to "Reset" options (#2576) 2025-06-10 16:38:05 +02:00
Ushie
83fc7f131a chore: Remove obsolete deleteLastPatchedApp call 2025-06-02 17:12:50 +03:00
oSumAtrIX
8e4a9088ea ci: Use install instead of clean install for NPM dependencies
Co-authored-by: Pun Butrach <pun.butrach@gmail.com>
2025-06-02 16:05:59 +02:00
github-actions[bot]
7ee2b1a026 chore: Sync translations (#2522)
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Pun Butrach <pun.butrach@gmail.com>
2025-05-31 15:36:42 +07:00
Pun Butrach
40c99ab4dc ci: Set missing translation actor/email for commit
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
2025-05-31 15:18:37 +07:00
Pun Butrach
1752fae9d9 chore: Better ignore rules
Based on https://github.com/github/gitignore/blob/main/Flutter.gitignore

Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
2025-05-31 15:12:26 +07:00
Pun Butrach
d264a2a363 build: Support Flutter v3.32
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
2025-05-31 15:11:49 +07:00
Pun Butrach
a5e909cfc8 fix: Obscure Flutter Impeller renderer bugs
This is workaround to entirely disabling Flutter Impeller in favour of Skia.

Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
2025-05-31 14:57:21 +07:00
Pun Butrach
aebad0b0e2 chore: Merge branch 'dev' into compose-dev 2025-05-31 14:26:25 +07:00
kitadai31
bdb0317a9e fix: "Save patched app" attempts to copy APK when patching fails (#2565) 2025-05-31 14:22:55 +07:00
oSumAtrIX
c0d7cf7c2c ci: Upload artifacts when building pull requests 2025-05-30 15:19:50 +02:00
Brosssh
2c1ff4d2cd fix: Patch process cancelation dialog conditions (#2554) 2025-05-30 15:13:30 +02:00
Brosssh
7c5552f93f fix: Correctly display universal patches warning (#2570) 2025-05-24 14:47:27 +02:00
Brosssh
1c5373ff61 fix: Handle open source licenses page crash (#2569) 2025-05-24 14:14:03 +02:00
oSumAtrIX
e629d2df0c ci: Set build attestation subject name 2025-05-22 14:54:04 +02:00
oSumAtrIX
7ca003a30d build: Do not sign all releases with debug key 2025-05-22 14:29:43 +02:00
oSumAtrIX
70a695017e docs: Update docs with all manager features and improve consistency 2025-05-22 14:15:44 +02:00
oSumAtrIX
9b2c99da05 feat: Use simpler strings 2025-05-22 14:15:19 +02:00
oSumAtrIX
07158ae1d1 chore: Move API project from separate repo to this
Set up CI to publish the API library package as well as release the app.
2025-05-22 10:56:23 +02:00
Brosssh
2b380b0d7c fix(Compose): Adjusted universal patches safeguard and warnings (#2550) 2025-05-20 14:12:38 +02:00
Ax333l
5153e5e0cb feat(Compose): hide developer settings (#2551) 2025-05-20 14:12:37 +02:00
Ax333l
a1f5dd3c26 fix: handle edge-to-edge properly in fullscreen dialogs 2025-05-20 14:12:36 +02:00
Brosssh
2b0784865a feat(Compose): Add confirmation dialog on multiple operations (#2529) 2025-05-20 14:12:34 +02:00
Robert
d7c0913277 refactor: Rename settings screens for consistency (#2547) 2025-05-20 14:12:33 +02:00
Brosssh
18199bb968 feat(Compose): Improve patches selector tab by adding the bundle version (#2545) 2025-05-20 14:12:32 +02:00
oSumAtrIX
9d329e0f54 ci: Adjust and modernize workflow files to match other repos 2025-05-20 14:12:30 +02:00
oSumAtrIX
e9fcb4a383 docs: Adjust issue templates to match other repos 2025-05-20 14:12:29 +02:00
oSumAtrIX
68f74f1651 docs: Add contribution guidelines and adjust README 2025-05-20 14:12:27 +02:00
oSumAtrIX
6264800a05 build: Update Gradle Wrapper 2025-05-20 14:12:26 +02:00
Ax333l
658699dd81 fix: patch count remaining at zero when using process runtime (#2542) 2025-05-20 14:12:24 +02:00
Robert
222089a7ec feat: Order bundles by number of patches 2025-05-20 14:12:23 +02:00
oSumAtrIX
eff5c4860b style: Apply formatting 2025-05-20 14:12:21 +02:00
oSumAtrIX
d4e60acbaa build: Sign releases using keystore if available 2025-05-20 14:12:20 +02:00
oSumAtrIX
1ab74acf1d feat: Use "Debug" and "Debug signed" for build names respectively 2025-05-20 14:12:19 +02:00
oSumAtrIX
28aad879ba feat: Move safeguards above patcher preference group 2025-05-20 14:12:17 +02:00
Brosssh
9f44541bbd fix: Reset cached theme on theme change to avoid broken colors (#2527) 2025-05-20 14:12:15 +02:00
Brosssh
02d2153195 feat(Compose): Move developer options to top level (#2528) 2025-05-20 14:12:14 +02:00
oSumAtrIX
39e821738f build: Remove repos that are not required 2025-05-20 14:12:12 +02:00
Ushie
7863fbb604 fix: Ignore long click when already in delete mode
closes #2503
2025-05-20 14:12:11 +02:00
Ax333l
94ab6996ae feat: add network checks for features that require it 2025-05-20 14:12:10 +02:00
Ax333l
1319a03651 feat: move plugin api to another repository 2025-05-20 14:12:08 +02:00
kitadai31
f93085f782 fix: Do not poll battery optimization status (#2491) 2025-05-20 14:12:07 +02:00
Ushie
40a4317993 feat: Improve update screen design (#2487) 2025-05-20 14:12:06 +02:00
Ushie
8095a1f963 fix: Use compatible rather than support when referring to patch compatibility (#2422) 2025-05-20 14:12:04 +02:00
Ushie
90c7600586 feat: Improve APK file name formatting on save (#2421) 2025-05-20 14:12:03 +02:00
dependabot[bot]
105492bfa5 build(deps): bump the gradle-compose group with 16 updates (#2407) 2025-05-20 14:12:01 +02:00
Ushie
ce63b799e6 feat: Reorder Import & Export settings (#2403) 2025-05-20 14:12:00 +02:00
Ushie
2fe2d46c72 feat: TopAppBar scroll behavior (#2397) 2025-05-20 14:11:58 +02:00
Pun Butrach
bc3888da79 ci: Generate release artifact provenance (#2324)
Signed-off-by: validcube <pun.butrach@gmail.com>
2025-05-20 14:11:57 +02:00
Robert
52b982d81f fix: improve keystore import error handling and show toast 2025-05-20 14:11:56 +02:00
validcube
ab48672621 build: Enable pseudo locale for debug variant 2025-05-20 14:11:54 +02:00
validcube
0027c90ed3 chore: Update project's dependencies to latest 2025-05-20 14:11:53 +02:00
Robert
b6ad686a26 fix: show install button when installation has been cancelled 2025-05-20 14:11:51 +02:00
Ushie
f59d57499d feat: Screen slide transition (#2396) 2025-05-20 14:11:50 +02:00
Ushie
5f65c12ec1 fix: Offset badge 2025-05-20 14:11:49 +02:00
validcube
65e44dc5a8 build: Bump AGP to 8.8.0
build: Bump AGP to 8.8.0
2025-05-20 14:11:47 +02:00
Pun Butrach
e6ed4a88c9 docs: Merge documentation from Flutter to Compose 2025-05-20 14:11:46 +02:00
Tornike Khintibidze
390e3533c9 feat: Redesign the patches screen (#2381) 2025-05-20 14:11:44 +02:00
Ax333l
c1ff2f9924 fix: available updates dialog list item color 2025-05-20 14:11:42 +02:00
Ax333l
d084925c0f refactor: use EventEffect for legacy import 2025-05-20 14:11:41 +02:00
Ax333l
3cf540f190 feat: add required options screen (#2378) 2025-05-20 14:11:39 +02:00
aAbed
5514c75061 feat: Add confirm dialogs when toggling dangerous settings (#2072)
Co-authored-by: Ax333l <main@axelen.xyz>
2025-05-20 14:11:37 +02:00
Ax333l
ca147cc6dc chore: add .kotlin to gitignore 2025-05-20 14:11:36 +02:00
Ax333l
3c3e995f31 fix: remove battery optimization notification if user grants the permission 2025-05-20 14:11:35 +02:00
Ax333l
67809700c7 feat: switch to androidx.navigation (#2362) 2025-05-20 14:11:33 +02:00
Ax333l
32c7eddb48 refactor: remove unnecessary function 2025-05-20 14:11:31 +02:00
Ax333l
756e3a815f fix: contributors screen repository name 2025-05-20 14:11:29 +02:00
Ax333l
f8f915563e fix: process death resilience and account for android 11 bug (#2355) 2025-05-20 14:11:27 +02:00
kitadai31
2733ce4915 fix: Screen turns off while patching due to wrong WakeLock (#2147) 2025-05-20 14:11:25 +02:00
Ax333l
04a78fabff feat: Add downloader plugin system (#2041) 2025-05-20 14:11:23 +02:00
Ax333l
94e26ba053 feat: switch to revanced api v4 2025-05-20 14:11:21 +02:00
Pun Butrach
1704947c52 chore: Nitpick on misspelling of comment 2025-05-20 14:11:20 +02:00
somni
e027f8cc9c feat: Make patch bundles list scrollable (#2322) 2025-05-20 14:11:18 +02:00
Ax333l
37e612febc chore: update dependencies
🦀 integrations are gone! 🦀
2025-05-20 14:11:17 +02:00
Ax333l
374531237f fix: only perform haptics on events 2025-05-20 14:11:15 +02:00
Benjamin
abe5a20c4a feat: Add haptic feedback (#1457)
Co-authored-by: Ushie <ushiekane@gmail.com>
2025-05-20 14:11:14 +02:00
kitadai31
0a29ff48ca fix: Match "Installation incompatible" dialog message with Flutter Manager (#2231) 2025-05-20 14:11:12 +02:00
alieRN
95cffcc0a0 feat(patcher): Improve installation (#2185) 2025-05-20 14:11:10 +02:00
oSumAtrIX
45ff64f26e feat: Add installer status dialog (#1473)
Co-authored-by: Benjamin Halko <benjaminhalko@hotmail.com>
Co-authored-by: Benjamin <73490201+BenjaminHalko@users.noreply.github.com>
Co-authored-by: Ushie <ushiekane@gmail.com>
Co-authored-by: Ax333l <main@axelen.xyz>
2025-05-20 14:11:09 +02:00
Ushie
7973b367ec feat: View bundle patches (#2065) 2025-05-20 14:11:07 +02:00
kitadai31
641f6af6da feat: Open the app-specific manage all files permission dialog (#2148) 2025-05-20 14:11:06 +02:00
Ushie
6d142e72a6 feat: Improve patch bundle screen (#2070) 2025-05-20 14:11:05 +02:00
Pun Butrach
e812f69740 feat: Improve Settings order (#2060)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
Co-authored-by: Ax333l <main@axelen.xyz>
2025-05-20 14:11:03 +02:00
Ax333l
df79e3d13a fix: remove the unique constraint for patch bundle names 2025-05-20 14:11:01 +02:00
kitadai31
207b005d56 fix: Move temporary files outside of the cache directory (#2122) 2025-05-20 14:11:00 +02:00
Ushie
4a1695a766 refactor: Add parameters for custom rotation values in ArrowButton 2025-05-20 14:10:58 +02:00
validcube
737e709287 ci: Actually enable caching of Gradle 2025-05-20 14:10:57 +02:00
aAbed
4727e8243c fix: Turn off filters by default (#2079) 2025-05-20 14:10:55 +02:00
aAbed
5e0ba77f4a fix: ExtendedFloatingActionButton not accessible by screen readers (#2080) 2025-05-20 14:10:54 +02:00
Ushie
a76a58d6ee feat: Improve unsupported patch warnings (#2066)
Closes #2052
2025-05-20 14:10:52 +02:00
Robert
4ebc33cd2a fix: show available and selected patches in patch selector screen 2025-05-20 14:10:51 +02:00
Ushie
89a1a3026e feat: Add reset button to custom API (#2076)
Closes #2051
2025-05-20 14:10:49 +02:00
Ushie
16f16e859b feat: Show manager update dialog (#2069)
Closes #1963, closes #1958
2025-05-20 14:10:48 +02:00
oSumAtrIX
83eb1a9fd7 fix: Support patching on ARMv7 by updating AAPT2 (#2084) 2025-05-20 14:10:46 +02:00
Ushie
9a336aa3ef feat: Improve update setting tile titles
Closes #1968
2025-05-20 14:10:45 +02:00
Pun Butrach
d0b8cba2bf build: Enable Gradle Configuration Cache (#2059) 2025-05-20 14:10:43 +02:00
Ax333l
7436d99532 fix: always use default patch selection if customization is disabled 2025-05-20 14:10:40 +02:00
Robert
c982babaeb fix: android icon not loading in app selector 2025-05-20 14:10:38 +02:00
Pun Butrach
211f7d2fa2 feat: Improve custom API URL dialog (#2033)
Signed-off-by: validcube <pun.butrach@gmail.com>
2025-05-20 14:10:35 +02:00
Ushie
d432ffbbe0 fix: Broken header padding in AlertDialogExtended when using an Icon 2025-05-20 14:10:34 +02:00
Ushie
500cd63507 fix: Remove unnecessary screen padding
Closes #2062
2025-05-20 14:10:33 +02:00
Ushie
9404c3c297 feat: Remove tag from changelog 2025-05-20 14:09:41 +02:00
Ushie
f2f89aa185 feat: Progressive AlertDialog for adding bundles
Closes #1992
2025-05-20 14:07:57 +02:00
Ushie
d6e931a876 fix: Use the correct icon in API URL dialog
Closes #1972
2025-05-20 14:06:10 +02:00
Ushie
260964c633 feat: Add sensitivity to isScrollingUp 2025-05-20 14:06:08 +02:00
Ushie
87addbff55 feat: Add isScrollingUp support for ScrollState 2025-05-20 14:06:05 +02:00
Ushie
5ff5298e0e fix: Use FAB instead of ListItem to patch in App Overview
Closes #1995
2025-05-20 14:06:04 +02:00
Ushie
0bb08c7afc feat: Improve device information in debugging section
Closes #1977
2025-05-20 14:06:01 +02:00
Ushie
a0e67a42e0 fix: Change the title in the Update screen from "Updates" to "Update"
Closes #1960
2025-05-20 14:05:59 +02:00
Ushie
585d54a8a8 feat: Change "Update" to "Show" in Update Available notification
Closes #1959
2025-05-20 14:05:57 +02:00
Ushie
abdae89434 feat: Highlight links in Markdown
Closes #1962
2025-05-20 14:05:54 +02:00
Ushie
c7c4da54fb feat: Improve initial update popup wording
Closes #1956
2025-05-20 14:05:53 +02:00
Ushie
2dacfce61d chore: Remove unused ARMv7 AAPT binary
Closes #1954
2025-05-20 14:05:52 +02:00
Ushie
f8da11e684 refactor: Improve naming consistency in libs.version.toml
Closes #1953
2025-05-20 14:05:50 +02:00
validcube
f384c66dd6 fix: Inconsistent padding for battery optimisation warning
The problem came after moving the card to DashboardScreen, this is because the card specified padding modifier but others does not. This commit remove the modifier completely.
2025-05-20 14:05:03 +02:00
validcube
1a9031193c refactor: Use TextButton instead of FilledButton for consistency 2025-05-20 14:05:02 +02:00
Pun Butrach
dc743021c3 ci: Bump dependencies to latest (#2039) 2025-05-20 14:05:00 +02:00
Ax333l
a81913c2f7 chore: update dependencies 2025-05-20 14:04:58 +02:00
Ax333l
494197b5dd fix: move battery warning to dashboard 2025-05-20 14:04:57 +02:00
Ax333l
285c55228d feat: improve the safeguards (#2038) 2025-05-20 14:04:54 +02:00
Ax333l
aedc6e9970 fix: run props flow on correct dispatcher (#2035) 2025-05-20 14:04:51 +02:00
Pun Butrach
3ed2c87f45 feat: Automatic language detection (#2032) 2025-05-20 14:04:49 +02:00
Ax333l
d5b22258a6 fix: improve bundle page strings 2025-05-20 14:04:46 +02:00
Ax333l
e6361118a7 fix: cleanup advanced settings screen 2025-05-20 14:04:45 +02:00
Ax333l
edf2f28eca feat: dont ask for root on launch 2025-05-20 14:04:44 +02:00
Ax333l
b5abe1bbc3 feat: improve UX for failed or missing bundles 2025-05-20 14:04:41 +02:00
Ax333l
8654da0dfe feat: implement more patch option types (#2015) 2025-05-20 14:04:39 +02:00
Ax333l
c48698334c fix: crash when removing used bundles 2025-05-20 14:04:36 +02:00
Ax333l
f53299b2a6 fix: import export screen UX 2025-05-20 14:04:34 +02:00
Ax333l
8c1b8e1ee1 feat: add ability to share debug logs 2025-05-20 14:04:32 +02:00
Ax333l
500e0ad9b7 fix: import bundles on another thread 2025-05-20 14:04:30 +02:00
Ax333l
0d6ee98609 feat: get bundle information from jar manifest (#2027) 2025-05-20 14:04:29 +02:00
Ax333l
231cf52f30 fix: add bounds checks in patch selector 2025-05-20 14:04:28 +02:00
Robert
994cb6c4b0 feat: rename main bundle to Default 2025-05-20 14:04:26 +02:00
Robert
7365fc241a fix: use proper update icon 2025-05-20 14:04:24 +02:00
Robert
e2f02ebf22 feat: improve patcher screen labels 2025-05-20 14:04:21 +02:00
Robert
5150adeaff fix: scrolling in patch selector 2025-05-20 14:04:19 +02:00
Robert
92612f9aec feat: rename debug build to ReVanced Manager (dev) 2025-05-20 14:04:17 +02:00
Robert
44b5f7b3bc fix(downloader): versions not loading correctly 2025-05-20 14:04:15 +02:00
Robert
46bd2f48a8 fix: automatically focus search views 2025-05-20 14:04:13 +02:00
Robert
3ac2062992 feat: move update to notification card (#1917) 2025-05-20 14:04:11 +02:00
Robert
95be465b39 feat: revert to blue theme colors 2025-05-20 14:04:09 +02:00
Ax333l
226d9c9c23 refactor: fix more warnings 2025-05-20 14:04:06 +02:00
Ax333l
dabf16a436 build(deps): update ksp 2025-05-20 14:04:04 +02:00
Ax333l
ff0bf43c7d refactor: replace deprecated functions 2025-05-20 14:04:02 +02:00
Ax333l
016de8bb0d fix: crash caused by compose inlining bug
This is a bug in jetpack compose. Inlining this function wasn't very
important anyways so it's best to just stop inlining it to avoid the
crash.
2025-05-20 14:04:00 +02:00
dependabot[bot]
b6c02b7be1 build(deps): bump aboutLibrariesGradlePlugin from 11.1.0 to 11.1.1 (#1813)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 14:03:58 +02:00
dependabot[bot]
9e7b26b1c8 build(deps): bump androidx.compose.ui:ui-tooling from 1.6.3 to 1.6.4 (#1814)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 14:03:56 +02:00
dependabot[bot]
05eb0d7457 build(deps): bump androidx.compose:compose-bom from 2024.02.02 to 2024.03.00 (#1812)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 14:03:55 +02:00
dependabot[bot]
4b0706f8b0 build(deps): bump libsu from 5.2.1 to 5.2.2 (#1810)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 14:03:52 +02:00
dependabot[bot]
9c6f0c324b build(deps): bump plugin.serialization from 1.9.22 to 1.9.23 (#1811)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 14:03:48 +02:00
Ax333l
c98ca70e08 fix: correctly patch apk files 2025-05-20 14:03:46 +02:00
Ax333l
424fe25dfb feat: add external process runtime (#1799) 2025-05-20 14:03:44 +02:00
Ax333l
666deda0b5 feat: check if the version being used is the recommended version (#1675) 2025-05-20 14:03:42 +02:00
Benjamin
5e4510eed5 feat: add social links (#1294)
Co-authored-by: Pun Butrach <pun.butrach@gmail.com>
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
Co-authored-by: Ax333l <main@axelen.xyz>
2025-05-20 14:03:39 +02:00
Benjamin
a9147ed0c0 chore: Upgrade dependencies (#1761)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 14:03:37 +02:00
Benjamin
bd7c4aa554 chore: upgrade dependencies (#1670) 2025-05-20 14:03:36 +02:00
Ax333l
9d0f3a3605 fix(VersionSelector): use correct LazyColumn item key 2025-05-20 14:03:33 +02:00
Benjamin
093a4ebf49 refactor: Disable update for dev build (#1673)
Co-authored-by: Pun Butrach <pun.butrach@gmail.com>
2025-05-20 14:03:31 +02:00
Ushie
17cc9f9e9e feat: Collapse ExtendedFAB on scroll (#1630) 2025-05-20 14:03:29 +02:00
Ax333l
bd85b254e4 feat: add toast feedback to the bundle update button 2025-05-20 14:03:26 +02:00
Ax333l
1460fd7be2 fix: patch options reset button being broken 2025-05-20 14:03:24 +02:00
Ax333l
bc3fe3f0f2 refactor: use consistent wording for the version compat check 2025-05-20 14:03:21 +02:00
Pun Butrach
bdc0fc89c3 docs(security): init (#1612)
Co-authored-by: Ax333l <main@axelen.xyz>
2025-05-20 14:03:20 +02:00
Ax333l
aeefe644c2 refactor: fix terminology and wording related to patches (#1623) 2025-05-20 14:03:17 +02:00
Ushie
48604804f9 feat: Scrollbars (#1479) 2025-05-20 14:03:14 +02:00
Robert
03ccea46e2 fix: progress bar not updating 2025-05-20 14:03:13 +02:00
Robert
b4bc14e4ed feat: improve patcher UI (#1494) 2025-05-20 14:03:09 +02:00
Ax333l
6dcbe271a7 feat: updater UI and code improvements (#1597) 2025-05-20 14:03:07 +02:00
Ushie
3317fd5649 feat: Select bundle type before adding bundle (#1490) 2025-05-20 14:03:05 +02:00
Benjamin
c9eb3ffa14 feat: Purple default theme (#1601) 2025-05-20 14:03:02 +02:00
Benjamin
fe1e65ce9c chore: upgrade AGP to 8.2.0 + migrate deprecated functions (#1574) 2025-05-20 14:03:00 +02:00
Ax333l
f7426309b4 chore(deps): update jetpack compose 2025-05-20 14:02:58 +02:00
aAbed
7f67a86413 feat(app-selector): show patchable installed apps first (#1496) 2025-05-20 14:02:56 +02:00
validcube
a22ef4d9b8 docs: update revanced url 2025-05-20 14:02:54 +02:00
validcube
ada99e80f9 build: bump Gradle to v8.5
build: update Gradle wrapper
2025-05-20 14:02:51 +02:00
validcube
111d8b6543 refactor: slight formatting of build.gradle.kts 2025-05-20 14:02:50 +02:00
validcube
15599dbb21 chore(template): update label name for feature 2025-05-20 14:02:49 +02:00
validcube
24cd2dca75 ci: caching with gradle-build-action
Allow for automatic capture of buildscan in job summary, and smarter
caching than the one provided by `setup-java`.
2025-05-20 14:02:46 +02:00
Ax333l
c48f5b2488 chore(deps): bump revanced patcher and library 2025-05-20 14:02:44 +02:00
Benjamin
06d4485032 fix: specify multithreadingDexFileWriter in PatcherOptions (#1402)
Co-authored-by: Ax333l <main@axelen.xyz>
2025-05-20 14:02:43 +02:00
Ax333l
73fdf92780 fix: load patch bundles earlier 2025-05-20 14:02:42 +02:00
Ushie
0fda344952 feat(Update Screen): changelogs & handle states (#1464)
Co-authored-by: Ax333l <main@axelen.xyz>
2025-05-20 14:02:38 +02:00
Ushie
9c0665acb2 feat(Contributors Screen): implement design from Figma (#1465)
Co-authored-by: Robert <72943079+CnC-Robert@users.noreply.github.com>
Co-authored-by: Ax333l <main@axelen.xyz>
2025-05-20 14:02:36 +02:00
Ax333l
6bafa23bb4 fix: parcel error for nullable types 2025-05-20 14:02:34 +02:00
oSumAtrIX
8387ada245 feat: Use correct casing in module description 2025-05-20 14:02:32 +02:00
Ax333l
048ba12703 chore: bump patcher 2025-05-20 14:02:31 +02:00
Robert
1b6a77a463 feat: check for updates on startup (#1462) 2025-05-20 14:02:29 +02:00
Ushie
ff9d021a2b feat(Changelogs): overall improvement (#1429) 2025-05-20 14:02:26 +02:00
Ushie
6ac4819478 feat(Installer): use BottomAppBar (#1428) 2025-05-20 14:02:25 +02:00
Benjamin
4b5e2e97f7 fix: option state crash (#1456)
Co-authored-by: Ax333l <main@axelen.xyz>
2025-05-20 14:02:21 +02:00
Ax333l
7a4b0bd7c8 refactor(ui-components): deduplicate colors and move to settings folder 2025-05-20 14:02:18 +02:00
Ushie
205650865a feat(NotificationCard): rewrite & consistent usage (#1426) 2025-05-20 14:02:17 +02:00
Ushie
39ff42db01 feat(Settings): use SettingsListItem consistently and overall improvements (#1427) 2025-05-20 14:02:13 +02:00
Ax333l
62f5acee1a feat: remember patch options (#1449) 2025-05-20 14:02:11 +02:00
Benjamin
67ecc13a28 chore: add issue template (#1432) 2025-05-20 14:02:09 +02:00
Ax333l
ad10a19acd feat(installer): sign apk in patcher worker 2025-05-20 14:02:07 +02:00
Ax333l
a35c62a99d fix: use correct checksum 2025-05-20 14:02:05 +02:00
Benjamin
8450243ddc chore: upgrade dependencies (#1401) 2025-05-20 14:02:03 +02:00
Ax333l
d239efcf14 fix: perform selected app operations in the correct order 2025-05-20 14:02:01 +02:00
Ax333l
691b615b02 feat(bundles tab): add BackHandler 2025-05-20 14:01:59 +02:00
Ax333l
3f34407741 docs: clarify license 2025-05-20 14:01:57 +02:00
Ax333l
da4153039c feat: show toast when no patches are selected 2025-05-20 14:01:54 +02:00
Ax333l
1de59f420b feat: add checkboxes to the downloaded apps page 2025-05-20 14:01:51 +02:00
Ax333l
464aa753f4 fix: more android 34 fixes 2025-05-20 14:01:48 +02:00
Ax333l
0cf49998e0 fix: handle exceptions when checking for bundle updates 2025-05-20 14:01:47 +02:00
Ax333l
991a8cb5d1 feat(patch-selector): remove TODO about an unplanned feature 2025-05-20 14:01:45 +02:00
Ax333l
ac75d1da27 fix: bundles not loading on Android 14 2025-05-20 14:01:42 +02:00
Ax333l
5907659cc8 fix: jvm signature clash error 2025-05-20 14:01:40 +02:00
Ax333l
d8d2478d0f fix: use upsert when modifying installed apps 2025-05-20 14:01:38 +02:00
Ax333l
4ad3c3fb72 feat: selected app info page (#1395) 2025-05-20 14:01:35 +02:00
Ax333l
0e00b9f526 refactor: move mount code to when block 2025-05-20 14:01:32 +02:00
Benjamin
88f3701a6c fix: hide patch button (#1284) 2025-05-20 14:01:31 +02:00
Benjamin
a15924617e feat: add user agent (#1382) 2025-05-20 14:01:30 +02:00
Ax333l
db04672d72 chore: bump patcher 2025-05-20 14:01:27 +02:00
Ax333l
977345e5aa chore: bump compose 2025-05-20 14:01:25 +02:00
Ax333l
76e5731eb8 fix: broken logo in about page on release builds 2025-05-20 14:01:23 +02:00
Ax333l
99bfd84e03 feat: hide unfinished pages in release mode 2025-05-20 14:01:20 +02:00
Ax333l
edb387e1a8 feat: armv7 warning 2025-05-20 14:01:18 +02:00
Ax333l
80ff6711f4 refactor(downloaders): improve file system code (#1379) 2025-05-20 14:01:16 +02:00
Benjamin
cadbb3f46d feat: settings migration (compose) (#1309) 2025-05-20 14:01:15 +02:00
Ax333l
e1742fd4c0 feat: add patches selector bottom sheet (#1360) 2025-05-20 14:01:14 +02:00
Ax333l
c2c4895a29 feat: use revanced api for changelogs 2025-05-20 14:01:11 +02:00
Ax333l
ffe5c058e0 fix: delete temporary files (#1341) 2025-05-20 14:01:08 +02:00
Ax333l
59ddd9f393 fix: use correct classes to determine option type
I can't believe this happened
2025-05-20 14:01:06 +02:00
Ax333l
57ba3ad374 feat(settings): move experimental patches option to advanced 2025-05-20 14:01:05 +02:00
Ax333l
6fed17705b feat(installer): adjust arrow icon size 2025-05-20 14:01:03 +02:00
Ax333l
f915b544c4 feat(installer): adjust step icon size and alignment 2025-05-20 14:01:02 +02:00
Ax333l
11a383a13a chore: switch to revanced library and bump patcher (#1314) 2025-05-20 14:01:01 +02:00
Ax333l
4b178d947c feat(patch-selector): default patches selection (#1272) 2025-05-20 14:00:58 +02:00
Ax333l
f2e7661b5c feat: remove dead help icons
These never did anything and were removed from the figma a while ago.
2025-05-20 14:00:57 +02:00
Ax333l
e33862f436 chore: fully remove idea project files 2025-05-20 14:00:55 +02:00
Benjamin Halko
51dc429330 fix: remove misc.xml and kotlinc.xml 2025-05-20 14:00:53 +02:00
Benjamin
5a41cc1162 docs: init (#1224) 2025-05-20 14:00:32 +02:00
Benjamin
28ab79d962 ci: Add release workflow (#1235) 2025-05-20 14:00:30 +02:00
Benjamin
8f2c18585f fix: Updates popup shows incorrect names (#1283) 2025-05-20 14:00:29 +02:00
Benjamin
2f533d12b9 fix: use ReVanced ring logo in about section (#1302) 2025-05-20 14:00:27 +02:00
Benjamin
44cec48a7f feat: implement Submit Issue button (#1276) 2025-05-20 14:00:25 +02:00
Benjamin
395da595a2 refactor: update progress onBackClick function (#1277) 2025-05-20 14:00:23 +02:00
Benjamin
18ea6adb20 fix: disable WebView history (#1278) 2025-05-20 14:00:21 +02:00
Benjamin
daeb534692 fix(ui): make entire patches view button selectable (#1271) 2025-05-20 14:00:19 +02:00
Benjamin Halko
0b2ddbe0bf feat: change appID and name of debug builds 2025-05-20 14:00:17 +02:00
Benjamin Halko
3c3ff64b18 ci: build pull requests (#1228) 2025-05-20 14:00:15 +02:00
Benjamin
43befa8713 fix: typo in string name import_keystore_description (#1273) 2025-05-20 14:00:13 +02:00
Benjamin
3c820405a8 fix: contributors screen fix (#1256) 2025-05-20 14:00:10 +02:00
Patryk Miś
d65e830467 chore: update dependencies (#1247) 2025-05-20 14:00:08 +02:00
Robert
154b23202c feat: root installation (#1243) 2025-05-20 14:00:05 +02:00
Benjamin
91e0d48721 fix: minify crash on building release (#1245) 2025-05-20 14:00:02 +02:00
Benjamin
ae5eef0f2c fix: providers.gradleProperty (#1223) 2025-05-20 14:00:01 +02:00
Tyff
382c068a03 feat: make bundles selectable (#1237) 2025-05-20 13:59:58 +02:00
Pun Butrach
cfaf874326 ci(config): appreciation for first-time contributors
Show appreciation message for new contributors
2025-05-20 13:59:55 +02:00
Benjamin
97d25b5602 docs: update readme badges (#1227) 2025-05-20 13:59:54 +02:00
Pun Butrach
cde470f867 ci(release): don't build when not necessary
Add paths-ignore to all markdown files, and .idea folder
2025-05-20 13:59:52 +02:00
Pun Butrach
1ad3e3423c docs: update badge's repository
The repository was moved from `revanced-manager-compose` to the main one, which is `revanced-manager`.

The organisation's name has also switched to `ReVanced` (used to be `revanced`).
2025-05-20 13:59:50 +02:00
Ax333l
d62f0a96fb chore: bump kotlinx.serialization plugin and patcher 2025-05-20 13:59:47 +02:00
Patryk Miś
d4ee3334e0 build: updates (#85) 2025-05-20 13:59:44 +02:00
Ax333l
384fb19ddf fix(deps): use correct work-runtime version string 2025-05-20 13:59:42 +02:00
Tyff
ab04ef99c3 feat: more info for the select from application screen (#81) 2025-05-20 13:59:41 +02:00
Pun Butrach
c812ce2011 ci(release): migrate from node12 to node16
This bump `actions/upload-artifact`@v2 to `actions/upload-artifact`@v3
2025-05-20 13:59:39 +02:00
Robert
fa8f154d65 feat: store patched apps (#79)
* feat: store patched apps

* fix: missing string

* feat: save patch selection

* feat: things

* fix: fix broken query

* fix: remove redundant `withContext`

* fix: fix
2025-05-20 13:59:36 +02:00
Palm
af03eec4b5 ci(release): use correct vars context object
why am i so stupid
2025-05-20 13:59:33 +02:00
Palm
da66b43497 ci(release): no longer store keystore alias in secrets
fixes an issue where GitHub Actions logs would be censored
2025-05-20 13:59:32 +02:00
Ax333l
e302ea9f9e fix: patches not being reloaded 2025-05-20 13:59:30 +02:00
Ax333l
6aa3b6c4b0 fix: permission error when using installed app 2025-05-20 13:59:27 +02:00
Ax333l
5cb887ebe6 feat: patch options UI (#80) 2025-05-20 13:59:25 +02:00
Ax333l
4cd00c122d feat: switch to the new api (#75) 2025-05-20 13:59:22 +02:00
Ax333l
5744bdda80 chore: bump patcher 2025-05-20 13:59:20 +02:00
Ax333l
89e373f98c feat: improve bundle dialog UI 2025-05-20 13:59:19 +02:00
Ax333l
ecd4b01108 feat: finish implementing the sources system (#70) 2025-05-20 13:59:15 +02:00
Ax333l
08686252bb fix: library info not being embedded 2025-05-20 13:59:13 +02:00
Pun Butrach
ab1dd8862d ci(release): task naming consistency 2025-05-20 13:59:11 +02:00
CnC-Robert
c4abf8a324 fix: don't store app list in parcel 2025-05-20 13:59:09 +02:00
Ax333l
05adb78932 fix(installer): progress tracking 2025-05-20 13:59:07 +02:00
Robert
3ae1d3374a ci: init 2025-05-20 13:59:06 +02:00
CnC-Robert
067f8adf4b feat: show installed app in version selector 2025-05-20 13:59:05 +02:00
Robert
83f6d287b3 feat: download apps in patcher screen (#73) 2025-05-20 13:59:01 +02:00
Pun
3dde82fc18 docs(readme): minor fix to displaying url
When you hover on Commit & Activity badges, `revanced` will appear in url display, but on License badge, `ReVanced` will display

This commit fix that by changing the organisation to what we're supposed to be using which is {org_name}/{repo_name} (ReVanced/...)
2025-05-20 13:58:59 +02:00
Tyff
de0af4c489 feat: add patch bundle info screen (#55) 2025-05-20 13:58:56 +02:00
Ax333l
f98386dcc8 fix: serialization not working 2025-05-20 13:58:54 +02:00
Patryk Miś
49209ca562 fix: buildfile syntax (#66)
Signed-off-by: Patryk Miś <foss@patrykmis.com>
2025-05-20 13:58:53 +02:00
Patryk Miś
d68e7f71e9 build: updates (#63)
* Update Java base to 17
* update Kotlin to 1.8.22
* update Bouncycastle
* update all dependencies
* follow the manifest on jni libs packaging
* enhance app optimization by specifying resource configurations, excluding dependencies info and unnessesary files
* Remove obsolete SDK check as we are already using minSdk 26

Signed-off-by: Patryk Miś <foss@patrykmis.com>
2025-05-20 13:58:49 +02:00
Patryk Miś
fffdb314a1 feat: improve accessibility (#64)
* Label Back button
* Mark group section headings as headings

Signed-off-by: Patryk Miś <foss@patrykmis.com>
2025-05-20 13:58:47 +02:00
Ax333l
ee41e315fb feat: switch to Preferences DataStore (#60) 2025-05-20 13:58:45 +02:00
CnC-Robert
cd3d654318 feat: disable filter chips when there are no patches 2025-05-20 13:58:43 +02:00
CnC-Robert
3bd1ef3de7 feat: ReVanced theme colors 2025-05-20 13:58:41 +02:00
Ax333l
ba1a152231 fix: release builds not working properly 2025-05-20 13:58:40 +02:00
Rom Reviewer
55573eb94f chore: migrate dependencies to version catalogs (#58) 2025-05-20 13:58:38 +02:00
Robert
80e78f544b feat: app downloader (#43) 2025-05-20 13:58:35 +02:00
Pun Butrach
7572944c9e build: update gradle to v8.2.1 2025-05-20 13:58:34 +02:00
Pun
f5e9826dfb docs(readme): minor changes to how badges works
* Better description for the repository license badge

* Clicking on badges open you the relevant url
2025-05-20 13:58:32 +02:00
Ax333l
c8ac94d82d feat: improve keystore UI and UX (#52) 2025-05-20 13:58:29 +02:00
Pun Butrach
d9ff833100 revert: downgrade Kotlin to 1.8.21
"A what? 1.8.22 isn't compatible, but the version bump indicate that it's supposed to be bug fixes????"
2025-05-20 13:58:27 +02:00
Ax333l
7150fb4435 feat: advanced settings page with device info (#51) 2025-05-20 13:58:25 +02:00
Pun Butrach
34c331f39b build: update dependencies
There are 9 dependencies update, changelog of this commit are available
below here.

Android Gradle Plugin: 8.0.1 -> 8.0.2
Kotlin: 1.8.21 -> 1.8.22
Android Compose BOM: 2023.05.01 -> 2023.06.01
Room: 2.5.1 -> 2.5.2
ReVanced Patcher: 11.0.1 -> 11.0.4
APKsig: 8.2.0-alpha05 -> 8.2.0-alpha10
Koin (Android, workmanager): 3.4.0 -> 3.4.2
Koin (Androidx Compose): 3.4.4 -> 3.4.5
Ktor: 2.3.0 -> 2.3.1
2025-05-20 13:58:24 +02:00
Ax333l
1ff76cf584 fix(installer): sign and install on threads
This is needed to avoid ANRs because it takes a while if the Apk is 100+
MB.
2025-05-20 13:58:23 +02:00
Ax333l
1de0e87983 feat: updater changelogs (#48)
---------

Co-authored-by: Aunali321 <aunvakil.aa@gmail.com>
2025-05-20 13:58:20 +02:00
Ax333l
93b2dd6176 feat: allow user to save logs 2025-05-20 13:58:18 +02:00
Ax333l
f3e2435fef feat: save patch options and selected patches in bundle (#50) 2025-05-20 13:58:17 +02:00
Ax333l
b42d8842d5 feat: patch options (#45) 2025-05-20 13:58:15 +02:00
Ax333l
c052a0c0f5 refactor: use getDir instead of filesDir directly 2025-05-20 13:58:13 +02:00
Ax333l
34cf91d4b6 fix: use correct directory 2025-05-20 13:58:10 +02:00
Ax333l
f99504d3e4 build: bump patcher 2025-05-20 13:58:08 +02:00
Pun Butrach
3ec1df9650 build: update gradle to v8.2 2025-05-20 13:58:05 +02:00
Ax333l
871a34df23 feat: licenses screen (#47) 2025-05-20 13:58:03 +02:00
Ax333l
b65ec4560f chore: update links in about page 2025-05-20 13:58:01 +02:00
Ax333l
0eaeb5d5ea feat: animate the arrow button 2025-05-20 13:57:59 +02:00
Ax333l
060f39fb9b refactor: use correct coroutine scopes 2025-05-20 13:57:57 +02:00
Ax333l
722dfadb3c fix(installer): save step incorrectly being marked as completed 2025-05-20 13:57:56 +02:00
Ax333l
6567be40cb fix: sources screen being misaligned during transitions 2025-05-20 13:57:54 +02:00
Tyff
9539d23c12 feat: contributors screen (#42)
* Contributors page
- https://github.com/revanced/revanced-manager-compose/issues/34

* feat: adding ContributorScreen as clickable icons like the website

* feat: adding ContributorScreen
- Made changes that were asked for in prev PR
- Currently just waiting on a git merge to get ArrowButton in

* feat: adding ContributorScreen
- Made changes that were asked for in prev PR
- ArrowButton is also in use

* feat: adding ContributorScreen
- Made changes that were asked for in prev PR
- ArrowButton is also in use
- Fixed other PR comment changes

* Apply suggestions from code review

* Remove unused string resources

---------

Co-authored-by: Ax333l <main@axelen.xyz>
2025-05-20 13:57:51 +02:00
Ax333l
d0d0a17a55 fix: pass worker inputs without serialization (#44)
Because androidx.work.Data sucks and causes our app to crash.
2025-05-20 13:57:50 +02:00
Ax333l
d2e965f056 fix(installer): make the correct column scrollable 2025-05-20 13:57:49 +02:00
Ax333l
cda0e127d9 feat: experimental patches setting 2025-05-20 13:57:45 +02:00
Ax333l
fea11dfef6 feat: save patch selection using room db (#38) 2025-05-20 13:57:43 +02:00
Ax333l
dcc4477e3e refactor: better PatchBundle docs and naming 2025-05-20 13:57:40 +02:00
Ax333l
6eb21e1fab build: bump patcher 2025-05-20 13:57:38 +02:00
Ax333l
b8902d04d7 feat: show stacktrace in installer ui (#36) 2025-05-20 13:57:36 +02:00
CnC-Robert
99efdb130f feat: filter options for patches 2025-05-20 13:57:34 +02:00
Ax333l
5177cd3083 fix: run blocking IO operations in the correct context 2025-05-20 13:57:32 +02:00
Ax333l
ff4b9ab960 fix(patcher): add notification and wakelock to worker; chore: add app icon 2025-05-20 13:57:30 +02:00
Ax333l
ad998ac22d feat: keystore import/export (#30) 2025-05-20 13:57:27 +02:00
Ax333l
881d2430c3 fix(installer): properly track worker state (#32) 2025-05-20 13:57:25 +02:00
Ax333l
b07ae90c86 feat(koin): use the android logger 2025-05-20 13:57:23 +02:00
CnC-Robert
8e6519cfb0 feat: ProGuard 2025-05-20 13:57:22 +02:00
CnC-Robert
bb90cc6e81 feat: rename package to app.revanced.manager 2025-05-20 13:57:18 +02:00
CnC-Robert
fd02e0799c feat: improved compose stability 2025-05-20 13:57:17 +02:00
CnC-Robert
f07204460c fix: use correct getViewModel 2025-05-20 13:57:15 +02:00
CnC-Robert
66be0f96e0 feat: rename ViewModels for consistency 2025-05-20 13:57:13 +02:00
CnC-Robert
a1ca19b289 feat: hide tabs when 1 bundle is used 2025-05-20 13:57:09 +02:00
Robert
af779153d5 refactor: PackageManager (#31)
* refactor: refactor `PM`

* feat: use plurals for patch count

* fix: support apk's from storage

* feat: use ViewModel for loading apps and bundles

* fix: fix file selector that has no reason to be broken

* refactor: rename parameter

* refactor: `MainViewModel`

* feat: make all apps use `path`

* build: target java 11
2025-05-20 13:57:05 +02:00
Ax333l
78966e13c4 refactor(logs): use consistent tag 2025-05-20 13:57:03 +02:00
Ax333l
8bdcf76832 refactor(di): use constructor DSL for VMs
Instead of doing it manually with viewModel { }
2025-05-20 13:57:00 +02:00
Ax333l
05ecbde6c2 chore(deps): bump revanced-patcher to 9.0.0 2025-05-20 13:56:57 +02:00
Ax333l
e558a47204 feat: better installer ui (#29)
based cossale

Co-authored-by: Aunali321 <aunvakil.aa@gmail.com>
2025-05-20 13:56:56 +02:00
Ax333l
61de7568cb feat: patch bundle sources system (#24) 2025-05-20 13:56:51 +02:00
Aunali321
2e7f8457d3 feat: in-app updater (#25) 2025-05-20 13:56:47 +02:00
Aunali321
332bad699d feat(settings screen): add battery optimization notification 2025-05-20 13:56:43 +02:00
Aunali321
0b5ab33b3e feat(update screen): complete main update screen 2025-05-20 13:56:40 +02:00
Aunali321
5b4242d28b feat(about screen): complete about screen 2025-05-20 13:56:38 +02:00
Aunali321
0c76ed3af0 feat(settings screen): match typography from figma 2025-05-20 13:56:36 +02:00
Aunali321
39d698e545 refactor(settings screen): clean code up a bit 2025-05-20 13:56:34 +02:00
Ax333l
18e91e7cbc fix: dont crash when the bundle cannot be downloaded 2025-05-20 13:56:32 +02:00
Ax333l
14dfe07795 feat(installer): apk signing and installation 2025-05-20 13:56:30 +02:00
Ax333l
8e011a5d6b fix(patches selector): copy the selected patches list 2025-05-20 13:56:28 +02:00
Ax333l
fc5f97e54b refactor(ui): move PatchItem to the only file where it is used 2025-05-20 13:56:27 +02:00
Ax333l
78728c1f2a refactor(net apis): remove unnecessary interfaces
Having interfaces like that is only really useful if you have unit
tests, which we don't.

Other similar compose projects don't make interfaces either.
Not having them is more readable.
2025-05-20 13:56:25 +02:00
Ax333l
90c95c0669 style: run formatter 2025-05-20 13:56:23 +02:00
Patryk Miś
fbd1e221da build: updates (#23) 2025-05-20 13:56:21 +02:00
Ax333l
c35c776ce2 feat: integrate revanced patcher (#22) 2025-05-20 13:56:18 +02:00
CnC-Robert
f275f57c11 feat: improved dashboard screen 2025-05-20 13:56:15 +02:00
CnC-Robert
520b86df0a feat: patches selector screen 2025-05-20 13:56:13 +02:00
CnC-Robert
8991827ac7 feat: settings screen 2025-05-20 13:56:11 +02:00
Patryk Miś
0871180dcc build: updates (#21)
* perf: obsolete sdk check

Signed-off-by: Patryk Mis <foss@patrykmis.com>

* chore: bump dependencies

Signed-off-by: Patryk Mis <foss@patrykmis.com>

---------

Signed-off-by: Patryk Mis <foss@patrykmis.com>
2025-05-20 13:56:08 +02:00
CnC-Robert
7103bd2ec1 feat: app selector screen 2025-05-20 13:56:06 +02:00
Aunali321
e5029c7d2c feat: Dashboard Screen (#18)
* feat: add Dashboard Screen and Sources Screen

* fix: fix tab onClick not working

* refactor: remove AppBar

---------

Co-authored-by: CnC-Robert <CnC.Rob3rt@gmail.com>
2025-05-20 13:56:05 +02:00
Ax333l
a512af50b5 fix: gradlew permissions on unix 2025-05-20 13:54:32 +02:00
Patryk Miś
cc59d60dfd build: dependency and syntax updates (#17)
* build: Update Gradle to v8.1.1

* build: Bump dependencies

* build: move repo configurations to settings

---------

Co-authored-by: Patryk Mis <24607131+PatrickMis@users.noreply.github.com>
2025-05-20 13:54:31 +02:00
CnC-Robert
4d894e908e feat: backend 2025-05-20 13:54:29 +02:00
Alexandre Teles
77b499ef29 Create README.md 2025-05-20 13:54:27 +02:00
Canny
0142b85ede feat: splash screen 2025-05-20 13:54:25 +02:00
Canny
d9633906f5 feat: implement navigation 2025-05-20 13:54:22 +02:00
Canny
3dd14fd34b feat: implement DI 2025-05-20 13:54:21 +02:00
oSumAtrIX
0b19a9865d chore: Migrate to compose-dev branch 2025-05-20 13:49:36 +02:00
LisoUseInAIKyrios
b0464408f1 ci: Run Crowdin Cron task on dev branch (#2543) 2025-05-12 14:51:42 +02:00
449 changed files with 23768 additions and 33050 deletions

View File

@@ -1,7 +1,7 @@
name: 🐞 Bug report
description: Report a bug or an issue.
title: "bug: "
labels: ["Bug report"]
title: 'bug: '
labels: ['Bug report']
body:
- type: markdown
attributes:
@@ -80,38 +80,21 @@ body:
- Describe your bug in detail
- Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...)
- Add images and videos if possible
- List used patches if applicable
validations:
required: true
- type: textarea
attributes:
label: Version of ReVanced Manager and version & name of app you are patching
validations:
required: true
- type: dropdown
attributes:
label: Installation method
options:
- Regular
- Mount
validations:
required: false
- type: textarea
attributes:
label: ReVanced Manager logs
description: Export logs from the ReVanced Manager settings.
render: shell
- List used patches, downloader and settings if applicable
validations:
required: true
- type: textarea
attributes:
label: Patch logs
description: Export logs from the "Patcher" screen.
description: Patch logs can be exported by clicking on the "Logs" button in the "Patcher" screen, when patching finishes.
render: shell
- type: textarea
attributes:
label: Debug logs
description: Debug logs can be exported by clicking on "Export debug logs" in "Settings" > "Advanced".
validations:
required: false
required: true
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your bug report will be closed if you don't follow the checklist below.

View File

@@ -2,4 +2,4 @@ blank_issues_enabled: false
contact_links:
- name: 🗨 Discussions
url: https://github.com/revanced/revanced-suggestions/discussions
about: Have something unspecific to ReVanced Manager in mind? Search for or start a new discussion!
about: Have something unspecific to ReVanced Manager in mind? Search for or start a new discussion!

View File

@@ -1,7 +1,3 @@
name: ⭐ Feature request
description: Create a detailed request for a new feature.
title: "feat: "
labels: ["Feature request"]
body:
- type: markdown
attributes:
@@ -84,7 +80,7 @@ body:
label: Motivation
description: |
A strong motivation is necessary for a feature request to be considered.
- Why should this feature be implemented?
- What is the explicit use case?
- What are the benefits?
@@ -97,9 +93,11 @@ body:
label: Acknowledgements
description: Your feature request will be closed if you don't follow the checklist below.
options:
- label: I have checked all open and closed feature requests and this is not a duplicate.
- label: I have checked all open and closed feature requests and this is not a duplicate
required: true
- label: I have chosen an appropriate title.
required: true
- label: All requested information has been provided properly.
required: true
- label: The feature request is only related to ReVanced Manager.
required: true

View File

@@ -1,78 +0,0 @@
version: 2
updates:
- package-ecosystem: github-actions
labels: []
directory: /
target-branch: dev
schedule:
interval: monthly
groups:
gh-actions:
applies-to: version-updates
patterns:
- "*"
update-types:
- "minor"
- "patch"
- package-ecosystem: npm
labels: []
directory: /
target-branch: dev
schedule:
interval: monthly
groups:
npm:
applies-to: version-updates
patterns:
- "*"
update-types:
- "minor"
- "patch"
# ReVanced Manager Flutter
- package-ecosystem: pub
labels: []
directory: /
target-branch: dev
schedule:
interval: monthly
groups:
pubspec:
applies-to: version-updates
patterns:
- "*"
update-types:
- "minor"
- "patch"
- package-ecosystem: gradle
labels: []
directory: /android
target-branch: dev
schedule:
interval: monthly
groups:
gradle:
applies-to: version-updates
patterns:
- "*"
update-types:
- "minor"
- "patch"
# ReVanced Manager Compose
- package-ecosystem: gradle
labels: [ "ReVanced Manager Compose" ]
directory: /
target-branch: compose-dev
schedule:
interval: monthly
groups:
gradle-compose:
applies-to: version-updates
patterns:
- "*"
update-types:
- "minor"
- "patch"

View File

@@ -2,69 +2,32 @@ name: Build pull request
on:
workflow_dispatch:
inputs:
pr-number:
description: PR number
required: true
app-flavor:
description: App flavor
default: release
type: choice
options:
- release
- debug
- profile
pull_request:
branches:
- dev
jobs:
build:
release:
name: Build
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout PR
- name: Checkout
uses: actions/checkout@v4
with:
ref: refs/pull/${{ inputs.pr-number }}/merge
fetch-depth: 0
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v1
with:
build-root-directory: ${{ github.workspace }}/android
- name: Get dependencies
run: flutter pub get
- name: Generate translations
run: dart run slang
- name: Generate code files
run: dart run build_runner build --delete-conflicting-outputs
- name: Build
id: flutter-build
run: flutter build apk --${{ inputs.app-flavor }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew assembleRelease --no-daemon
- name: Upload artifacts
if: steps.flutter-build.outcome == 'success'
uses: actions/upload-artifact@v4
with:
name: revanced-manager-(${{ env.COMMIT_HASH }}
name: revanced-manager
path: |
build/app/outputs/flutter-apk/app-*.apk
app/build/outputs/apk/release/revanced-manager*.apk
app/build/outputs/apk/release/revanced-manager*.apk.asc

View File

@@ -12,8 +12,6 @@ env:
jobs:
pull-request:
name: Open pull request
permissions:
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -24,6 +22,5 @@ jobs:
with:
destination_branch: 'main'
pr_title: 'chore: ${{ env.MESSAGE }}'
pr_body: |
This pull request will ${{ env.MESSAGE }}.
pr_body: 'This pull request will ${{ env.MESSAGE }}.'
pr_draft: true

View File

@@ -27,38 +27,33 @@ jobs:
distribution: 'temurin'
java-version: '17'
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v1
- name: Build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew assembleRelease
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"
cache: 'npm'
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v1
with:
build-root-directory: ${{ github.workspace }}/android
- name: Install dependencies
run: npm ci
- name: Get dependencies
run: flutter pub get
- name: Generate translations
run: dart run slang
- name: Generate code files
run: dart run build_runner build --delete-conflicting-outputs
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
fingerprint: ${{ vars.GPG_FINGERPRINT }}
- name: Setup keystore
run: |
echo "${{ secrets.KEYSTORE }}" | base64 --decode > "android/app/keystore.jks"
echo "${{ secrets.KEYSTORE }}" | base64 --decode > "app/keystore.jks"
- name: Semantic Release
uses: cycjimmy/semantic-release-action@v4
@@ -68,9 +63,10 @@ jobs:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEYSTORE_ENTRY_ALIAS: ${{ secrets.KEYSTORE_ENTRY_ALIAS }}
KEYSTORE_ENTRY_PASSWORD: ${{ secrets.KEYSTORE_ENTRY_PASSWORD }}
- name: Attest
if: steps.semantic.outputs.new_release_published == 'true'
uses: actions/attest-build-provenance@v2
with:
subject-path: build/app/outputs/apk/release/revanced-manager-*.apk
subject-name: 'ReVanced Manager ${{ steps.release.outputs.new_release_git_tag }}'
subject-path: app/build/outputs/apk/release/revanced-manager*.apk

View File

@@ -1,75 +0,0 @@
name: Sync Crowdin
on:
workflow_dispatch:
schedule:
- cron: 00 12 * * 1
push:
branches: dev
paths:
- assets/i18n/*.json
- assets/i18n/*.dart
- .github/workflows/sync_crowdin.yml
jobs:
sync:
name: Sync
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
cache: true
- name: Sync translations from Crowdin
uses: crowdin/github-action@v2
with:
config: crowdin.yml
upload_sources: true
upload_translations: false
download_translations: true
localization_branch_name: feat/translations
create_pull_request: true
pull_request_title: "chore: Sync translations"
pull_request_body: "Sync translations from [crowdin.com/project/revanced](https://crowdin.com/project/revanced)"
pull_request_base_branch_name: "dev"
commit_message: "chore: Sync translations"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Validation of synced translations
run: |
dart pub get
dart run slang validate
- name: Normalization of Translation Strings
run: |
sudo chmod 766 assets/i18n/*.i18n.json
dart run slang analyze
dart run slang clean
dart run slang normalize
dart run slang
cd assets/i18n
dart nuke.dart >> $GITHUB_STEP_SUMMARY
cd ../..
flutter analyze lib/gen/strings.g.dart --no-fatal-infos --no-fatal-warnings
- name: Commit translations
run: |
sudo chown -R $USER:$USER .git
git commit -m "chore: Remove empty values from JSON" assets/i18n/*.i18n.json
git push origin HEAD:feat/translations

View File

@@ -16,4 +16,4 @@ jobs:
token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }}
repository: revanced/revanced-documentation
event-type: update-documentation
client-payload: '{"repo": "${{ github.event.repository.name }}", "ref": "${{ github.ref }}"}'
client-payload: '{"repo": "${{ github.event.repository.name }}", "ref": "${{ github.ref }}"}'

159
.gitignore vendored
View File

@@ -1,55 +1,132 @@
# Miscellaneous
### Java template
# Compiled class file
*.class
# Log file
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# BlueJ files
*.ctxt
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# Symbolication related
app.*.symbols
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
# Obfuscation related
app.*.map.json
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
android/app/.cxx
**/*.g.dart
**/*.locator.dart
**/*.router.dart
.idea/**/contentModel.xml
# Project specific
node_modules/
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
.idea/artifacts
.idea/compiler.xml
.idea/jarRepositories.xml
.idea/modules.xml
.idea/*.iml
.idea/modules
*.iml
*.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Gradle template
.gradle
**/build/
!src/**/build/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Cache of project
.gradletasknamecache
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
# gradle/wrapper/gradle-wrapper.properties
# Potentially copyrighted test APK
*.apk
# Ignore vscode config
.vscode/
# Dependency directories
node_modules/
# Ignore IDEA files
.idea/
.kotlin/
local.properties
.cxx

View File

@@ -1,30 +0,0 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "2663184aa79047d0a33a14a3b607954f8fdd8730"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
- platform: android
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View File

@@ -14,27 +14,17 @@
]
}
],
"@semantic-release/changelog",
"@semantic-release/release-notes-generator",
[
"semantic-release-pub",
{
"publishPub": false,
"updateBuildNumber": true
}
],
[
"@semantic-release/exec",
{
"prepareCmd": "flutter build apk"
}
],
"@semantic-release/changelog",
"gradle-semantic-release-plugin",
[
"@semantic-release/git",
{
"assets": [
"pubspec.yaml"
]
"CHANGELOG.md",
"gradle.properties",
],
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
],
[
@@ -42,20 +32,17 @@
{
"assets": [
{
"path": "build/app/outputs/apk/release/revanced-manager*.apk"
"path": "app/build/outputs/apk/release/revanced-manager*.apk?(.asc)"
},
],
"commits": [
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
],
"successComment": false
successComment: false
}
],
[
"@saithodev/semantic-release-backmerge",
{
"backmergeBranches": [{"from": "main", "to": "dev"}],
"clearWorkspace": true
backmergeBranches: [{"from": "main", "to": "dev"}],
clearWorkspace: true
}
]
]

View File

@@ -96,7 +96,7 @@ If you encounter a bug while using ReVanced Manager, open an issue using the
## 🤚 I want to contribute but don't know how to code
Even if you don't know how to code, you can still contribute by
Even if you don't know how to code, you can still contribute by
translating ReVanced Manager on [Crowdin](https://translate.revanced.app/).
❤️ Thank you for considering contributing to ReVanced Manager,

View File

@@ -73,10 +73,9 @@ ReVanced Manager is an application that uses [ReVanced Patcher](https://github.c
Some of the features ReVanced Manager provides are:
- 💉 **Patch apps**: Apply any patch of your choice to Android apps
- 📱 **Portable**: ReVanced Patcher that fits in your pocket
- 🤗 **Simple UI**: Quickly understand the ins and outs of ReVanced Manager
- 🛠️ **Customization**: Configurable API, custom sources, language, signing keystore, theme and more
- ⬇️ **Download**: Automatically download apps using the ReVanced Manager downloader plugin system
- 💉 **Patch**: Select and apply patches to any Android app
- 🛠️ **Customize**: Manage patches, apps, signing, themes, updates, and many more settings
## 🔽 Download

View File

@@ -1,77 +0,0 @@
<p align="center">
<picture>
<source
width="256px"
media="(prefers-color-scheme: dark)"
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="assets/revanced-headline/revanced-headline-vertical-light.svg"
>
</picture>
<br>
<a href="https://revanced.app/">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="assets/revanced-logo/revanced-logo.svg" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="http://revanced.app/discord">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://x.com/revancedapp">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</picture>
</a>
<br>
<br>
Continuing the legacy of Vanced
</p>
# 🔒 Security Policy
This document describes how to report security vulnerabilities for ReVanced Manager.
## 🚨 Reporting a Vulnerability
Please open an issue in our [advisory tracker](https://github.com/ReVanced/revanced-manager/security/advisories/new) or reach out privately to us on [Discord](https://discord.gg/revanced).
If a vulnerability is confirmed and accepted, you can join our [Discord](https://discord.gg/revanced) server to receive a special contributor role.
### ⏳ Supported Versions
| Version | Branch | Supported |
| --------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------ |
| ![Latest stable release](https://img.shields.io/github/v/release/ReVanced/revanced-manager?style=for-the-badge "Latest stable release") | main | :white_check_mark: |
| ![Latest version](https://img.shields.io/badge/version-latest-brightgreen?style=for-the-badge "Latest version") | dev | :white_check_mark: |
| ![Latest version](https://img.shields.io/badge/version-latest-brightgreen?style=for-the-badge "Latest version") | compose-dev | :white_check_mark: |

View File

@@ -1,156 +0,0 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
analyzer:
exclude:
- lib/app/app.locator.dart
- lib/app/app.router.dart
- lib/models/patch.g.dart
- lib/models/patched_application.g.dart
- lib/gen/
linter:
rules:
- always_declare_return_types
- require_trailing_commas
- always_put_control_body_on_new_line
- always_use_package_imports # we do this commonly
- annotate_overrides
- avoid_bool_literals_in_conditional_expressions
- avoid_double_and_int_checks
- avoid_empty_else
- avoid_equals_and_hash_code_on_mutable_classes
- avoid_escaping_inner_quotes
- avoid_field_initializers_in_const_classes
- avoid_function_literals_in_foreach_calls
- avoid_implementing_value_types
- avoid_init_to_null
- avoid_js_rounded_ints
- avoid_null_checks_in_equality_operators
- avoid_print
- avoid_redundant_argument_values
- avoid_relative_lib_imports
- avoid_renaming_method_parameters
- avoid_return_types_on_setters
- avoid_returning_null_for_void
- avoid_setters_without_getters
- avoid_shadowing_type_parameters
- avoid_single_cascade_in_expression_statements
- avoid_type_to_string
- avoid_types_as_parameter_names
- avoid_unnecessary_containers
- avoid_void_async
- avoid_web_libraries_in_flutter # we use web libraries in web-specific code, and our tests prevent us from using them elsewhere
- await_only_futures
- camel_case_extensions
- camel_case_types
- cancel_subscriptions
- cast_nullable_to_non_nullable
- close_sinks # not reliable enough
- control_flow_in_finally
- curly_braces_in_flow_control_structures
- depend_on_referenced_packages
- deprecated_consistency
- directives_ordering
- empty_catches
- empty_constructor_bodies
- empty_statements
- eol_at_end_of_file
- exhaustive_cases
- file_names
- flutter_style_todos
- hash_and_equals
- implementation_imports
- collection_methods_unrelated_type
- leading_newlines_in_multiline_strings
- library_prefixes
- library_private_types_in_public_api
- missing_whitespace_between_adjacent_strings
- no_adjacent_strings_in_list
- no_duplicate_case_values
- no_logic_in_create_state
- non_constant_identifier_names
- noop_primitive_operations
- null_check_on_nullable_type_parameter
- null_closures
- overridden_fields
- package_names
- prefer_adjacent_string_concatenation
- prefer_asserts_in_initializer_lists
- prefer_collection_literals
- prefer_conditional_assignment
- prefer_const_constructors
- prefer_const_constructors_in_immutables
- prefer_const_declarations
- prefer_const_literals_to_create_immutables
- prefer_contains
- prefer_final_fields
- prefer_final_in_for_each
- prefer_final_locals
- prefer_for_elements_to_map_fromIterable
- prefer_foreach
- prefer_function_declarations_over_variables
- prefer_generic_function_type_aliases
- prefer_if_elements_to_conditional_expressions
- prefer_if_null_operators
- prefer_initializing_formals
- prefer_inlined_adds
- prefer_interpolation_to_compose_strings
- prefer_is_empty
- prefer_is_not_empty
- prefer_is_not_operator
- prefer_iterable_whereType
- prefer_null_aware_method_calls # "call()" is confusing to people new to the language since it's not documented anywhere
- prefer_null_aware_operators
- prefer_single_quotes
- prefer_spread_collections
- prefer_typing_uninitialized_variables
- provide_deprecation_message
- recursive_getters
- sized_box_for_whitespace
- slash_for_doc_comments
- sort_child_properties_last
- sort_constructors_first
- sort_pub_dependencies
- sort_unnamed_constructors_first
- test_types_in_equals
- throw_in_finally
- tighten_type_of_initializing_formals
- type_init_formals
- unnecessary_brace_in_string_interps
- unnecessary_const
- unnecessary_getters_setters
- unnecessary_new
- unnecessary_null_aware_assignments
- unnecessary_null_checks
- unnecessary_null_in_if_null_operators
- unnecessary_nullable_for_final_variable_declarations
- unnecessary_overrides
- unnecessary_parenthesis
- unnecessary_statements
- unnecessary_string_escapes
- unnecessary_string_interpolations
- unnecessary_this
- unrelated_type_equality_checks
- use_build_context_synchronously
- use_full_hex_values_for_flutter_colors
- use_function_type_syntax_for_parameters
- use_if_null_to_convert_nulls_to_bools
- use_is_even_rather_than_modulo
- use_key_in_widget_constructors
- use_late_for_private_fields_and_variables
- use_named_constants
- use_raw_strings
- use_rethrow_when_possible
- use_setters_to_change_properties
- use_test_throws_matchers
- valid_regexps
- void_checks

13
android/.gitignore vendored
View File

@@ -1,13 +0,0 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@@ -1,104 +0,0 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "app.revanced.manager.flutter"
compileSdk = 35
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
applicationId = "app.revanced.manager.flutter"
minSdk = 26
targetSdk = 35
versionCode = flutter.versionCode
versionName = flutter.versionName
resValue("string", "app_name", "ReVanced Manager")
}
applicationVariants.all {
outputs.all {
this as com.android.build.gradle.internal.api.ApkVariantOutputImpl
outputFileName = "revanced-manager-$versionName.apk"
}
}
buildTypes {
configureEach {
isShrinkResources = false
isMinifyEnabled = false
signingConfig = signingConfigs["debug"]
ndk.abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86_64")
}
release {
isShrinkResources = true
isMinifyEnabled = true
val keystoreFile = file("keystore.jks")
if (keystoreFile.exists()) {
signingConfig = signingConfigs.create("release") {
storeFile = keystoreFile
storePassword = System.getenv("KEYSTORE_PASSWORD")
keyAlias = System.getenv("KEYSTORE_ENTRY_ALIAS")
keyPassword = System.getenv("KEYSTORE_ENTRY_PASSWORD")
}
resValue("string", "app_name", "ReVanced Manager")
} else {
applicationIdSuffix = ".development"
resValue("string", "app_name", "ReVanced Manager (Development)")
signingConfig = signingConfigs["debug"]
}
}
debug {
applicationIdSuffix = ".debug"
resValue("string", "app_name", "ReVanced Manager (Debug)")
}
named("profile") {
initWith(getByName("debug"))
applicationIdSuffix = ".profile"
resValue("string", "app_name", "ReVanced Manager (Profile)")
}
}
packaging {
jniLibs {
useLegacyPackaging = true
excludes.add("/prebuilt/**")
}
resources {
excludes.add("/prebuilt/**")
}
}
}
flutter {
source = "../.."
}
dependencies {
coreLibraryDesugaring(libs.desugar.jdk.libs) // https://pub.dev/packages/flutter_local_notifications#gradle-setup
implementation(libs.revanced.patcher)
implementation(libs.revanced.library)
}

View File

@@ -1,17 +0,0 @@
-dontobfuscate
-keep class app.revanced.** { *; }
-keep class com.android.tools.smali.** { *; }
-keep class kotlin.** { *; }
-keep class com.google.auto.value.** { *; }
-keep class com.android.apksig.internal.** { *; }
-keepnames class com.google.common.collect.**
-keepnames class org.xmlpull.** { *; }
-dontwarn com.google.auto.value.**
-dontwarn com.google.j2objc.annotations.*
-dontwarn java.awt.**
-dontwarn javax.**
# Required for Share Plus, ref: ReVanced/revanced-manager#2474
-keep interface android.content.res.XmlResourceParser { *; }

View File

@@ -1,7 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -1,85 +0,0 @@
<manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<application
android:label="@string/app_name"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:largeHeap="true"
android:requestLegacyExternalStorage="true"
android:enableOnBackInvokedCallback="true">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name="de.julianassmann.flutter_background.IsolateHolderService"
android:exported="false"
android:foregroundServiceType="shortService" />
<activity
android:name=".ExportSettingsActivity"
android:exported="true">
</activity>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<receiver
android:name=".utils.packageInstaller.InstallerReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="APP_INSTALL_ACTION" />
</intent-filter>
</receiver>
<receiver
android:name=".utils.packageInstaller.UninstallerReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="APP_UNINSTALL_ACTION" />
</intent-filter>
</receiver>
</application>
</manifest>

View File

@@ -1,81 +0,0 @@
package app.revanced.manager.flutter
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Base64
import org.json.JSONObject
import java.io.ByteArrayInputStream
import java.io.File
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.MessageDigest
class ExportSettingsActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (getFingerprint(callingPackage!!) == getFingerprint(packageName)) {
// Create JSON Object
val json = JSONObject()
// Default Data
json.put("keystorePassword", "s3cur3p@ssw0rd")
// Load Shared Preferences
val sharedPreferences = getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
val allEntries: Map<String, *> = sharedPreferences.getAll()
for ((key, value) in allEntries.entries) {
json.put(key.replace("flutter.", ""), value)
}
// Load keystore
val keystoreFile = File(getExternalFilesDir(null), "/revanced-manager.keystore")
if (keystoreFile.exists()) {
val keystoreBytes = keystoreFile.readBytes()
val keystoreBase64 = Base64.encodeToString(keystoreBytes, Base64.DEFAULT)
json.put("keystore", keystoreBase64)
}
// Load saved patches
val storedPatchesFile = File(filesDir.parentFile.absolutePath, "/app_flutter/selected-patches.json")
if (storedPatchesFile.exists()) {
val patchesBytes = storedPatchesFile.readBytes()
val patches = String(patchesBytes, Charsets.UTF_8)
json.put("patches", JSONObject(patches))
}
// Send data back
val resultIntent = Intent()
resultIntent.putExtra("data", json.toString())
setResult(Activity.RESULT_OK, resultIntent)
finish()
} else {
val resultIntent = Intent()
setResult(Activity.RESULT_CANCELED)
finish()
}
}
fun getFingerprint(packageName: String): String {
// Get the signature of the app that matches the package name
val packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
val signature = packageInfo.signatures!![0]
// Get the raw certificate data
val rawCert = signature.toByteArray()
// Generate an X509Certificate from the data
val certFactory = CertificateFactory.getInstance("X509")
val x509Cert = certFactory.generateCertificate(ByteArrayInputStream(rawCert)) as X509Certificate
// Get the SHA256 fingerprint
val fingerprint = MessageDigest.getInstance("SHA256").digest(x509Cert.encoded).joinToString("") {
"%02x".format(it)
}
return fingerprint
}
}

View File

@@ -1,427 +0,0 @@
package app.revanced.manager.flutter
import android.app.PendingIntent
import android.app.SearchManager
import android.content.Intent
import android.content.pm.PackageInstaller
import android.os.Build
import android.os.Handler
import android.os.Looper
import app.revanced.library.ApkUtils
import app.revanced.library.ApkUtils.applyTo
import app.revanced.manager.flutter.utils.Aapt
import app.revanced.manager.flutter.utils.packageInstaller.InstallerReceiver
import app.revanced.manager.flutter.utils.packageInstaller.UninstallerReceiver
import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherConfig
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.PatchResult
import app.revanced.patcher.patch.loadPatchesFromDex
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.runBlocking
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
import java.util.logging.LogRecord
import java.util.logging.Logger
class MainActivity : FlutterActivity() {
private val handler = Handler(Looper.getMainLooper())
private lateinit var installerChannel: MethodChannel
private var cancel: Boolean = false
private var stopResult: MethodChannel.Result? = null
private lateinit var patches: Set<Patch<*>>
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
val patcherChannel = "app.revanced.manager.flutter/patcher"
val installerChannel = "app.revanced.manager.flutter/installer"
val openBrowserChannel = "app.revanced.manager.flutter/browser"
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
openBrowserChannel
).setMethodCallHandler { call, result ->
if (call.method == "openBrowser") {
val searchQuery = call.argument<String>("query")
openBrowser(searchQuery)
result.success(null)
} else {
result.notImplemented()
}
}
val mainChannel =
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, patcherChannel)
this.installerChannel =
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, installerChannel)
mainChannel.setMethodCallHandler { call, result ->
when (call.method) {
"runPatcher" -> {
val inFilePath = call.argument<String>("inFilePath")
val outFilePath = call.argument<String>("outFilePath")
val selectedPatches = call.argument<List<String>>("selectedPatches")
val options = call.argument<Map<String, Map<String, Any>>>("options")
val tmpDirPath = call.argument<String>("tmpDirPath")
val keyStoreFilePath = call.argument<String>("keyStoreFilePath")
val keystorePassword = call.argument<String>("keystorePassword")
if (
inFilePath != null &&
outFilePath != null &&
selectedPatches != null &&
options != null &&
tmpDirPath != null &&
keyStoreFilePath != null &&
keystorePassword != null
) {
cancel = false
runPatcher(
result,
inFilePath,
outFilePath,
selectedPatches,
options,
tmpDirPath,
keyStoreFilePath,
keystorePassword
)
} else result.error(
"INVALID_ARGUMENTS",
"Invalid arguments",
"One or more arguments are missing"
)
}
"stopPatcher" -> {
cancel = true
stopResult = result
}
"getPatches" -> {
val patchBundleFilePath = call.argument<String>("patchBundleFilePath")!!
try {
val patchBundleFile = File(patchBundleFilePath)
patchBundleFile.setWritable(false)
patches = loadPatchesFromDex(
setOf(patchBundleFile),
optimizedDexDirectory = codeCacheDir
)
} catch (t: Throwable) {
return@setMethodCallHandler result.error(
"PATCH_BUNDLE_ERROR",
"Failed to load patch bundle",
t.stackTraceToString()
)
}
JSONArray().apply {
patches.forEach {
JSONObject().apply {
put("name", it.name)
put("description", it.description)
put("excluded", !it.use)
put("compatiblePackages", JSONArray().apply {
it.compatiblePackages?.forEach { (name, versions) ->
val compatiblePackageJson = JSONObject().apply {
put("name", name)
put(
"versions",
JSONArray().apply {
versions?.forEach { version ->
put(version)
}
})
}
put(compatiblePackageJson)
}
})
put("options", JSONArray().apply {
it.options.values.forEach { option ->
JSONObject().apply {
put("key", option.key)
put("title", option.title)
put("description", option.description)
put("required", option.required)
fun JSONObject.putValue(
value: Any?,
key: String = "value"
) = if (value is Array<*>) put(
key,
JSONArray().apply {
value.forEach { put(it) }
})
else put(key, value)
putValue(option.default)
option.values?.let { values ->
put(
"values",
JSONObject().apply {
values.forEach { (key, value) ->
putValue(value, key)
}
})
} ?: put("values", null)
put("type", option.type)
}.let(::put)
}
})
}.let(::put)
}
}.toString().let(result::success)
}
"installApk" -> {
val apkPath = call.argument<String>("apkPath")!!
PackageInstallerManager.result = result
installApk(apkPath)
}
"uninstallApp" -> {
val packageName = call.argument<String>("packageName")!!
uninstallApp(packageName)
PackageInstallerManager.result = result
}
else -> result.notImplemented()
}
}
}
private fun openBrowser(query: String?) {
val intent = Intent(Intent.ACTION_WEB_SEARCH).apply {
putExtra(SearchManager.QUERY, query)
}
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
}
}
private fun runPatcher(
result: MethodChannel.Result,
inFilePath: String,
outFilePath: String,
selectedPatches: List<String>,
options: Map<String, Map<String, Any>>,
tmpDirPath: String,
keyStoreFilePath: String,
keystorePassword: String
) {
val inFile = File(inFilePath)
// Necessary because the file is copied from a nonwriteable location.
inFile.setWritable(true)
inFile.setReadable(true)
val outFile = File(outFilePath)
val keyStoreFile = File(keyStoreFilePath)
val tmpDir = File(tmpDirPath)
Thread {
fun updateProgress(progress: Double, header: String, log: String) {
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to progress,
"header" to header,
"log" to log
)
)
}
}
fun postStop() = handler.post { stopResult!!.success(null) }
fun cancel(block: () -> Unit = {}): Boolean {
if (cancel) {
block()
postStop()
}
return cancel
}
// Setup logger
Logger.getLogger("").apply {
handlers.forEach { handler ->
handler.close()
removeHandler(handler)
}
object : java.util.logging.Handler() {
override fun publish(record: LogRecord) {
if (cancel) return
if (
record.loggerName?.startsWith("app.revanced") == true ||
// Logger in class brut.util.OS.
record.loggerName == ""
) updateProgress(-1.0, "", record.message)
}
override fun flush() = Unit
override fun close() = flush()
}.let(::addHandler)
}
try {
updateProgress(0.0, "Reading APK...", "Reading APK")
val patcher = Patcher(
PatcherConfig(
inFile,
tmpDir,
Aapt.binary(applicationContext).absolutePath,
tmpDir.path,
)
)
if (cancel(patcher::close)) return@Thread
updateProgress(0.02, "Loading patches...", "Loading patches")
val patches = patches.filter { patch ->
val isCompatible = patch.compatiblePackages?.any { (name, _) ->
name == patcher.context.packageMetadata.packageName
} ?: false
val compatibleOrUniversal =
isCompatible || patch.compatiblePackages.isNullOrEmpty()
compatibleOrUniversal && selectedPatches.any { it == patch.name }
}.onEach { patch ->
options[patch.name]?.forEach { (key, value) ->
patch.options[key] = value
}
}.toSet()
if (cancel(patcher::close)) return@Thread
updateProgress(0.05, "Executing...", "")
val patcherResult = patcher.use {
it += patches
runBlocking {
// Update the progress bar every time a patch is executed from 0.15 to 0.7
val totalPatchesCount = patches.size
val progressStep = 0.55 / totalPatchesCount
var progress = 0.05
patcher().collect(FlowCollector { patchResult: PatchResult ->
if (cancel(patcher::close)) return@FlowCollector
val msg = patchResult.exception?.let {
val writer = StringWriter()
it.printStackTrace(PrintWriter(writer))
"${patchResult.patch.name} failed: $writer"
} ?: run {
"${patchResult.patch.name} succeeded"
}
updateProgress(progress, "", msg)
progress += progressStep
})
}
if (cancel(patcher::close)) return@Thread
updateProgress(0.75, "Building...", "")
patcher.get()
}
if (cancel(patcher::close)) return@Thread
patcherResult.applyTo(inFile)
if (cancel(patcher::close)) return@Thread
ApkUtils.signApk(
inFile,
outFile,
"ReVanced",
ApkUtils.KeyStoreDetails(
keyStoreFile,
keystorePassword,
"alias",
keystorePassword
)
)
updateProgress(.85, "Patched", "Patched APK")
} catch (ex: Throwable) {
if (!cancel) {
val stack = ex.stackTraceToString()
updateProgress(
-100.0,
"Failed",
"An error occurred:\n$stack"
)
}
} finally {
inFile.delete()
tmpDir.deleteRecursively()
}
handler.post { result.success(null) }
}.start()
}
private fun installApk(apkPath: String) {
val packageInstaller: PackageInstaller = applicationContext.packageManager.packageInstaller
val sessionParams =
PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
val sessionId: Int = packageInstaller.createSession(sessionParams)
val session: PackageInstaller.Session = packageInstaller.openSession(sessionId)
session.use { activeSession ->
val sessionOutputStream = activeSession.openWrite(applicationContext.packageName, 0, -1)
sessionOutputStream.use { outputStream ->
val apkFile = File(apkPath)
apkFile.inputStream().use { inputStream ->
inputStream.copyTo(outputStream)
}
}
}
val receiverIntent = Intent(applicationContext, InstallerReceiver::class.java).apply {
action = "APP_INSTALL_ACTION"
}
val receiverPendingIntent = PendingIntent.getBroadcast(
context,
sessionId,
receiverIntent,
PackageInstallerManager.flags
)
session.commit(receiverPendingIntent.intentSender)
session.close()
}
private fun uninstallApp(packageName: String) {
val packageInstaller: PackageInstaller = applicationContext.packageManager.packageInstaller
val receiverIntent = Intent(applicationContext, UninstallerReceiver::class.java).apply {
action = "APP_UNINSTALL_ACTION"
}
val receiverPendingIntent =
PendingIntent.getBroadcast(context, 0, receiverIntent, PackageInstallerManager.flags)
packageInstaller.uninstall(packageName, receiverPendingIntent.intentSender)
}
object PackageInstallerManager {
var result: MethodChannel.Result? = null
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
}
}

View File

@@ -1,12 +0,0 @@
package app.revanced.manager.flutter.utils
import android.content.Context
import java.io.File
object Aapt {
fun binary(context: Context): File {
return File(context.applicationInfo.nativeLibraryDir).resolveAapt()
}
}
private fun File.resolveAapt() = resolve(list { _, f -> !File(f).isDirectory && f.contains("aapt") }!!.first())

View File

@@ -1,32 +0,0 @@
package app.revanced.manager.flutter.utils.packageInstaller
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import app.revanced.manager.flutter.MainActivity
class InstallerReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
if (confirmationIntent != null) {
context.startActivity(confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
}
}
else -> {
val packageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
val otherPackageName = intent.getStringExtra(PackageInstaller.EXTRA_OTHER_PACKAGE_NAME)
MainActivity.PackageInstallerManager.result!!.success(mapOf(
"status" to status,
"packageName" to packageName,
"message" to message,
"otherPackageName" to otherPackageName
))
}
}
}
}

View File

@@ -1,24 +0,0 @@
package app.revanced.manager.flutter.utils.packageInstaller
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import app.revanced.manager.flutter.MainActivity
class UninstallerReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
if (confirmationIntent != null) {
context.startActivity(confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
}
}
else -> {
MainActivity.PackageInstallerManager.result!!.success(status)
}
}
}
}

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@drawable/ic_notification" />

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_launcher_round</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_launcher_round</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#1B1B1B</color>
</resources>

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="cache" path="." />
</paths>

View File

@@ -1,7 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -1,40 +0,0 @@
import com.android.build.api.dsl.CommonExtension
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
allprojects {
repositories {
google()
mavenCentral()
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/revanced/registry")
credentials {
username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
password = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
}
}
}
}
layout.buildDirectory = File("../build")
project(":screenshot_callback") {
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = "17"
}
}
}
subprojects {
afterEvaluate {
extensions.findByName("android")?.let {
it as CommonExtension<*, *, *, *, *, *>
if (it.compileSdk != null && it.compileSdk!! < 31)
it.compileSdk = 34
}
}
layout.buildDirectory = rootProject.layout.buildDirectory.file(name).get().asFile
evaluationDependsOn(":app")
}

View File

@@ -1,7 +0,0 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
org.gradle.parallel=true
org.gradle.daemon=true
org.gradle.caching=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false

View File

@@ -1,9 +0,0 @@
[versions]
revanced-patcher = "21.0.0"
revanced-library = "3.1.0"
desugar_jdk_libs = "2.1.4"
[libraries]
revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" }
revanced-library = { module = "app.revanced:revanced-library", version.ref = "revanced-library" }
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }

View File

@@ -1,24 +0,0 @@
pluginManagement {
val properties = java.util.Properties().apply {
load(file("local.properties").inputStream())
}
val flutterSdkPath = properties.getProperty("flutter.sdk")
assert(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.9.0" apply false
id("org.jetbrains.kotlin.android") version "2.1.10" apply false
}
include(":app")

182
api/api/api.api Normal file
View File

@@ -0,0 +1,182 @@
public abstract interface class app/revanced/manager/plugin/downloader/BaseDownloadScope : app/revanced/manager/plugin/downloader/Scope {
}
public final class app/revanced/manager/plugin/downloader/ConstantsKt {
public static final field PLUGIN_HOST_PERMISSION Ljava/lang/String;
}
public final class app/revanced/manager/plugin/downloader/DownloadUrl : android/os/Parcelable {
public static final field $stable I
public static final field CREATOR Landroid/os/Parcelable$Creator;
public fun <init> (Ljava/lang/String;Ljava/util/Map;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/util/Map;
public final fun copy (Ljava/lang/String;Ljava/util/Map;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
public static synthetic fun copy$default (Lapp/revanced/manager/plugin/downloader/DownloadUrl;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
public final fun describeContents ()I
public fun equals (Ljava/lang/Object;)Z
public final fun getHeaders ()Ljava/util/Map;
public final fun getUrl ()Ljava/lang/String;
public fun hashCode ()I
public final fun toDownloadResult ()Lkotlin/Pair;
public fun toString ()Ljava/lang/String;
public final fun writeToParcel (Landroid/os/Parcel;I)V
}
public final class app/revanced/manager/plugin/downloader/DownloadUrl$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/DownloadUrl;
public synthetic fun newArray (I)[Ljava/lang/Object;
}
public final class app/revanced/manager/plugin/downloader/Downloader {
public static final field $stable I
}
public final class app/revanced/manager/plugin/downloader/DownloaderBuilder {
public static final field $stable I
}
public final class app/revanced/manager/plugin/downloader/DownloaderKt {
public static final fun Downloader (Lkotlin/jvm/functions/Function1;)Lapp/revanced/manager/plugin/downloader/DownloaderBuilder;
}
public final class app/revanced/manager/plugin/downloader/DownloaderScope : app/revanced/manager/plugin/downloader/Scope {
public static final field $stable I
public final fun download (Lkotlin/jvm/functions/Function3;)V
public final fun get (Lkotlin/jvm/functions/Function4;)V
public fun getHostPackageName ()Ljava/lang/String;
public fun getPluginPackageName ()Ljava/lang/String;
public final fun useService (Landroid/content/Intent;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class app/revanced/manager/plugin/downloader/ExtensionsKt {
public static final fun download (Lapp/revanced/manager/plugin/downloader/DownloaderScope;Lkotlin/jvm/functions/Function4;)V
}
public abstract interface class app/revanced/manager/plugin/downloader/GetScope : app/revanced/manager/plugin/downloader/Scope {
public abstract fun requestStartActivity (Landroid/content/Intent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public abstract interface class app/revanced/manager/plugin/downloader/InputDownloadScope : app/revanced/manager/plugin/downloader/BaseDownloadScope {
}
public abstract interface class app/revanced/manager/plugin/downloader/OutputDownloadScope : app/revanced/manager/plugin/downloader/BaseDownloadScope {
public abstract fun reportSize (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class app/revanced/manager/plugin/downloader/Package : android/os/Parcelable {
public static final field $stable I
public static final field CREATOR Landroid/os/Parcelable$Creator;
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/manager/plugin/downloader/Package;
public static synthetic fun copy$default (Lapp/revanced/manager/plugin/downloader/Package;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lapp/revanced/manager/plugin/downloader/Package;
public final fun describeContents ()I
public fun equals (Ljava/lang/Object;)Z
public final fun getName ()Ljava/lang/String;
public final fun getVersion ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public final fun writeToParcel (Landroid/os/Parcel;I)V
}
public final class app/revanced/manager/plugin/downloader/Package$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/Package;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/Package;
public synthetic fun newArray (I)[Ljava/lang/Object;
}
public abstract interface annotation class app/revanced/manager/plugin/downloader/PluginHostApi : java/lang/annotation/Annotation {
}
public abstract interface class app/revanced/manager/plugin/downloader/Scope {
public abstract fun getHostPackageName ()Ljava/lang/String;
public abstract fun getPluginPackageName ()Ljava/lang/String;
}
public abstract class app/revanced/manager/plugin/downloader/UserInteractionException : java/lang/Exception {
public static final field $stable I
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public abstract class app/revanced/manager/plugin/downloader/UserInteractionException$Activity : app/revanced/manager/plugin/downloader/UserInteractionException {
public static final field $stable I
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$Cancelled : app/revanced/manager/plugin/downloader/UserInteractionException$Activity {
public static final field $stable I
}
public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$NotCompleted : app/revanced/manager/plugin/downloader/UserInteractionException$Activity {
public static final field $stable I
public final fun getIntent ()Landroid/content/Intent;
public final fun getResultCode ()I
}
public final class app/revanced/manager/plugin/downloader/UserInteractionException$RequestDenied : app/revanced/manager/plugin/downloader/UserInteractionException {
public static final field $stable I
}
public final class app/revanced/manager/plugin/downloader/webview/APIKt {
public static final fun WebViewDownloader (Lkotlin/jvm/functions/Function4;)Lapp/revanced/manager/plugin/downloader/DownloaderBuilder;
public static final fun runWebView (Lapp/revanced/manager/plugin/downloader/GetScope;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public class app/revanced/manager/plugin/downloader/webview/IWebView$Default : app/revanced/manager/plugin/downloader/webview/IWebView {
public fun <init> ()V
public fun asBinder ()Landroid/os/IBinder;
public fun finish ()V
public fun load (Ljava/lang/String;)V
}
public abstract class app/revanced/manager/plugin/downloader/webview/IWebView$Stub : android/os/Binder, app/revanced/manager/plugin/downloader/webview/IWebView {
public fun <init> ()V
public fun asBinder ()Landroid/os/IBinder;
public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/plugin/downloader/webview/IWebView;
public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
}
public class app/revanced/manager/plugin/downloader/webview/IWebViewEvents$Default : app/revanced/manager/plugin/downloader/webview/IWebViewEvents {
public fun <init> ()V
public fun asBinder ()Landroid/os/IBinder;
public fun download (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public fun pageLoad (Ljava/lang/String;)V
public fun ready (Lapp/revanced/manager/plugin/downloader/webview/IWebView;)V
}
public abstract class app/revanced/manager/plugin/downloader/webview/IWebViewEvents$Stub : android/os/Binder, app/revanced/manager/plugin/downloader/webview/IWebViewEvents {
public fun <init> ()V
public fun asBinder ()Landroid/os/IBinder;
public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/plugin/downloader/webview/IWebViewEvents;
public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
}
public final class app/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters;
public synthetic fun newArray (I)[Ljava/lang/Object;
}
public abstract interface class app/revanced/manager/plugin/downloader/webview/WebViewCallbackScope : app/revanced/manager/plugin/downloader/Scope {
public abstract fun finish (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun load (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class app/revanced/manager/plugin/downloader/webview/WebViewScope : app/revanced/manager/plugin/downloader/Scope {
public static final field $stable I
public final fun download (Lkotlin/jvm/functions/Function5;)V
public fun getHostPackageName ()Ljava/lang/String;
public fun getPluginPackageName ()Ljava/lang/String;
public final fun pageLoad (Lkotlin/jvm/functions/Function3;)V
}

151
api/build.gradle.kts Normal file
View File

@@ -0,0 +1,151 @@
import java.io.IOException
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.binary.compatibility.validator)
`maven-publish`
signing
}
group = "app.revanced"
dependencies {
implementation(libs.androidx.ktx)
implementation(libs.runtime.ktx)
implementation(libs.activity.compose)
implementation(libs.appcompat)
}
fun String.runCommand(): String {
val process = ProcessBuilder(split("\\s".toRegex()))
.redirectErrorStream(true)
.directory(rootDir)
.start()
val output = StringBuilder()
val reader = process.inputStream.bufferedReader()
val thread = Thread {
reader.forEachLine {
output.appendLine(it)
}
}
thread.start()
if (!process.waitFor(10, TimeUnit.SECONDS)) {
process.destroy()
throw IOException("Command timed out: $this")
}
thread.join()
return output.toString().trim()
}
val projectPath: String = projectDir.relativeTo(rootDir).path
val lastTag = "git describe --tags --abbrev=0".runCommand()
val hasChangesInThisModule = "git diff --name-only $lastTag..HEAD".runCommand().lineSequence().any {
it.startsWith(projectPath)
}
tasks.matching { it.name.startsWith("publish") }.configureEach {
onlyIf {
hasChangesInThisModule
}
}
android {
namespace = "app.revanced.manager.plugin.downloader"
compileSdk = 35
buildToolsVersion = "35.0.1"
defaultConfig {
minSdk = 26
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
aidl = true
}
}
apiValidation {
nonPublicMarkers += "app.revanced.manager.plugin.downloader.PluginHostApi"
}
publishing {
repositories {
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/revanced/revanced-manager")
credentials {
username = System.getenv("GITHUB_ACTOR") ?: extra["gpr.user"] as String?
password = System.getenv("GITHUB_TOKEN") ?: extra["gpr.key"] as String?
}
}
}
publications {
create<MavenPublication>("Api") {
afterEvaluate {
from(components["release"])
}
groupId = "app.revanced"
artifactId = "revanced-manager-api"
version = project.version.toString()
pom {
name = "ReVanced Manager API"
description = "API for ReVanced Manager."
url = "https://revanced.app"
licenses {
license {
name = "GNU General Public License v3.0"
url = "https://www.gnu.org/licenses/gpl-3.0.en.html"
}
}
developers {
developer {
id = "ReVanced"
name = "ReVanced"
email = "contact@revanced.app"
}
}
scm {
connection = "scm:git:git://github.com/revanced/revanced-manager.git"
developerConnection = "scm:git:git@github.com:revanced/revanced-manager.git"
url = "https://github.com/revanced/revanced-manager"
}
}
}
}
}
signing {
useGpgCmd()
sign(publishing.publications["Api"])
}

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@@ -0,0 +1,8 @@
// IWebView.aidl
package app.revanced.manager.plugin.downloader.webview;
@JavaPassthrough(annotation="@app.revanced.manager.plugin.downloader.PluginHostApi")
oneway interface IWebView {
void load(String url);
void finish();
}

View File

@@ -0,0 +1,11 @@
// IWebViewEvents.aidl
package app.revanced.manager.plugin.downloader.webview;
import app.revanced.manager.plugin.downloader.webview.IWebView;
@JavaPassthrough(annotation="@app.revanced.manager.plugin.downloader.PluginHostApi")
oneway interface IWebViewEvents {
void ready(IWebView iface);
void pageLoad(String url);
void download(String url, String mimetype, String userAgent);
}

View File

@@ -0,0 +1,7 @@
package app.revanced.manager.plugin.downloader
/**
* The permission ID of the special plugin host permission. Only ReVanced Manager will have this permission.
* Plugin UI activities and internal services can be protected using this permission.
*/
const val PLUGIN_HOST_PERMISSION = "app.revanced.manager.permission.PLUGIN_HOST"

View File

@@ -0,0 +1,165 @@
package app.revanced.manager.plugin.downloader
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.app.Activity
import android.os.Parcelable
import kotlinx.coroutines.withTimeout
import java.io.InputStream
import java.io.OutputStream
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@RequiresOptIn(
level = RequiresOptIn.Level.ERROR,
message = "This API is only intended for plugin hosts, don't use it in a plugin.",
)
@Retention(AnnotationRetention.BINARY)
annotation class PluginHostApi
/**
* The base interface for all DSL scopes.
*/
interface Scope {
/**
* The package name of ReVanced Manager.
*/
val hostPackageName: String
/**
* The package name of the plugin.
*/
val pluginPackageName: String
}
/**
* The scope of [DownloaderScope.get].
*/
interface GetScope : Scope {
/**
* Ask the user to perform some required interaction in the activity specified by the provided [Intent].
* This function returns normally with the resulting [Intent] when the activity finishes with code [Activity.RESULT_OK].
*
* @throws UserInteractionException.RequestDenied User decided to skip this plugin.
* @throws UserInteractionException.Activity.Cancelled The activity was cancelled.
* @throws UserInteractionException.Activity.NotCompleted The activity finished with an unknown result code.
*/
suspend fun requestStartActivity(intent: Intent): Intent?
}
interface BaseDownloadScope : Scope
/**
* The scope for [DownloaderScope.download].
*/
interface InputDownloadScope : BaseDownloadScope
typealias Size = Long
typealias DownloadResult = Pair<InputStream, Size?>
typealias Version = String
typealias GetResult<T> = Pair<T, Version?>
class DownloaderScope<T : Parcelable> internal constructor(
private val scopeImpl: Scope,
internal val context: Context
) : Scope by scopeImpl {
// Returning an InputStream is the primary way for plugins to implement the download function, but we also want to offer an OutputStream API since using InputStream might not be convenient in all cases.
// It is much easier to implement the main InputStream API on top of OutputStreams compared to doing it the other way around, which is why we are using OutputStream here. This detail is not visible to plugins.
internal var download: (suspend OutputDownloadScope.(T, OutputStream) -> Unit)? = null
internal var get: (suspend GetScope.(String, String?) -> GetResult<T>?)? = null
private val inputDownloadScopeImpl = object : InputDownloadScope, Scope by scopeImpl {}
/**
* Define the download block of the plugin.
*/
fun download(block: suspend InputDownloadScope.(data: T) -> DownloadResult) {
download = { app, outputStream ->
val (inputStream, size) = inputDownloadScopeImpl.block(app)
inputStream.use {
if (size != null) reportSize(size)
it.copyTo(outputStream)
}
}
}
/**
* Define the get block of the plugin.
* The block should return null if the app cannot be found. The version in the result must match the version argument unless it is null.
*/
fun get(block: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?) {
get = block
}
/**
* Utilize the service specified by the provided [Intent]. The service will be unbound when the scope ends.
*/
suspend fun <R : Any?> useService(intent: Intent, block: suspend (IBinder) -> R): R {
var onBind: ((IBinder) -> Unit)? = null
val serviceConn = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) =
onBind!!(service!!)
override fun onServiceDisconnected(name: ComponentName?) {}
}
return try {
val binder = withTimeout(10000L) {
suspendCoroutine { continuation ->
onBind = continuation::resume
context.bindService(intent, serviceConn, Context.BIND_AUTO_CREATE)
}
}
block(binder)
} finally {
onBind = null
context.unbindService(serviceConn)
}
}
}
class DownloaderBuilder<T : Parcelable> internal constructor(private val block: DownloaderScope<T>.() -> Unit) {
@PluginHostApi
fun build(scopeImpl: Scope, context: Context) =
with(DownloaderScope<T>(scopeImpl, context)) {
block()
Downloader(
download = download!!,
get = get!!
)
}
}
class Downloader<T : Parcelable> internal constructor(
@property:PluginHostApi val get: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?,
@property:PluginHostApi val download: suspend OutputDownloadScope.(data: T, outputStream: OutputStream) -> Unit
)
/**
* Define a downloader plugin.
*/
fun <T : Parcelable> Downloader(block: DownloaderScope<T>.() -> Unit) = DownloaderBuilder(block)
/**
* @see GetScope.requestStartActivity
*/
sealed class UserInteractionException(message: String) : Exception(message) {
class RequestDenied @PluginHostApi constructor() :
UserInteractionException("Request denied by user")
sealed class Activity(message: String) : UserInteractionException(message) {
class Cancelled @PluginHostApi constructor() : Activity("Interaction cancelled")
/**
* @param resultCode The result code of the activity.
* @param intent The [Intent] of the activity.
*/
class NotCompleted @PluginHostApi constructor(val resultCode: Int, val intent: Intent?) :
Activity("Unexpected activity result code: $resultCode")
}
}

View File

@@ -0,0 +1,42 @@
package app.revanced.manager.plugin.downloader
import android.app.Activity
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.os.Parcelable
import java.io.OutputStream
/**
* The scope of the [OutputStream] version of [DownloaderScope.download].
*/
interface OutputDownloadScope : BaseDownloadScope {
suspend fun reportSize(size: Long)
}
/**
* A replacement for [DownloaderScope.download] that uses [OutputStream].
* The provided [OutputStream] does not need to be closed manually.
*/
fun <T : Parcelable> DownloaderScope<T>.download(block: suspend OutputDownloadScope.(T, OutputStream) -> Unit) {
download = block
}
/**
* Performs [GetScope.requestStartActivity] with an [Intent] created using the type information of [ACTIVITY].
* @see [GetScope.requestStartActivity]
*/
suspend inline fun <reified ACTIVITY : Activity> GetScope.requestStartActivity() =
requestStartActivity(
Intent().apply { setClassName(pluginPackageName, ACTIVITY::class.qualifiedName!!) }
)
/**
* Performs [DownloaderScope.useService] with an [Intent] created using the type information of [SERVICE].
* @see [DownloaderScope.useService]
*/
suspend inline fun <reified SERVICE : Service, R : Any?> DownloaderScope<*>.useService(
noinline block: suspend (IBinder) -> R
) = useService(
Intent().apply { setClassName(pluginPackageName, SERVICE::class.qualifiedName!!) }, block
)

View File

@@ -0,0 +1,39 @@
package app.revanced.manager.plugin.downloader
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import java.net.HttpURLConnection
import java.net.URI
/**
* A simple parcelable data class for storing a package name and version.
* This can be used as the data type for plugins that only need a name and version to implement their [DownloaderScope.download] function.
*
* @param name The package name.
* @param version The version.
*/
@Parcelize
data class Package(val name: String, val version: String) : Parcelable
/**
* A data class for storing a download URL.
*
* @param url The download URL.
* @param headers The headers to use for the request.
*/
@Parcelize
data class DownloadUrl(val url: String, val headers: Map<String, String> = emptyMap()) : Parcelable {
/**
* Converts this into a [DownloadResult].
*/
fun toDownloadResult(): DownloadResult = with(URI.create(url).toURL().openConnection() as HttpURLConnection) {
useCaches = false
allowUserInteraction = false
headers.forEach(::setRequestProperty)
connectTimeout = 10_000
connect()
inputStream to getHeaderField("Content-Length").toLong()
}
}

View File

@@ -0,0 +1,176 @@
package app.revanced.manager.plugin.downloader.webview
import android.content.Intent
import app.revanced.manager.plugin.downloader.DownloadUrl
import app.revanced.manager.plugin.downloader.DownloaderScope
import app.revanced.manager.plugin.downloader.GetScope
import app.revanced.manager.plugin.downloader.Scope
import app.revanced.manager.plugin.downloader.Downloader
import app.revanced.manager.plugin.downloader.PluginHostApi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.withContext
import kotlin.properties.Delegates
typealias InitialUrl = String
typealias PageLoadCallback<T> = suspend WebViewCallbackScope<T>.(url: String) -> Unit
typealias DownloadCallback<T> = suspend WebViewCallbackScope<T>.(url: String, mimeType: String, userAgent: String) -> Unit
interface WebViewCallbackScope<T> : Scope {
/**
* Finishes the activity and returns the [result].
*/
suspend fun finish(result: T)
/**
* Tells the WebView to load the specified [url].
*/
suspend fun load(url: String)
}
@OptIn(PluginHostApi::class)
class WebViewScope<T> internal constructor(
coroutineScope: CoroutineScope,
private val scopeImpl: Scope,
setResult: (T) -> Unit
) : Scope by scopeImpl {
private var onPageLoadCallback: PageLoadCallback<T> = {}
private var onDownloadCallback: DownloadCallback<T> = { _, _, _ -> }
@OptIn(ExperimentalCoroutinesApi::class)
private val dispatcher = Dispatchers.Default.limitedParallelism(1)
private lateinit var webView: IWebView
internal lateinit var initialUrl: String
internal val binder = object : IWebViewEvents.Stub() {
override fun ready(iface: IWebView?) {
coroutineScope.launch(dispatcher) {
webView = iface!!.also {
it.load(initialUrl)
}
}
}
override fun pageLoad(url: String?) {
coroutineScope.launch(dispatcher) { onPageLoadCallback(callbackScope, url!!) }
}
override fun download(url: String?, mimetype: String?, userAgent: String?) {
coroutineScope.launch(dispatcher) {
onDownloadCallback(
callbackScope,
url!!,
mimetype!!,
userAgent!!
)
}
}
}
private val callbackScope = object : WebViewCallbackScope<T>, Scope by scopeImpl {
override suspend fun finish(result: T) {
setResult(result)
// Tell the WebViewActivity to finish
webView.let { withContext(Dispatchers.IO) { it.finish() } }
}
override suspend fun load(url: String) {
webView.let { withContext(Dispatchers.IO) { it.load(url) } }
}
}
/**
* Called when the WebView attempts to download a file to disk.
*/
fun download(block: DownloadCallback<T>) {
onDownloadCallback = block
}
/**
* Called when the WebView finishes loading a page.
*/
fun pageLoad(block: PageLoadCallback<T>) {
onPageLoadCallback = block
}
}
@JvmInline
private value class Container<U>(val value: U)
/**
* Run a [android.webkit.WebView] Activity controlled by the provided code block.
* The activity will keep running until it is cancelled or an event handler calls [WebViewCallbackScope.finish].
* The [block] defines the event handlers and returns the initial URL.
*
* @param title The string displayed in the action bar.
* @param block The control block.
*/
@OptIn(PluginHostApi::class)
suspend fun <T> GetScope.runWebView(
title: String,
block: suspend WebViewScope<T>.() -> InitialUrl
) = supervisorScope {
var result by Delegates.notNull<Container<T>>()
val scope = WebViewScope<T>(this@supervisorScope, this@runWebView) { result = Container(it) }
scope.initialUrl = scope.block()
// Start the webview activity and wait until it finishes.
requestStartActivity(Intent().apply {
putExtra(
WebViewActivity.KEY,
WebViewActivity.Parameters(title, scope.binder)
)
setClassName(
hostPackageName,
WebViewActivity::class.qualifiedName!!
)
})
// Return the result and cancel any leftover coroutines.
coroutineContext.cancelChildren()
result.value
}
/**
* Implement a downloader using [runWebView] and [DownloadUrl]. This function will automatically define a handler for download events unlike [runWebView].
* Returning null inside the [block] is equivalent to returning null inside [DownloaderScope.get].
*
* @see runWebView
*/
fun WebViewDownloader(block: suspend WebViewScope<DownloadUrl>.(packageName: String, version: String?) -> InitialUrl?) =
Downloader<DownloadUrl> {
val label = context.applicationInfo.loadLabel(
context.packageManager
).toString()
get { packageName, version ->
class ReturnNull : Exception()
try {
runWebView(label) {
download { url, _, userAgent ->
finish(
DownloadUrl(
url,
mapOf("User-Agent" to userAgent)
)
)
}
block(this@runWebView, packageName, version) ?: throw ReturnNull()
} to version
} catch (_: ReturnNull) {
null
}
}
download {
it.toDownloadResult()
}
}

View File

@@ -0,0 +1,161 @@
package app.revanced.manager.plugin.downloader.webview
import android.annotation.SuppressLint
import android.os.Bundle
import android.os.IBinder
import android.os.Parcelable
import android.view.MenuItem
import android.webkit.CookieManager
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.ComponentActivity
import androidx.activity.addCallback
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewModelScope
import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.plugin.downloader.R
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@OptIn(PluginHostApi::class)
@PluginHostApi
class WebViewActivity : ComponentActivity() {
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val vm by viewModels<WebViewModel>()
enableEdgeToEdge()
setContentView(R.layout.activity_webview)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
val webView = findViewById<WebView>(R.id.webview)
onBackPressedDispatcher.addCallback {
if (webView.canGoBack()) webView.goBack()
else cancelActivity()
}
val params = intent.getParcelableExtra<Parameters>(KEY)!!
actionBar?.apply {
title = params.title
setHomeAsUpIndicator(android.R.drawable.ic_menu_close_clear_cancel)
setDisplayHomeAsUpEnabled(true)
}
val events = IWebViewEvents.Stub.asInterface(params.events)!!
vm.setup(events)
webView.apply {
settings.apply {
cacheMode = WebSettings.LOAD_NO_CACHE
allowContentAccess = false
domStorageEnabled = true
javaScriptEnabled = true
}
webViewClient = vm.webViewClient
setDownloadListener { url, userAgent, _, mimetype, _ ->
vm.onDownload(url, mimetype, userAgent)
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
vm.commands.collect {
when (it) {
is WebViewModel.Command.Finish -> {
setResult(RESULT_OK)
finish()
}
is WebViewModel.Command.Load -> webView.loadUrl(it.url)
}
}
}
}
}
private fun cancelActivity() {
setResult(RESULT_CANCELED)
finish()
}
override fun onOptionsItemSelected(item: MenuItem) = if (item.itemId == android.R.id.home) {
cancelActivity()
true
} else super.onOptionsItemSelected(item)
@Parcelize
internal class Parameters(
val title: String, val events: IBinder
) : Parcelable
internal companion object {
const val KEY = "params"
}
}
@OptIn(PluginHostApi::class)
internal class WebViewModel : ViewModel() {
init {
CookieManager.getInstance().apply {
removeAllCookies(null)
setAcceptCookie(true)
}
}
private val commandChannel = Channel<Command>()
val commands = commandChannel.receiveAsFlow()
private var eventBinder: IWebViewEvents? = null
private val ctrlBinder = object : IWebView.Stub() {
override fun load(url: String?) {
viewModelScope.launch {
commandChannel.send(Command.Load(url!!))
}
}
override fun finish() {
viewModelScope.launch {
commandChannel.send(Command.Finish)
}
}
}
val webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
eventBinder!!.pageLoad(url)
}
}
fun onDownload(url: String, mimeType: String, userAgent: String) {
eventBinder!!.download(url, mimeType, userAgent)
}
fun setup(binder: IWebViewEvents) {
if (eventBinder != null) return
eventBinder = binder
binder.ready(ctrlBinder)
}
sealed interface Command {
data class Load(val url: String) : Command
data object Finish : Command
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/main">
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@@ -0,0 +1 @@
<resources></resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.WebViewActivity" parent="Theme.AppCompat.DayNight">
<item name="android:windowActionBar">true</item>
<item name="android:windowNoTitle">false</item>
</style>
</resources>

1
app/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

252
app/build.gradle.kts Normal file
View File

@@ -0,0 +1,252 @@
import kotlin.random.Random
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.devtools)
alias(libs.plugins.about.libraries)
signing
}
val outputApkFileName = "${rootProject.name}-$version.apk"
dependencies {
// AndroidX Core
implementation(libs.androidx.ktx)
implementation(libs.runtime.ktx)
implementation(libs.runtime.compose)
implementation(libs.splash.screen)
implementation(libs.activity.compose)
implementation(libs.work.runtime.ktx)
implementation(libs.preferences.datastore)
implementation(libs.appcompat)
// Compose
implementation(platform(libs.compose.bom))
implementation(libs.compose.ui)
implementation(libs.compose.ui.preview)
implementation(libs.compose.ui.tooling)
implementation(libs.compose.livedata)
implementation(libs.compose.material.icons.extended)
implementation(libs.compose.material3)
implementation(libs.navigation.compose)
// Accompanist
implementation(libs.accompanist.drawablepainter)
// Placeholder
implementation(libs.placeholder.material3)
// Coil (async image loading, network image)
implementation(libs.coil.compose)
implementation(libs.coil.appiconloader)
// KotlinX
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.collection.immutable)
implementation(libs.kotlinx.datetime)
// Room
implementation(libs.room.runtime)
implementation(libs.room.ktx)
annotationProcessor(libs.room.compiler)
ksp(libs.room.compiler)
// ReVanced
implementation(libs.revanced.patcher)
implementation(libs.revanced.library)
// Downloader plugins
implementation(project(":api"))
// Native processes
implementation(libs.kotlin.process)
// HiddenAPI
compileOnly(libs.hidden.api.stub)
// LibSU
implementation(libs.libsu.core)
implementation(libs.libsu.service)
implementation(libs.libsu.nio)
// Koin
implementation(libs.koin.android)
implementation(libs.koin.compose)
implementation(libs.koin.compose.navigation)
implementation(libs.koin.workmanager)
// Licenses
implementation(libs.about.libraries)
// Ktor
implementation(libs.ktor.core)
implementation(libs.ktor.logging)
implementation(libs.ktor.okhttp)
implementation(libs.ktor.content.negotiation)
implementation(libs.ktor.serialization)
// Markdown
implementation(libs.markdown.renderer)
// Fading Edges
implementation(libs.fading.edges)
// Scrollbars
implementation(libs.scrollbars)
// EnumUtil
implementation(libs.enumutil)
ksp(libs.enumutil.ksp)
// Reorderable lists
implementation(libs.reorderable)
// Compose Icons
implementation(libs.compose.icons.fontawesome)
}
android {
namespace = "app.revanced.manager"
compileSdk = 35
buildToolsVersion = "35.0.1"
defaultConfig {
applicationId = "app.revanced.manager"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "0.0.1"
vectorDrawables.useSupportLibrary = true
}
buildTypes {
debug {
applicationIdSuffix = ".debug"
resValue("string", "app_name", "ReVanced Manager (Debug)")
isPseudoLocalesEnabled = true
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
}
release {
if (!project.hasProperty("noProguard")) {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
val keystoreFile = file("keystore.jks")
if (project.hasProperty("signAsDebug") || !keystoreFile.exists()) {
applicationIdSuffix = ".debug_signed"
resValue("string", "app_name", "ReVanced Manager (Debug signed)")
signingConfig = signingConfigs.getByName("debug")
isPseudoLocalesEnabled = true
} else {
signingConfig = signingConfigs.create("release") {
storeFile = keystoreFile
storePassword = System.getenv("KEYSTORE_PASSWORD")
keyAlias = System.getenv("KEYSTORE_ENTRY_ALIAS")
keyPassword = System.getenv("KEYSTORE_ENTRY_PASSWORD")
}
}
buildConfigField("long", "BUILD_ID", "0L")
}
}
applicationVariants.all {
outputs.all {
this as com.android.build.gradle.internal.api.ApkVariantOutputImpl
outputFileName = outputApkFileName
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
dependenciesInfo {
includeInApk = false
includeInBundle = false
}
packaging {
resources.excludes.addAll(
listOf(
"/prebuilt/**",
"META-INF/DEPENDENCIES",
"META-INF/**.version",
"DebugProbesKt.bin",
"kotlin-tooling-metadata.json",
"org/bouncycastle/pqc/**.properties",
"org/bouncycastle/x509/**.properties",
)
)
jniLibs {
useLegacyPackaging = true
}
}
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
aidl = true
buildConfig = true
}
android {
androidResources {
generateLocaleConfig = true
}
}
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
version = "3.22.1"
}
}
}
kotlin {
jvmToolchain(17)
}
tasks {
// Needed by gradle-semantic-release-plugin.
// Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435.
val publish by registering {
group = "publishing"
description = "Build the release APK"
dependsOn("assembleRelease")
val apk = project.layout.buildDirectory.file("outputs/apk/release/${outputApkFileName}")
val ascFile = apk.map { it.asFile.resolveSibling("${it.asFile.name}.asc") }
inputs.file(apk).withPropertyName("inputApk")
outputs.file(ascFile).withPropertyName("outputAsc")
doLast {
signing {
useGpgCmd()
sign(apk.get().asFile)
}
}
}
}

63
app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,63 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
-dontobfuscate
# Required for serialization to work properly
-if @kotlinx.serialization.Serializable class **
-keepclassmembers class <1> {
static <1>$Companion Companion;
}
-if @kotlinx.serialization.Serializable class ** {
static **$* *;
}
-keepclassmembers class <2>$<3> {
kotlinx.serialization.KSerializer serializer(...);
}
-if @kotlinx.serialization.Serializable class ** {
public static ** INSTANCE;
}
-keepclassmembers class <1> {
public static <1> INSTANCE;
kotlinx.serialization.KSerializer serializer(...);
}
# This required for the process runtime.
-keep class app.revanced.manager.patcher.runtime.process.* {
*;
}
# Required for the patcher to function correctly
-keep class app.revanced.patcher.** {
*;
}
-keep class brut.** {
*;
}
-keep class org.xmlpull.** {
*;
}
-keep class kotlin.** {
*;
}
-keep class org.jf.** {
*;
}
-keep class com.android.** {
*;
}
-keep class app.revanced.manager.plugin.** {
*;
}
-dontwarn com.google.auto.value.**
-dontwarn java.awt.**
-dontwarn javax.**
-dontwarn org.slf4j.**
-dontwarn it.skrape.fetcher.*
-dontwarn com.google.j2objc.annotations.*
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault

View File

@@ -0,0 +1,429 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "d0119047505da435972c5247181de675",
"entities": [
{
"tableName": "patch_bundles",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `name` TEXT NOT NULL, `version` TEXT, `source` TEXT NOT NULL, `auto_update` INTEGER NOT NULL, PRIMARY KEY(`uid`))",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "version",
"columnName": "version",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "source",
"columnName": "source",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "autoUpdate",
"columnName": "auto_update",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"uid"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "patch_selections",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `patch_bundle` INTEGER NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`uid`), FOREIGN KEY(`patch_bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "patchBundle",
"columnName": "patch_bundle",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"uid"
]
},
"indices": [
{
"name": "index_patch_selections_patch_bundle_package_name",
"unique": true,
"columnNames": [
"patch_bundle",
"package_name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_patch_selections_patch_bundle_package_name` ON `${TABLE_NAME}` (`patch_bundle`, `package_name`)"
}
],
"foreignKeys": [
{
"table": "patch_bundles",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"patch_bundle"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "selected_patches",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`selection` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`selection`, `patch_name`), FOREIGN KEY(`selection`) REFERENCES `patch_selections`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "selection",
"columnName": "selection",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "patchName",
"columnName": "patch_name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"selection",
"patch_name"
]
},
"indices": [],
"foreignKeys": [
{
"table": "patch_selections",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"selection"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "downloaded_app",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `directory` TEXT NOT NULL, `last_used` INTEGER NOT NULL, PRIMARY KEY(`package_name`, `version`))",
"fields": [
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "version",
"columnName": "version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "directory",
"columnName": "directory",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastUsed",
"columnName": "last_used",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"package_name",
"version"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "installed_app",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`current_package_name` TEXT NOT NULL, `original_package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `install_type` TEXT NOT NULL, PRIMARY KEY(`current_package_name`))",
"fields": [
{
"fieldPath": "currentPackageName",
"columnName": "current_package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "originalPackageName",
"columnName": "original_package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "version",
"columnName": "version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "installType",
"columnName": "install_type",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"current_package_name"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "applied_patch",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `bundle` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`package_name`, `bundle`, `patch_name`), FOREIGN KEY(`package_name`) REFERENCES `installed_app`(`current_package_name`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "bundle",
"columnName": "bundle",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "patchName",
"columnName": "patch_name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"package_name",
"bundle",
"patch_name"
]
},
"indices": [
{
"name": "index_applied_patch_bundle",
"unique": false,
"columnNames": [
"bundle"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_applied_patch_bundle` ON `${TABLE_NAME}` (`bundle`)"
}
],
"foreignKeys": [
{
"table": "installed_app",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"package_name"
],
"referencedColumns": [
"current_package_name"
]
},
{
"table": "patch_bundles",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"bundle"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "option_groups",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `patch_bundle` INTEGER NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`uid`), FOREIGN KEY(`patch_bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "patchBundle",
"columnName": "patch_bundle",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"uid"
]
},
"indices": [
{
"name": "index_option_groups_patch_bundle_package_name",
"unique": true,
"columnNames": [
"patch_bundle",
"package_name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_option_groups_patch_bundle_package_name` ON `${TABLE_NAME}` (`patch_bundle`, `package_name`)"
}
],
"foreignKeys": [
{
"table": "patch_bundles",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"patch_bundle"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "options",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, `key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`group`, `patch_name`, `key`), FOREIGN KEY(`group`) REFERENCES `option_groups`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "group",
"columnName": "group",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "patchName",
"columnName": "patch_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"group",
"patch_name",
"key"
]
},
"indices": [],
"foreignKeys": [
{
"table": "option_groups",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"group"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "trusted_downloader_plugins",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `signature` BLOB NOT NULL, PRIMARY KEY(`package_name`))",
"fields": [
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "signature",
"columnName": "signature",
"affinity": "BLOB",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"package_name"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd0119047505da435972c5247181de675')"
]
}
}

View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<permission
android:name="app.revanced.manager.permission.PLUGIN_HOST"
android:protectionLevel="signature"
android:label="@string/plugin_host_permission_label"
android:description="@string/plugin_host_permission_description"
/>
<uses-permission android:name="app.revanced.manager.permission.PLUGIN_HOST" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ENFORCE_UPDATE_OWNERSHIP" />
<application
android:name=".ManagerApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:largeHeap="true"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.ReVancedManager"
android:enableOnBackInvokedCallback="true"
tools:targetApi="34">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.ReVancedManager">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".plugin.downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" />
<service android:name=".service.InstallService" />
<service android:name=".service.UninstallService" />
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="specialUse"
android:exported="false"
tools:node="merge">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="patching"
/>
</service>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
</application>
</manifest>

View File

@@ -0,0 +1,8 @@
// IRootService.aidl
package app.revanced.manager;
// Declare any non-default types here with import statements
interface IRootSystemService {
IBinder getFileSystemService();
}

View File

@@ -0,0 +1,11 @@
// IPatcherEvents.aidl
package app.revanced.manager.patcher.runtime.process;
// Interface for sending events back to the main app process.
oneway interface IPatcherEvents {
void log(String level, String msg);
void patchSucceeded();
void progress(String name, String state, String msg);
// The patching process has ended. The exceptionStackTrace is null if it finished successfully.
void finished(String exceptionStackTrace);
}

View File

@@ -0,0 +1,14 @@
// IPatcherProcess.aidl
package app.revanced.manager.patcher.runtime.process;
import app.revanced.manager.patcher.runtime.process.Parameters;
import app.revanced.manager.patcher.runtime.process.IPatcherEvents;
interface IPatcherProcess {
// Returns BuildConfig.BUILD_ID, which is used to ensure the main app and runner process are running the same code.
long buildId();
// Makes the patcher process exit with code 0
oneway void exit();
// Starts patching.
oneway void start(in Parameters parameters, IPatcherEvents events);
}

View File

@@ -0,0 +1,4 @@
// Parameters.aidl
package app.revanced.manager.patcher.runtime.process;
parcelable Parameters;

View File

@@ -0,0 +1,6 @@
id=__PKG_NAME__-ReVanced
name=__LABEL__ ReVanced
version=__VERSION__
versionCode=0
author=ReVanced
description=Mounts the patched APK on top of the original one

View File

@@ -0,0 +1,40 @@
#!/system/bin/sh
DIR=${0%/*}
package_name="__PKG_NAME__"
version="__VERSION__"
rm "$DIR/log"
{
until [ "$(getprop sys.boot_completed)" = 1 ]; do sleep 5; done
sleep 5
base_path="$DIR/$package_name.apk"
stock_path="$(pm path "$package_name" | grep base | sed 's/package://g')"
stock_version="$(dumpsys package "$package_name" | grep versionName | cut -d "=" -f2)"
echo "base_path: $base_path"
echo "stock_path: $stock_path"
echo "base_version: $version"
echo "stock_version: $stock_version"
if mount | grep -q "$stock_path" ; then
echo "Not mounting as stock path is already mounted"
exit 1
fi
if [ "$version" != "$stock_version" ]; then
echo "Not mounting as versions don't match"
exit 1
fi
if [ -z "$stock_path" ]; then
echo "Not mounting as app info could not be loaded"
exit 1
fi
mount -o bind "$base_path" "$stock_path"
} >> "$DIR/log"

View File

@@ -0,0 +1,38 @@
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html.
# For more examples on how to use CMake, see https://github.com/android/ndk-samples.
# Sets the minimum CMake version required for this project.
cmake_minimum_required(VERSION 3.22.1)
# Declares the project name. The project name can be accessed via ${ PROJECT_NAME},
# Since this is the top level CMakeLists.txt, the project name is also accessible
# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
# build script scope).
project("prop_override")
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
#
# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
# is preferred for the same purpose.
#
# In order to load a library into your app from Java/Kotlin, you must call
# System.loadLibrary() and pass the name of the library defined here;
# for GameActivity/NativeActivity derived applications, the same library name must be
# used in the AndroidManifest.xml file.
add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
prop_override.cpp)
# Specifies libraries CMake should link to your target library. You
# can link libraries from various origins, such as libraries defined in this
# build script, prebuilt third-party libraries, or Android system libraries.
target_link_libraries(${CMAKE_PROJECT_NAME}
# List libraries link to the target library
android
log)

View File

@@ -0,0 +1,62 @@
// Library for overriding Android system properties via environment variables.
//
// Usage: LD_PRELOAD=prop_override.so PROP_dalvik.vm.heapsize=123M getprop dalvik.vm.heapsize
// Output: 123M
#include <string>
#include <cstring>
#include <cstdlib>
#include <dlfcn.h>
// Source: https://android.googlesource.com/platform/system/core/+/100b08a848d018eeb1caa5d5e7c7c2aaac65da15/libcutils/include/cutils/properties.h
#define PROP_VALUE_MAX 92
// This is the mangled name of "android::base::GetProperty".
#define GET_PROPERTY_MANGLED_NAME "_ZN7android4base11GetPropertyERKNSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEES9_"
extern "C" typedef int (*property_get_ptr)(const char *, char *, const char *);
typedef std::string (*GetProperty_ptr)(const std::string &, const std::string &);
char *GetPropOverride(const std::string &key) {
auto envKey = "PROP_" + key;
return getenv(envKey.c_str());
}
// See: https://android.googlesource.com/platform/system/core/+/100b08a848d018eeb1caa5d5e7c7c2aaac65da15/libcutils/properties.cpp
extern "C" int property_get(const char *key, char *value, const char *default_value) {
auto replacement = GetPropOverride(std::string(key));
if (replacement) {
int len = strnlen(replacement, PROP_VALUE_MAX);
strncpy(value, replacement, len);
return len;
}
static property_get_ptr original = NULL;
if (!original) {
// Get the address of the original function.
original = reinterpret_cast<property_get_ptr>(dlsym(RTLD_NEXT, "property_get"));
}
return original(key, value, default_value);
}
// Defining android::base::GetProperty ourselves won't work because std::string has a slightly different "path" in the NDK version of the C++ standard library.
// We can get around this by forcing the function to adopt a specific name using the asm keyword.
std::string GetProperty(const std::string &, const std::string &) asm(GET_PROPERTY_MANGLED_NAME);
// See: https://android.googlesource.com/platform/system/libbase/+/1a34bb67c4f3ba0a1ea6f4f20ac9fe117ba4fe64/properties.cpp
// This isn't used for the properties we want to override, but property_get is deprecated so that could change in the future.
std::string GetProperty(const std::string &key, const std::string &default_value) {
auto replacement = GetPropOverride(key);
if (replacement) {
return std::string(replacement);
}
static GetProperty_ptr original = NULL;
if (!original) {
original = reinterpret_cast<GetProperty_ptr>(dlsym(RTLD_NEXT, GET_PROPERTY_MANGLED_NAME));
}
return original(key, default_value);
}

View File

@@ -0,0 +1,336 @@
package app.revanced.manager
import android.content.ActivityNotFoundException
import android.os.Bundle
import android.os.Parcelable
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import app.revanced.manager.ui.model.navigation.AppSelector
import app.revanced.manager.ui.model.navigation.ComplexParameter
import app.revanced.manager.ui.model.navigation.Dashboard
import app.revanced.manager.ui.model.navigation.InstalledApplicationInfo
import app.revanced.manager.ui.model.navigation.Patcher
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
import app.revanced.manager.ui.model.navigation.Settings
import app.revanced.manager.ui.model.navigation.Update
import app.revanced.manager.ui.screen.AppSelectorScreen
import app.revanced.manager.ui.screen.DashboardScreen
import app.revanced.manager.ui.screen.InstalledAppInfoScreen
import app.revanced.manager.ui.screen.PatcherScreen
import app.revanced.manager.ui.screen.PatchesSelectorScreen
import app.revanced.manager.ui.screen.RequiredOptionsScreen
import app.revanced.manager.ui.screen.SelectedAppInfoScreen
import app.revanced.manager.ui.screen.SettingsScreen
import app.revanced.manager.ui.screen.UpdateScreen
import app.revanced.manager.ui.screen.settings.AboutSettingsScreen
import app.revanced.manager.ui.screen.settings.AdvancedSettingsScreen
import app.revanced.manager.ui.screen.settings.ContributorSettingsScreen
import app.revanced.manager.ui.screen.settings.DeveloperSettingsScreen
import app.revanced.manager.ui.screen.settings.DownloadsSettingsScreen
import app.revanced.manager.ui.screen.settings.GeneralSettingsScreen
import app.revanced.manager.ui.screen.settings.ImportExportSettingsScreen
import app.revanced.manager.ui.screen.settings.LicensesSettingsScreen
import app.revanced.manager.ui.screen.settings.update.ChangelogsSettingsScreen
import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen
import app.revanced.manager.ui.theme.ReVancedManagerTheme
import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.ui.viewmodel.MainViewModel
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
import app.revanced.manager.util.EventEffect
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
import org.koin.androidx.compose.navigation.koinNavViewModel
import org.koin.core.parameter.parametersOf
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
class MainActivity : ComponentActivity() {
@ExperimentalAnimationApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
enableEdgeToEdge()
installSplashScreen()
val vm: MainViewModel = getActivityViewModel()
setContent {
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
onResult = vm::applyLegacySettings
)
val theme by vm.prefs.theme.getAsState()
val dynamicColor by vm.prefs.dynamicColor.getAsState()
EventEffect(vm.legacyImportActivityFlow) {
try {
launcher.launch(it)
} catch (_: ActivityNotFoundException) {
}
}
ReVancedManagerTheme(
darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK,
dynamicColor = dynamicColor
) {
ReVancedManager(vm)
}
}
}
}
@Composable
private fun ReVancedManager(vm: MainViewModel) {
val navController = rememberNavController()
EventEffect(vm.appSelectFlow) { app ->
navController.navigateComplex(
SelectedApplicationInfo,
SelectedApplicationInfo.ViewModelParams(app)
)
}
NavHost(
navController = navController,
startDestination = Dashboard,
enterTransition = { slideInHorizontally(initialOffsetX = { it }) },
exitTransition = { slideOutHorizontally(targetOffsetX = { -it / 3 }) },
popEnterTransition = { slideInHorizontally(initialOffsetX = { -it / 3 }) },
popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) },
) {
composable<Dashboard> {
DashboardScreen(
onSettingsClick = { navController.navigate(Settings) },
onAppSelectorClick = {
navController.navigate(AppSelector)
},
onUpdateClick = {
navController.navigate(Update())
},
onDownloaderPluginClick = {
navController.navigate(Settings.Downloads)
},
onAppClick = { packageName ->
navController.navigate(InstalledApplicationInfo(packageName))
}
)
}
composable<InstalledApplicationInfo> {
val data = it.toRoute<InstalledApplicationInfo>()
InstalledAppInfoScreen(
onPatchClick = vm::selectApp,
onBackClick = navController::popBackStack,
viewModel = koinViewModel { parametersOf(data.packageName) }
)
}
composable<AppSelector> {
AppSelectorScreen(
onSelect = vm::selectApp,
onStorageSelect = vm::selectApp,
onBackClick = navController::popBackStack
)
}
composable<Patcher> {
PatcherScreen(
onBackClick = {
navController.navigate(route = Dashboard) {
launchSingleTop = true
popUpTo<Dashboard> {
inclusive = false
}
}
},
viewModel = koinViewModel { parametersOf(it.getComplexArg<Patcher.ViewModelParams>()) }
)
}
composable<Update> {
val data = it.toRoute<Update>()
UpdateScreen(
onBackClick = navController::popBackStack,
vm = koinViewModel { parametersOf(data.downloadOnScreenEntry) }
)
}
navigation<SelectedApplicationInfo>(startDestination = SelectedApplicationInfo.Main) {
composable<SelectedApplicationInfo.Main> {
val parentBackStackEntry = navController.navGraphEntry(it)
val data =
parentBackStackEntry.getComplexArg<SelectedApplicationInfo.ViewModelParams>()
val viewModel =
koinNavViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
parametersOf(data)
}
SelectedAppInfoScreen(
onBackClick = navController::popBackStack,
onPatchClick = {
it.lifecycleScope.launch {
navController.navigateComplex(
Patcher,
viewModel.getPatcherParams()
)
}
},
onPatchSelectorClick = { app, patches, options ->
navController.navigateComplex(
SelectedApplicationInfo.PatchesSelector,
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
app,
patches,
options
)
)
},
onRequiredOptions = { app, patches, options ->
navController.navigateComplex(
SelectedApplicationInfo.RequiredOptions,
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
app,
patches,
options
)
)
},
vm = viewModel
)
}
composable<SelectedApplicationInfo.PatchesSelector> {
val data =
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
viewModelStoreOwner = navController.navGraphEntry(it)
)
PatchesSelectorScreen(
onBackClick = navController::popBackStack,
onSave = { patches, options ->
selectedAppInfoVm.updateConfiguration(patches, options)
navController.popBackStack()
},
viewModel = koinViewModel { parametersOf(data) }
)
}
composable<SelectedApplicationInfo.RequiredOptions> {
val data =
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
viewModelStoreOwner = navController.navGraphEntry(it)
)
RequiredOptionsScreen(
onBackClick = navController::popBackStack,
onContinue = { patches, options ->
selectedAppInfoVm.updateConfiguration(patches, options)
it.lifecycleScope.launch {
navController.navigateComplex(
Patcher,
selectedAppInfoVm.getPatcherParams()
)
}
},
vm = koinViewModel { parametersOf(data) }
)
}
}
navigation<Settings>(startDestination = Settings.Main) {
composable<Settings.Main> {
SettingsScreen(
onBackClick = navController::popBackStack,
navigate = navController::navigate
)
}
composable<Settings.General> {
GeneralSettingsScreen(onBackClick = navController::popBackStack)
}
composable<Settings.Advanced> {
AdvancedSettingsScreen(onBackClick = navController::popBackStack)
}
composable<Settings.Developer> {
DeveloperSettingsScreen(onBackClick = navController::popBackStack)
}
composable<Settings.Updates> {
UpdatesSettingsScreen(
onBackClick = navController::popBackStack,
onChangelogClick = { navController.navigate(Settings.Changelogs) },
onUpdateClick = { navController.navigate(Update()) }
)
}
composable<Settings.Downloads> {
DownloadsSettingsScreen(onBackClick = navController::popBackStack)
}
composable<Settings.ImportExport> {
ImportExportSettingsScreen(onBackClick = navController::popBackStack)
}
composable<Settings.About> {
AboutSettingsScreen(
onBackClick = navController::popBackStack,
navigate = navController::navigate
)
}
composable<Settings.Changelogs> {
ChangelogsSettingsScreen(onBackClick = navController::popBackStack)
}
composable<Settings.Contributors> {
ContributorSettingsScreen(onBackClick = navController::popBackStack)
}
composable<Settings.Licenses> {
LicensesSettingsScreen(onBackClick = navController::popBackStack)
}
}
}
}
@Composable
private fun NavController.navGraphEntry(entry: NavBackStackEntry) =
remember(entry) { getBackStackEntry(entry.destination.parent!!.id) }
// Androidx Navigation does not support storing complex types in route objects, so we have to store them inside the saved state handle of the back stack entry instead.
private fun <T : Parcelable, R : ComplexParameter<T>> NavController.navigateComplex(
route: R,
data: T
) {
navigate(route)
getBackStackEntry(route).savedStateHandle["args"] = data
}
private fun <T : Parcelable> NavBackStackEntry.getComplexArg() = savedStateHandle.get<T>("args")!!

View File

@@ -0,0 +1,110 @@
package app.revanced.manager
import android.app.Activity
import android.app.Application
import android.os.Bundle
import android.util.Log
import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.di.*
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.DownloaderPluginRepository
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.util.tag
import kotlinx.coroutines.Dispatchers
import coil.Coil
import coil.ImageLoader
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.internal.BuilderImpl
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
import org.koin.android.ext.android.inject
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.androidx.workmanager.koin.workManagerFactory
import org.koin.core.context.startKoin
class ManagerApplication : Application() {
private val scope = MainScope()
private val prefs: PreferencesManager by inject()
private val patchBundleRepository: PatchBundleRepository by inject()
private val downloaderPluginRepository: DownloaderPluginRepository by inject()
private val fs: Filesystem by inject()
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@ManagerApplication)
androidLogger()
workManagerFactory()
modules(
httpModule,
preferencesModule,
repositoryModule,
serviceModule,
managerModule,
workerModule,
viewModelModule,
databaseModule,
rootModule
)
}
val pixels = 512
Coil.setImageLoader(
ImageLoader.Builder(this)
.components {
add(AppIconKeyer())
add(AppIconFetcher.Factory(pixels, true, this@ManagerApplication))
}
.build()
)
val shellBuilder = BuilderImpl.create().setFlags(Shell.FLAG_MOUNT_MASTER)
Shell.setDefaultBuilder(shellBuilder)
scope.launch {
prefs.preload()
}
scope.launch(Dispatchers.Default) {
downloaderPluginRepository.reload()
}
scope.launch(Dispatchers.Default) {
with(patchBundleRepository) {
reload()
updateCheck()
}
}
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
private var firstActivityCreated = false
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
if (firstActivityCreated) return
firstActivityCreated = true
// We do not want to call onFreshProcessStart() if there is state to restore.
// This can happen on system-initiated process death.
if (savedInstanceState == null) {
Log.d(tag, "Fresh process created")
onFreshProcessStart()
} else Log.d(tag, "System-initiated process death detected")
}
override fun onActivityStarted(activity: Activity) {}
override fun onActivityResumed(activity: Activity) {}
override fun onActivityPaused(activity: Activity) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {}
})
}
private fun onFreshProcessStart() {
fs.uiTempDir.apply {
deleteRecursively()
mkdirs()
}
}
}

View File

@@ -0,0 +1,51 @@
package app.revanced.manager.data.platform
import android.Manifest
import android.app.Application
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Environment
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts
import app.revanced.manager.util.RequestManageStorageContract
import java.io.File
import java.nio.file.Path
class Filesystem(private val app: Application) {
val contentResolver = app.contentResolver // TODO: move Content Resolver operations to here.
/**
* A directory that gets cleared when the app restarts.
* Do not store paths to this directory in a parcel.
*/
val tempDir: File = app.getDir("ephemeral", Context.MODE_PRIVATE).apply {
deleteRecursively()
mkdirs()
}
/**
* A directory for storing temporary files related to UI.
* This is the same as [tempDir], but does not get cleared on system-initiated process death.
* Paths to this directory can be safely stored in parcels.
*/
val uiTempDir: File = app.getDir("ui_ephemeral", Context.MODE_PRIVATE)
fun externalFilesDir(): Path = Environment.getExternalStorageDirectory().toPath()
private fun usesManagePermission() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
private val storagePermissionName =
if (usesManagePermission()) Manifest.permission.MANAGE_EXTERNAL_STORAGE else Manifest.permission.READ_EXTERNAL_STORAGE
fun permissionContract(): Pair<ActivityResultContract<String, Boolean>, String> {
val contract =
if (usesManagePermission()) RequestManageStorageContract() else ActivityResultContracts.RequestPermission()
return contract to storagePermissionName
}
fun hasStoragePermission() =
if (usesManagePermission()) Environment.isExternalStorageManager() else app.checkSelfPermission(
storagePermissionName
) == PackageManager.PERMISSION_GRANTED
}

View File

@@ -0,0 +1,19 @@
package app.revanced.manager.data.platform
import android.app.Application
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import androidx.core.content.getSystemService
class NetworkInfo(app: Application) {
private val connectivityManager = app.getSystemService<ConnectivityManager>()!!
private fun getCapabilities() = connectivityManager.activeNetwork?.let { connectivityManager.getNetworkCapabilities(it) }
fun isConnected() = connectivityManager.activeNetwork != null
fun isUnmetered() = getCapabilities()?.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) ?: true
/**
* Returns true if it is safe to download large files.
*/
fun isSafe() = isConnected() && isUnmetered()
}

View File

@@ -0,0 +1,74 @@
package app.revanced.manager.data.redux
import android.util.Log
import app.revanced.manager.util.tag
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeoutOrNull
// This file implements React Redux-like state management.
class Store<S>(private val coroutineScope: CoroutineScope, initialState: S) : ActionContext {
private val _state = MutableStateFlow(initialState)
val state = _state.asStateFlow()
// Do not touch these without the lock.
private var isRunningActions = false
private val queueChannel = Channel<Action<S>>(capacity = 10)
private val lock = Mutex()
suspend fun dispatch(action: Action<S>) = lock.withLock {
Log.d(tag, "Dispatching $action")
queueChannel.send(action)
if (isRunningActions) return@withLock
isRunningActions = true
coroutineScope.launch {
runActions()
}
}
@OptIn(ExperimentalCoroutinesApi::class)
private suspend fun runActions() {
while (true) {
val action = withTimeoutOrNull(200L) { queueChannel.receive() }
if (action == null) {
Log.d(tag, "Stopping action runner")
lock.withLock {
// New actions may be dispatched during the timeout.
isRunningActions = !queueChannel.isEmpty
if (!isRunningActions) return
}
continue
}
Log.d(tag, "Running $action")
_state.value = try {
with(action) { this@Store.execute(_state.value) }
} catch (c: CancellationException) {
// This is done without the lock, but cancellation usually means the store is no longer needed.
isRunningActions = false
throw c
} catch (e: Exception) {
action.catch(e)
continue
}
}
}
}
interface ActionContext
interface Action<S> {
suspend fun ActionContext.execute(current: S): S
suspend fun catch(exception: Exception) {
Log.e(tag, "Got exception while executing $this", exception)
}
}

View File

@@ -0,0 +1,39 @@
package app.revanced.manager.data.room
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import app.revanced.manager.data.room.apps.downloaded.DownloadedAppDao
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
import app.revanced.manager.data.room.apps.installed.AppliedPatch
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.data.room.apps.installed.InstalledAppDao
import app.revanced.manager.data.room.selection.PatchSelection
import app.revanced.manager.data.room.selection.SelectedPatch
import app.revanced.manager.data.room.selection.SelectionDao
import app.revanced.manager.data.room.bundles.PatchBundleDao
import app.revanced.manager.data.room.bundles.PatchBundleEntity
import app.revanced.manager.data.room.options.Option
import app.revanced.manager.data.room.options.OptionDao
import app.revanced.manager.data.room.options.OptionGroup
import app.revanced.manager.data.room.plugins.TrustedDownloaderPlugin
import app.revanced.manager.data.room.plugins.TrustedDownloaderPluginDao
import kotlin.random.Random
@Database(
entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, OptionGroup::class, Option::class, TrustedDownloaderPlugin::class],
version = 1
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun patchBundleDao(): PatchBundleDao
abstract fun selectionDao(): SelectionDao
abstract fun downloadedAppDao(): DownloadedAppDao
abstract fun installedAppDao(): InstalledAppDao
abstract fun optionDao(): OptionDao
abstract fun trustedDownloaderPluginDao(): TrustedDownloaderPluginDao
companion object {
fun generateUid() = Random.Default.nextInt()
}
}

View File

@@ -0,0 +1,26 @@
package app.revanced.manager.data.room
import androidx.room.TypeConverter
import app.revanced.manager.data.room.bundles.Source
import app.revanced.manager.data.room.options.Option.SerializedValue
import java.io.File
class Converters {
@TypeConverter
fun sourceFromString(value: String) = Source.from(value)
@TypeConverter
fun sourceToString(value: Source) = value.toString()
@TypeConverter
fun fileFromString(value: String) = File(value)
@TypeConverter
fun fileToString(file: File): String = file.path
@TypeConverter
fun serializedOptionFromString(value: String) = SerializedValue.fromJsonString(value)
@TypeConverter
fun serializedOptionToString(value: SerializedValue) = value.toJsonString()
}

View File

@@ -0,0 +1,16 @@
package app.revanced.manager.data.room.apps.downloaded
import androidx.room.ColumnInfo
import androidx.room.Entity
import java.io.File
@Entity(
tableName = "downloaded_app",
primaryKeys = ["package_name", "version"]
)
data class DownloadedApp(
@ColumnInfo(name = "package_name") val packageName: String,
@ColumnInfo(name = "version") val version: String,
@ColumnInfo(name = "directory") val directory: File,
@ColumnInfo(name = "last_used") val lastUsed: Long = System.currentTimeMillis()
)

View File

@@ -0,0 +1,26 @@
package app.revanced.manager.data.room.apps.downloaded
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow
@Dao
interface DownloadedAppDao {
@Query("SELECT * FROM downloaded_app")
fun getAllApps(): Flow<List<DownloadedApp>>
@Query("SELECT * FROM downloaded_app WHERE package_name = :packageName AND version = :version")
suspend fun get(packageName: String, version: String): DownloadedApp?
@Upsert
suspend fun upsert(downloadedApp: DownloadedApp)
@Query("UPDATE downloaded_app SET last_used = :newValue WHERE package_name = :packageName AND version = :version")
suspend fun markUsed(packageName: String, version: String, newValue: Long = System.currentTimeMillis())
@Delete
suspend fun delete(downloadedApps: Collection<DownloadedApp>)
}

View File

@@ -0,0 +1,35 @@
package app.revanced.manager.data.room.apps.installed
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import app.revanced.manager.data.room.bundles.PatchBundleEntity
import kotlinx.parcelize.Parcelize
@Parcelize
@Entity(
tableName = "applied_patch",
primaryKeys = ["package_name", "bundle", "patch_name"],
foreignKeys = [
ForeignKey(
InstalledApp::class,
parentColumns = ["current_package_name"],
childColumns = ["package_name"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
PatchBundleEntity::class,
parentColumns = ["uid"],
childColumns = ["bundle"],
onDelete = ForeignKey.CASCADE
)
],
indices = [Index(value = ["bundle"], unique = false)]
)
data class AppliedPatch(
@ColumnInfo(name = "package_name") val packageName: String,
@ColumnInfo(name = "bundle") val bundle: Int,
@ColumnInfo(name = "patch_name") val patchName: String
) : Parcelable

View File

@@ -0,0 +1,20 @@
package app.revanced.manager.data.room.apps.installed
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import app.revanced.manager.R
enum class InstallType(val stringResource: Int) {
DEFAULT(R.string.default_install),
MOUNT(R.string.mount_install)
}
@Entity(tableName = "installed_app")
data class InstalledApp(
@PrimaryKey
@ColumnInfo(name = "current_package_name") val currentPackageName: String,
@ColumnInfo(name = "original_package_name") val originalPackageName: String,
@ColumnInfo(name = "version") val version: String,
@ColumnInfo(name = "install_type") val installType: InstallType
)

View File

@@ -0,0 +1,46 @@
package app.revanced.manager.data.room.apps.installed
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.MapColumn
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow
@Dao
interface InstalledAppDao {
@Query("SELECT * FROM installed_app")
fun getAll(): Flow<List<InstalledApp>>
@Query("SELECT * FROM installed_app WHERE current_package_name = :packageName")
suspend fun get(packageName: String): InstalledApp?
@Query(
"SELECT bundle, patch_name FROM applied_patch" +
" WHERE package_name = :packageName"
)
suspend fun getPatchesSelection(packageName: String): Map<@MapColumn("bundle") Int, List<@MapColumn(
"patch_name"
) String>>
@Transaction
suspend fun upsertApp(installedApp: InstalledApp, appliedPatches: List<AppliedPatch>) {
upsertApp(installedApp)
deleteAppliedPatches(installedApp.currentPackageName)
insertAppliedPatches(appliedPatches)
}
@Upsert
suspend fun upsertApp(installedApp: InstalledApp)
@Insert
suspend fun insertAppliedPatches(appliedPatches: List<AppliedPatch>)
@Query("DELETE FROM applied_patch WHERE package_name = :packageName")
suspend fun deleteAppliedPatches(packageName: String)
@Delete
suspend fun delete(installedApp: InstalledApp)
}

View File

@@ -0,0 +1,30 @@
package app.revanced.manager.data.room.bundles
import androidx.room.*
@Dao
interface PatchBundleDao {
@Query("SELECT * FROM patch_bundles")
suspend fun all(): List<PatchBundleEntity>
@Query("UPDATE patch_bundles SET version = :patches WHERE uid = :uid")
suspend fun updateVersionHash(uid: Int, patches: String?)
@Query("DELETE FROM patch_bundles WHERE uid != 0")
suspend fun purgeCustomBundles()
@Transaction
suspend fun reset() {
purgeCustomBundles()
updateVersionHash(0, null) // Reset the main source
}
@Query("DELETE FROM patch_bundles WHERE uid = :uid")
suspend fun remove(uid: Int)
@Query("SELECT name, version, auto_update, source FROM patch_bundles WHERE uid = :uid")
suspend fun getProps(uid: Int): PatchBundleProperties?
@Upsert
suspend fun upsert(source: PatchBundleEntity)
}

View File

@@ -0,0 +1,46 @@
package app.revanced.manager.data.room.bundles
import androidx.room.*
import io.ktor.http.*
sealed class Source {
object Local : Source() {
const val SENTINEL = "local"
override fun toString() = SENTINEL
}
object API : Source() {
const val SENTINEL = "api"
override fun toString() = SENTINEL
}
data class Remote(val url: Url) : Source() {
override fun toString() = url.toString()
}
companion object {
fun from(value: String) = when (value) {
Local.SENTINEL -> Local
API.SENTINEL -> API
else -> Remote(Url(value))
}
}
}
@Entity(tableName = "patch_bundles")
data class PatchBundleEntity(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "version") val versionHash: String? = null,
@ColumnInfo(name = "source") val source: Source,
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
)
data class PatchBundleProperties(
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "version") val versionHash: String? = null,
@ColumnInfo(name = "source") val source: Source,
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
)

View File

@@ -0,0 +1,116 @@
package app.revanced.manager.data.room.options
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import app.revanced.manager.patcher.patch.Option
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.add
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.float
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.typeOf
@Entity(
tableName = "options",
primaryKeys = ["group", "patch_name", "key"],
foreignKeys = [ForeignKey(
OptionGroup::class,
parentColumns = ["uid"],
childColumns = ["group"],
onDelete = ForeignKey.CASCADE
)]
)
data class Option(
@ColumnInfo(name = "group") val group: Int,
@ColumnInfo(name = "patch_name") val patchName: String,
@ColumnInfo(name = "key") val key: String,
// Encoded as Json.
@ColumnInfo(name = "value") val value: SerializedValue,
) {
@Serializable
data class SerializedValue(val raw: JsonElement) {
fun toJsonString() = json.encodeToString(raw)
fun deserializeFor(option: Option<*>): Any? {
if (raw is JsonNull) return null
val errorMessage = "Cannot deserialize value as ${option.type}"
try {
if (option.type.classifier == List::class) {
val elementType = option.type.arguments.first().type!!
return raw.jsonArray.map { deserializeBasicType(elementType, it.jsonPrimitive) }
}
return deserializeBasicType(option.type, raw.jsonPrimitive)
} catch (e: IllegalArgumentException) {
throw SerializationException(errorMessage, e)
} catch (e: IllegalStateException) {
throw SerializationException(errorMessage, e)
} catch (e: kotlinx.serialization.SerializationException) {
throw SerializationException(errorMessage, e)
}
}
companion object {
private val json = Json {
// Patcher does not forbid the use of these values, so we should support them.
allowSpecialFloatingPointValues = true
}
private fun deserializeBasicType(type: KType, value: JsonPrimitive) = when (type) {
typeOf<Boolean>() -> value.boolean
typeOf<Int>() -> value.int
typeOf<Long>() -> value.long
typeOf<Float>() -> value.float
typeOf<String>() -> value.content.also {
if (!value.isString) throw SerializationException(
"Expected value to be a string: $value"
)
}
else -> throw SerializationException("Unknown type: $type")
}
fun fromJsonString(value: String) = SerializedValue(json.decodeFromString(value))
fun fromValue(value: Any?) = SerializedValue(when (value) {
null -> JsonNull
is Number -> JsonPrimitive(value)
is Boolean -> JsonPrimitive(value)
is String -> JsonPrimitive(value)
is List<*> -> buildJsonArray {
var elementClass: KClass<out Any>? = null
value.forEach {
when (it) {
null -> throw SerializationException("List elements must not be null")
is Number -> add(it)
is Boolean -> add(it)
is String -> add(it)
else -> throw SerializationException("Unknown element type: ${it::class.simpleName}")
}
if (elementClass == null) elementClass = it::class
else if (elementClass != it::class) throw SerializationException("List elements must have the same type")
}
}
else -> throw SerializationException("Unknown type: ${value::class.simpleName}")
})
}
}
class SerializationException(message: String, cause: Throwable? = null) :
Exception(message, cause)
}

View File

@@ -0,0 +1,50 @@
package app.revanced.manager.data.room.options
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.MapColumn
import androidx.room.Query
import androidx.room.Transaction
import kotlinx.coroutines.flow.Flow
@Dao
abstract class OptionDao {
@Transaction
@Query(
"SELECT patch_bundle, `group`, patch_name, `key`, value FROM option_groups" +
" LEFT JOIN options ON uid = options.`group`" +
" WHERE package_name = :packageName"
)
abstract suspend fun getOptions(packageName: String): Map<@MapColumn("patch_bundle") Int, List<Option>>
@Query("SELECT uid FROM option_groups WHERE patch_bundle = :bundleUid AND package_name = :packageName")
abstract suspend fun getGroupId(bundleUid: Int, packageName: String): Int?
@Query("SELECT package_name FROM option_groups")
abstract fun getPackagesWithOptions(): Flow<List<String>>
@Insert
abstract suspend fun createOptionGroup(group: OptionGroup)
@Query("DELETE FROM option_groups WHERE patch_bundle = :uid")
abstract suspend fun resetOptionsForPatchBundle(uid: Int)
@Query("DELETE FROM option_groups WHERE package_name = :packageName")
abstract suspend fun resetOptionsForPackage(packageName: String)
@Query("DELETE FROM option_groups")
abstract suspend fun reset()
@Insert
protected abstract suspend fun insertOptions(patches: List<Option>)
@Query("DELETE FROM options WHERE `group` = :groupId")
protected abstract suspend fun clearGroup(groupId: Int)
@Transaction
open suspend fun updateOptions(options: Map<Int, List<Option>>) =
options.forEach { (groupId, options) ->
clearGroup(groupId)
insertOptions(options)
}
}

View File

@@ -0,0 +1,24 @@
package app.revanced.manager.data.room.options
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import app.revanced.manager.data.room.bundles.PatchBundleEntity
@Entity(
tableName = "option_groups",
foreignKeys = [ForeignKey(
PatchBundleEntity::class,
parentColumns = ["uid"],
childColumns = ["patch_bundle"],
onDelete = ForeignKey.CASCADE
)],
indices = [Index(value = ["patch_bundle", "package_name"], unique = true)]
)
data class OptionGroup(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "patch_bundle") val patchBundle: Int,
@ColumnInfo(name = "package_name") val packageName: String
)

View File

@@ -0,0 +1,11 @@
package app.revanced.manager.data.room.plugins
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "trusted_downloader_plugins")
class TrustedDownloaderPlugin(
@PrimaryKey @ColumnInfo(name = "package_name") val packageName: String,
@ColumnInfo(name = "signature") val signature: ByteArray
)

View File

@@ -0,0 +1,22 @@
package app.revanced.manager.data.room.plugins
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Upsert
@Dao
interface TrustedDownloaderPluginDao {
@Query("SELECT signature FROM trusted_downloader_plugins WHERE package_name = :packageName")
suspend fun getTrustedSignature(packageName: String): ByteArray?
@Upsert
suspend fun upsertTrust(plugin: TrustedDownloaderPlugin)
@Query("DELETE FROM trusted_downloader_plugins WHERE package_name = :packageName")
suspend fun remove(packageName: String)
@Transaction
@Query("DELETE FROM trusted_downloader_plugins WHERE package_name IN (:packageNames)")
suspend fun removeAll(packageNames: Set<String>)
}

View File

@@ -0,0 +1,24 @@
package app.revanced.manager.data.room.selection
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import app.revanced.manager.data.room.bundles.PatchBundleEntity
@Entity(
tableName = "patch_selections",
foreignKeys = [ForeignKey(
PatchBundleEntity::class,
parentColumns = ["uid"],
childColumns = ["patch_bundle"],
onDelete = ForeignKey.CASCADE
)],
indices = [Index(value = ["patch_bundle", "package_name"], unique = true)]
)
data class PatchSelection(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "patch_bundle") val patchBundle: Int,
@ColumnInfo(name = "package_name") val packageName: String
)

View File

@@ -0,0 +1,20 @@
package app.revanced.manager.data.room.selection
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
@Entity(
tableName = "selected_patches",
primaryKeys = ["selection", "patch_name"],
foreignKeys = [ForeignKey(
PatchSelection::class,
parentColumns = ["uid"],
childColumns = ["selection"],
onDelete = ForeignKey.CASCADE
)]
)
data class SelectedPatch(
@ColumnInfo(name = "selection") val selection: Int,
@ColumnInfo(name = "patch_name") val patchName: String
)

View File

@@ -0,0 +1,62 @@
package app.revanced.manager.data.room.selection
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.MapColumn
import androidx.room.Query
import androidx.room.Transaction
import kotlinx.coroutines.flow.Flow
@Dao
abstract class SelectionDao {
@Transaction
@Query(
"SELECT patch_bundle, patch_name FROM patch_selections" +
" LEFT JOIN selected_patches ON uid = selected_patches.selection" +
" WHERE package_name = :packageName"
)
abstract suspend fun getSelectedPatches(packageName: String): Map<@MapColumn("patch_bundle") Int, List<@MapColumn(
"patch_name"
) String>>
@Transaction
@Query(
"SELECT package_name, patch_name FROM patch_selections" +
" LEFT JOIN selected_patches ON uid = selected_patches.selection" +
" WHERE patch_bundle = :bundleUid"
)
abstract suspend fun exportSelection(bundleUid: Int): Map<@MapColumn("package_name") String, List<@MapColumn(
"patch_name"
) String>>
@Query("SELECT uid FROM patch_selections WHERE patch_bundle = :bundleUid AND package_name = :packageName")
abstract suspend fun getSelectionId(bundleUid: Int, packageName: String): Int?
@Insert
abstract suspend fun createSelection(selection: PatchSelection)
@Query("SELECT package_name FROM patch_selections")
abstract fun getPackagesWithSelection(): Flow<List<String>>
@Query("DELETE FROM patch_selections WHERE patch_bundle = :uid")
abstract suspend fun resetForPatchBundle(uid: Int)
@Query("DELETE FROM patch_selections WHERE package_name = :packageName")
abstract suspend fun resetForPackage(packageName: String)
@Query("DELETE FROM patch_selections")
abstract suspend fun reset()
@Insert
protected abstract suspend fun selectPatches(patches: List<SelectedPatch>)
@Query("DELETE FROM selected_patches WHERE selection = :selectionId")
protected abstract suspend fun clearSelection(selectionId: Int)
@Transaction
open suspend fun updateSelections(selections: Map<Int, Set<String>>) =
selections.forEach { (selectionUid, patches) ->
clearSelection(selectionUid)
selectPatches(patches.map { SelectedPatch(selectionUid, it) })
}
}

View File

@@ -0,0 +1,15 @@
package app.revanced.manager.di
import android.content.Context
import androidx.room.Room
import app.revanced.manager.data.room.AppDatabase
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val databaseModule = module {
fun provideAppDatabase(context: Context) = Room.databaseBuilder(context, AppDatabase::class.java, "manager").build()
single {
provideAppDatabase(androidContext())
}
}

View File

@@ -0,0 +1,60 @@
package app.revanced.manager.di
import android.content.Context
import app.revanced.manager.BuildConfig
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.UserAgent
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
import okhttp3.Cache
import okhttp3.Dns
import org.koin.android.ext.koin.androidContext
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
import java.net.Inet4Address
import java.net.InetAddress
val httpModule = module {
fun provideHttpClient(context: Context, json: Json) = HttpClient(OkHttp) {
engine {
config {
dns(object : Dns {
override fun lookup(hostname: String): List<InetAddress> {
val addresses = Dns.SYSTEM.lookup(hostname)
return if (hostname == "raw.githubusercontent.com") {
addresses.filterIsInstance<Inet4Address>()
} else {
addresses
}
}
})
cache(Cache(context.cacheDir.resolve("cache").also { it.mkdirs() }, 1024 * 1024 * 100))
followRedirects(true)
followSslRedirects(true)
}
}
install(ContentNegotiation) {
json(json)
}
install(HttpTimeout) {
socketTimeoutMillis = 10000
}
install(UserAgent) {
agent = "ReVanced-Manager/${BuildConfig.VERSION_CODE}"
}
}
fun provideJson() = Json {
encodeDefaults = true
isLenient = true
ignoreUnknownKeys = true
}
single {
provideHttpClient(androidContext(), get())
}
singleOf(::provideJson)
}

View File

@@ -0,0 +1,11 @@
package app.revanced.manager.di
import app.revanced.manager.domain.manager.KeystoreManager
import app.revanced.manager.util.PM
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val managerModule = module {
singleOf(::KeystoreManager)
singleOf(::PM)
}

View File

@@ -0,0 +1,9 @@
package app.revanced.manager.di
import app.revanced.manager.domain.manager.PreferencesManager
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val preferencesModule = module {
singleOf(::PreferencesManager)
}

View File

@@ -0,0 +1,28 @@
package app.revanced.manager.di
import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.domain.repository.*
import app.revanced.manager.domain.worker.WorkerRepository
import app.revanced.manager.network.api.ReVancedAPI
import org.koin.core.module.dsl.createdAtStart
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val repositoryModule = module {
singleOf(::ReVancedAPI)
singleOf(::Filesystem) {
createdAtStart()
}
singleOf(::NetworkInfo)
singleOf(::PatchSelectionRepository)
singleOf(::PatchOptionsRepository)
singleOf(::PatchBundleRepository) {
// It is best to load patch bundles ASAP
createdAtStart()
}
singleOf(::DownloaderPluginRepository)
singleOf(::WorkerRepository)
singleOf(::DownloadedAppRepository)
singleOf(::InstalledAppRepository)
}

View File

@@ -0,0 +1,9 @@
package app.revanced.manager.di
import app.revanced.manager.domain.installer.RootInstaller
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val rootModule = module {
singleOf(::RootInstaller)
}

View File

@@ -0,0 +1,9 @@
package app.revanced.manager.di
import app.revanced.manager.network.service.HttpService
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val serviceModule = module {
singleOf(::HttpService)
}

View File

@@ -0,0 +1,27 @@
package app.revanced.manager.di
import app.revanced.manager.ui.viewmodel.*
import org.koin.androidx.viewmodel.dsl.viewModelOf
import org.koin.dsl.module
val viewModelModule = module {
viewModelOf(::MainViewModel)
viewModelOf(::DashboardViewModel)
viewModelOf(::SelectedAppInfoViewModel)
viewModelOf(::PatchesSelectorViewModel)
viewModelOf(::GeneralSettingsViewModel)
viewModelOf(::AdvancedSettingsViewModel)
viewModelOf(::AppSelectorViewModel)
viewModelOf(::PatcherViewModel)
viewModelOf(::UpdateViewModel)
viewModelOf(::ChangelogsViewModel)
viewModelOf(::ImportExportViewModel)
viewModelOf(::AboutViewModel)
viewModelOf(::DeveloperOptionsViewModel)
viewModelOf(::ContributorViewModel)
viewModelOf(::DownloadsViewModel)
viewModelOf(::InstalledAppsViewModel)
viewModelOf(::InstalledAppInfoViewModel)
viewModelOf(::UpdatesSettingsViewModel)
viewModelOf(::BundleListViewModel)
}

View File

@@ -0,0 +1,9 @@
package app.revanced.manager.di
import app.revanced.manager.patcher.worker.PatcherWorker
import org.koin.androidx.workmanager.dsl.workerOf
import org.koin.dsl.module
val workerModule = module {
workerOf(::PatcherWorker)
}

View File

@@ -0,0 +1,29 @@
package app.revanced.manager.domain.bundles
import app.revanced.manager.data.redux.ActionContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.InputStream
class LocalPatchBundle(
name: String,
uid: Int,
error: Throwable?,
directory: File
) : PatchBundleSource(name, uid, error, directory) {
suspend fun ActionContext.replace(patches: InputStream) {
withContext(Dispatchers.IO) {
patchBundleOutputStream().use { outputStream ->
patches.copyTo(outputStream)
}
}
}
override fun copy(error: Throwable?, name: String) = LocalPatchBundle(
name,
uid,
error,
directory
)
}

View File

@@ -0,0 +1,62 @@
package app.revanced.manager.domain.bundles
import androidx.compose.runtime.Stable
import app.revanced.manager.data.redux.ActionContext
import app.revanced.manager.patcher.patch.PatchBundle
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.OutputStream
/**
* A [PatchBundle] source.
*/
@Stable
sealed class PatchBundleSource(
val name: String,
val uid: Int,
error: Throwable?,
protected val directory: File
) {
protected val patchesFile = directory.resolve("patches.jar")
val state = when {
error != null -> State.Failed(error)
!hasInstalled() -> State.Missing
else -> State.Available(PatchBundle(patchesFile.absolutePath))
}
val patchBundle get() = (state as? State.Available)?.bundle
val version get() = patchBundle?.manifestAttributes?.version
val isNameOutOfDate get() = patchBundle?.manifestAttributes?.name?.let { it != name } == true
val error get() = (state as? State.Failed)?.throwable
suspend fun ActionContext.deleteLocalFile() = withContext(Dispatchers.IO) {
patchesFile.delete()
}
abstract fun copy(error: Throwable? = this.error, name: String = this.name): PatchBundleSource
protected fun hasInstalled() = patchesFile.exists()
protected fun patchBundleOutputStream(): OutputStream = with(patchesFile) {
// Android 14+ requires dex containers to be readonly.
try {
setWritable(true, true)
outputStream()
} finally {
setReadOnly()
}
}
sealed interface State {
data object Missing : State
data class Failed(val throwable: Throwable) : State
data class Available(val bundle: PatchBundle) : State
}
companion object Extensions {
val PatchBundleSource.isDefault inline get() = uid == 0
val PatchBundleSource.asRemoteOrNull inline get() = this as? RemotePatchBundle
}
}

Some files were not shown because too many files have changed in this diff Show More