Compare commits

..

227 Commits

Author SHA1 Message Date
8b9314078c feat: allow bundles to use classes from other bundles 2024-06-16 21:55:22 +02:00
413083c58d fix(downloader): versions not loading correctly 2024-05-29 23:25:15 +02:00
1f671aba33 fix: automatically focus search views 2024-05-27 22:49:08 +02:00
6bfd9098d6 feat: move update to notification card (#1917) 2024-05-27 21:50:02 +02:00
4e7d96e91d feat: revert to blue theme colors 2024-05-22 20:05:27 +02:00
ac0a036035 refactor: fix more warnings 2024-04-05 20:04:27 +02:00
2a1445d61f build(deps): update ksp 2024-04-05 19:22:54 +02:00
0df39a1136 refactor: replace deprecated functions 2024-04-05 19:09:39 +02:00
634d793839 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.
2024-04-04 17:50:31 +02:00
afd6c5d6b7 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>
2024-04-04 15:28:19 +00:00
ab0682cc5c 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>
2024-04-04 15:27:59 +00:00
60ca901ac7 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>
2024-04-04 15:27:45 +00:00
91ca5be57a 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>
2024-04-04 15:27:24 +00:00
ac47a7eaa4 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>
2024-04-04 15:26:17 +00:00
ce134224a8 fix: correctly patch apk files 2024-04-03 17:07:29 +02:00
ca49d3a465 feat: add external process runtime (#1799) 2024-03-29 15:00:52 +00:00
5d7f9d1387 feat: check if the version being used is the recommended version (#1675) 2024-03-15 17:57:53 +00:00
8d5d86fea8 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>
2024-03-12 17:09:39 -07:00
6709505e9e chore: Upgrade dependencies (#1761)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-10 11:05:47 -07:00
088de60c91 chore: upgrade dependencies (#1670) 2024-03-05 06:35:13 -08:00
ef041869e5 fix(VersionSelector): use correct LazyColumn item key 2024-03-05 14:34:45 +01:00
16cdc7aca4 refactor: Disable update for dev build (#1673)
Co-authored-by: Pun Butrach <pun.butrach@gmail.com>
2024-02-17 18:17:27 +00:00
39536c0e18 feat: Collapse ExtendedFAB on scroll (#1630) 2024-02-03 19:12:12 +00:00
607d8b67c9 feat: add toast feedback to the bundle update button 2024-01-21 14:34:18 +01:00
0b9889ea44 fix: patch options reset button being broken 2024-01-21 14:24:39 +01:00
4acef776b2 refactor: use consistent wording for the version compat check 2024-01-20 17:48:08 +01:00
e186dfdaa9 docs(security): init (#1612)
Co-authored-by: Ax333l <main@axelen.xyz>
2024-01-20 16:35:32 +00:00
c0f3d02e6f refactor: fix terminology and wording related to patches (#1623) 2024-01-18 19:50:24 +00:00
36c8f59d6f feat: Scrollbars (#1479) 2024-01-08 01:28:16 +03:00
f38b31a591 fix: progress bar not updating 2024-01-07 13:29:09 +01:00
3232bb10e6 feat: improve patcher UI (#1494) 2024-01-06 16:51:11 +01:00
b7cb6b94f5 feat: updater UI and code improvements (#1597) 2024-01-05 22:05:02 +00:00
aa6e612fba feat: Select bundle type before adding bundle (#1490) 2024-01-04 21:54:44 +00:00
d9d7b98409 feat: Purple default theme (#1601) 2023-12-27 16:35:23 -08:00
4fdd6bbe5f chore: upgrade AGP to 8.2.0 + migrate deprecated functions (#1574) 2023-12-26 20:37:42 +00:00
439f6250f3 chore(deps): update jetpack compose 2023-12-25 23:28:42 +01:00
d55abf5dda feat(app-selector): show patchable installed apps first (#1496) 2023-12-14 11:50:14 +01:00
a8b9d9316f docs: update revanced url 2023-12-02 17:18:21 +07:00
55c7800f39 build: bump Gradle to v8.5
build: update Gradle wrapper
2023-12-02 17:17:03 +07:00
d9eb1c42bc refactor: slight formatting of build.gradle.kts 2023-12-02 17:00:52 +07:00
8cd617e32d chore(template): update label name for feature 2023-12-02 16:58:47 +07:00
a17a05995a 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`.
2023-12-02 16:55:04 +07:00
de4e616dcc chore(deps): bump revanced patcher and library 2023-12-01 13:21:20 +01:00
12b00e5c8d fix: specify multithreadingDexFileWriter in PatcherOptions (#1402)
Co-authored-by: Ax333l <main@axelen.xyz>
2023-11-29 21:33:00 +00:00
9cab91959e fix: load patch bundles earlier 2023-11-29 22:11:57 +01:00
bd9778a3d1 feat(Update Screen): changelogs & handle states (#1464)
Co-authored-by: Ax333l <main@axelen.xyz>
2023-11-19 23:28:28 +03:00
62a5fce66c 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>
2023-11-19 23:27:13 +03:00
2bd84636d6 fix: parcel error for nullable types 2023-11-15 21:32:54 +01:00
ac561e7aca feat: Use correct casing in module description 2023-11-07 23:59:33 +01:00
59daceef99 chore: bump patcher 2023-11-06 19:33:06 +01:00
1dc41badd9 feat: check for updates on startup (#1462) 2023-11-05 13:19:55 +01:00
1a83315424 feat(Changelogs): overall improvement (#1429) 2023-11-03 18:03:14 +00:00
3c5776214f feat(Installer): use BottomAppBar (#1428) 2023-11-03 09:15:17 +00:00
5fff0a2923 fix: option state crash (#1456)
Co-authored-by: Ax333l <main@axelen.xyz>
2023-11-02 15:46:53 +00:00
8df7f2992d refactor(ui-components): deduplicate colors and move to settings folder 2023-11-01 21:57:00 +01:00
7741394c9c feat(NotificationCard): rewrite & consistent usage (#1426) 2023-11-01 20:54:06 +03:00
25bd91debc feat(Settings): use SettingsListItem consistently and overall improvements (#1427) 2023-11-01 20:11:43 +03:00
7fe4724e10 feat: remember patch options (#1449) 2023-10-31 20:16:02 +00:00
123ae37524 chore: add issue template (#1432) 2023-10-28 12:54:33 +07:00
172604fcdb feat(installer): sign apk in patcher worker 2023-10-27 23:30:45 +02:00
7d887c73e8 fix: use correct checksum 2023-10-27 17:33:11 +02:00
6abaac25d9 chore: upgrade dependencies (#1401) 2023-10-26 07:25:12 -07:00
cc897840e2 fix: perform selected app operations in the correct order 2023-10-26 09:06:42 +02:00
757840b76f feat(bundles tab): add BackHandler 2023-10-26 09:03:26 +02:00
50e8d1f8f4 docs: clarify license 2023-10-21 16:10:10 +02:00
65f8d38c59 feat: show toast when no patches are selected 2023-10-20 23:16:00 +02:00
e70c10adbd feat: add checkboxes to the downloaded apps page 2023-10-20 23:02:35 +02:00
64ec73d821 fix: more android 34 fixes 2023-10-20 22:59:16 +02:00
32e8a37f33 fix: handle exceptions when checking for bundle updates 2023-10-20 19:43:26 +02:00
5290713504 feat(patch-selector): remove TODO about an unplanned feature 2023-10-20 19:24:17 +02:00
18cfb56b45 fix: bundles not loading on Android 14 2023-10-20 18:49:44 +02:00
4b12ae1531 fix: jvm signature clash error 2023-10-20 17:21:59 +02:00
9df98edca5 fix: use upsert when modifying installed apps 2023-10-19 22:01:32 +02:00
c3af6acb2c feat: selected app info page (#1395) 2023-10-19 19:44:50 +00:00
7ba00cafd9 refactor: move mount code to when block 2023-10-17 09:21:06 +02:00
5aefb3bc59 fix: hide patch button (#1284) 2023-10-16 12:48:51 -07:00
212e55ffd8 feat: add user agent (#1382) 2023-10-16 17:39:17 +00:00
bf54d38c91 chore: bump patcher 2023-10-15 14:03:53 +02:00
cee2240cdc chore: bump compose 2023-10-15 13:17:07 +02:00
4c1ad868a9 fix: broken logo in about page on release builds 2023-10-15 00:22:12 +02:00
f5b3b29d6d feat: hide unfinished pages in release mode 2023-10-14 18:48:07 +02:00
8f5449527d feat: armv7 warning 2023-10-14 18:24:40 +02:00
8f6d720454 refactor(downloaders): improve file system code (#1379) 2023-10-14 15:42:10 +00:00
56a4a7043d feat: settings migration (compose) (#1309) 2023-10-13 10:39:10 -07:00
5762859906 feat: add patches selector bottom sheet (#1360) 2023-10-13 16:11:40 +00:00
608bac6854 feat: use revanced api for changelogs 2023-10-07 17:07:19 +02:00
723f9cd98c fix: delete temporary files (#1341) 2023-10-07 14:09:02 +00:00
abf4d91703 fix: use correct classes to determine option type
I can't believe this happened
2023-10-06 16:41:00 +02:00
d8392ad3eb feat(settings): move experimental patches option to advanced 2023-10-05 22:16:58 +02:00
39caad18a5 feat(installer): adjust arrow icon size 2023-10-05 22:06:20 +02:00
6437f7bb65 feat(installer): adjust step icon size and alignment 2023-10-05 21:51:48 +02:00
e232044157 chore: switch to revanced library and bump patcher (#1314) 2023-10-05 15:36:33 +00:00
f78b56ef0a feat(patch-selector): default patches selection (#1272) 2023-10-01 18:56:16 +00:00
ca3c9af3b8 feat: remove dead help icons
These never did anything and were removed from the figma a while ago.
2023-10-01 19:02:11 +02:00
b8b2e74151 chore: fully remove idea project files 2023-10-01 18:12:08 +02:00
1f8341ac42 fix: remove misc.xml and kotlinc.xml 2023-09-27 10:20:18 -07:00
0964f15475 docs: init (#1224) 2023-09-26 16:54:02 -07:00
63fd7957c6 ci: Add release workflow (#1235) 2023-09-25 19:02:18 +02:00
65377ffd9e fix: Updates popup shows incorrect names (#1283) 2023-09-23 19:42:27 -07:00
f79320c013 fix: use ReVanced ring logo in about section (#1302) 2023-09-22 14:19:48 -07:00
cf71ea26ec feat: implement Submit Issue button (#1276) 2023-09-21 19:18:42 +00:00
ee96c37c20 refactor: update progress onBackClick function (#1277) 2023-09-21 19:16:39 +00:00
a86923aee1 fix: disable WebView history (#1278) 2023-09-21 19:13:57 +00:00
e0f8d06152 fix(ui): make entire patches view button selectable (#1271) 2023-09-21 19:13:25 +00:00
4cb4ce298a feat: change appID and name of debug builds 2023-09-20 20:15:55 -07:00
36de61a57f ci: build pull requests (#1228) 2023-09-20 20:14:26 -07:00
6f2ca5bb89 fix: typo in string name import_keystore_description (#1273) 2023-09-18 16:59:03 +07:00
940885768d fix: contributors screen fix (#1256) 2023-09-15 16:33:14 +00:00
fc577b4c3e chore: update dependencies (#1247) 2023-09-15 16:30:46 +00:00
bf10af2ae2 feat: root installation (#1243) 2023-09-09 13:18:00 +00:00
b4dfcf1bb4 fix: minify crash on building release (#1245) 2023-09-06 17:49:11 +00:00
0b0ba21852 fix: providers.gradleProperty (#1223) 2023-09-06 17:48:48 +00:00
42e0346e25 feat: make bundles selectable (#1237) 2023-09-04 07:05:55 +02:00
212db84d0b ci(config): appreciation for first-time contributors
Show appreciation message for new contributors
2023-09-03 21:47:05 +07:00
e6eb8accf2 docs: update readme badges (#1227) 2023-09-03 09:40:54 +07:00
8bd73c3afa ci(release): don't build when not necessary
Add paths-ignore to all markdown files, and .idea folder
2023-09-02 21:59:23 +07:00
5369a25fa2 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`).
2023-09-02 21:51:48 +07:00
eeae46a415 chore: bump kotlinx.serialization plugin and patcher 2023-09-01 10:50:13 +02:00
c0badbe96b build: updates (#85) 2023-08-26 14:03:44 +00:00
2bb51c136a fix(deps): use correct work-runtime version string 2023-08-26 15:52:40 +02:00
3cfa4ea6d6 feat: more info for the select from application screen (#81) 2023-08-23 20:05:21 +02:00
f01adf5eb0 ci(release): migrate from node12 to node16
This bump `actions/upload-artifact`@v2 to `actions/upload-artifact`@v3
2023-08-19 16:00:01 +07:00
a0b92554e9 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
2023-08-17 17:42:10 +02:00
ac4c7e06e7 ci(release): use correct vars context object
why am i so stupid
2023-08-17 19:30:31 +07:00
0f9a6f4340 ci(release): no longer store keystore alias in secrets
fixes an issue where GitHub Actions logs would be censored
2023-08-17 19:13:18 +07:00
9586a9c0dd fix: patches not being reloaded 2023-08-14 18:29:56 +02:00
f6563b265b fix: permission error when using installed app 2023-08-12 14:52:34 +02:00
7aea9473de feat: patch options UI (#80) 2023-08-12 08:41:22 +00:00
3f059d7748 feat: switch to the new api (#75) 2023-08-07 09:03:50 +00:00
7e3c31c4b2 chore: bump patcher 2023-08-04 12:55:14 +02:00
1707a9690a feat: improve bundle dialog UI 2023-08-04 12:46:07 +02:00
379ce917a9 feat: finish implementing the sources system (#70) 2023-08-03 11:15:42 +00:00
299aaa2b68 fix: library info not being embedded 2023-08-01 21:14:15 +02:00
5cf5e87fa8 ci(release): task naming consistency 2023-08-01 15:30:56 +07:00
55f22562eb fix: don't store app list in parcel 2023-07-31 13:24:49 +02:00
272d911464 fix(installer): progress tracking 2023-07-31 12:16:13 +02:00
6beb34baa8 ci: init 2023-07-30 20:23:51 +00:00
61de0b67fa feat: show installed app in version selector 2023-07-30 19:45:40 +02:00
aec8cec9b8 feat: download apps in patcher screen (#73) 2023-07-30 10:29:22 +00:00
Pun
83b9573b52 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/...)
2023-07-24 16:24:21 +07:00
21d99a1f24 feat: add patch bundle info screen (#55) 2023-07-23 15:27:07 +00:00
1331479072 fix: serialization not working 2023-07-17 16:48:29 +02:00
b472a36a9a fix: buildfile syntax (#66)
Signed-off-by: Patryk Miś <foss@patrykmis.com>
2023-07-17 14:41:17 +00:00
3238fcdae7 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>
2023-07-17 12:27:19 +00:00
cd2587b1fd feat: improve accessibility (#64)
* Label Back button
* Mark group section headings as headings

Signed-off-by: Patryk Miś <foss@patrykmis.com>
2023-07-17 12:20:54 +00:00
879884a9fa feat: switch to Preferences DataStore (#60) 2023-07-15 09:52:12 +00:00
5d3b963682 feat: disable filter chips when there are no patches 2023-07-14 21:37:50 +02:00
955e7a4f1c feat: ReVanced theme colors 2023-07-14 21:35:17 +02:00
d2dcd4209d fix: release builds not working properly 2023-07-14 13:11:34 +02:00
6299ff5b48 chore: migrate dependencies to version catalogs (#58) 2023-07-14 10:33:42 +00:00
94a4dbaba1 feat: app downloader (#43) 2023-07-14 08:54:42 +00:00
c36deea045 build: update gradle to v8.2.1 2023-07-12 20:36:23 +07:00
Pun
7030d43aa5 docs(readme): minor changes to how badges works
* Better description for the repository license badge

* Clicking on badges open you the relevant url
2023-07-08 20:43:10 +07:00
aa02e9f8cf feat: improve keystore UI and UX (#52) 2023-07-07 18:48:36 +00:00
37e177b56e 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????"
2023-07-07 23:16:07 +07:00
453f4da8ec feat: advanced settings page with device info (#51) 2023-07-07 15:35:36 +00:00
400163b820 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
2023-07-07 20:27:53 +07:00
4ae9904c8a fix(installer): sign and install on threads
This is needed to avoid ANRs because it takes a while if the Apk is 100+
MB.
2023-07-07 12:31:31 +02:00
fe5e191cb5 feat: updater changelogs (#48)
---------

Co-authored-by: Aunali321 <aunvakil.aa@gmail.com>
2023-07-07 08:56:04 +00:00
d9d83df9de feat: allow user to save logs 2023-07-06 20:01:44 +02:00
8dd8f88d2b feat: save patch options and selected patches in bundle (#50) 2023-07-04 09:09:16 +00:00
01fd4c8ffa feat: patch options (#45) 2023-07-03 09:12:34 +00:00
7ac3bb74e0 refactor: use getDir instead of filesDir directly 2023-07-03 10:09:01 +02:00
3b65cd0edc fix: use correct directory 2023-07-01 16:02:34 +02:00
a9606728bf build: bump patcher 2023-07-01 15:09:21 +02:00
4d4f1a242c build: update gradle to v8.2 2023-07-01 12:40:01 +07:00
6b7143dd8f feat: licenses screen (#47) 2023-06-29 20:05:43 +00:00
7e4ee00cb2 chore: update links in about page 2023-06-29 18:10:40 +02:00
4868c45b43 feat: animate the arrow button 2023-06-29 17:26:49 +02:00
81f485da6b refactor: use correct coroutine scopes 2023-06-29 10:35:23 +02:00
18cbe51e6b fix(installer): save step incorrectly being marked as completed 2023-06-29 10:11:44 +02:00
149c8cc8b2 fix: sources screen being misaligned during transitions 2023-06-29 10:03:15 +02:00
0dccb8c27b 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>
2023-06-28 19:51:12 +00:00
4302ea8832 fix: pass worker inputs without serialization (#44)
Because androidx.work.Data sucks and causes our app to crash.
2023-06-27 14:39:30 +00:00
1eac42dab8 fix(installer): make the correct column scrollable 2023-06-27 15:12:55 +02:00
9dd74f1f22 feat: experimental patches setting 2023-06-27 15:05:31 +02:00
923ce74735 feat: save patch selection using room db (#38) 2023-06-22 10:20:30 +00:00
2d9f9adfee refactor: better PatchBundle docs and naming 2023-06-17 13:59:37 +02:00
9a55e51a3a build: bump patcher 2023-06-17 13:47:55 +02:00
5681c917c5 feat: show stacktrace in installer ui (#36) 2023-06-17 11:45:52 +00:00
6309e8bdf5 feat: filter options for patches 2023-06-15 22:20:17 +02:00
535efa3d73 fix: run blocking IO operations in the correct context 2023-06-11 17:52:43 +02:00
b8a51d32f5 fix(patcher): add notification and wakelock to worker; chore: add app icon 2023-06-11 17:49:42 +02:00
919b6b7014 feat: keystore import/export (#30) 2023-06-11 14:38:56 +00:00
971277ed39 fix(installer): properly track worker state (#32) 2023-06-09 15:34:10 +00:00
7ce4de7a8b feat(koin): use the android logger 2023-06-06 12:27:42 +02:00
9591f4e14f feat: ProGuard 2023-06-04 19:37:23 +02:00
27426b1390 feat: rename package to app.revanced.manager 2023-06-04 18:27:40 +02:00
fcb75dd780 feat: improved compose stability 2023-06-04 17:50:40 +02:00
1be9c9c1bd fix: use correct getViewModel 2023-06-04 17:42:21 +02:00
e088d053ab feat: rename ViewModels for consistency 2023-06-03 23:27:01 +02:00
ffa8d9c063 feat: hide tabs when 1 bundle is used 2023-06-03 20:12:03 +02:00
7a5596a281 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
2023-06-03 18:03:14 +00:00
9f46f74357 refactor(logs): use consistent tag 2023-05-26 15:39:56 +02:00
36c4e2dfe0 refactor(di): use constructor DSL for VMs
Instead of doing it manually with viewModel { }
2023-05-26 15:25:08 +02:00
5cb31dbe9d chore(deps): bump revanced-patcher to 9.0.0 2023-05-26 15:17:57 +02:00
399fc98dec feat: better installer ui (#29)
based cossale

Co-authored-by: Aunali321 <aunvakil.aa@gmail.com>
2023-05-26 13:14:21 +00:00
c22371e0c5 feat: patch bundle sources system (#24) 2023-05-26 12:58:14 +00:00
a4842c078b feat: in-app updater (#25) 2023-05-23 11:02:22 +02:00
c332760786 feat(settings screen): add battery optimization notification 2023-05-22 05:17:26 +05:30
ea4247c688 feat(update screen): complete main update screen 2023-05-22 04:14:43 +05:30
fec8c0cc14 feat(about screen): complete about screen 2023-05-22 03:23:14 +05:30
9b585c73fb feat(settings screen): match typography from figma 2023-05-22 02:34:07 +05:30
c695fa525f refactor(settings screen): clean code up a bit 2023-05-22 02:29:19 +05:30
93f3e27d48 fix: dont crash when the bundle cannot be downloaded 2023-05-20 17:14:05 +02:00
52ab7937bd feat(installer): apk signing and installation 2023-05-20 12:30:24 +02:00
762bfa8514 fix(patches selector): copy the selected patches list 2023-05-20 09:47:42 +02:00
ca20996b62 refactor(ui): move PatchItem to the only file where it is used 2023-05-19 21:21:37 +02:00
ad14818de8 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.
2023-05-19 21:01:33 +02:00
32839656f8 style: run formatter 2023-05-19 20:58:44 +02:00
a48faad17a build: updates (#23) 2023-05-19 18:54:21 +00:00
40487923f9 feat: integrate revanced patcher (#22) 2023-05-19 18:49:32 +00:00
f1656c6d1e feat: improved dashboard screen 2023-05-18 13:46:59 +02:00
4c3dbbd8d5 feat: patches selector screen 2023-05-18 13:44:19 +02:00
4088ed747e feat: settings screen 2023-05-18 13:38:02 +02:00
bca8df8efd 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>
2023-05-13 12:00:06 +00:00
54f0a69596 feat: app selector screen 2023-05-06 12:42:30 +02:00
9065c0d260 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>
2023-04-30 19:27:14 +00:00
cb0150a0f9 fix: gradlew permissions on unix 2023-04-23 14:45:07 +02:00
ec0f7e3f7a 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>
2023-04-23 12:36:52 +00:00
e5d898f025 feat: backend 2023-03-18 11:53:25 +01:00
52bdb1cd6a Create README.md 2023-01-31 19:14:18 -03:00
49f9dfcf95 feat: splash screen 2023-01-28 02:30:39 +03:00
9536cdcae1 feat: implement navigation 2023-01-28 02:28:39 +03:00
57e2632f38 feat: implement DI 2023-01-28 02:19:44 +03:00
b372f7ee84 feat: initialize project 2023-01-28 02:00:52 +03:00
70e8253b63 Migrate to compose branch
This commit was made to allow cherry-picking the first commit of the followup commits according to https://github.com/ReVanced/revanced-manager-compose/issues/65#issue-1806335545
2023-08-26 18:07:57 +02:00
425 changed files with 16341 additions and 33049 deletions

61
.github/ISSUE_TEMPLATE/bug-issue.yml vendored Normal file
View File

@ -0,0 +1,61 @@
name: 🐞 Bug report
description: Create a new bug report.
title: 'bug: <title>'
labels: [bug]
body:
- type: markdown
attributes:
value: |
# ReVanced Manager bug report
Please check for existing issues [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
- type: textarea
attributes:
label: Bug description
description: |
- Describe your bug in detail
- Add steps to reproduce the bug if possible (Step 1. Download some files. Step 2. ...)
- Add images and videos if possible
- List selected patches if applicable
validations:
required: true
- type: textarea
attributes:
label: Version of ReVanced Manager and version & name of application you tried to patch
validations:
required: true
- type: dropdown
attributes:
label: Installation type
options:
- Non-root
- Root
validations:
required: false
- type: textarea
attributes:
label: Device logs
description: Export logs in ReVanced Manager settings.
render: shell
validations:
required: true
- type: textarea
attributes:
label: Patcher logs
description: Export logs in "Patcher" screen.
render: shell
validations:
required: false
- type: checkboxes
attributes:
label: Acknowledgements
description: Your issue will be closed if you don't follow the checklist below!
options:
- label: This request is not a duplicate of an existing issue.
required: true
- label: I have chosen an appropriate title.
required: true
- label: All requested information has been provided properly.
required: true
- label: The issue is solely related to the ReVanced Manager
required: true

View File

@ -1,126 +0,0 @@
name: 🐞 Bug report
description: Report a bug or an issue.
title: "bug: "
labels: ["Bug report"]
body:
- type: markdown
attributes:
value: |
<p align="center">
<picture>
<source
width="256px"
media="(prefers-color-scheme: dark)"
srcset="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="https://raw.githubusercontent.com/revanced/revanced-manager/main/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="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="https://raw.githubusercontent.com/revanced/revanced-manager/main/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>
# ReVanced Manager bug report
Before creating a new bug report, please keep the following in mind:
- **Do not submit a duplicate bug report**: You can review existing bug reports [here](https://github.com/ReVanced/revanced-manager/labels/Bug%20report).
- **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-manager/blob/main/CONTRIBUTING.md).
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
- type: textarea
attributes:
label: Bug description
description: |
- 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 in ReVanced Manager settings.
render: shell
validations:
required: true
- type: textarea
attributes:
label: Patch logs
description: Export logs in "Patcher" screen.
render: shell
validations:
required: false
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your bug report will be closed if you don't follow the checklist below.
options:
- label: This issue is not a duplicate of an existing bug report.
required: true
- label: I have chosen an appropriate title.
required: true
- label: All requested information has been provided properly.
required: true
- label: The bug is only related to ReVanced Manager
required: true

View File

@ -1,5 +1 @@
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!
blank_issues_enabled: false

View File

@ -0,0 +1,42 @@
name: ⭐ Feature request
description: Create a new feature request.
title: 'feat: <title>'
labels: [feature request]
body:
- type: markdown
attributes:
value: |
# ReVanced Manager feature request
Please check for existing feature requests [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
- type: textarea
attributes:
label: Feature description
description: Describe your feature in detail.
validations:
required: true
- type: textarea
attributes:
label: Motivation
description: Explain why the lack of it is a problem.
validations:
required: true
- type: textarea
attributes:
label: Additional context
description: In case there is something else you want to add.
validations:
required: false
- type: checkboxes
attributes:
label: Acknowledgements
description: Your issue will be closed if you don't follow the checklist below!
options:
- label: This request is not a duplicate of an existing issue.
required: true
- label: I have chosen an appropriate title.
required: true
- label: All requested information has been provided properly.
required: true
- label: The issue is solely related to the ReVanced Manager
required: true

View File

@ -1,106 +0,0 @@
name: ⭐ Feature request
description: Create a detailed request for a new feature.
title: "feat: "
labels: ["Feature request"]
body:
- type: markdown
attributes:
value: |
<p align="center">
<picture>
<source
width="256px"
media="(prefers-color-scheme: dark)"
srcset="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="https://raw.githubusercontent.com/revanced/revanced-manager/main/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="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="https://raw.githubusercontent.com/revanced/revanced-manager/main/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>
# ReVanced Manager feature request
Before creating a new feature request, please keep the following in mind:
- **Do not submit a duplicate feature request**: You can review existing feature requests [here](https://github.com/ReVanced/revanced-manager//labels/Feature%20request).
- **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-manager/blob/main/CONTRIBUTING.md).
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
- type: textarea
attributes:
label: Feature description
description: |
- Describe your feature in detail
- Add images, videos, links, examples, references, etc. if possible
- Add the target application name in case you request a new patch
- type: textarea
attributes:
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?
- What makes this feature important?
validations:
required: true
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your feature request will be closed if you don't follow the checklist below.
options:
- label: This issue is not a duplicate of an existing feature request.
required: true
- label: I have chosen an appropriate title.
required: true
- label: The feature request is only related to ReVanced Manager
required: true

2
.github/config.yaml vendored
View File

@ -1,2 +1,2 @@
firstPRMergeComment: >
Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) to receive a role for your contribution.
❤️ Thank you for contributing to ReVanced Manager. Join us on [Discord](https://revanced.app/discord) if you want to receive a contributor role.

View File

@ -1,29 +0,0 @@
version: 2
updates:
- package-ecosystem: github-actions
labels: []
directory: /
target-branch: dev
schedule:
interval: monthly
- package-ecosystem: npm
labels: []
directory: /
target-branch: dev
schedule:
interval: monthly
- package-ecosystem: pub
labels: []
directory: /
target-branch: dev
schedule:
interval: monthly
- package-ecosystem: gradle
labels: [ "ReVanced Manager Compose" ]
directory: /
target-branch: compose-dev
schedule:
interval: monthly

View File

@ -1,116 +0,0 @@
name: Build pull request
on:
workflow_dispatch:
inputs:
# Enable or disable cache
flutter-cache:
description: Cache
type: boolean
default: true
# Select app flavour
app-flavour:
description: App flavour
default: 'release'
type: choice
options:
- release
- debug
- profile
# Select pull request
pr-number:
description: PR number (Without hashtag)
required: true
run-name: "Build pull request ${{ inputs.pr-number }}"
jobs:
build:
name: Build
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Setup pull request
env:
GH_TOKEN: ${{ github.token }}
run: |
gh repo clone "${{ github.repository }}"
cd revanced-manager
gh repo set-default "${{ github.repository }}"
gh pr checkout "${{ inputs.pr-number }}"
echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false
fetch-depth: 0
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v1
- name: Setup Java
run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> $GITHUB_ENV
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: ${{ inputs.flutter-cache }}
- 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
continue-on-error: true
id: flutter-build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
flutter build apk --"${{ inputs.app-flavour }}";
- name: Prepare comment
run: |
if [[ "${{ steps.flutter-build.outcome }}" == "success" ]]; then
echo "MESSAGE=✅ Failed build on ${{ env.COMMIT_HASH }}." >> $GITHUB_ENV
else
echo "MESSAGE=🚫 Failed build on ${{ env.COMMIT_HASH }}." >> $GITHUB_ENV
fi
- name: "Comment on pull request #${{ inputs.pr-number }}"
uses: thollander/actions-comment-pull-request@v2
with:
GITHUB_TOKEN: ${{ github.token }}
pr_number: ${{ inputs.pr-number }}
mode: recreate
message: |
## ⚒️ Build status
${{ env.MESSAGE }}
Details: [${{ github.run_id }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})!
### ⚙️ Workflow run configuration
- Flutter cache: ${{ inputs.flutter-cache }}
- App flavor: ${{ inputs.app-flavour }}
- name: Upload
uses: actions/upload-artifact@v4
with:
if-no-files-found: error
name: revanced-manager-(${{ env.COMMIT_HASH }}-${{ inputs.pr-number }}-${{ inputs.app-flavour }})
path: |
build/app/outputs/flutter-apk/app-${{ inputs.app-flavour }}.apk
build/app/outputs/flutter-apk/app-${{ inputs.app-flavour }}.apk.sha1

View File

@ -1,27 +0,0 @@
name: Open a PR to main
on:
push:
branches:
- dev
workflow_dispatch:
env:
MESSAGE: Merge branch `${{ github.head_ref || github.ref_name }}` to `main`
jobs:
pull-request:
name: Open pull request
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Open pull request
uses: repo-sync/pull-request@v2
with:
destination_branch: 'main'
pr_title: 'chore: ${{ env.MESSAGE }}'
pr_body: |
This pull request will ${{ env.MESSAGE }}.
pr_draft: true

44
.github/workflows/pr-build.yml vendored Normal file
View File

@ -0,0 +1,44 @@
name: Build pull request
on:
pull_request:
paths:
- ".github/workflows/pr-build.yml"
- "app/**"
- "gradle/**"
- "*.properties"
- ".kts"
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Build with Gradle
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew assembleRelease --no-daemon -PnoProguard -PsignAsDebug
- name: Set env
run: echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Add hash to APK
run: mv app/build/outputs/apk/release/app-release.apk revanced-manager-${{ env.COMMIT_HASH }}.apk
- name: Upload build
uses: actions/upload-artifact@v3
with:
name: revanced-manager
path: revanced-manager-${{ env.COMMIT_HASH }}.apk

51
.github/workflows/release-build.yml vendored Normal file
View File

@ -0,0 +1,51 @@
name: Release Build
on:
push:
tags:
- "v*"
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
with:
cache-disabled: true
- name: Build with Gradle
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew assembleRelease --no-daemon
- name: Sign APK
id: sign_apk
uses: ilharp/sign-android-release@v1
with:
releaseDir: ./app/build/outputs/apk/release/
signingKey: ${{ secrets.SIGNING_KEYSTORE }}
keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
keyAlias: ${{ secrets.SIGNING_KEY_ALIAS }}
keyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
- name: Add version to APK
run: mv ${{ steps.sign_apk.outputs.signedFile }} revanced-manager-${{ env.RELEASE_VERSION }}.apk
- name: Publish release APK
uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
prerelease: false
files: revanced-manager-${{ env.RELEASE_VERSION }}.apk

View File

@ -1,61 +0,0 @@
name: Release
on:
workflow_dispatch:
push:
branches:
- main
- dev
paths:
- ".github/workflows/release.yml"
- "android/**"
- "assets/**"
- "lib/**"
- "pubspec.yaml"
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Java
run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> $GITHUB_ENV
- 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"
- name: Install dependencies
run: npm install
- 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: Release
env:
GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }}
signingKey: "keystore.jks"
keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
keyAlias: ${{ secrets.SIGNING_KEY_ALIAS }}
keyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
run: |
echo "${{ secrets.SIGNING_KEYSTORE }}" | base64 --decode > android/app/keystore.jks
npx semantic-release

View File

@ -1,55 +0,0 @@
name: Sync Crowdin
on:
workflow_dispatch:
push:
branches:
- dev
jobs:
sync:
name: Sync
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Dart
uses: dart-lang/setup-dart@v1
- name: Sync translations from Crowdin
uses: crowdin/github-action@v1
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: Remove empty values from JSON
run: |
cd assets/i18n
sudo chmod 777 *
dart nuke.dart >> $GITHUB_STEP_SUMMARY
- name: Commit translations
run: |
git config user.name revanced-bot
git config user.email github@revanced.app
sudo chown -R $USER:$USER .git
git add assets/i18n/*.json
git commit -m "chore: Remove empty values from JSON" assets/i18n/*.json
git push origin HEAD:feat/translations
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -11,7 +11,7 @@ jobs:
name: Dispatch event to documentation repository
if: github.ref == 'refs/heads/main'
steps:
- uses: peter-evans/repository-dispatch@v3
- uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }}
repository: revanced/revanced-documentation

57
.gitignore vendored
View File

@ -1,50 +1,11 @@
# See https://www.dartlang.org/guides/libraries/private-files
# Files and directories created by pub
.dart_tool/
.packages
build/
# If you're building an application, you may want to check-in your pubspec.lock
# pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/
# dotenv environment variables file
.env*
# Avoid committing generated Javascript files:
*.dart.js
*.info.json # Produced by the --dump-info flag.
*.js # When generated by dart2js. Don't specify *.js if your
# project includes source files written in JavaScript.
*.js_
*.js.deps
*.js.map
.flutter-plugins
.flutter-plugins-dependencies
# Generated Builder file
**/*.g.dart
**/*.locator.dart
**/*.router.dart
flutter_*.png
#### Custom
*.iml
.gradle
/local.properties
/.idea
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# Node Dependency directories
node_modules/
# FVM
.fvm

View File

@ -1,45 +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.
version:
revision: 85684f9300908116a78138ea4c6036c35c9a1236
channel: stable
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
- platform: android
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
- platform: ios
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
- platform: linux
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
- platform: macos
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
- platform: web
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
- platform: windows
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
# 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

@ -1,64 +0,0 @@
{
"branches": [
"main",
{
"name": "dev",
"prerelease": true
}
],
"plugins": [
[
"@semantic-release/commit-analyzer", {
"releaseRules": [
{ "type": "build", "scope": "Needs bump", "release": "patch" }
]
}
],
"@semantic-release/changelog",
"@semantic-release/release-notes-generator",
[
"@droidsolutions-oss/semantic-release-update-file",
{
"files": [
{
"path": ["pubspec.yaml"],
"type": "flutter",
"branches": ["main", "dev"]
}
]
}
],
[
"@semantic-release/exec",
{
"prepareCmd": "flutter build apk"
}
],
[
"@semantic-release/git",
{
"assets": [
"pubspec.yaml"
]
}
],
[
"@semantic-release/github",
{
"assets": [
{
"path": "build/app/outputs/apk/release/revanced-manager*.apk"
}
],
"successComment": false
}
],
[
"@saithodev/semantic-release-backmerge",
{
"backmergeBranches": [{"from": "main", "to": "dev"}],
"clearWorkspace": true
}
]
]
}

91
.vscode/tasks.json vendored
View File

@ -1,91 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Generate (Builder)",
"type": "shell",
"command": "flutter packages pub run build_runner build --delete-conflicting-outputs",
"problemMatcher": []
},
{
"label": "Build (Android)",
"type": "shell",
"command": "flutter build apk",
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "Install (Android)",
"type": "shell",
"command": "adb install build\\app\\outputs\\flutter-apk\\app-release.apk",
"problemMatcher": []
},
{
"label": "Clean (Flutter)",
"type": "shell",
"command": "flutter clean && flutter pub get",
"problemMatcher": []
},
{
"label": "Clean (Builder)",
"type": "shell",
"command": "flutter packages pub run build_runner clean",
"problemMatcher": []
},
{
"label": "Build all (Android)",
"dependsOrder": "sequence",
"dependsOn": [
"Generate (Builder)",
"Build (Android)"
],
"problemMatcher": []
},
{
"label": "Clean all",
"dependsOrder": "sequence",
"dependsOn": [
"Clean (Flutter)",
"Clean (Builder)"
],
"problemMatcher": []
},
{
"label": "Clean all & Build all (Android)",
"dependsOrder": "sequence",
"dependsOn": [
"Clean all",
"Build all (Android)"
],
"problemMatcher": []
},
{
"label": "Clean all & Install (Android)",
"dependsOrder": "sequence",
"dependsOn": [
"Clean all",
"Build all (Android)",
"Install (Android)",
],
"problemMatcher": []
},
{
"label": "Build & Install (Android)",
"dependsOrder": "sequence",
"dependsOn": [
"Build (Android)",
"Install (Android)"
],
"problemMatcher": []
},
{
"label": "Validate translations",
"type": "shell",
"command": "flutter pub run flutter_i18n diff en.json pt.json",
"problemMatcher": []
}
]
}

View File

@ -1,103 +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-round.svg" />
<img height="24px" src="assets/revanced-logo/revanced-logo-round.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>
# 👋 Contribution guidelines
This document describes how to contribute to ReVanced Manager.
## 📖 Resources to help you get started
* The [documentation](/docs/README.md) provides steps to build ReVanced Manager from source
* Our [backlog](https://github.com/orgs/ReVanced/projects/12) is where we keep track of what we're working on
* [Issues](https://github.com/ReVanced/revanced-manager/issues) are where we keep track of bugs and feature requests
## 🙏 Submitting a feature request
Features can be requested by opening an issue using the
[Feature request issue template](https://github.com/ReVanced/revanced-manager/issues/new?assignees=&labels=Feature+request&projects=&template=feature_request.yml&title=feat%3A+).
> **Note**
> Requests can be accepted or rejected at the discretion of maintainers of ReVanced Manager.
> Good motivation has to be provided for a request to be accepted.
## 🐞 Submitting a bug report
If you encounter a bug while using ReVanced Manager, open an issue using the
[Bug report issue template](https://github.com/ReVanced/revanced-manager/issues/new?assignees=&labels=Bug+report&projects=&template=bug_report.yml&title=bug%3A+).
## 📝 How to contribute
1. Before contributing, it is recommended to open an issue to discuss your change
with the maintainers of ReVanced Manager. This will help you determine whether your change is acceptable
and whether it is worth your time to implement it
2. Development happens on the `dev` branch. Fork the repository and create your branch from `dev`
3. Commit your changes
4. Submit a pull request to the `dev` branch of the repository and reference issues
that your pull request closes in the description of your pull request
5. Our team will review your pull request and provide feedback. Once your pull request is approved,
it will be merged into the `dev` branch and will be included in the next release of ReVanced Manager
## 🤚 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
translating ReVanced Manager on [Crowdin](https://translate.revanced.app/).
❤️ Thank you for considering contributing to ReVanced Manager,
ReVanced

124
README.md
View File

@ -1,109 +1,55 @@
<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-round.svg" />
<img height="24px" src="assets/revanced-logo/revanced-logo-round.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>
# ReVanced Manager (Compose Rewrite)
# 💊 ReVanced Manager
[![GitHub license](https://img.shields.io/github/license/revanced/revanced-manager)](../../blob/main/LICENSE)
[![GitHub last commit](https://img.shields.io/github/last-commit/revanced/revanced-manager/compose-dev)](https://github.com/ReVanced/revanced-manager/commits/compose-dev)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/w/revanced/revanced-manager/compose-dev)](https://github.com/ReVanced/revanced-manager/commits/compose-dev)
![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/ReVanced/revanced-manager/release.yml)
![GPLv3 License](https://img.shields.io/badge/License-GPL%20v3-yellow.svg)
_(Yet another)_ rewrite of the ReVanced Manager using Kotlin and Jetpack Compose.
Application to use ReVanced on Android
## Design system
## ❓ About
In this rewrite, we are adopting the latest Material Design principles and guidelines by using Material 3 and Material You.
ReVanced Manager is an application that uses [ReVanced Patcher](https://github.com/revanced/revanced-patcher) to patch Android apps.
Material Design is a design system developed by Google that provides a unified visual language for building beautiful and consistent user interfaces across all platforms and devices. Material You is an extension of Material Design that provides even more customization options for users, making it possible for them to personalize their device and create a unique look and feel.
## 💪 Features
### Why Material 3?
Some of the features ReVanced Manager provides are:
* **Consistent design language**
* **Improved accessibility**
* **Better user experience**
- 💉 **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
By using Material 3 and Material You, we are ensuring that the app's user interface is consistent, customizable, accessible, and engaging for our users. This will help to improve the overall user experience and increase user satisfaction with the the manager.
## Technology stack
* Kotlin: Kotlin is a modern and concise programming language that is fully interoperable with Java and provides improved safety, readability, and maintainability compared to Java.
* Jetpack Compose: Jetpack Compose is a modern UI toolkit for Android development that allows developers to build beautiful and performant user interfaces using declarative programming. It provides a unified and efficient way of building UI that is well-integrated with the Android framework.
## Why Kotlin and Compose?
* **Improved safety:** Kotlin provides improved safety compared to Java, which reduces the likelihood of common programming mistakes that can cause security vulnerabilities or crashes.
* **Concise and readable code:** Kotlin's concise syntax and expressive type system make the code more readable, which makes it easier for developers to understand and maintain the codebase.
* **Better performance:** Jetpack Compose uses the power of the Android framework to provide smooth and fast performance, which enhances the user experience.
* **Modern and efficient UI development:** Jetpack Compose provides a modern and efficient way of building UI, which makes it easier for developers to create beautiful and performant user interfaces.
## 🔽 Download
You can download the most recent version of ReVanced Manager at [revanced.app/download](https://revanced.app/download) or from [GitHub releases](https://github.com/ReVanced/revanced-manager/releases).
Learn how to use ReVanced Manager by following the [documentation](/docs).
You can obtain ReVanced Manager by downloading it from either [revanced.app/download](https://revanced.app/download) or [GitHub Releases](https://github.com/ReVanced/revanced-manager/releases)
## 📚 Everything else
## 📝 Prerequisites
### 📙 Contributing
For a list of prerequisites, refer to [docs/0_prerequisites.md](docs/0_prerequisites.md)
Thank you for considering contributing to ReVanced Manager.
You can find the contribution guidelines [here](CONTRIBUTING.md).
## 🔴 Issues
### 🛠️ Building
For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose).
To build a ReVanced Manager, you can follow the [documentation](/docs).
## 🌐 Translation
### 📄 Documentation
[![Crowdin](https://badges.crowdin.net/revanced/localized.svg)](https://crowdin.com/project/revanced)
You can find the documentation for ReVanced Manager [here](/docs).
We're accepting translations on [Crowdin](https://translate.revanced.app)
### 👋 Contributing
## 🛠️ Building Manager from source
Thank you for considering contributing to ReVanced Manager. You can find the contribution guidelines [here](/CONTRIBUTING.md).
## ⚖️ License
ReVanced Manager is licensed under the GPLv3 license. Please see the [license file](LICENSE) for more information.
[tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3) you may copy, distribute and modify ReVanced Manager as long as you track changes/dates in source files.
Any modifications to ReVanced Manager must also be made available under the GPL, along with build & install instructions.
For instructions on how to build ReVanced Manager from source, refer to [docs/4_building.md](docs/4_building.md)

View File

@ -70,8 +70,9 @@ If a vulnerability is confirmed and accepted, you can join our [Discord](https:/
### ⏳ Supported Versions
| Version | Branch | Supported |
| -------------------------------------------------------------------------------------------------------- | ----------- | ------------------ |
| ![GitHub Release](https://img.shields.io/github/v/release/ReVanced/revanced-manager?style=for-the-badge) | main | :white_check_mark: |
| ![Static Badge](https://img.shields.io/badge/version-latest-brightgreen?style=for-the-badge) | dev | :white_check_mark: |
| ![Static Badge](https://img.shields.io/badge/version-latest-brightgreen?style=for-the-badge) | compose-dev | :white_check_mark: |
| Version | Branch | Supported |
| ------- | ------------|------------------- |
| v1.18.0 | main | :white_check_mark: |
| latest | dev | :white_check_mark: |
| latest | compose-dev | :white_check_mark: |

View File

@ -1,161 +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
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_names
- 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_api_docs
- package_names
- package_prefixed_library_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_mixin # Has false positives, see https://github.com/dart-lang/linter/issues/3018
- 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
- prefer_void_to_null
- 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
- unsafe_html
- 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/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks

View File

@ -1,3 +0,0 @@
source "https://rubygems.org"
gem "fastlane"

View File

@ -1,121 +0,0 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
android {
compileSdk flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
applicationId "app.revanced.manager.flutter"
minSdk 26
targetSdk 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
release {
ndk {
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
}
if (System.getenv("signingKey") != null) {
signingConfigs {
create("release") {
storeFile = file(System.getenv("signingKey"))
storePassword = System.getenv("keyStorePassword")
keyAlias = System.getenv("keyAlias")
keyPassword = System.getenv("keyPassword")
}
}
signingConfig = signingConfigs.release
resValue "string", "app_name", "ReVanced Manager"
applicationVariants.all { variant ->
variant.outputs.all {
outputFileName = "revanced-manager-v${flutterVersionName}.apk"
}
}
} else {
signingConfig = signingConfigs.debug
resValue "string", "app_name", "ReVanced Manager Debug"
applicationIdSuffix ".debug"
}
}
debug {
shrinkResources false
minifyEnabled false
resValue "string", "app_name", "ReVanced Manager Debug"
applicationIdSuffix ".debug"
signingConfig signingConfigs.debug
ndk {
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
}
}
profile {
shrinkResources false
minifyEnabled false
resValue "string", "app_name", "ReVanced Manager Profile"
applicationIdSuffix ".profile"
signingConfig signingConfigs.debug
ndk {
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
}
}
}
packagingOptions {
jniLibs {
useLegacyPackaging true
excludes += ['/prebuilt/**']
}
resources {
excludes += ['/prebuilt/**']
}
}
namespace 'app.revanced.manager.flutter'
}
kotlin {
jvmToolchain(17)
}
flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.22"
// ReVanced
implementation "app.revanced:revanced-patcher:19.3.1"
implementation "app.revanced:revanced-library:2.1.0"
}

View File

@ -1,19 +0,0 @@
# 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
-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.**

View File

@ -1,3 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -1,80 +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">
<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>
<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,83 +0,0 @@
package app.revanced.manager.flutter
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInfo
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)
val callingPackageName = getCallingPackage()!!
if (getFingerprint(callingPackageName) == getFingerprint(getPackageName())) {
// 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,422 +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.library.ApkUtils.sign
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.PatchBundleLoader
import app.revanced.patcher.PatchSet
import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherConfig
import app.revanced.patcher.patch.PatchResult
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: PatchSet
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 integrationsPath = call.argument<String>("integrationsPath")
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 &&
integrationsPath != null &&
selectedPatches != null &&
options != null &&
tmpDirPath != null &&
keyStoreFilePath != null &&
keystorePassword != null
) {
cancel = false
runPatcher(
result,
inFilePath,
outFilePath,
integrationsPath,
selectedPatches,
options,
tmpDirPath,
keyStoreFilePath,
keystorePassword
)
} else result.notImplemented()
}
"stopPatcher" -> {
cancel = true
stopResult = result
}
"getPatches" -> {
val patchBundleFilePath = call.argument<String>("patchBundleFilePath")!!
val cacheDirPath = call.argument<String>("cacheDirPath")!!
try {
val patchBundleFile = File(patchBundleFilePath)
patchBundleFile.setWritable(false)
patches = PatchBundleLoader.Dex(
patchBundleFile,
optimizedDexDirectory = File(cacheDirPath)
)
} catch (ex: Exception) {
return@setMethodCallHandler result.notImplemented()
} catch (err: Error) {
return@setMethodCallHandler result.notImplemented()
}
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 { compatiblePackage ->
val compatiblePackageJson = JSONObject().apply {
put("name", compatiblePackage.name)
put(
"versions",
JSONArray().apply {
compatiblePackage.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("valueType", option.valueType)
}.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,
integrationsPath: String,
selectedPatches: List<String>,
options: Map<String, Map<String, Any>>,
tmpDirPath: String,
keyStoreFilePath: String,
keystorePassword: String
) {
val inFile = File(inFilePath)
val outFile = File(outFilePath)
val integrations = File(integrationsPath)
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 {
it.close()
removeHandler(it)
}
object : java.util.logging.Handler() {
override fun publish(record: LogRecord) {
if (record.loggerName?.startsWith("app.revanced") != true || cancel) return
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,
true // TODO: Add option to disable this
)
)
if (cancel(patcher::close)) return@Thread
updateProgress(0.02, "Loading patches...", "Loading patches")
val patches = patches.filter { patch ->
val isCompatible = patch.compatiblePackages?.any {
it.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 {
patcher.apply {
acceptIntegrations(setOf(integrations))
acceptPatches(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.apply(false).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()
}
inFile.copyTo(outFile)
if (cancel(patcher::close)) return@Thread
patcherResult.applyTo(outFile)
if (cancel(patcher::close)) return@Thread
updateProgress(0.8, "Signing...", "")
outFile.sign(
ApkUtils.SigningOptions(
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"
)
}
}
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,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,3 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -1,27 +0,0 @@
allprojects {
repositories {
google()
mavenCentral()
maven {
// A repository must be speficied for some reason. "registry" is a dummy.
url = uri("https://maven.pkg.github.com/revanced/registry")
credentials {
username = project.findProperty("gpr.user") as String ?: System.getenv("GITHUB_ACTOR")
password = project.findProperty("gpr.key") as String ?: System.getenv("GITHUB_TOKEN")
}
}
mavenLocal()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@ -1,8 +0,0 @@
org.gradle.jvmargs=-Xmx1536M -XX:+UseParallelGC
org.gradle.parallel=true
org.gradle.daemon=true
org.gradle.caching=true
android.useAndroidX=true
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false

View File

@ -1,25 +0,0 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}
settings.ext.flutterSdkPath = flutterSdkPath()
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.1.2" apply false
id "org.jetbrains.kotlin.android" version "1.9.10" apply false
}
include ":app"

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

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

@ -0,0 +1,194 @@
import kotlin.random.Random
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.devtools)
alias(libs.plugins.about.libraries)
id("kotlin-parcelize")
kotlin("plugin.serialization") version "1.9.23"
}
android {
namespace = "app.revanced.manager"
compileSdk = 34
buildToolsVersion = "34.0.0"
defaultConfig {
applicationId = "app.revanced.manager"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "0.0.1"
resourceConfigurations.addAll(listOf(
"en",
))
vectorDrawables.useSupportLibrary = true
}
buildTypes {
debug {
applicationIdSuffix = ".debug"
resValue("string", "app_name", "ReVanced Manager Debug")
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")
}
if (project.hasProperty("signAsDebug")) {
applicationIdSuffix = ".debug"
resValue("string", "app_name", "ReVanced Manager Debug")
signingConfig = signingConfigs.getByName("debug")
}
buildConfigField("long", "BUILD_ID", "0L")
}
}
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
buildFeatures.aidl = true
buildFeatures.buildConfig=true
composeOptions.kotlinCompilerExtensionVersion = "1.5.10"
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
version = "3.22.1"
}
}
}
kotlin {
jvmToolchain(17)
}
dependencies {
// AndroidX Core
implementation(libs.androidx.ktx)
implementation(libs.runtime.ktx)
implementation(libs.runtime.compose)
implementation(libs.splash.screen)
implementation(libs.compose.activity)
implementation(libs.paging.common.ktx)
implementation(libs.work.runtime.ktx)
implementation(libs.preferences.datastore)
// 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)
// Accompanist
implementation(libs.accompanist.drawablepainter)
// Placeholder
implementation(libs.placeholder.material3)
// HTML Scraper
implementation(libs.skrapeit.dsl)
implementation(libs.skrapeit.parser)
// 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)
// 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)
implementation(libs.revanced.multidexlib2)
// 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.workmanager)
// Compose Navigation
implementation(libs.reimagined.navigation)
// 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)
// Compose Icons
implementation(libs.compose.icons.fontawesome)
}

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

@ -0,0 +1,59 @@
# 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.** {
*;
}
-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,413 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "802fa2fda94b930bf0ebb85d195f1022",
"entities": [
{
"tableName": "patch_bundles",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `name` TEXT NOT NULL, `source` TEXT NOT NULL, `auto_update` INTEGER NOT NULL, `version` TEXT, `integrations_version` TEXT, PRIMARY KEY(`uid`))",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "source",
"columnName": "source",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "autoUpdate",
"columnName": "auto_update",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionInfo.patches",
"columnName": "version",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionInfo.integrations",
"columnName": "integrations_version",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"uid"
]
},
"indices": [
{
"name": "index_patch_bundles_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_patch_bundles_name` ON `${TABLE_NAME}` (`name`)"
}
],
"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, 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
}
],
"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 NO ACTION )",
"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": "NO ACTION",
"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"
]
}
]
}
],
"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, '802fa2fda94b930bf0ebb85d195f1022')"
]
}
}

View File

@ -0,0 +1,75 @@
<?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="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="ReservedSystemPermission" />
<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" />
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
</intent>
</queries>
<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>
<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,150 @@
package app.revanced.manager
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import app.revanced.manager.ui.destination.Destination
import app.revanced.manager.ui.destination.SettingsDestination
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.SelectedAppInfoScreen
import app.revanced.manager.ui.screen.SettingsScreen
import app.revanced.manager.ui.screen.VersionSelectorScreen
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 dev.olshevski.navigation.reimagined.AnimatedNavHost
import dev.olshevski.navigation.reimagined.NavBackHandler
import dev.olshevski.navigation.reimagined.navigate
import dev.olshevski.navigation.reimagined.pop
import dev.olshevski.navigation.reimagined.popUpTo
import dev.olshevski.navigation.reimagined.rememberNavController
import org.koin.core.parameter.parametersOf
import org.koin.androidx.compose.koinViewModel as getComposeViewModel
import org.koin.androidx.viewmodel.ext.android.getViewModel as getAndroidViewModel
class MainActivity : ComponentActivity() {
@ExperimentalAnimationApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
installSplashScreen()
val vm: MainViewModel = getAndroidViewModel()
vm.importLegacySettings(this)
setContent {
val theme by vm.prefs.theme.getAsState()
val dynamicColor by vm.prefs.dynamicColor.getAsState()
ReVancedManagerTheme(
darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK,
dynamicColor = dynamicColor
) {
val navController =
rememberNavController<Destination>(startDestination = Destination.Dashboard)
NavBackHandler(navController)
AnimatedNavHost(
controller = navController
) { destination ->
when (destination) {
is Destination.Dashboard -> DashboardScreen(
onSettingsClick = { navController.navigate(Destination.Settings()) },
onAppSelectorClick = { navController.navigate(Destination.AppSelector) },
onUpdateClick = { navController.navigate(
Destination.Settings(SettingsDestination.Update())
) },
onAppClick = { installedApp ->
navController.navigate(
Destination.InstalledApplicationInfo(
installedApp
)
)
}
)
is Destination.InstalledApplicationInfo -> InstalledAppInfoScreen(
onPatchClick = { packageName, patchSelection ->
navController.navigate(
Destination.VersionSelector(
packageName,
patchSelection
)
)
},
onBackClick = { navController.pop() },
viewModel = getComposeViewModel { parametersOf(destination.installedApp) }
)
is Destination.Settings -> SettingsScreen(
onBackClick = { navController.pop() },
startDestination = destination.startDestination
)
is Destination.AppSelector -> AppSelectorScreen(
onAppClick = { navController.navigate(Destination.VersionSelector(it)) },
onStorageClick = {
navController.navigate(
Destination.SelectedApplicationInfo(
it
)
)
},
onBackClick = { navController.pop() }
)
is Destination.VersionSelector -> VersionSelectorScreen(
onBackClick = { navController.pop() },
onAppClick = { selectedApp ->
navController.navigate(
Destination.SelectedApplicationInfo(
selectedApp,
destination.patchSelection,
)
)
},
viewModel = getComposeViewModel {
parametersOf(
destination.packageName,
destination.patchSelection
)
}
)
is Destination.SelectedApplicationInfo -> SelectedAppInfoScreen(
onPatchClick = { app, patches, options ->
navController.navigate(
Destination.Patcher(
app, patches, options
)
)
},
onBackClick = navController::pop,
vm = getComposeViewModel {
parametersOf(
SelectedAppInfoViewModel.Params(
destination.selectedApp,
destination.patchSelection
)
)
}
)
is Destination.Patcher -> PatcherScreen(
onBackClick = { navController.popUpTo { it is Destination.Dashboard } },
vm = getComposeViewModel { parametersOf(destination) }
)
}
}
}
}
}
}

View File

@ -0,0 +1,77 @@
package app.revanced.manager
import android.app.Application
import android.content.Intent
import app.revanced.manager.di.*
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.service.ManagerRootService
import app.revanced.manager.service.RootConnection
import kotlinx.coroutines.Dispatchers
import coil.Coil
import coil.ImageLoader
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.internal.BuilderImpl
import com.topjohnwu.superuser.ipc.RootService
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.get
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()
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)
val intent = Intent(this, ManagerRootService::class.java)
RootService.bind(intent, get<RootConnection>())
scope.launch {
prefs.preload()
}
scope.launch(Dispatchers.Default) {
with(patchBundleRepository) {
reload()
updateCheck()
}
}
}
}

View File

@ -0,0 +1,36 @@
package app.revanced.manager.data.platform
import android.app.Application
import android.os.Build
import android.os.Environment
import android.Manifest
import android.content.pm.PackageManager
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts
import app.revanced.manager.util.RequestManageStorageContract
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 = app.cacheDir.resolve("ephemeral").apply {
deleteRecursively()
mkdirs()
}
fun externalFilesDir() = 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,33 @@
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 kotlin.random.Random
@Database(entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, OptionGroup::class, Option::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
companion object {
fun generateUid() = Random.Default.nextInt()
}
}

View File

@ -0,0 +1,20 @@
package app.revanced.manager.data.room
import androidx.room.TypeConverter
import app.revanced.manager.data.room.bundles.Source
import io.ktor.http.*
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
}

View File

@ -0,0 +1,15 @@
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,
)

View File

@ -0,0 +1,22 @@
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 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?
@Insert
suspend fun insert(downloadedApp: DownloadedApp)
@Delete
suspend fun delete(downloadedApps: Collection<DownloadedApp>)
}

View File

@ -0,0 +1,34 @@
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"]
)
],
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,23 @@
package app.revanced.manager.data.room.apps.installed
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import app.revanced.manager.R
import kotlinx.parcelize.Parcelize
enum class InstallType(val stringResource: Int) {
DEFAULT(R.string.default_install),
ROOT(R.string.root_install)
}
@Parcelize
@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
) : Parcelable

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,34 @@
package app.revanced.manager.data.room.bundles
import androidx.room.*
import kotlinx.coroutines.flow.Flow
@Dao
interface PatchBundleDao {
@Query("SELECT * FROM patch_bundles")
suspend fun all(): List<PatchBundleEntity>
@Query("SELECT version, integrations_version, auto_update FROM patch_bundles WHERE uid = :uid")
fun getPropsById(uid: Int): Flow<BundleProperties>
@Query("UPDATE patch_bundles SET version = :patches, integrations_version = :integrations WHERE uid = :uid")
suspend fun updateVersion(uid: Int, patches: String?, integrations: String?)
@Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid")
suspend fun setAutoUpdate(uid: Int, value: Boolean)
@Query("DELETE FROM patch_bundles WHERE uid != 0")
suspend fun purgeCustomBundles()
@Transaction
suspend fun reset() {
purgeCustomBundles()
updateVersion(0, null, null) // Reset the main source
}
@Query("DELETE FROM patch_bundles WHERE uid = :uid")
suspend fun remove(uid: Int)
@Insert
suspend fun add(source: PatchBundleEntity)
}

View File

@ -0,0 +1,49 @@
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))
}
}
}
data class VersionInfo(
@ColumnInfo(name = "version") val patches: String? = null,
@ColumnInfo(name = "integrations_version") val integrations: String? = null,
)
@Entity(tableName = "patch_bundles", indices = [Index(value = ["name"], unique = true)])
data class PatchBundleEntity(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "name") val name: String,
@Embedded val versionInfo: VersionInfo,
@ColumnInfo(name = "source") val source: Source,
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
)
data class BundleProperties(
@Embedded val versionInfo: VersionInfo,
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
)

View File

@ -0,0 +1,23 @@
package app.revanced.manager.data.room.options
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
@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: String,
)

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 clearForPatchBundle(uid: Int)
@Query("DELETE FROM option_groups WHERE package_name = :packageName")
abstract suspend fun clearForPackage(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,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,58 @@
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
@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("DELETE FROM patch_selections WHERE patch_bundle = :uid")
abstract suspend fun clearForPatchBundle(uid: Int)
@Query("DELETE FROM patch_selections WHERE package_name = :packageName")
abstract suspend fun clearForPackage(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(::PatchBundlePersistenceRepository)
singleOf(::PatchSelectionRepository)
singleOf(::PatchOptionsRepository)
singleOf(::PatchBundleRepository) {
// It is best to load patch bundles ASAP
createdAtStart()
}
singleOf(::WorkerRepository)
singleOf(::DownloadedAppRepository)
singleOf(::InstalledAppRepository)
}

View File

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

View File

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

View File

@ -0,0 +1,26 @@
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(::SettingsViewModel)
viewModelOf(::AdvancedSettingsViewModel)
viewModelOf(::AppSelectorViewModel)
viewModelOf(::VersionSelectorViewModel)
viewModelOf(::PatcherViewModel)
viewModelOf(::UpdateViewModel)
viewModelOf(::ChangelogsViewModel)
viewModelOf(::ImportExportViewModel)
viewModelOf(::AboutViewModel)
viewModelOf(::ContributorViewModel)
viewModelOf(::DownloadsViewModel)
viewModelOf(::InstalledAppsViewModel)
viewModelOf(::InstalledAppInfoViewModel)
viewModelOf(::UpdatesSettingsViewModel)
}

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,25 @@
package app.revanced.manager.domain.bundles
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.StandardCopyOption
class LocalPatchBundle(name: String, id: Int, directory: File) : PatchBundleSource(name, id, directory) {
suspend fun replace(patches: InputStream? = null, integrations: InputStream? = null) {
withContext(Dispatchers.IO) {
patches?.let { inputStream ->
patchBundleOutputStream().use { outputStream ->
inputStream.copyTo(outputStream)
}
}
integrations?.let {
Files.copy(it, this@LocalPatchBundle.integrationsFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
}
}
refresh()
}
}

View File

@ -0,0 +1,64 @@
package app.revanced.manager.domain.bundles
import androidx.compose.runtime.Stable
import app.revanced.manager.patcher.patch.PatchBundle
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flowOf
import java.io.File
import java.io.OutputStream
/**
* A [PatchBundle] source.
*/
@Stable
sealed class PatchBundleSource(val name: String, val uid: Int, directory: File) {
protected val patchesFile = directory.resolve("patches.jar")
protected val integrationsFile = directory.resolve("integrations.apk")
private val _state = MutableStateFlow(getPatchBundle())
val state = _state.asStateFlow()
/**
* Returns true if the bundle has been downloaded to local storage.
*/
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()
}
}
private fun getPatchBundle() =
if (!hasInstalled()) State.Missing
else State.Available(PatchBundle(patchesFile, integrationsFile.takeIf(File::exists)))
fun refresh() {
_state.value = getPatchBundle()
}
fun markAsFailed(e: Throwable) {
_state.value = State.Failed(e)
}
sealed interface State {
fun patchBundleOrNull(): PatchBundle? = null
data object Missing : State
data class Failed(val throwable: Throwable) : State
data class Available(val bundle: PatchBundle) : State {
override fun patchBundleOrNull() = bundle
}
}
companion object {
val PatchBundleSource.isDefault get() = uid == 0
val PatchBundleSource.asRemoteOrNull get() = this as? RemotePatchBundle
fun PatchBundleSource.propsOrNullFlow() = asRemoteOrNull?.propsFlow() ?: flowOf(null)
}
}

View File

@ -0,0 +1,122 @@
package app.revanced.manager.domain.bundles
import androidx.compose.runtime.Stable
import app.revanced.manager.data.room.bundles.VersionInfo
import app.revanced.manager.domain.repository.PatchBundlePersistenceRepository
import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType
import app.revanced.manager.network.dto.BundleAsset
import app.revanced.manager.network.dto.BundleInfo
import app.revanced.manager.network.service.HttpService
import app.revanced.manager.network.utils.getOrThrow
import app.revanced.manager.util.APK_MIMETYPE
import app.revanced.manager.util.JAR_MIMETYPE
import io.ktor.client.request.url
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File
@Stable
sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpoint: String) :
PatchBundleSource(name, id, directory), KoinComponent {
private val configRepository: PatchBundlePersistenceRepository by inject()
protected val http: HttpService by inject()
protected abstract suspend fun getLatestInfo(): BundleInfo
private suspend fun download(info: BundleInfo) = withContext(Dispatchers.IO) {
val (patches, integrations) = info
coroutineScope {
launch {
patchBundleOutputStream().use {
http.streamTo(it) {
url(patches.url)
}
}
}
launch {
http.download(integrationsFile) {
url(integrations.url)
}
}
}
saveVersion(patches.version, integrations.version)
refresh()
}
suspend fun downloadLatest() {
download(getLatestInfo())
}
suspend fun update(): Boolean = withContext(Dispatchers.IO) {
val info = getLatestInfo()
if (hasInstalled() && VersionInfo(
info.patches.version,
info.integrations.version
) == currentVersion()
) {
return@withContext false
}
download(info)
true
}
private suspend fun currentVersion() = configRepository.getProps(uid).first().versionInfo
private suspend fun saveVersion(patches: String, integrations: String) =
configRepository.updateVersion(uid, patches, integrations)
suspend fun deleteLocalFiles() = withContext(Dispatchers.Default) {
arrayOf(patchesFile, integrationsFile).forEach(File::delete)
refresh()
}
fun propsFlow() = configRepository.getProps(uid)
suspend fun setAutoUpdate(value: Boolean) = configRepository.setAutoUpdate(uid, value)
companion object {
const val updateFailMsg = "Failed to update patch bundle(s)"
}
}
class JsonPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
RemotePatchBundle(name, id, directory, endpoint) {
override suspend fun getLatestInfo() = withContext(Dispatchers.IO) {
http.request<BundleInfo> {
url(endpoint)
}.getOrThrow()
}
}
class APIPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
RemotePatchBundle(name, id, directory, endpoint) {
private val api: ReVancedAPI by inject()
override suspend fun getLatestInfo() = coroutineScope {
fun getAssetAsync(repo: String, mime: String) = async(Dispatchers.IO) {
api
.getLatestRelease(repo)
.getOrThrow()
.let {
BundleAsset(it.version, it.findAssetByType(mime).downloadUrl)
}
}
val patches = getAssetAsync("revanced-patches", JAR_MIMETYPE)
val integrations = getAssetAsync("revanced-integrations", APK_MIMETYPE)
BundleInfo(
patches.await(),
integrations.await()
)
}
}

View File

@ -0,0 +1,131 @@
package app.revanced.manager.domain.installer
import android.app.Application
import app.revanced.manager.service.RootConnection
import app.revanced.manager.util.PM
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
class RootInstaller(
private val app: Application,
private val rootConnection: RootConnection,
private val pm: PM
) {
fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false
fun isAppInstalled(packageName: String) =
rootConnection.remoteFS?.getFile("$modulesPath/$packageName-revanced")
?.exists() ?: throw RootServiceException()
fun isAppMounted(packageName: String): Boolean {
return pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir?.let {
Shell.cmd("mount | grep \"$it\"").exec().isSuccess
} ?: false
}
fun mount(packageName: String) {
if (isAppMounted(packageName)) return
val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir
?: throw Exception("Failed to load application info")
val patchedAPK = "$modulesPath/$packageName-revanced/$packageName.apk"
Shell.cmd("mount -o bind \"$patchedAPK\" \"$stockAPK\"").exec()
.also { if (!it.isSuccess) throw Exception("Failed to mount APK") }
}
fun unmount(packageName: String) {
if (!isAppMounted(packageName)) return
val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir
?: throw Exception("Failed to load application info")
Shell.cmd("umount -l \"$stockAPK\"").exec()
.also { if (!it.isSuccess) throw Exception("Failed to unmount APK") }
}
suspend fun install(
patchedAPK: File,
stockAPK: File?,
packageName: String,
version: String,
label: String
) {
withContext(Dispatchers.IO) {
rootConnection.remoteFS?.let { remoteFS ->
val assets = app.assets
val modulePath = "$modulesPath/$packageName-revanced"
unmount(packageName)
stockAPK?.let { stockApp ->
pm.getPackageInfo(packageName)?.let { packageInfo ->
if (packageInfo.versionName <= version)
Shell.cmd("pm uninstall -k --user 0 $packageName").exec()
.also { if (!it.isSuccess) throw Exception("Failed to uninstall stock app") }
}
Shell.cmd("pm install \"${stockApp.absolutePath}\"").exec()
.also { if (!it.isSuccess) throw Exception("Failed to install stock app") }
}
remoteFS.getFile(modulePath).mkdir()
listOf(
"service.sh",
"module.prop",
).forEach { file ->
assets.open("root/$file").use { inputStream ->
remoteFS.getFile("$modulePath/$file").newOutputStream()
.use { outputStream ->
val content = String(inputStream.readBytes())
.replace("__PKG_NAME__", packageName)
.replace("__VERSION__", version)
.replace("__LABEL__", label)
.toByteArray()
outputStream.write(content)
}
}
}
"$modulePath/$packageName.apk".let { apkPath ->
remoteFS.getFile(patchedAPK.absolutePath)
.also { if (!it.exists()) throw Exception("File doesn't exist") }
.newInputStream().use { inputStream ->
remoteFS.getFile(apkPath).newOutputStream().use { outputStream ->
inputStream.copyTo(outputStream)
}
}
Shell.cmd(
"chmod 644 $apkPath",
"chown system:system $apkPath",
"chcon u:object_r:apk_data_file:s0 $apkPath",
"chmod +x $modulePath/service.sh"
).exec()
.let { if (!it.isSuccess) throw Exception("Failed to set file permissions") }
}
} ?: throw RootServiceException()
}
}
fun uninstall(packageName: String) {
rootConnection.remoteFS?.let { remoteFS ->
if (isAppMounted(packageName))
unmount(packageName)
remoteFS.getFile("$modulesPath/$packageName-revanced").deleteRecursively()
.also { if (!it) throw Exception("Failed to delete files") }
} ?: throw RootServiceException()
}
companion object {
const val modulesPath = "/data/adb/modules"
}
}
class RootServiceException: Exception("Root not available")

View File

@ -0,0 +1,87 @@
package app.revanced.manager.domain.manager
import android.app.Application
import android.content.Context
import app.revanced.library.ApkSigner
import app.revanced.library.ApkUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.ByteArrayInputStream
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Files
import java.security.UnrecoverableKeyException
class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
companion object Constants {
/**
* Default alias and password for the keystore.
*/
const val DEFAULT = "ReVanced"
}
private val keystorePath =
app.getDir("signing", Context.MODE_PRIVATE).resolve("manager.keystore")
private suspend fun updatePrefs(cn: String, pass: String) = prefs.edit {
prefs.keystoreCommonName.value = cn
prefs.keystorePass.value = pass
}
private suspend fun signingOptions(path: File = keystorePath) = ApkUtils.SigningOptions(
keyStore = path,
keyStorePassword = null,
alias = prefs.keystoreCommonName.get(),
signer = prefs.keystoreCommonName.get(),
password = prefs.keystorePass.get()
)
suspend fun sign(input: File, output: File) = withContext(Dispatchers.Default) {
ApkUtils.sign(input, output, signingOptions())
}
suspend fun regenerate() = withContext(Dispatchers.Default) {
val ks = ApkSigner.newKeyStore(
setOf(
ApkSigner.KeyStoreEntry(
DEFAULT, DEFAULT
)
)
)
keystorePath.outputStream().use {
ks.store(it, null)
}
updatePrefs(DEFAULT, DEFAULT)
}
suspend fun import(cn: String, pass: String, keystore: InputStream): Boolean {
val keystoreData = keystore.readBytes()
try {
val ks = ApkSigner.readKeyStore(ByteArrayInputStream(keystoreData), null)
ApkSigner.readKeyCertificatePair(ks, cn, pass)
} catch (_: UnrecoverableKeyException) {
return false
} catch (_: IllegalArgumentException) {
return false
}
withContext(Dispatchers.IO) {
Files.write(keystorePath.toPath(), keystoreData)
}
updatePrefs(cn, pass)
return true
}
fun hasKeystore() = keystorePath.exists()
suspend fun export(target: OutputStream) {
withContext(Dispatchers.IO) {
Files.copy(keystorePath.toPath(), target)
}
}
}

View File

@ -0,0 +1,32 @@
package app.revanced.manager.domain.manager
import android.content.Context
import app.revanced.manager.domain.manager.base.BasePreferencesManager
import app.revanced.manager.ui.theme.Theme
class PreferencesManager(
context: Context
) : BasePreferencesManager(context, "settings") {
val dynamicColor = booleanPreference("dynamic_color", true)
val theme = enumPreference("theme", Theme.SYSTEM)
val api = stringPreference("api_url", "https://api.revanced.app")
val multithreadingDexFileWriter = booleanPreference("multithreading_dex_file_writer", true)
val useProcessRuntime = booleanPreference("use_process_runtime", false)
val patcherProcessMemoryLimit = intPreference("process_runtime_memory_limit", 700)
val disablePatchVersionCompatCheck = booleanPreference("disable_patch_version_compatibility_check", false)
val keystoreCommonName = stringPreference("keystore_cn", KeystoreManager.DEFAULT)
val keystorePass = stringPreference("keystore_pass", KeystoreManager.DEFAULT)
val preferSplits = booleanPreference("prefer_splits", false)
val firstLaunch = booleanPreference("first_launch", true)
val managerAutoUpdates = booleanPreference("manager_auto_updates", false)
val disableSelectionWarning = booleanPreference("disable_selection_warning", false)
val enableSelectionWarningCountdown = booleanPreference("enable_selection_warning_countdown", true)
val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true)
}

View File

@ -0,0 +1,133 @@
package app.revanced.manager.domain.manager.base
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.preferencesDataStore
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.domain.manager.base.BasePreferencesManager.Companion.editor
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
abstract class BasePreferencesManager(private val context: Context, name: String) {
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = name)
protected val dataStore get() = context.dataStore
suspend fun preload() {
dataStore.data.first()
}
suspend fun edit(block: EditorContext.() -> Unit) = dataStore.editor(block)
protected fun stringPreference(key: String, default: String) =
StringPreference(dataStore, key, default)
protected fun booleanPreference(key: String, default: Boolean) =
BooleanPreference(dataStore, key, default)
protected fun intPreference(key: String, default: Int) = IntPreference(dataStore, key, default)
protected fun floatPreference(key: String, default: Float) =
FloatPreference(dataStore, key, default)
protected inline fun <reified E : Enum<E>> enumPreference(
key: String,
default: E
) = EnumPreference(dataStore, key, default, enumValues())
companion object {
suspend inline fun DataStore<Preferences>.editor(crossinline block: EditorContext.() -> Unit) {
edit {
EditorContext(it).run(block)
}
}
}
}
class EditorContext(private val prefs: MutablePreferences) {
var <T> Preference<T>.value
get() = prefs.run { read() }
set(value) = prefs.run { write(value) }
}
abstract class Preference<T>(
private val dataStore: DataStore<Preferences>,
protected val default: T
) {
internal abstract fun Preferences.read(): T
internal abstract fun MutablePreferences.write(value: T)
val flow = dataStore.data.map { with(it) { read() } ?: default }.distinctUntilChanged()
suspend fun get() = flow.first()
fun getBlocking() = runBlocking { get() }
@Composable
fun getAsState() = flow.collectAsStateWithLifecycle(initialValue = remember {
getBlocking()
})
suspend fun update(value: T) = dataStore.editor {
this@Preference.value = value
}
}
class EnumPreference<E : Enum<E>>(
dataStore: DataStore<Preferences>,
key: String,
default: E,
private val enumValues: Array<E>
) : Preference<E>(dataStore, default) {
private val key = stringPreferencesKey(key)
override fun Preferences.read() =
this[key]?.let { name ->
enumValues.find { it.name == name }
} ?: default
override fun MutablePreferences.write(value: E) {
this[key] = value.name
}
}
abstract class BasePreference<T>(dataStore: DataStore<Preferences>, default: T) :
Preference<T>(dataStore, default) {
protected abstract val key: Preferences.Key<T>
override fun Preferences.read() = this[key] ?: default
override fun MutablePreferences.write(value: T) {
this[key] = value
}
}
class StringPreference(
dataStore: DataStore<Preferences>,
key: String,
default: String
) : BasePreference<String>(dataStore, default) {
override val key = stringPreferencesKey(key)
}
class BooleanPreference(
dataStore: DataStore<Preferences>,
key: String,
default: Boolean
) : BasePreference<Boolean>(dataStore, default) {
override val key = booleanPreferencesKey(key)
}
class IntPreference(
dataStore: DataStore<Preferences>,
key: String,
default: Int
) : BasePreference<Int>(dataStore, default) {
override val key = intPreferencesKey(key)
}
class FloatPreference(
dataStore: DataStore<Preferences>,
key: String,
default: Float
) : BasePreference<Float>(dataStore, default) {
override val key = floatPreferencesKey(key)
}

View File

@ -0,0 +1,63 @@
package app.revanced.manager.domain.repository
import android.app.Application
import android.content.Context
import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
import app.revanced.manager.network.downloader.AppDownloader
import kotlinx.coroutines.flow.distinctUntilChanged
import java.io.File
class DownloadedAppRepository(
app: Application,
db: AppDatabase
) {
private val dir = app.getDir("downloaded-apps", Context.MODE_PRIVATE)
private val dao = db.downloadedAppDao()
fun getAll() = dao.getAllApps().distinctUntilChanged()
fun getApkFileForApp(app: DownloadedApp): File = getApkFileForDir(dir.resolve(app.directory))
private fun getApkFileForDir(directory: File) = directory.listFiles()!!.first()
suspend fun download(
app: AppDownloader.App,
preferSplits: Boolean,
onDownload: suspend (downloadProgress: Pair<Float, Float>?) -> Unit = {},
): File {
this.get(app.packageName, app.version)?.let { downloaded ->
return getApkFileForApp(downloaded)
}
// Converted integers cannot contain / or .. unlike the package name or version, so they are safer to use here.
val relativePath = File(generateUid().toString())
val savePath = dir.resolve(relativePath).also { it.mkdirs() }
try {
app.download(savePath, preferSplits, onDownload)
dao.insert(DownloadedApp(
packageName = app.packageName,
version = app.version,
directory = relativePath,
))
} catch (e: Exception) {
savePath.deleteRecursively()
throw e
}
// Return the Apk file.
return getApkFileForDir(savePath)
}
suspend fun get(packageName: String, version: String) = dao.get(packageName, version)
suspend fun delete(downloadedApps: Collection<DownloadedApp>) {
downloadedApps.forEach {
dir.resolve(it.directory).deleteRecursively()
}
dao.delete(downloadedApps)
}
}

View File

@ -0,0 +1,51 @@
package app.revanced.manager.domain.repository
import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.apps.installed.AppliedPatch
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.util.PatchSelection
import kotlinx.coroutines.flow.distinctUntilChanged
class InstalledAppRepository(
db: AppDatabase
) {
private val dao = db.installedAppDao()
fun getAll() = dao.getAll().distinctUntilChanged()
suspend fun get(packageName: String) = dao.get(packageName)
suspend fun getAppliedPatches(packageName: String): PatchSelection =
dao.getPatchesSelection(packageName).mapValues { (_, patches) -> patches.toSet() }
suspend fun addOrUpdate(
currentPackageName: String,
originalPackageName: String,
version: String,
installType: InstallType,
patchSelection: PatchSelection
) {
dao.upsertApp(
InstalledApp(
currentPackageName = currentPackageName,
originalPackageName = originalPackageName,
version = version,
installType = installType
),
patchSelection.flatMap { (uid, patches) ->
patches.map { patch ->
AppliedPatch(
packageName = currentPackageName,
bundle = uid,
patchName = patch
)
}
}
)
}
suspend fun delete(installedApp: InstalledApp) {
dao.delete(installedApp)
}
}

View File

@ -0,0 +1,56 @@
package app.revanced.manager.domain.repository
import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
import app.revanced.manager.data.room.bundles.PatchBundleEntity
import app.revanced.manager.data.room.bundles.Source
import app.revanced.manager.data.room.bundles.VersionInfo
import io.ktor.http.*
import kotlinx.coroutines.flow.distinctUntilChanged
class PatchBundlePersistenceRepository(db: AppDatabase) {
private val dao = db.patchBundleDao()
suspend fun loadConfiguration(): List<PatchBundleEntity> {
val all = dao.all()
if (all.isEmpty()) {
dao.add(defaultSource)
return listOf(defaultSource)
}
return all
}
suspend fun reset() = dao.reset()
suspend fun create(name: String, source: Source, autoUpdate: Boolean = false) =
PatchBundleEntity(
uid = generateUid(),
name = name,
versionInfo = VersionInfo(),
source = source,
autoUpdate = autoUpdate
).also {
dao.add(it)
}
suspend fun delete(uid: Int) = dao.remove(uid)
suspend fun updateVersion(uid: Int, patches: String, integrations: String) =
dao.updateVersion(uid, patches, integrations)
suspend fun setAutoUpdate(uid: Int, value: Boolean) = dao.setAutoUpdate(uid, value)
fun getProps(id: Int) = dao.getPropsById(id).distinctUntilChanged()
private companion object {
val defaultSource = PatchBundleEntity(
uid = 0,
name = "Main",
versionInfo = VersionInfo(),
source = Source.API,
autoUpdate = false
)
}
}

View File

@ -0,0 +1,226 @@
package app.revanced.manager.domain.repository
import android.app.Application
import android.content.Context
import android.util.Log
import app.revanced.library.PatchUtils
import app.revanced.manager.R
import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.data.room.bundles.PatchBundleEntity
import app.revanced.manager.domain.bundles.APIPatchBundle
import app.revanced.manager.domain.bundles.JsonPatchBundle
import app.revanced.manager.data.room.bundles.Source as SourceInfo
import app.revanced.manager.domain.bundles.LocalPatchBundle
import app.revanced.manager.domain.bundles.RemotePatchBundle
import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.patcher.patch.PatchBundleInfo
import app.revanced.manager.patcher.patch.PatchBundleLoader
import app.revanced.manager.util.flatMapLatestAndCombine
import app.revanced.manager.util.tag
import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.InputStream
class PatchBundleRepository(
private val app: Application,
private val persistenceRepo: PatchBundlePersistenceRepository,
private val networkInfo: NetworkInfo,
private val prefs: PreferencesManager,
) {
private val bundlesDir = app.getDir("patch_bundles", Context.MODE_PRIVATE)
private val _sources: MutableStateFlow<Map<Int, PatchBundleSource>> =
MutableStateFlow(emptyMap())
val sources = _sources.map { it.values.toList() }
val bundles = sources.flatMapLatestAndCombine(
combiner = {
it.mapNotNull { (uid, state) ->
val bundle = state.patchBundleOrNull() ?: return@mapNotNull null
uid to bundle
}.toMap()
}
) {
it.state.map { state -> it.uid to state }
}
val bundleInfoFlow = sources.flatMapLatestAndCombine(
transformer = { source ->
source.state.map {
source to it
}
},
combiner = { states ->
val patchBundleLoader by lazy {
PatchBundleLoader(states.mapNotNull { (_, state) -> state.patchBundleOrNull() })
}
states.mapNotNull { (source, state) ->
val bundle = state.patchBundleOrNull() ?: return@mapNotNull null
try {
source.uid to PatchBundleInfo.Global(
source.name,
source.uid,
patchBundleLoader.loadMetadata(bundle)
)
} catch (t: Throwable) {
Log.e(tag, "Failed to load patches from ${source.name}", t)
source.markAsFailed(t)
null
}
}.toMap()
}
).flowOn(Dispatchers.Default)
fun scopedBundleInfoFlow(packageName: String, version: String) = bundleInfoFlow.map {
it.map { (_, bundle) ->
bundle.forPackage(
packageName,
version
)
}
}
val suggestedVersions = bundleInfoFlow.map {
val allPatches =
it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet()
PatchUtils.getMostCommonCompatibleVersions(allPatches, countUnusedPatches = true)
.mapValues { (_, versions) ->
if (versions.keys.size < 2)
return@mapValues versions.keys.firstOrNull()
// The entries are ordered from most compatible to least compatible.
// If there are entries with the same number of compatible patches, older versions will be first, which is undesirable.
// This means we have to pick the last entry we find that has the highest patch count.
// The order may change in future versions of ReVanced Library.
var currentHighestPatchCount = -1
versions.entries.last { (_, patchCount) ->
if (patchCount >= currentHighestPatchCount) {
currentHighestPatchCount = patchCount
true
} else false
}.key
}
}
suspend fun isVersionAllowed(packageName: String, version: String) =
withContext(Dispatchers.Default) {
if (!prefs.suggestedVersionSafeguard.get()) return@withContext true
val suggestedVersion = suggestedVersions.first()[packageName] ?: return@withContext true
suggestedVersion == version
}
/**
* Get the directory of the [PatchBundleSource] with the specified [uid], creating it if needed.
*/
private fun directoryOf(uid: Int) = bundlesDir.resolve(uid.toString()).also { it.mkdirs() }
private fun PatchBundleEntity.load(): PatchBundleSource {
val dir = directoryOf(uid)
return when (source) {
is SourceInfo.Local -> LocalPatchBundle(name, uid, dir)
is SourceInfo.API -> APIPatchBundle(name, uid, dir, SourceInfo.API.SENTINEL)
is SourceInfo.Remote -> JsonPatchBundle(
name,
uid,
dir,
source.url.toString()
)
}
}
suspend fun reload() = withContext(Dispatchers.Default) {
val entities = persistenceRepo.loadConfiguration().onEach {
Log.d(tag, "Bundle: $it")
}
_sources.value = entities.associate {
it.uid to it.load()
}
}
suspend fun reset() = withContext(Dispatchers.Default) {
persistenceRepo.reset()
_sources.value = emptyMap()
bundlesDir.apply {
deleteRecursively()
mkdirs()
}
reload()
}
suspend fun remove(bundle: PatchBundleSource) = withContext(Dispatchers.Default) {
persistenceRepo.delete(bundle.uid)
directoryOf(bundle.uid).deleteRecursively()
_sources.update {
it.filterKeys { key ->
key != bundle.uid
}
}
}
private fun addBundle(patchBundle: PatchBundleSource) =
_sources.update { it.toMutableMap().apply { put(patchBundle.uid, patchBundle) } }
suspend fun createLocal(name: String, patches: InputStream, integrations: InputStream?) {
val id = persistenceRepo.create(name, SourceInfo.Local).uid
val bundle = LocalPatchBundle(name, id, directoryOf(id))
bundle.replace(patches, integrations)
addBundle(bundle)
}
suspend fun createRemote(name: String, url: String, autoUpdate: Boolean) {
val entity = persistenceRepo.create(name, SourceInfo.from(url), autoUpdate)
addBundle(entity.load())
}
private suspend inline fun <reified T> getBundlesByType() =
sources.first().filterIsInstance<T>()
suspend fun reloadApiBundles() {
getBundlesByType<APIPatchBundle>().forEach {
it.deleteLocalFiles()
}
reload()
}
suspend fun redownloadRemoteBundles() =
getBundlesByType<RemotePatchBundle>().forEach { it.downloadLatest() }
suspend fun updateCheck() =
uiSafe(app, R.string.source_download_fail, "Failed to update bundles") {
coroutineScope {
if (!networkInfo.isSafe()) {
Log.d(tag, "Skipping update check because the network is down or metered.")
return@coroutineScope
}
getBundlesByType<RemotePatchBundle>().forEach {
launch {
if (!it.propsFlow().first().autoUpdate) return@launch
Log.d(tag, "Updating patch bundle: ${it.name}")
it.update()
}
}
}
}
}

View File

@ -0,0 +1,89 @@
package app.revanced.manager.domain.repository
import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.options.Option
import app.revanced.manager.data.room.options.OptionGroup
import app.revanced.manager.util.Options
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.floatOrNull
import kotlinx.serialization.json.intOrNull
class PatchOptionsRepository(db: AppDatabase) {
private val dao = db.optionDao()
private suspend fun getOrCreateGroup(bundleUid: Int, packageName: String) =
dao.getGroupId(bundleUid, packageName) ?: OptionGroup(
uid = AppDatabase.generateUid(),
patchBundle = bundleUid,
packageName = packageName
).also { dao.createOptionGroup(it) }.uid
suspend fun getOptions(packageName: String): Options {
val options = dao.getOptions(packageName)
// Bundle -> Patches
return buildMap<Int, MutableMap<String, MutableMap<String, Any?>>>(options.size) {
options.forEach { (sourceUid, bundlePatchOptionsList) ->
// Patches -> Patch options
this[sourceUid] = bundlePatchOptionsList.fold(mutableMapOf()) { bundlePatchOptions, option ->
val patchOptions = bundlePatchOptions.getOrPut(option.patchName, ::mutableMapOf)
patchOptions[option.key] = deserialize(option.value)
bundlePatchOptions
}
}
}
}
suspend fun saveOptions(packageName: String, options: Options) =
dao.updateOptions(options.entries.associate { (sourceUid, bundlePatchOptions) ->
val groupId = getOrCreateGroup(sourceUid, packageName)
groupId to bundlePatchOptions.flatMap { (patchName, patchOptions) ->
patchOptions.mapNotNull { (key, value) ->
val serialized = serialize(value)
?: return@mapNotNull null // Don't save options that we can't serialize.
Option(groupId, patchName, key, serialized)
}
}
})
fun getPackagesWithSavedOptions() =
dao.getPackagesWithOptions().map(Iterable<String>::toSet).distinctUntilChanged()
suspend fun clearOptionsForPackage(packageName: String) = dao.clearForPackage(packageName)
suspend fun clearOptionsForPatchBundle(uid: Int) = dao.clearForPatchBundle(uid)
suspend fun reset() = dao.reset()
private companion object {
fun deserialize(value: String): Any? {
val primitive = Json.decodeFromString<JsonPrimitive>(value)
return when {
primitive.isString -> primitive.content
primitive is JsonNull -> null
else -> primitive.booleanOrNull ?: primitive.intOrNull ?: primitive.floatOrNull
}
}
fun serialize(value: Any?): String? {
val primitive = when (value) {
null -> JsonNull
is String -> JsonPrimitive(value)
is Int -> JsonPrimitive(value)
is Float -> JsonPrimitive(value)
is Boolean -> JsonPrimitive(value)
else -> return null
}
return Json.encodeToString(primitive)
}
}
}

View File

@ -0,0 +1,47 @@
package app.revanced.manager.domain.repository
import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
import app.revanced.manager.data.room.selection.PatchSelection
class PatchSelectionRepository(db: AppDatabase) {
private val dao = db.selectionDao()
private suspend fun getOrCreateSelection(bundleUid: Int, packageName: String) =
dao.getSelectionId(bundleUid, packageName) ?: PatchSelection(
uid = generateUid(),
patchBundle = bundleUid,
packageName = packageName
).also { dao.createSelection(it) }.uid
suspend fun getSelection(packageName: String): Map<Int, Set<String>> =
dao.getSelectedPatches(packageName).mapValues { it.value.toSet() }
suspend fun updateSelection(packageName: String, selection: Map<Int, Set<String>>) =
dao.updateSelections(selection.mapKeys { (sourceUid, _) ->
getOrCreateSelection(
sourceUid,
packageName
)
})
suspend fun clearSelection(packageName: String) {
dao.clearForPackage(packageName)
}
suspend fun reset() = dao.reset()
suspend fun export(bundleUid: Int): SerializedSelection = dao.exportSelection(bundleUid)
suspend fun import(bundleUid: Int, selection: SerializedSelection) {
dao.clearForPatchBundle(bundleUid)
dao.updateSelections(selection.entries.associate { (packageName, patches) ->
getOrCreateSelection(bundleUid, packageName) to patches.toSet()
})
}
}
/**
* A [Map] of package name -> selected patches.
*/
typealias SerializedSelection = Map<String, List<String>>

View File

@ -0,0 +1,7 @@
package app.revanced.manager.domain.worker
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
abstract class Worker<ARGS>(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters)

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