mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2025-06-28 21:05:39 +08:00
Compare commits
353 Commits
v1.25.0
...
feat/hide-
Author | SHA1 | Date | |
---|---|---|---|
4f8fd23a87 | |||
98c0262d10 | |||
f412c99c60 | |||
62d5863846 | |||
e71e6fb797 | |||
111e8808a0 | |||
a30ff45111 | |||
eb03daaebd | |||
9e2a7917ba | |||
88fc639751 | |||
2fc099568e | |||
e641b91e57 | |||
d09e9b79cb | |||
3f5d04a513 | |||
3ad027007f | |||
813ec4dbe2 | |||
9fac5126df | |||
5e03cf2307 | |||
4d522664b6 | |||
0058b5f504 | |||
8fdd5074a0 | |||
5555373deb | |||
6e57a6e977 | |||
10001b492b | |||
d37ed050bc | |||
ee07621e37 | |||
fc05f95837 | |||
d5c63ead26 | |||
1956982060 | |||
e10e5e4e3f | |||
ede1ab5ed4 | |||
1092188ab0 | |||
f348eba115 | |||
3d234820a3 | |||
cd06d36f68 | |||
242c4570ce | |||
71b73a3b42 | |||
067020f38f | |||
2aef67872d | |||
818dc09aa4 | |||
a762969966 | |||
74338931b8 | |||
0ab424bfdb | |||
fff1a41fee | |||
7644a74648 | |||
9db3bd5b3f | |||
b81bd17fbc | |||
cf3866f892 | |||
5d3a81f4b9 | |||
f9831d4da5 | |||
8a20d8cf9b | |||
49f75f9edd | |||
9916e4da4d | |||
2ec1c0238d | |||
9dc716b1c8 | |||
31fb8b1404 | |||
0685479d53 | |||
20c13ee71c | |||
cf322147d5 | |||
b4c37e6ddc | |||
697386c36c | |||
f6f72387b9 | |||
d201bdc422 | |||
2055400565 | |||
10e7e4b39f | |||
edb4e421e2 | |||
747017a5f9 | |||
e992a99783 | |||
e869db0555 | |||
936a9efd0b | |||
edc11b6e1d | |||
32f2710763 | |||
4257c32bf5 | |||
c18901c35b | |||
f126fe9fa8 | |||
4cbd480e84 | |||
afc72ffd85 | |||
cdc69ea8ff | |||
52a89b1638 | |||
641d518b6e | |||
2eca45d397 | |||
de6fddf405 | |||
45f32040f8 | |||
42ef2ca99d | |||
563f2f37f7 | |||
881b13740a | |||
d39804f7ed | |||
b8a85c4891 | |||
37e9630b9c | |||
40492b67d1 | |||
f655a6e03a | |||
36f864efbb | |||
a995f43b7b | |||
1ee1330e47 | |||
3f4a234915 | |||
032ca39cf6 | |||
6f9a984541 | |||
e6e043f168 | |||
7c7fb7b343 | |||
b26fe30861 | |||
f99cdfe926 | |||
ec0a077539 | |||
a22158d070 | |||
48fe3a707e | |||
8d3d500b7b | |||
d8248cc915 | |||
397a1f8f9c | |||
a0f187354f | |||
1bf004ddee | |||
495100dea9 | |||
e3bd8a8b22 | |||
d987ac6c7a | |||
9883dcd0a7 | |||
d63133189d | |||
39fbb87010 | |||
f197760efd | |||
fa13d4a538 | |||
71b5f539c1 | |||
603a827e45 | |||
47bdc69a43 | |||
1ce56af3b1 | |||
a12c5c583b | |||
5455cf20ab | |||
75fcdb139c | |||
269fa79136 | |||
16fea59605 | |||
c7d183ee8d | |||
413083c58d | |||
1f671aba33 | |||
6bfd9098d6 | |||
4e7d96e91d | |||
ac0a036035 | |||
2a1445d61f | |||
0df39a1136 | |||
634d793839 | |||
afd6c5d6b7 | |||
ab0682cc5c | |||
60ca901ac7 | |||
91ca5be57a | |||
ac47a7eaa4 | |||
ce134224a8 | |||
ca49d3a465 | |||
5d7f9d1387 | |||
8d5d86fea8 | |||
6709505e9e | |||
088de60c91 | |||
ef041869e5 | |||
16cdc7aca4 | |||
39536c0e18 | |||
607d8b67c9 | |||
0b9889ea44 | |||
4acef776b2 | |||
e186dfdaa9 | |||
c0f3d02e6f | |||
36c8f59d6f | |||
f38b31a591 | |||
3232bb10e6 | |||
b7cb6b94f5 | |||
aa6e612fba | |||
d9d7b98409 | |||
4fdd6bbe5f | |||
439f6250f3 | |||
d55abf5dda | |||
a8b9d9316f | |||
55c7800f39 | |||
d9eb1c42bc | |||
8cd617e32d | |||
a17a05995a | |||
de4e616dcc | |||
12b00e5c8d | |||
9cab91959e | |||
bd9778a3d1 | |||
62a5fce66c | |||
2bd84636d6 | |||
ac561e7aca | |||
59daceef99 | |||
1dc41badd9 | |||
1a83315424 | |||
3c5776214f | |||
5fff0a2923 | |||
8df7f2992d | |||
7741394c9c | |||
25bd91debc | |||
7fe4724e10 | |||
123ae37524 | |||
172604fcdb | |||
7d887c73e8 | |||
6abaac25d9 | |||
cc897840e2 | |||
757840b76f | |||
50e8d1f8f4 | |||
65f8d38c59 | |||
e70c10adbd | |||
64ec73d821 | |||
32e8a37f33 | |||
5290713504 | |||
18cfb56b45 | |||
4b12ae1531 | |||
9df98edca5 | |||
c3af6acb2c | |||
7ba00cafd9 | |||
5aefb3bc59 | |||
212e55ffd8 | |||
bf54d38c91 | |||
cee2240cdc | |||
4c1ad868a9 | |||
f5b3b29d6d | |||
8f5449527d | |||
8f6d720454 | |||
56a4a7043d | |||
5762859906 | |||
608bac6854 | |||
723f9cd98c | |||
abf4d91703 | |||
d8392ad3eb | |||
39caad18a5 | |||
6437f7bb65 | |||
e232044157 | |||
f78b56ef0a | |||
ca3c9af3b8 | |||
b8b2e74151 | |||
1f8341ac42 | |||
0964f15475 | |||
63fd7957c6 | |||
65377ffd9e | |||
f79320c013 | |||
cf71ea26ec | |||
ee96c37c20 | |||
a86923aee1 | |||
e0f8d06152 | |||
4cb4ce298a | |||
36de61a57f | |||
6f2ca5bb89 | |||
940885768d | |||
fc577b4c3e | |||
bf10af2ae2 | |||
b4dfcf1bb4 | |||
0b0ba21852 | |||
42e0346e25 | |||
212db84d0b | |||
e6eb8accf2 | |||
8bd73c3afa | |||
5369a25fa2 | |||
eeae46a415 | |||
c0badbe96b | |||
2bb51c136a | |||
3cfa4ea6d6 | |||
f01adf5eb0 | |||
a0b92554e9 | |||
ac4c7e06e7 | |||
0f9a6f4340 | |||
9586a9c0dd | |||
f6563b265b | |||
7aea9473de | |||
3f059d7748 | |||
7e3c31c4b2 | |||
1707a9690a | |||
379ce917a9 | |||
299aaa2b68 | |||
5cf5e87fa8 | |||
55f22562eb | |||
272d911464 | |||
6beb34baa8 | |||
61de0b67fa | |||
aec8cec9b8 | |||
83b9573b52 | |||
21d99a1f24 | |||
1331479072 | |||
b472a36a9a | |||
3238fcdae7 | |||
cd2587b1fd | |||
879884a9fa | |||
5d3b963682 | |||
955e7a4f1c | |||
d2dcd4209d | |||
6299ff5b48 | |||
94a4dbaba1 | |||
c36deea045 | |||
7030d43aa5 | |||
aa02e9f8cf | |||
37e177b56e | |||
453f4da8ec | |||
400163b820 | |||
4ae9904c8a | |||
fe5e191cb5 | |||
d9d83df9de | |||
8dd8f88d2b | |||
01fd4c8ffa | |||
7ac3bb74e0 | |||
3b65cd0edc | |||
a9606728bf | |||
4d4f1a242c | |||
6b7143dd8f | |||
7e4ee00cb2 | |||
4868c45b43 | |||
81f485da6b | |||
18cbe51e6b | |||
149c8cc8b2 | |||
0dccb8c27b | |||
4302ea8832 | |||
1eac42dab8 | |||
9dd74f1f22 | |||
923ce74735 | |||
2d9f9adfee | |||
9a55e51a3a | |||
5681c917c5 | |||
6309e8bdf5 | |||
535efa3d73 | |||
b8a51d32f5 | |||
919b6b7014 | |||
971277ed39 | |||
7ce4de7a8b | |||
9591f4e14f | |||
27426b1390 | |||
fcb75dd780 | |||
1be9c9c1bd | |||
e088d053ab | |||
ffa8d9c063 | |||
7a5596a281 | |||
9f46f74357 | |||
36c4e2dfe0 | |||
5cb31dbe9d | |||
399fc98dec | |||
c22371e0c5 | |||
a4842c078b | |||
c332760786 | |||
ea4247c688 | |||
fec8c0cc14 | |||
9b585c73fb | |||
c695fa525f | |||
93f3e27d48 | |||
52ab7937bd | |||
762bfa8514 | |||
ca20996b62 | |||
ad14818de8 | |||
32839656f8 | |||
a48faad17a | |||
40487923f9 | |||
f1656c6d1e | |||
4c3dbbd8d5 | |||
4088ed747e | |||
bca8df8efd | |||
54f0a69596 | |||
9065c0d260 | |||
cb0150a0f9 | |||
ec0f7e3f7a | |||
e5d898f025 | |||
52bdb1cd6a | |||
49f9dfcf95 | |||
9536cdcae1 | |||
57e2632f38 | |||
b372f7ee84 | |||
70e8253b63 |
120
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
120
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
@ -1,120 +0,0 @@
|
|||||||
name: 🐞 Bug report
|
|
||||||
description: Report a very clearly broken issue.
|
|
||||||
title: 'bug: <title>'
|
|
||||||
labels: [bug]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
# ReVanced Manager bug report
|
|
||||||
|
|
||||||
Important to note that your issue may have already been reported before. Please check for existing issues [here](https://github.com/revanced/revanced-manager/labels/bug).
|
|
||||||
|
|
||||||
- type: dropdown
|
|
||||||
attributes:
|
|
||||||
label: Type
|
|
||||||
options:
|
|
||||||
- Error while running the manager
|
|
||||||
- Error at runtime
|
|
||||||
- Cosmetic
|
|
||||||
- Other
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Bug description
|
|
||||||
description: How did you find the bug? Any additional details that might help?
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Steps to reproduce
|
|
||||||
description: Add the steps to reproduce this bug, including your environment.
|
|
||||||
placeholder: Step 1. Download some files. Step 2. ...
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Android version
|
|
||||||
description: Android version used.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Manager version
|
|
||||||
description: Manager version used.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Target package name
|
|
||||||
description: App you tried to patch.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Target package version.
|
|
||||||
description: Version of the app you tried to patch.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
attributes:
|
|
||||||
label: Installation type
|
|
||||||
options:
|
|
||||||
- Non-root
|
|
||||||
- Root
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Patches selected.
|
|
||||||
description: Patches you selected for the app.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Device logs (exported using Manager settings).
|
|
||||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so there is no need for backticks.
|
|
||||||
render: shell
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Installer logs (exported using Installer menu option) [unneeded if the issue is not during patching].
|
|
||||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so there is no need for backticks.
|
|
||||||
render: shell
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Screenshots or video
|
|
||||||
description: Add screenshots or videos that show the bug here.
|
|
||||||
placeholder: Drag and drop the screenshots/videos into this box.
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Solution
|
|
||||||
description: If applicable, add a possible solution.
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Additional context
|
|
||||||
description: Add additional context here.
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: checkboxes
|
|
||||||
id: acknowledgments
|
|
||||||
attributes:
|
|
||||||
label: Acknowledgments
|
|
||||||
description: Your issue will be closed if you haven't done these steps.
|
|
||||||
options:
|
|
||||||
- label: I have searched the existing issues; this is new and no duplicate or related to another open issue.
|
|
||||||
required: true
|
|
||||||
- label: I have written a short but informative title.
|
|
||||||
required: true
|
|
||||||
- label: I properly filled out all of the requested information in this issue.
|
|
||||||
required: true
|
|
||||||
- label: The issue is solely related to ReVanced Manager and not caused by patches.
|
|
||||||
required: true
|
|
109
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
109
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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**: Search for existing bug reports [here](https://github.com/ReVanced/revanced-manager/issues?q=label%3A%22Bug+report%22).
|
||||||
|
- **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, downloader and settings if applicable
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Patch logs
|
||||||
|
description: Patch logs can be exported by clicking on the "Logs" button in the "Patcher" screen, when patching finishes.
|
||||||
|
render: shell
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Debug logs
|
||||||
|
description: Debug logs can be exported by clicking on "Export debug logs" in "Settings" > "Advanced".
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Acknowledgements
|
||||||
|
description: Your bug report will be closed if you don't follow the checklist below.
|
||||||
|
options:
|
||||||
|
- label: I have checked all open and closed bug reports and this is not a duplicate.
|
||||||
|
required: true
|
||||||
|
- label: I have chosen an appropriate title.
|
||||||
|
required: true
|
||||||
|
- label: All requested information has been provided properly.
|
||||||
|
required: true
|
||||||
|
- label: The bug is only related to ReVanced Manager.
|
||||||
|
required: true
|
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1 +1,5 @@
|
|||||||
blank_issues_enabled: false
|
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!
|
52
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
52
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
@ -1,52 +0,0 @@
|
|||||||
name: ⭐ Feature request
|
|
||||||
description: Create a detailed feature request.
|
|
||||||
title: 'feat: <title>'
|
|
||||||
labels: [feature-request]
|
|
||||||
body:
|
|
||||||
- type: dropdown
|
|
||||||
attributes:
|
|
||||||
label: Type
|
|
||||||
options:
|
|
||||||
- Functionality
|
|
||||||
- Cosmetic
|
|
||||||
- Other
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Issue
|
|
||||||
description: What is the current problem. Why does it require a feature request?
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Feature
|
|
||||||
description: Describe your feature in detail. How does it solve the issue?
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Motivation
|
|
||||||
description: Why should your feature should be considered?
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Additional context
|
|
||||||
description: Add additional context here.
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: checkboxes
|
|
||||||
id: acknowledgements
|
|
||||||
attributes:
|
|
||||||
label: Acknowledgements
|
|
||||||
description: Your issue will be closed if you haven't done these steps.
|
|
||||||
options:
|
|
||||||
- label: I have searched the existing issues and this is a new and no duplicate or related to another open issue.
|
|
||||||
required: true
|
|
||||||
- label: I have written a short but informative title.
|
|
||||||
required: true
|
|
||||||
- label: I filled out all of the requested information in this issue properly.
|
|
||||||
required: true
|
|
||||||
- label: The issue is related solely to the ReVanced Manager
|
|
||||||
required: true
|
|
103
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
103
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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**: Search for existing feature requests [here](https://github.com/ReVanced/revanced-manager/issues?q=label%3A%22Feature+request%22).
|
||||||
|
- **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
|
||||||
|
- 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: I have checked all open and closed feature requests and this is not a duplicate
|
||||||
|
required: true
|
||||||
|
- label: I have chosen an appropriate title.
|
||||||
|
required: true
|
||||||
|
- label: All requested information has been provided properly.
|
||||||
|
required: true
|
||||||
|
- label: The feature request is only related to ReVanced Manager.
|
||||||
|
required: true
|
2
.github/config.yaml
vendored
2
.github/config.yaml
vendored
@ -1,2 +1,2 @@
|
|||||||
firstPRMergeComment: >
|
firstPRMergeComment: >
|
||||||
Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) if you want to receive a contributor role.
|
Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) to receive a role for your contribution.
|
||||||
|
38
.github/workflows/analyze.yml
vendored
38
.github/workflows/analyze.yml
vendored
@ -1,38 +0,0 @@
|
|||||||
name: Analyze Code
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "dev" ]
|
|
||||||
paths:
|
|
||||||
- "**.dart"
|
|
||||||
- ".github/workflows/analyze.yml"
|
|
||||||
pull_request:
|
|
||||||
branches: [ "main", "dev" ]
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
- reopened
|
|
||||||
- synchronize
|
|
||||||
- ready_for_review
|
|
||||||
paths:
|
|
||||||
- "**.dart"
|
|
||||||
- ".github/workflows/analyze.yml"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: "Static analysis & format check"
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Setup Flutter
|
|
||||||
uses: subosito/flutter-action@v2
|
|
||||||
with:
|
|
||||||
channel: 'stable'
|
|
||||||
cache: true
|
|
||||||
- name: Install Flutter dependencies
|
|
||||||
run: flutter pub get
|
|
||||||
- name: Generate files with Builder
|
|
||||||
run: flutter packages pub run build_runner build --delete-conflicting-outputs
|
|
||||||
- name: Analyze code
|
|
||||||
uses: ValentinVignal/action-dart-analyze@v0.15
|
|
||||||
with:
|
|
||||||
fail-on: warning
|
|
25
.github/workflows/build_pull_request.yml
vendored
Normal file
25
.github/workflows/build_pull_request.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
name: Build pull request
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Cache Gradle
|
||||||
|
uses: burrunan/gradle-cache-action@v1
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: ./gradlew build --no-daemon
|
26
.github/workflows/open_pull_request.yml
vendored
Normal file
26
.github/workflows/open_pull_request.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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
|
45
.github/workflows/pr-build.yml
vendored
45
.github/workflows/pr-build.yml
vendored
@ -1,45 +0,0 @@
|
|||||||
name: PR Build
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- ".github/workflows/pr-build.yml"
|
|
||||||
- "android/**"
|
|
||||||
- "assets/**"
|
|
||||||
- "lib/**"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
# Make sure the release step uses its own credentials:
|
|
||||||
# https://github.com/cycjimmy/semantic-release-action#private-packages
|
|
||||||
persist-credentials: false
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Setup JDK
|
|
||||||
uses: actions/setup-java@v3
|
|
||||||
with:
|
|
||||||
java-version: '11'
|
|
||||||
distribution: 'zulu'
|
|
||||||
- name: Setup Flutter
|
|
||||||
uses: subosito/flutter-action@v2
|
|
||||||
with:
|
|
||||||
channel: 'stable'
|
|
||||||
cache: true
|
|
||||||
- name: Install Flutter dependencies
|
|
||||||
run: flutter pub get
|
|
||||||
- name: Generate files with Builder
|
|
||||||
run: flutter packages pub run build_runner build --delete-conflicting-outputs
|
|
||||||
- name: Build with Flutter
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: flutter build apk --debug
|
|
||||||
- name: Upload build
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: revanced-manager
|
|
||||||
path: build/app/outputs/flutter-apk/app-debug.apk
|
|
50
.github/workflows/release-build.yml
vendored
50
.github/workflows/release-build.yml
vendored
@ -1,50 +0,0 @@
|
|||||||
name: "Release Build"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Set env
|
|
||||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
|
||||||
- name: Set up JDK 11
|
|
||||||
uses: actions/setup-java@v3
|
|
||||||
with:
|
|
||||||
java-version: "11"
|
|
||||||
distribution: "zulu"
|
|
||||||
- uses: subosito/flutter-action@v2
|
|
||||||
with:
|
|
||||||
channel: "stable"
|
|
||||||
- name: Set up Flutter
|
|
||||||
run: flutter pub get
|
|
||||||
- name: Generate files with Builder
|
|
||||||
run: flutter packages pub run build_runner build --delete-conflicting-outputs
|
|
||||||
- name: Build with Flutter
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
|
||||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
|
||||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
|
|
||||||
run: flutter build apk
|
|
||||||
- name: Sign APK
|
|
||||||
id: sign_apk
|
|
||||||
uses: ilharp/sign-android-release@v1
|
|
||||||
with:
|
|
||||||
releaseDir: build/app/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
|
|
64
.github/workflows/release.yml
vendored
Normal file
64
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Release
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
id-token: write
|
||||||
|
attestations: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '17'
|
||||||
|
|
||||||
|
- name: Cache Gradle
|
||||||
|
uses: burrunan/gradle-cache-action@v1
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: ./gradlew assembleRelease
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "lts/*"
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Setup keystore
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.KEYSTORE }}" | base64 --decode > "app/keystore.jks"
|
||||||
|
|
||||||
|
- name: Semantic Release
|
||||||
|
uses: cycjimmy/semantic-release-action@v4
|
||||||
|
id: semantic
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||||
|
KEYSTORE_ENTRY_ALIAS: ${{ secrets.KEYSTORE_ENTRY_ALIAS }}
|
||||||
|
KEYSTORE_ENTRY_PASSWORD: ${{ secrets.KEYSTORE_ENTRY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Attest
|
||||||
|
if: steps.semantic.outputs.new_release_published == 'true'
|
||||||
|
uses: actions/attest-build-provenance@v2
|
||||||
|
with:
|
||||||
|
subject-path: build/app/outputs/apk/release/revanced-manager-*.apk
|
@ -11,9 +11,9 @@ jobs:
|
|||||||
name: Dispatch event to documentation repository
|
name: Dispatch event to documentation repository
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
steps:
|
steps:
|
||||||
- uses: peter-evans/repository-dispatch@v2
|
- uses: peter-evans/repository-dispatch@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }}
|
token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }}
|
||||||
repository: revanced/revanced-documentation
|
repository: revanced/revanced-documentation
|
||||||
event-type: update-documentation
|
event-type: update-documentation
|
||||||
client-payload: '{"repo": "${{ github.event.repository.name }}", "ref": "${{ github.ref }}"}'
|
client-payload: '{"repo": "${{ github.event.repository.name }}", "ref": "${{ github.ref }}"}'
|
152
.gitignore
vendored
152
.gitignore
vendored
@ -1,144 +1,12 @@
|
|||||||
# Miscellaneous
|
|
||||||
*.class
|
|
||||||
*.lock
|
|
||||||
*.log
|
|
||||||
*.pyc
|
|
||||||
*.swp
|
|
||||||
.DS_Store
|
|
||||||
.atom/
|
|
||||||
.buildlog/
|
|
||||||
.history
|
|
||||||
.svn/
|
|
||||||
|
|
||||||
# IntelliJ related
|
|
||||||
*.iml
|
*.iml
|
||||||
*.ipr
|
.gradle
|
||||||
*.iws
|
/local.properties
|
||||||
.idea/
|
/.idea
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
|
local.properties
|
||||||
|
|
||||||
# Visual Studio Code related
|
.kotlin/
|
||||||
.classpath
|
|
||||||
.project
|
|
||||||
.settings/
|
|
||||||
|
|
||||||
# Flutter repo-specific
|
|
||||||
/bin/cache/
|
|
||||||
/bin/mingit/
|
|
||||||
/dev/benchmarks/mega_gallery/
|
|
||||||
/dev/bots/.recipe_deps
|
|
||||||
/dev/bots/android_tools/
|
|
||||||
/dev/docs/doc/
|
|
||||||
/dev/docs/flutter.docs.zip
|
|
||||||
/dev/docs/lib/
|
|
||||||
/dev/docs/pubspec.yaml
|
|
||||||
/dev/integration_tests/**/xcuserdata
|
|
||||||
/dev/integration_tests/**/Pods
|
|
||||||
/packages/flutter/coverage/
|
|
||||||
version
|
|
||||||
|
|
||||||
# packages file containing multi-root paths
|
|
||||||
.packages.generated
|
|
||||||
|
|
||||||
# Flutter/Dart/Pub related
|
|
||||||
**/doc/api/
|
|
||||||
**/*.g.dart
|
|
||||||
**/*.locator.dart
|
|
||||||
**/*.router.dart
|
|
||||||
.dart_tool/
|
|
||||||
.flutter-plugins
|
|
||||||
.flutter-plugins-dependencies
|
|
||||||
**/generated_plugin_registrant.dart
|
|
||||||
.packages
|
|
||||||
.pub-cache/
|
|
||||||
.pub/
|
|
||||||
build/
|
|
||||||
flutter_*.png
|
|
||||||
linked_*.ds
|
|
||||||
unlinked.ds
|
|
||||||
unlinked_spec.ds
|
|
||||||
|
|
||||||
# Android related
|
|
||||||
**/android/**/gradle-wrapper.jar
|
|
||||||
**/android/.gradle
|
|
||||||
**/android/captures/
|
|
||||||
**/android/gradlew
|
|
||||||
**/android/gradlew.bat
|
|
||||||
**/android/local.properties
|
|
||||||
**/android/**/GeneratedPluginRegistrant.java
|
|
||||||
**/android/key.properties
|
|
||||||
*.jks
|
|
||||||
|
|
||||||
# iOS/XCode related
|
|
||||||
**/ios/**/*.mode1v3
|
|
||||||
**/ios/**/*.mode2v3
|
|
||||||
**/ios/**/*.moved-aside
|
|
||||||
**/ios/**/*.pbxuser
|
|
||||||
**/ios/**/*.perspectivev3
|
|
||||||
**/ios/**/*sync/
|
|
||||||
**/ios/**/.sconsign.dblite
|
|
||||||
**/ios/**/.tags*
|
|
||||||
**/ios/**/.vagrant/
|
|
||||||
**/ios/**/DerivedData/
|
|
||||||
**/ios/**/Icon?
|
|
||||||
**/ios/**/Pods/
|
|
||||||
**/ios/**/.symlinks/
|
|
||||||
**/ios/**/profile
|
|
||||||
**/ios/**/xcuserdata
|
|
||||||
**/ios/.generated/
|
|
||||||
**/ios/Flutter/.last_build_id
|
|
||||||
**/ios/Flutter/App.framework
|
|
||||||
**/ios/Flutter/Flutter.framework
|
|
||||||
**/ios/Flutter/Flutter.podspec
|
|
||||||
**/ios/Flutter/Generated.xcconfig
|
|
||||||
**/ios/Flutter/app.flx
|
|
||||||
**/ios/Flutter/app.zip
|
|
||||||
**/ios/Flutter/flutter_assets/
|
|
||||||
**/ios/Flutter/flutter_export_environment.sh
|
|
||||||
**/ios/ServiceDefinitions.json
|
|
||||||
**/ios/Runner/GeneratedPluginRegistrant.*
|
|
||||||
|
|
||||||
# macOS related
|
|
||||||
**/macos/Flutter/GeneratedPluginRegistrant.swift
|
|
||||||
**/macos/Flutter/Flutter-Debug.xcconfig
|
|
||||||
**/macos/Flutter/Flutter-Release.xcconfig
|
|
||||||
**/macos/Flutter/Flutter-Profile.xcconfig
|
|
||||||
|
|
||||||
# Windows related
|
|
||||||
**/windows/flutter/ephemeral/
|
|
||||||
**/windows/**/*.suo
|
|
||||||
**/windows/**/*.user
|
|
||||||
**/windows/**/*.userosscache
|
|
||||||
**/windows/**/*.sln.docstates
|
|
||||||
**/windows/x64/
|
|
||||||
**/windows/x86/
|
|
||||||
**/windows/**/*.[Cc]ache
|
|
||||||
**/windows/**/!*.[Cc]ache/
|
|
||||||
|
|
||||||
# Web related
|
|
||||||
lib/generated_plugin_registrant.dart
|
|
||||||
|
|
||||||
# Coverage
|
|
||||||
coverage/
|
|
||||||
|
|
||||||
# Symbolication related
|
|
||||||
app.*.symbols
|
|
||||||
|
|
||||||
# Obfuscation related
|
|
||||||
app.*.map.json
|
|
||||||
|
|
||||||
# Exceptions to above rules.
|
|
||||||
!**/ios/**/default.mode1v3
|
|
||||||
!**/ios/**/default.mode2v3
|
|
||||||
!**/ios/**/default.pbxuser
|
|
||||||
!**/ios/**/default.perspectivev3
|
|
||||||
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
|
||||||
!/dev/ci/**/Gemfile.lock
|
|
||||||
|
|
||||||
# Firebase related
|
|
||||||
.firebase
|
|
||||||
|
|
||||||
# Dependency directories
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# FVM
|
|
||||||
.fvm
|
|
||||||
|
45
.metadata
45
.metadata
@ -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'
|
|
75
.releaserc
75
.releaserc
@ -1,75 +0,0 @@
|
|||||||
{
|
|
||||||
"branches": [
|
|
||||||
"main",
|
|
||||||
{
|
|
||||||
"name": "dev",
|
|
||||||
"prerelease": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"semantic-release-export-data",
|
|
||||||
"@semantic-release/commit-analyzer",
|
|
||||||
[
|
|
||||||
"@semantic-release/release-notes-generator",
|
|
||||||
{
|
|
||||||
"presetConfig": {
|
|
||||||
"types": [
|
|
||||||
{
|
|
||||||
"type": "build",
|
|
||||||
"section": "Dependency Updates"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "chore",
|
|
||||||
"section": "Other Changes",
|
|
||||||
"hidden": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "perf",
|
|
||||||
"section": "Performance Improvements",
|
|
||||||
"hidden": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "refactor",
|
|
||||||
"section": "Code Improvements",
|
|
||||||
"hidden": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@semantic-release/changelog",
|
|
||||||
"semantic-release-flutter-plugin",
|
|
||||||
[
|
|
||||||
"@semantic-release/git",
|
|
||||||
{
|
|
||||||
"assets": [
|
|
||||||
"CHANGELOG.md",
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="main.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
|
|
||||||
<option name="filePath" value="$PROJECT_DIR$/lib/main.dart" />
|
|
||||||
<method v="2" />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
91
.vscode/tasks.json
vendored
91
.vscode/tasks.json
vendored
@ -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": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
|
|
103
CONTRIBUTING.md
Normal file
103
CONTRIBUTING.md
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<p align="center">
|
||||||
|
<picture>
|
||||||
|
<source
|
||||||
|
width="256px"
|
||||||
|
media="(prefers-color-scheme: dark)"
|
||||||
|
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
width="256px"
|
||||||
|
src="assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||||
|
>
|
||||||
|
</picture>
|
||||||
|
<br>
|
||||||
|
<a href="https://revanced.app/">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo.svg" />
|
||||||
|
<img height="24px" src="assets/revanced-logo/revanced-logo.svg" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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
|
113
README.md
113
README.md
@ -1,31 +1,104 @@
|
|||||||
|
<p align="center">
|
||||||
|
<picture>
|
||||||
|
<source
|
||||||
|
width="256px"
|
||||||
|
media="(prefers-color-scheme: dark)"
|
||||||
|
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
width="256px"
|
||||||
|
src="assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||||
|
>
|
||||||
|
</picture>
|
||||||
|
<br>
|
||||||
|
<a href="https://revanced.app/">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo.svg" />
|
||||||
|
<img height="24px" src="assets/revanced-logo/revanced-logo.svg" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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
|
# 💊 ReVanced Manager
|
||||||
|
|
||||||
The official ReVanced Manager based on Flutter.
|

|
||||||
|

|
||||||
|
|
||||||
|
Application to use ReVanced on Android
|
||||||
|
|
||||||
|
## ❓ About
|
||||||
|
|
||||||
|
ReVanced Manager is an application that uses [ReVanced Patcher](https://github.com/revanced/revanced-patcher) to patch Android apps.
|
||||||
|
|
||||||
|
## 💪 Features
|
||||||
|
|
||||||
|
Some of the features ReVanced Manager provides are:
|
||||||
|
|
||||||
|
- ⬇️ **Download**: Automatically download apps using the ReVanced Manager downloader plugin system
|
||||||
|
- 💉 **Patch**: Select and apply patches to any Android app
|
||||||
|
- 🛠️ **Customize**: Manage patches, apps, signing, themes, updates, and many more settings
|
||||||
|
|
||||||
## 🔽 Download
|
## 🔽 Download
|
||||||
To download latest Manager, go [here](https://github.com/revanced/revanced-manager/releases/latest) and install the provided APK file.
|
|
||||||
|
|
||||||
## 📝 Prerequisites
|
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/latest).
|
||||||
1. Android 8 or higher
|
Learn how to use ReVanced Manager by following the [documentation](/docs).
|
||||||
2. Does not work on some armv7 devices
|
|
||||||
|
|
||||||
## 🔴 Issues
|
## 📚 Everything else
|
||||||
For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose).
|
|
||||||
|
|
||||||
## 💭 Discussion
|
### 📙 Contributing
|
||||||
If you wish to discuss the Manager, a thread has been made under the [#development](https://discord.com/channels/952946952348270622/1002922226443632761) channel in the Discord server, please note that this thread may be temporary and may be removed in the future.
|
|
||||||
|
|
||||||
|
Thank you for considering contributing to ReVanced Manager.
|
||||||
|
You can find the contribution guidelines [here](CONTRIBUTING.md).
|
||||||
|
|
||||||
## 🌐 Translation
|
### 🛠️ Building
|
||||||
[](https://crowdin.com/project/revanced)
|
|
||||||
|
|
||||||
If you wish to translate ReVanced Manager, we're accepting translations on [Crowdin](https://translate.revanced.app)
|
To build a ReVanced Manager, you can follow the [documentation](/docs).
|
||||||
|
|
||||||
## 🛠️ Building Manager from source
|
### 📄 Documentation
|
||||||
1. Setup flutter environment for your [platform](https://docs.flutter.dev/get-started/install)
|
|
||||||
2. Clone the repository locally
|
You can find the documentation for ReVanced Manager [here](/docs).
|
||||||
3. Add your github token in gradle.properties like [this](/docs/4_building.md)
|
|
||||||
4. Open the project in terminal
|
## ⚖️ License
|
||||||
5. Run `flutter pub get` in terminal
|
|
||||||
6. Then `flutter packages pub run build_runner build --delete-conflicting-outputs` (Must be done on each git pull)
|
ReVanced Manager is licensed under the GPLv3 license. Please see the [license file](LICENSE) for more information.
|
||||||
7. To build release apk run `flutter build apk`
|
[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.
|
||||||
|
@ -1,163 +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_require_non_null_named_parameters
|
|
||||||
- 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
|
|
||||||
- avoid_returning_null_for_future
|
|
||||||
- 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_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
13
android/.gitignore
vendored
@ -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
|
|
@ -1,3 +0,0 @@
|
|||||||
source "https://rubygems.org"
|
|
||||||
|
|
||||||
gem "fastlane"
|
|
@ -1,92 +0,0 @@
|
|||||||
def localProperties = new Properties()
|
|
||||||
def localPropertiesFile = rootProject.file('local.properties')
|
|
||||||
if (localPropertiesFile.exists()) {
|
|
||||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
|
||||||
localProperties.load(reader)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
|
||||||
if (flutterRoot == null) {
|
|
||||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
|
||||||
}
|
|
||||||
|
|
||||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
|
||||||
if (flutterVersionCode == null) {
|
|
||||||
flutterVersionCode = '1'
|
|
||||||
}
|
|
||||||
|
|
||||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
|
||||||
if (flutterVersionName == null) {
|
|
||||||
flutterVersionName = '1.0'
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdkVersion flutter.compileSdkVersion
|
|
||||||
ndkVersion flutter.ndkVersion
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_11
|
|
||||||
targetCompatibility JavaVersion.VERSION_11
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = '11'
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
main.java.srcDirs += 'src/main/kotlin'
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "app.revanced.manager.flutter"
|
|
||||||
minSdkVersion 26
|
|
||||||
targetSdkVersion 33
|
|
||||||
versionCode flutterVersionCode.toInteger()
|
|
||||||
versionName flutterVersionName
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
resValue "string", "app_name", "ReVanced Manager"
|
|
||||||
signingConfig signingConfigs.debug
|
|
||||||
ndk {
|
|
||||||
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
packagingOptions {
|
|
||||||
exclude '/prebuilt/**'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
flutter {
|
|
||||||
source '../..'
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
|
||||||
|
|
||||||
// ReVanced
|
|
||||||
implementation "app.revanced:revanced-patcher:11.0.4"
|
|
||||||
|
|
||||||
// Signing & aligning
|
|
||||||
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
|
|
||||||
implementation("com.android.tools.build:apksig:7.2.2")
|
|
||||||
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="app.revanced.manager.flutter">
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
|
||||||
</manifest>
|
|
@ -1,59 +0,0 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="app.revanced.manager.flutter">
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
|
|
||||||
<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" />
|
|
||||||
<application
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:name="${applicationName}"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:largeHeap="true"
|
|
||||||
android:requestLegacyExternalStorage="true"
|
|
||||||
android:extractNativeLibs="true"
|
|
||||||
android:enableOnBackInvokedCallback="true">
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:exported="true"
|
|
||||||
android:launchMode="singleTop"
|
|
||||||
android:theme="@style/LaunchTheme"
|
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
|
||||||
android:hardwareAccelerated="true"
|
|
||||||
android:windowSoftInputMode="adjustResize">
|
|
||||||
<meta-data
|
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
|
||||||
android:resource="@style/NormalTheme"/>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<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>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
@ -1,385 +0,0 @@
|
|||||||
package app.revanced.manager.flutter
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import androidx.annotation.NonNull
|
|
||||||
import app.revanced.manager.flutter.utils.Aapt
|
|
||||||
import app.revanced.manager.flutter.utils.aligning.ZipAligner
|
|
||||||
import app.revanced.manager.flutter.utils.signing.Signer
|
|
||||||
import app.revanced.manager.flutter.utils.zip.ZipFile
|
|
||||||
import app.revanced.manager.flutter.utils.zip.structures.ZipEntry
|
|
||||||
import app.revanced.patcher.Patcher
|
|
||||||
import app.revanced.patcher.PatcherOptions
|
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
|
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
|
||||||
import app.revanced.patcher.logging.Logger
|
|
||||||
import app.revanced.patcher.util.patch.PatchBundle
|
|
||||||
import dalvik.system.DexClassLoader
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
|
||||||
import io.flutter.plugin.common.MethodChannel
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
private const val PATCHER_CHANNEL = "app.revanced.manager.flutter/patcher"
|
|
||||||
private const val INSTALLER_CHANNEL = "app.revanced.manager.flutter/installer"
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
|
||||||
super.configureFlutterEngine(flutterEngine)
|
|
||||||
val mainChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, PATCHER_CHANNEL)
|
|
||||||
installerChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, INSTALLER_CHANNEL)
|
|
||||||
mainChannel.setMethodCallHandler { call, result ->
|
|
||||||
when (call.method) {
|
|
||||||
"runPatcher" -> {
|
|
||||||
val patchBundleFilePath = call.argument<String>("patchBundleFilePath")
|
|
||||||
val originalFilePath = call.argument<String>("originalFilePath")
|
|
||||||
val inputFilePath = call.argument<String>("inputFilePath")
|
|
||||||
val patchedFilePath = call.argument<String>("patchedFilePath")
|
|
||||||
val outFilePath = call.argument<String>("outFilePath")
|
|
||||||
val integrationsPath = call.argument<String>("integrationsPath")
|
|
||||||
val selectedPatches = call.argument<List<String>>("selectedPatches")
|
|
||||||
val cacheDirPath = call.argument<String>("cacheDirPath")
|
|
||||||
val keyStoreFilePath = call.argument<String>("keyStoreFilePath")
|
|
||||||
val keystorePassword = call.argument<String>("keystorePassword")
|
|
||||||
|
|
||||||
if (patchBundleFilePath != null &&
|
|
||||||
originalFilePath != null &&
|
|
||||||
inputFilePath != null &&
|
|
||||||
patchedFilePath != null &&
|
|
||||||
outFilePath != null &&
|
|
||||||
integrationsPath != null &&
|
|
||||||
selectedPatches != null &&
|
|
||||||
cacheDirPath != null &&
|
|
||||||
keyStoreFilePath != null &&
|
|
||||||
keystorePassword != null
|
|
||||||
) {
|
|
||||||
cancel = false
|
|
||||||
runPatcher(
|
|
||||||
result,
|
|
||||||
patchBundleFilePath,
|
|
||||||
originalFilePath,
|
|
||||||
inputFilePath,
|
|
||||||
patchedFilePath,
|
|
||||||
outFilePath,
|
|
||||||
integrationsPath,
|
|
||||||
selectedPatches,
|
|
||||||
cacheDirPath,
|
|
||||||
keyStoreFilePath,
|
|
||||||
keystorePassword
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
result.notImplemented()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"stopPatcher" -> {
|
|
||||||
cancel = true
|
|
||||||
stopResult = result
|
|
||||||
}
|
|
||||||
else -> result.notImplemented()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun runPatcher(
|
|
||||||
result: MethodChannel.Result,
|
|
||||||
patchBundleFilePath: String,
|
|
||||||
originalFilePath: String,
|
|
||||||
inputFilePath: String,
|
|
||||||
patchedFilePath: String,
|
|
||||||
outFilePath: String,
|
|
||||||
integrationsPath: String,
|
|
||||||
selectedPatches: List<String>,
|
|
||||||
cacheDirPath: String,
|
|
||||||
keyStoreFilePath: String,
|
|
||||||
keystorePassword: String
|
|
||||||
) {
|
|
||||||
val originalFile = File(originalFilePath)
|
|
||||||
val inputFile = File(inputFilePath)
|
|
||||||
val patchedFile = File(patchedFilePath)
|
|
||||||
val outFile = File(outFilePath)
|
|
||||||
val integrations = File(integrationsPath)
|
|
||||||
val keyStoreFile = File(keyStoreFilePath)
|
|
||||||
|
|
||||||
Thread {
|
|
||||||
try {
|
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf(
|
|
||||||
"progress" to 0.1,
|
|
||||||
"header" to "",
|
|
||||||
"log" to "Copying original apk"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(cancel) {
|
|
||||||
handler.post { stopResult!!.success(null) }
|
|
||||||
return@Thread
|
|
||||||
}
|
|
||||||
|
|
||||||
originalFile.copyTo(inputFile, true)
|
|
||||||
|
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf(
|
|
||||||
"progress" to 0.2,
|
|
||||||
"header" to "Unpacking apk...",
|
|
||||||
"log" to "Unpacking input apk"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(cancel) {
|
|
||||||
handler.post { stopResult!!.success(null) }
|
|
||||||
return@Thread
|
|
||||||
}
|
|
||||||
|
|
||||||
val patcher =
|
|
||||||
Patcher(
|
|
||||||
PatcherOptions(
|
|
||||||
inputFile,
|
|
||||||
cacheDirPath,
|
|
||||||
Aapt.binary(applicationContext).absolutePath,
|
|
||||||
cacheDirPath,
|
|
||||||
logger = ManagerLogger()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if(cancel) {
|
|
||||||
handler.post { stopResult!!.success(null) }
|
|
||||||
return@Thread
|
|
||||||
}
|
|
||||||
|
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf("progress" to 0.3, "header" to "", "log" to "")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf(
|
|
||||||
"progress" to 0.4,
|
|
||||||
"header" to "Merging integrations...",
|
|
||||||
"log" to "Merging integrations"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(cancel) {
|
|
||||||
handler.post { stopResult!!.success(null) }
|
|
||||||
return@Thread
|
|
||||||
}
|
|
||||||
|
|
||||||
patcher.addIntegrations(listOf(integrations)) {}
|
|
||||||
|
|
||||||
if(cancel) {
|
|
||||||
handler.post { stopResult!!.success(null) }
|
|
||||||
return@Thread
|
|
||||||
}
|
|
||||||
|
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf(
|
|
||||||
"progress" to 0.5,
|
|
||||||
"header" to "Applying patches...",
|
|
||||||
"log" to ""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(cancel) {
|
|
||||||
handler.post { stopResult!!.success(null) }
|
|
||||||
return@Thread
|
|
||||||
}
|
|
||||||
|
|
||||||
val patches = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) {
|
|
||||||
PatchBundle.Dex(
|
|
||||||
patchBundleFilePath,
|
|
||||||
DexClassLoader(
|
|
||||||
patchBundleFilePath,
|
|
||||||
cacheDirPath,
|
|
||||||
null,
|
|
||||||
javaClass.classLoader
|
|
||||||
)
|
|
||||||
).loadPatches().filter { patch ->
|
|
||||||
(patch.compatiblePackages?.any { it.name == patcher.context.packageMetadata.packageName } == true || patch.compatiblePackages.isNullOrEmpty()) &&
|
|
||||||
selectedPatches.any { it == patch.patchName }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
TODO("VERSION.SDK_INT < CUPCAKE")
|
|
||||||
}
|
|
||||||
|
|
||||||
if(cancel) {
|
|
||||||
handler.post { stopResult!!.success(null) }
|
|
||||||
return@Thread
|
|
||||||
}
|
|
||||||
|
|
||||||
patcher.addPatches(patches)
|
|
||||||
patcher.executePatches().forEach { (patch, res) ->
|
|
||||||
if (res.isSuccess) {
|
|
||||||
val msg = "Applied $patch"
|
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf(
|
|
||||||
"progress" to 0.5,
|
|
||||||
"header" to "",
|
|
||||||
"log" to msg
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if(cancel) {
|
|
||||||
handler.post { stopResult!!.success(null) }
|
|
||||||
return@Thread
|
|
||||||
}
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
val msg =
|
|
||||||
"Failed to apply $patch: " + "${res.exceptionOrNull()!!.message ?: res.exceptionOrNull()!!.cause!!::class.simpleName}"
|
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf("progress" to 0.5, "header" to "", "log" to msg)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if(cancel) {
|
|
||||||
handler.post { stopResult!!.success(null) }
|
|
||||||
return@Thread
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf(
|
|
||||||
"progress" to 0.7,
|
|
||||||
"header" to "Repacking apk...",
|
|
||||||
"log" to "Repacking patched apk"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if(cancel) {
|
|
||||||
handler.post { stopResult!!.success(null) }
|
|
||||||
return@Thread
|
|
||||||
}
|
|
||||||
val res = patcher.save()
|
|
||||||
ZipFile(patchedFile).use { file ->
|
|
||||||
res.dexFiles.forEach {
|
|
||||||
if(cancel) {
|
|
||||||
handler.post { stopResult!!.success(null) }
|
|
||||||
return@Thread
|
|
||||||
}
|
|
||||||
file.addEntryCompressData(
|
|
||||||
ZipEntry.createWithName(it.name),
|
|
||||||
it.stream.readBytes()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
res.resourceFile?.let {
|
|
||||||
file.copyEntriesFromFileAligned(
|
|
||||||
ZipFile(it),
|
|
||||||
ZipAligner::getEntryAlignment
|
|
||||||
)
|
|
||||||
}
|
|
||||||
file.copyEntriesFromFileAligned(
|
|
||||||
ZipFile(inputFile),
|
|
||||||
ZipAligner::getEntryAlignment
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if(cancel) {
|
|
||||||
handler.post { stopResult!!.success(null) }
|
|
||||||
return@Thread
|
|
||||||
}
|
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf(
|
|
||||||
"progress" to 0.9,
|
|
||||||
"header" to "Signing apk...",
|
|
||||||
"log" to ""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Signer("ReVanced", keystorePassword).signApk(
|
|
||||||
patchedFile,
|
|
||||||
outFile,
|
|
||||||
keyStoreFile
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
//log to console
|
|
||||||
print("Error signing apk: ${e.message}")
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
|
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf(
|
|
||||||
"progress" to 1.0,
|
|
||||||
"header" to "Finished!",
|
|
||||||
"log" to "Finished!"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (ex: Throwable) {
|
|
||||||
val stack = ex.stackTraceToString()
|
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf(
|
|
||||||
"progress" to -100.0,
|
|
||||||
"header" to "Aborted...",
|
|
||||||
"log" to "An error occurred! Aborted\nError:\n$stack"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handler.post { result.success(null) }
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class ManagerLogger : Logger {
|
|
||||||
override fun error(msg: String) {
|
|
||||||
handler.post {
|
|
||||||
installerChannel
|
|
||||||
.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf("progress" to -1.0, "header" to "", "log" to msg)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun warn(msg: String) {
|
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf("progress" to -1.0, "header" to "", "log" to msg)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun info(msg: String) {
|
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf("progress" to -1.0, "header" to "", "log" to msg)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun trace(_msg: String) { /* unused */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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())
|
|
@ -1,11 +0,0 @@
|
|||||||
package app.revanced.manager.flutter.utils.aligning
|
|
||||||
|
|
||||||
import app.revanced.manager.flutter.utils.zip.structures.ZipEntry
|
|
||||||
|
|
||||||
internal object ZipAligner {
|
|
||||||
private const val DEFAULT_ALIGNMENT = 4
|
|
||||||
private const val LIBRARY_ALIGNMENT = 4096
|
|
||||||
|
|
||||||
fun getEntryAlignment(entry: ZipEntry): Int? =
|
|
||||||
if (entry.compression.toUInt() != 0u) null else if (entry.fileName.endsWith(".so")) LIBRARY_ALIGNMENT else DEFAULT_ALIGNMENT
|
|
||||||
}
|
|
@ -1,74 +0,0 @@
|
|||||||
package app.revanced.manager.flutter.utils.signing
|
|
||||||
|
|
||||||
import com.android.apksig.ApkSigner
|
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
|
||||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
|
||||||
import org.bouncycastle.cert.X509v3CertificateBuilder
|
|
||||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
|
||||||
import org.bouncycastle.operator.ContentSigner
|
|
||||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.math.BigInteger
|
|
||||||
import java.security.*
|
|
||||||
import java.security.cert.X509Certificate
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
internal class Signer(
|
|
||||||
private val cn: String, password: String
|
|
||||||
) {
|
|
||||||
private val passwordCharArray = password.toCharArray()
|
|
||||||
private fun newKeystore(out: File) {
|
|
||||||
val (publicKey, privateKey) = createKey()
|
|
||||||
val privateKS = KeyStore.getInstance("BKS", "BC")
|
|
||||||
privateKS.load(null, passwordCharArray)
|
|
||||||
privateKS.setKeyEntry("alias", privateKey, passwordCharArray, arrayOf(publicKey))
|
|
||||||
privateKS.store(FileOutputStream(out), passwordCharArray)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createKey(): Pair<X509Certificate, PrivateKey> {
|
|
||||||
val gen = KeyPairGenerator.getInstance("RSA")
|
|
||||||
gen.initialize(2048)
|
|
||||||
val pair = gen.generateKeyPair()
|
|
||||||
var serialNumber: BigInteger
|
|
||||||
do serialNumber =
|
|
||||||
BigInteger.valueOf(SecureRandom().nextLong()) while (serialNumber < BigInteger.ZERO)
|
|
||||||
val x500Name = X500Name("CN=$cn")
|
|
||||||
val builder = X509v3CertificateBuilder(
|
|
||||||
x500Name,
|
|
||||||
serialNumber,
|
|
||||||
Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L),
|
|
||||||
Date(System.currentTimeMillis() + 1000L * 60L * 60L * 24L * 366L * 30L),
|
|
||||||
Locale.ENGLISH,
|
|
||||||
x500Name,
|
|
||||||
SubjectPublicKeyInfo.getInstance(pair.public.encoded)
|
|
||||||
)
|
|
||||||
val signer: ContentSigner = JcaContentSignerBuilder("SHA256withRSA").build(pair.private)
|
|
||||||
return JcaX509CertificateConverter().getCertificate(builder.build(signer)) to pair.private
|
|
||||||
}
|
|
||||||
|
|
||||||
fun signApk(input: File, output: File, ks: File) {
|
|
||||||
Security.addProvider(BouncyCastleProvider())
|
|
||||||
|
|
||||||
if (!ks.exists()) newKeystore(ks)
|
|
||||||
|
|
||||||
val keyStore = KeyStore.getInstance("BKS", "BC")
|
|
||||||
FileInputStream(ks).use { fis -> keyStore.load(fis, null) }
|
|
||||||
val alias = keyStore.aliases().nextElement()
|
|
||||||
|
|
||||||
val config = ApkSigner.SignerConfig.Builder(
|
|
||||||
cn,
|
|
||||||
keyStore.getKey(alias, passwordCharArray) as PrivateKey,
|
|
||||||
listOf(keyStore.getCertificate(alias) as X509Certificate)
|
|
||||||
).build()
|
|
||||||
|
|
||||||
val signer = ApkSigner.Builder(listOf(config))
|
|
||||||
signer.setCreatedBy(cn)
|
|
||||||
signer.setInputApk(input)
|
|
||||||
signer.setOutputApk(output)
|
|
||||||
|
|
||||||
signer.build().sign()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
@file:Suppress("unused")
|
|
||||||
|
|
||||||
package app.revanced.manager.flutter.utils.zip
|
|
||||||
|
|
||||||
import java.io.DataInput
|
|
||||||
import java.io.DataOutput
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
|
|
||||||
fun UInt.toLittleEndian() =
|
|
||||||
(((this.toInt() and 0xff000000.toInt()) shr 24) or ((this.toInt() and 0x00ff0000) shr 8) or ((this.toInt() and 0x0000ff00) shl 8) or (this.toInt() shl 24)).toUInt()
|
|
||||||
|
|
||||||
fun UShort.toLittleEndian() = (this.toUInt() shl 16).toLittleEndian().toUShort()
|
|
||||||
|
|
||||||
fun UInt.toBigEndian() = (((this.toInt() and 0xff) shl 24) or ((this.toInt() and 0xff00) shl 8)
|
|
||||||
or ((this.toInt() and 0x00ff0000) ushr 8) or (this.toInt() ushr 24)).toUInt()
|
|
||||||
|
|
||||||
fun UShort.toBigEndian() = (this.toUInt() shl 16).toBigEndian().toUShort()
|
|
||||||
|
|
||||||
fun ByteBuffer.getUShort() = this.short.toUShort()
|
|
||||||
fun ByteBuffer.getUInt() = this.int.toUInt()
|
|
||||||
|
|
||||||
fun ByteBuffer.putUShort(ushort: UShort): ByteBuffer = this.putShort(ushort.toShort())
|
|
||||||
fun ByteBuffer.putUInt(uint: UInt): ByteBuffer = this.putInt(uint.toInt())
|
|
||||||
|
|
||||||
fun DataInput.readUShort() = this.readShort().toUShort()
|
|
||||||
fun DataInput.readUInt() = this.readInt().toUInt()
|
|
||||||
|
|
||||||
fun DataOutput.writeUShort(ushort: UShort) = this.writeShort(ushort.toInt())
|
|
||||||
fun DataOutput.writeUInt(uint: UInt) = this.writeInt(uint.toInt())
|
|
||||||
|
|
||||||
fun DataInput.readUShortLE() = this.readUShort().toBigEndian()
|
|
||||||
fun DataInput.readUIntLE() = this.readUInt().toBigEndian()
|
|
||||||
|
|
||||||
fun DataOutput.writeUShortLE(ushort: UShort) = this.writeUShort(ushort.toLittleEndian())
|
|
||||||
fun DataOutput.writeUIntLE(uint: UInt) = this.writeUInt(uint.toLittleEndian())
|
|
@ -1,176 +0,0 @@
|
|||||||
package app.revanced.manager.flutter.utils.zip
|
|
||||||
|
|
||||||
import app.revanced.manager.flutter.utils.zip.structures.ZipEndRecord
|
|
||||||
import app.revanced.manager.flutter.utils.zip.structures.ZipEntry
|
|
||||||
import java.io.Closeable
|
|
||||||
import java.io.File
|
|
||||||
import java.io.RandomAccessFile
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.nio.channels.FileChannel
|
|
||||||
import java.util.zip.CRC32
|
|
||||||
import java.util.zip.Deflater
|
|
||||||
|
|
||||||
class ZipFile(file: File) : Closeable {
|
|
||||||
var entries: MutableList<ZipEntry> = mutableListOf()
|
|
||||||
|
|
||||||
private val filePointer: RandomAccessFile = RandomAccessFile(file, "rw")
|
|
||||||
private var CDNeedsRewrite = false
|
|
||||||
|
|
||||||
private val compressionLevel = 5
|
|
||||||
|
|
||||||
init {
|
|
||||||
//if file isn't empty try to load entries
|
|
||||||
if (file.length() > 0) {
|
|
||||||
val endRecord = findEndRecord()
|
|
||||||
|
|
||||||
if (endRecord.diskNumber > 0u || endRecord.totalEntries != endRecord.diskEntries)
|
|
||||||
throw IllegalArgumentException("Multi-file archives are not supported")
|
|
||||||
|
|
||||||
entries = readEntries(endRecord).toMutableList()
|
|
||||||
}
|
|
||||||
|
|
||||||
//seek back to start for writing
|
|
||||||
filePointer.seek(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findEndRecord(): ZipEndRecord {
|
|
||||||
//look from end to start since end record is at the end
|
|
||||||
for (i in filePointer.length() - 1 downTo 0) {
|
|
||||||
filePointer.seek(i)
|
|
||||||
//possible beginning of signature
|
|
||||||
if (filePointer.readByte() == 0x50.toByte()) {
|
|
||||||
//seek back to get the full int
|
|
||||||
filePointer.seek(i)
|
|
||||||
val possibleSignature = filePointer.readUIntLE()
|
|
||||||
if (possibleSignature == ZipEndRecord.ECD_SIGNATURE) {
|
|
||||||
filePointer.seek(i)
|
|
||||||
return ZipEndRecord.fromECD(filePointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw Exception("Couldn't find end record")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun readEntries(endRecord: ZipEndRecord): List<ZipEntry> {
|
|
||||||
filePointer.seek(endRecord.centralDirectoryStartOffset.toLong())
|
|
||||||
|
|
||||||
val numberOfEntries = endRecord.diskEntries.toInt()
|
|
||||||
|
|
||||||
return buildList(numberOfEntries) {
|
|
||||||
for (i in 1..numberOfEntries) {
|
|
||||||
add(
|
|
||||||
ZipEntry.fromCDE(filePointer).also
|
|
||||||
{
|
|
||||||
//for some reason the local extra field can be different from the central one
|
|
||||||
it.readLocalExtra(
|
|
||||||
filePointer.channel.map(
|
|
||||||
FileChannel.MapMode.READ_ONLY,
|
|
||||||
it.localHeaderOffset.toLong() + 28,
|
|
||||||
2
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun writeCD() {
|
|
||||||
val CDStart = filePointer.channel.position().toUInt()
|
|
||||||
|
|
||||||
entries.forEach {
|
|
||||||
filePointer.channel.write(it.toCDE())
|
|
||||||
}
|
|
||||||
|
|
||||||
val entriesCount = entries.size.toUShort()
|
|
||||||
|
|
||||||
val endRecord = ZipEndRecord(
|
|
||||||
0u,
|
|
||||||
0u,
|
|
||||||
entriesCount,
|
|
||||||
entriesCount,
|
|
||||||
filePointer.channel.position().toUInt() - CDStart,
|
|
||||||
CDStart,
|
|
||||||
""
|
|
||||||
)
|
|
||||||
|
|
||||||
filePointer.channel.write(endRecord.toECD())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addEntry(entry: ZipEntry, data: ByteBuffer) {
|
|
||||||
CDNeedsRewrite = true
|
|
||||||
|
|
||||||
entry.localHeaderOffset = filePointer.channel.position().toUInt()
|
|
||||||
|
|
||||||
filePointer.channel.write(entry.toLFH())
|
|
||||||
filePointer.channel.write(data)
|
|
||||||
|
|
||||||
entries.add(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addEntryCompressData(entry: ZipEntry, data: ByteArray) {
|
|
||||||
val compressor = Deflater(compressionLevel, true)
|
|
||||||
compressor.setInput(data)
|
|
||||||
compressor.finish()
|
|
||||||
|
|
||||||
val uncompressedSize = data.size
|
|
||||||
val compressedData =
|
|
||||||
ByteArray(uncompressedSize) //i'm guessing compression won't make the data bigger
|
|
||||||
|
|
||||||
val compressedDataLength = compressor.deflate(compressedData)
|
|
||||||
val compressedBuffer =
|
|
||||||
ByteBuffer.wrap(compressedData.take(compressedDataLength).toByteArray())
|
|
||||||
|
|
||||||
compressor.end()
|
|
||||||
|
|
||||||
val crc = CRC32()
|
|
||||||
crc.update(data)
|
|
||||||
|
|
||||||
entry.compression = 8u //deflate compression
|
|
||||||
entry.uncompressedSize = uncompressedSize.toUInt()
|
|
||||||
entry.compressedSize = compressedDataLength.toUInt()
|
|
||||||
entry.crc32 = crc.value.toUInt()
|
|
||||||
|
|
||||||
addEntry(entry, compressedBuffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addEntryCopyData(entry: ZipEntry, data: ByteBuffer, alignment: Int? = null) {
|
|
||||||
alignment?.let {
|
|
||||||
//calculate where data would end up
|
|
||||||
val dataOffset = filePointer.filePointer + entry.LFHSize
|
|
||||||
|
|
||||||
val mod = dataOffset % alignment
|
|
||||||
|
|
||||||
//wrong alignment
|
|
||||||
if (mod != 0L) {
|
|
||||||
//add padding at end of extra field
|
|
||||||
entry.localExtraField =
|
|
||||||
entry.localExtraField.copyOf((entry.localExtraField.size + (alignment - mod)).toInt())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addEntry(entry, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDataForEntry(entry: ZipEntry): ByteBuffer {
|
|
||||||
return filePointer.channel.map(
|
|
||||||
FileChannel.MapMode.READ_ONLY,
|
|
||||||
entry.dataOffset.toLong(),
|
|
||||||
entry.compressedSize.toLong()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun copyEntriesFromFileAligned(file: ZipFile, entryAlignment: (entry: ZipEntry) -> Int?) {
|
|
||||||
for (entry in file.entries) {
|
|
||||||
if (entries.any { it.fileName == entry.fileName }) continue //don't add duplicates
|
|
||||||
|
|
||||||
val data = file.getDataForEntry(entry)
|
|
||||||
addEntryCopyData(entry, data, entryAlignment(entry))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
if (CDNeedsRewrite) writeCD()
|
|
||||||
filePointer.close()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
package app.revanced.manager.flutter.utils.zip.structures
|
|
||||||
|
|
||||||
import app.revanced.manager.flutter.utils.zip.putUInt
|
|
||||||
import app.revanced.manager.flutter.utils.zip.putUShort
|
|
||||||
import app.revanced.manager.flutter.utils.zip.readUIntLE
|
|
||||||
import app.revanced.manager.flutter.utils.zip.readUShortLE
|
|
||||||
import java.io.DataInput
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.nio.ByteOrder
|
|
||||||
|
|
||||||
data class ZipEndRecord(
|
|
||||||
val diskNumber: UShort,
|
|
||||||
val startingDiskNumber: UShort,
|
|
||||||
val diskEntries: UShort,
|
|
||||||
val totalEntries: UShort,
|
|
||||||
val centralDirectorySize: UInt,
|
|
||||||
val centralDirectoryStartOffset: UInt,
|
|
||||||
val fileComment: String,
|
|
||||||
) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val ECD_HEADER_SIZE = 22
|
|
||||||
const val ECD_SIGNATURE = 0x06054b50u
|
|
||||||
|
|
||||||
fun fromECD(input: DataInput): ZipEndRecord {
|
|
||||||
val signature = input.readUIntLE()
|
|
||||||
|
|
||||||
if (signature != ECD_SIGNATURE)
|
|
||||||
throw IllegalArgumentException("Input doesn't start with end record signature")
|
|
||||||
|
|
||||||
val diskNumber = input.readUShortLE()
|
|
||||||
val startingDiskNumber = input.readUShortLE()
|
|
||||||
val diskEntries = input.readUShortLE()
|
|
||||||
val totalEntries = input.readUShortLE()
|
|
||||||
val centralDirectorySize = input.readUIntLE()
|
|
||||||
val centralDirectoryStartOffset = input.readUIntLE()
|
|
||||||
val fileCommentLength = input.readUShortLE()
|
|
||||||
var fileComment = ""
|
|
||||||
|
|
||||||
if (fileCommentLength > 0u) {
|
|
||||||
val fileCommentBytes = ByteArray(fileCommentLength.toInt())
|
|
||||||
input.readFully(fileCommentBytes)
|
|
||||||
fileComment = fileCommentBytes.toString(Charsets.UTF_8)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ZipEndRecord(
|
|
||||||
diskNumber,
|
|
||||||
startingDiskNumber,
|
|
||||||
diskEntries,
|
|
||||||
totalEntries,
|
|
||||||
centralDirectorySize,
|
|
||||||
centralDirectoryStartOffset,
|
|
||||||
fileComment
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toECD(): ByteBuffer {
|
|
||||||
val commentBytes = fileComment.toByteArray(Charsets.UTF_8)
|
|
||||||
|
|
||||||
val buffer = ByteBuffer.allocate(ECD_HEADER_SIZE + commentBytes.size)
|
|
||||||
.also { it.order(ByteOrder.LITTLE_ENDIAN) }
|
|
||||||
|
|
||||||
buffer.putUInt(ECD_SIGNATURE)
|
|
||||||
buffer.putUShort(diskNumber)
|
|
||||||
buffer.putUShort(startingDiskNumber)
|
|
||||||
buffer.putUShort(diskEntries)
|
|
||||||
buffer.putUShort(totalEntries)
|
|
||||||
buffer.putUInt(centralDirectorySize)
|
|
||||||
buffer.putUInt(centralDirectoryStartOffset)
|
|
||||||
buffer.putUShort(commentBytes.size.toUShort())
|
|
||||||
|
|
||||||
buffer.put(commentBytes)
|
|
||||||
|
|
||||||
buffer.flip()
|
|
||||||
return buffer
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,190 +0,0 @@
|
|||||||
package app.revanced.manager.flutter.utils.zip.structures
|
|
||||||
|
|
||||||
import app.revanced.manager.flutter.utils.zip.*
|
|
||||||
import java.io.DataInput
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.nio.ByteOrder
|
|
||||||
|
|
||||||
data class ZipEntry(
|
|
||||||
val version: UShort,
|
|
||||||
val versionNeeded: UShort,
|
|
||||||
val flags: UShort,
|
|
||||||
var compression: UShort,
|
|
||||||
val modificationTime: UShort,
|
|
||||||
val modificationDate: UShort,
|
|
||||||
var crc32: UInt,
|
|
||||||
var compressedSize: UInt,
|
|
||||||
var uncompressedSize: UInt,
|
|
||||||
val diskNumber: UShort,
|
|
||||||
val internalAttributes: UShort,
|
|
||||||
val externalAttributes: UInt,
|
|
||||||
var localHeaderOffset: UInt,
|
|
||||||
val fileName: String,
|
|
||||||
val extraField: ByteArray,
|
|
||||||
val fileComment: String,
|
|
||||||
var localExtraField: ByteArray = ByteArray(0), //separate for alignment
|
|
||||||
) {
|
|
||||||
val LFHSize: Int
|
|
||||||
get() = LFH_HEADER_SIZE + fileName.toByteArray(Charsets.UTF_8).size + localExtraField.size
|
|
||||||
|
|
||||||
val dataOffset: UInt
|
|
||||||
get() = localHeaderOffset + LFHSize.toUInt()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val CDE_HEADER_SIZE = 46
|
|
||||||
const val CDE_SIGNATURE = 0x02014b50u
|
|
||||||
|
|
||||||
const val LFH_HEADER_SIZE = 30
|
|
||||||
const val LFH_SIGNATURE = 0x04034b50u
|
|
||||||
|
|
||||||
fun createWithName(fileName: String): ZipEntry {
|
|
||||||
return ZipEntry(
|
|
||||||
0x1403u, //made by unix, version 20
|
|
||||||
0u,
|
|
||||||
0u,
|
|
||||||
0u,
|
|
||||||
0x0821u, //seems to be static time google uses, no idea
|
|
||||||
0x0221u, //same as above
|
|
||||||
0u,
|
|
||||||
0u,
|
|
||||||
0u,
|
|
||||||
0u,
|
|
||||||
0u,
|
|
||||||
0u,
|
|
||||||
0u,
|
|
||||||
fileName,
|
|
||||||
ByteArray(0),
|
|
||||||
""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fromCDE(input: DataInput): ZipEntry {
|
|
||||||
val signature = input.readUIntLE()
|
|
||||||
|
|
||||||
if (signature != CDE_SIGNATURE)
|
|
||||||
throw IllegalArgumentException("Input doesn't start with central directory entry signature")
|
|
||||||
|
|
||||||
val version = input.readUShortLE()
|
|
||||||
val versionNeeded = input.readUShortLE()
|
|
||||||
var flags = input.readUShortLE()
|
|
||||||
val compression = input.readUShortLE()
|
|
||||||
val modificationTime = input.readUShortLE()
|
|
||||||
val modificationDate = input.readUShortLE()
|
|
||||||
val crc32 = input.readUIntLE()
|
|
||||||
val compressedSize = input.readUIntLE()
|
|
||||||
val uncompressedSize = input.readUIntLE()
|
|
||||||
val fileNameLength = input.readUShortLE()
|
|
||||||
var fileName = ""
|
|
||||||
val extraFieldLength = input.readUShortLE()
|
|
||||||
val extraField = ByteArray(extraFieldLength.toInt())
|
|
||||||
val fileCommentLength = input.readUShortLE()
|
|
||||||
var fileComment = ""
|
|
||||||
val diskNumber = input.readUShortLE()
|
|
||||||
val internalAttributes = input.readUShortLE()
|
|
||||||
val externalAttributes = input.readUIntLE()
|
|
||||||
val localHeaderOffset = input.readUIntLE()
|
|
||||||
|
|
||||||
val variableFieldsLength =
|
|
||||||
fileNameLength.toInt() + extraFieldLength.toInt() + fileCommentLength.toInt()
|
|
||||||
|
|
||||||
if (variableFieldsLength > 0) {
|
|
||||||
val fileNameBytes = ByteArray(fileNameLength.toInt())
|
|
||||||
input.readFully(fileNameBytes)
|
|
||||||
fileName = fileNameBytes.toString(Charsets.UTF_8)
|
|
||||||
|
|
||||||
input.readFully(extraField)
|
|
||||||
|
|
||||||
val fileCommentBytes = ByteArray(fileCommentLength.toInt())
|
|
||||||
input.readFully(fileCommentBytes)
|
|
||||||
fileComment = fileCommentBytes.toString(Charsets.UTF_8)
|
|
||||||
}
|
|
||||||
|
|
||||||
flags = (flags and 0b1000u.inv()
|
|
||||||
.toUShort()) //disable data descriptor flag as they are not used
|
|
||||||
|
|
||||||
return ZipEntry(
|
|
||||||
version,
|
|
||||||
versionNeeded,
|
|
||||||
flags,
|
|
||||||
compression,
|
|
||||||
modificationTime,
|
|
||||||
modificationDate,
|
|
||||||
crc32,
|
|
||||||
compressedSize,
|
|
||||||
uncompressedSize,
|
|
||||||
diskNumber,
|
|
||||||
internalAttributes,
|
|
||||||
externalAttributes,
|
|
||||||
localHeaderOffset,
|
|
||||||
fileName,
|
|
||||||
extraField,
|
|
||||||
fileComment,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun readLocalExtra(buffer: ByteBuffer) {
|
|
||||||
buffer.order(ByteOrder.LITTLE_ENDIAN)
|
|
||||||
localExtraField = ByteArray(buffer.getUShort().toInt())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toLFH(): ByteBuffer {
|
|
||||||
val nameBytes = fileName.toByteArray(Charsets.UTF_8)
|
|
||||||
|
|
||||||
val buffer = ByteBuffer.allocate(LFH_HEADER_SIZE + nameBytes.size + localExtraField.size)
|
|
||||||
.also { it.order(ByteOrder.LITTLE_ENDIAN) }
|
|
||||||
|
|
||||||
buffer.putUInt(LFH_SIGNATURE)
|
|
||||||
buffer.putUShort(versionNeeded)
|
|
||||||
buffer.putUShort(flags)
|
|
||||||
buffer.putUShort(compression)
|
|
||||||
buffer.putUShort(modificationTime)
|
|
||||||
buffer.putUShort(modificationDate)
|
|
||||||
buffer.putUInt(crc32)
|
|
||||||
buffer.putUInt(compressedSize)
|
|
||||||
buffer.putUInt(uncompressedSize)
|
|
||||||
buffer.putUShort(nameBytes.size.toUShort())
|
|
||||||
buffer.putUShort(localExtraField.size.toUShort())
|
|
||||||
|
|
||||||
buffer.put(nameBytes)
|
|
||||||
buffer.put(localExtraField)
|
|
||||||
|
|
||||||
buffer.flip()
|
|
||||||
return buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toCDE(): ByteBuffer {
|
|
||||||
val nameBytes = fileName.toByteArray(Charsets.UTF_8)
|
|
||||||
val commentBytes = fileComment.toByteArray(Charsets.UTF_8)
|
|
||||||
|
|
||||||
val buffer =
|
|
||||||
ByteBuffer.allocate(CDE_HEADER_SIZE + nameBytes.size + extraField.size + commentBytes.size)
|
|
||||||
.also { it.order(ByteOrder.LITTLE_ENDIAN) }
|
|
||||||
|
|
||||||
buffer.putUInt(CDE_SIGNATURE)
|
|
||||||
buffer.putUShort(version)
|
|
||||||
buffer.putUShort(versionNeeded)
|
|
||||||
buffer.putUShort(flags)
|
|
||||||
buffer.putUShort(compression)
|
|
||||||
buffer.putUShort(modificationTime)
|
|
||||||
buffer.putUShort(modificationDate)
|
|
||||||
buffer.putUInt(crc32)
|
|
||||||
buffer.putUInt(compressedSize)
|
|
||||||
buffer.putUInt(uncompressedSize)
|
|
||||||
buffer.putUShort(nameBytes.size.toUShort())
|
|
||||||
buffer.putUShort(extraField.size.toUShort())
|
|
||||||
buffer.putUShort(commentBytes.size.toUShort())
|
|
||||||
buffer.putUShort(diskNumber)
|
|
||||||
buffer.putUShort(internalAttributes)
|
|
||||||
buffer.putUInt(externalAttributes)
|
|
||||||
buffer.putUInt(localHeaderOffset)
|
|
||||||
|
|
||||||
buffer.put(nameBytes)
|
|
||||||
buffer.put(extraField)
|
|
||||||
buffer.put(commentBytes)
|
|
||||||
|
|
||||||
buffer.flip()
|
|
||||||
return buffer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="ic_launcher_background">#1B1B1B</color>
|
|
||||||
</resources>
|
|
@ -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>
|
|
@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<paths>
|
|
||||||
<cache-path name="cache" path="." />
|
|
||||||
</paths>
|
|
@ -1,4 +0,0 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="app.revanced.manager.flutter">
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
|
||||||
</manifest>
|
|
@ -1,37 +0,0 @@
|
|||||||
buildscript {
|
|
||||||
ext.kotlin_version = '1.7.10'
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
classpath 'com.android.tools.build:gradle:7.1.3'
|
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allprojects {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
maven {
|
|
||||||
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
|
|
||||||
credentials {
|
|
||||||
username = (project.findProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR")) as String
|
|
||||||
password = (project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN")) as String
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mavenLocal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rootProject.buildDir = '../build'
|
|
||||||
subprojects {
|
|
||||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
|
||||||
project.evaluationDependsOn(':app')
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register("clean", Delete) {
|
|
||||||
delete rootProject.buildDir
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M -XX:+UseParallelGC
|
|
||||||
org.gradle.parallel=true
|
|
||||||
org.gradle.daemon=true
|
|
||||||
org.gradle.caching=true
|
|
||||||
android.useAndroidX=true
|
|
||||||
android.enableJetifier=true
|
|
@ -1,11 +0,0 @@
|
|||||||
include ':app'
|
|
||||||
|
|
||||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
|
||||||
def properties = new Properties()
|
|
||||||
|
|
||||||
assert localPropertiesFile.exists()
|
|
||||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
|
||||||
|
|
||||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
|
||||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
|
||||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
|
1
app/.gitignore
vendored
Normal file
1
app/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
222
app/build.gradle.kts
Normal file
222
app/build.gradle.kts
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application)
|
||||||
|
alias(libs.plugins.kotlin.android)
|
||||||
|
alias(libs.plugins.kotlin.serialization)
|
||||||
|
alias(libs.plugins.kotlin.parcelize)
|
||||||
|
alias(libs.plugins.compose.compiler)
|
||||||
|
alias(libs.plugins.devtools)
|
||||||
|
alias(libs.plugins.about.libraries)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "app.revanced.manager"
|
||||||
|
compileSdk = 35
|
||||||
|
buildToolsVersion = "35.0.1"
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "app.revanced.manager"
|
||||||
|
minSdk = 26
|
||||||
|
targetSdk = 35
|
||||||
|
versionCode = 1
|
||||||
|
versionName = "0.0.1"
|
||||||
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
applicationIdSuffix = ".debug"
|
||||||
|
resValue("string", "app_name", "ReVanced Manager (Debug)")
|
||||||
|
isPseudoLocalesEnabled = true
|
||||||
|
|
||||||
|
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
|
||||||
|
}
|
||||||
|
|
||||||
|
release {
|
||||||
|
if (!project.hasProperty("noProguard")) {
|
||||||
|
isMinifyEnabled = true
|
||||||
|
isShrinkResources = true
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
|
}
|
||||||
|
|
||||||
|
val keystoreFile = file("keystore.jks")
|
||||||
|
|
||||||
|
if (project.hasProperty("signAsDebug") || !keystoreFile.exists()) {
|
||||||
|
applicationIdSuffix = ".debug_signed"
|
||||||
|
resValue("string", "app_name", "ReVanced Manager (Debug signed)")
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
|
||||||
|
isPseudoLocalesEnabled = true
|
||||||
|
} else {
|
||||||
|
signingConfig = signingConfigs.create("release") {
|
||||||
|
storeFile = keystoreFile
|
||||||
|
storePassword = System.getenv("KEYSTORE_PASSWORD")
|
||||||
|
keyAlias = System.getenv("KEYSTORE_ENTRY_ALIAS")
|
||||||
|
keyPassword = System.getenv("KEYSTORE_ENTRY_PASSWORD")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConfigField("long", "BUILD_ID", "0L")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
dependenciesInfo {
|
||||||
|
includeInApk = false
|
||||||
|
includeInBundle = false
|
||||||
|
}
|
||||||
|
|
||||||
|
packaging {
|
||||||
|
resources.excludes.addAll(
|
||||||
|
listOf(
|
||||||
|
"/prebuilt/**",
|
||||||
|
"META-INF/DEPENDENCIES",
|
||||||
|
"META-INF/**.version",
|
||||||
|
"DebugProbesKt.bin",
|
||||||
|
"kotlin-tooling-metadata.json",
|
||||||
|
"org/bouncycastle/pqc/**.properties",
|
||||||
|
"org/bouncycastle/x509/**.properties",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
jniLibs {
|
||||||
|
useLegacyPackaging = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ksp {
|
||||||
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
compose = true
|
||||||
|
aidl = true
|
||||||
|
buildConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
androidResources {
|
||||||
|
generateLocaleConfig = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
path = file("src/main/cpp/CMakeLists.txt")
|
||||||
|
version = "3.22.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
// AndroidX Core
|
||||||
|
implementation(libs.androidx.ktx)
|
||||||
|
implementation(libs.runtime.ktx)
|
||||||
|
implementation(libs.runtime.compose)
|
||||||
|
implementation(libs.splash.screen)
|
||||||
|
implementation(libs.activity.compose)
|
||||||
|
implementation(libs.work.runtime.ktx)
|
||||||
|
implementation(libs.preferences.datastore)
|
||||||
|
implementation(libs.appcompat)
|
||||||
|
|
||||||
|
// Compose
|
||||||
|
implementation(platform(libs.compose.bom))
|
||||||
|
implementation(libs.compose.ui)
|
||||||
|
implementation(libs.compose.ui.preview)
|
||||||
|
implementation(libs.compose.ui.tooling)
|
||||||
|
implementation(libs.compose.livedata)
|
||||||
|
implementation(libs.compose.material.icons.extended)
|
||||||
|
implementation(libs.compose.material3)
|
||||||
|
implementation(libs.navigation.compose)
|
||||||
|
|
||||||
|
// Accompanist
|
||||||
|
implementation(libs.accompanist.drawablepainter)
|
||||||
|
|
||||||
|
// Placeholder
|
||||||
|
implementation(libs.placeholder.material3)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
implementation(libs.kotlinx.datetime)
|
||||||
|
|
||||||
|
// Room
|
||||||
|
implementation(libs.room.runtime)
|
||||||
|
implementation(libs.room.ktx)
|
||||||
|
annotationProcessor(libs.room.compiler)
|
||||||
|
ksp(libs.room.compiler)
|
||||||
|
|
||||||
|
// ReVanced
|
||||||
|
implementation(libs.revanced.patcher)
|
||||||
|
implementation(libs.revanced.library)
|
||||||
|
|
||||||
|
// Downloader plugins
|
||||||
|
implementation(libs.plugin.api)
|
||||||
|
|
||||||
|
// Native processes
|
||||||
|
implementation(libs.kotlin.process)
|
||||||
|
|
||||||
|
// HiddenAPI
|
||||||
|
compileOnly(libs.hidden.api.stub)
|
||||||
|
|
||||||
|
// LibSU
|
||||||
|
implementation(libs.libsu.core)
|
||||||
|
implementation(libs.libsu.service)
|
||||||
|
implementation(libs.libsu.nio)
|
||||||
|
|
||||||
|
// Koin
|
||||||
|
implementation(libs.koin.android)
|
||||||
|
implementation(libs.koin.compose)
|
||||||
|
implementation(libs.koin.compose.navigation)
|
||||||
|
implementation(libs.koin.workmanager)
|
||||||
|
|
||||||
|
// Licenses
|
||||||
|
implementation(libs.about.libraries)
|
||||||
|
|
||||||
|
// Ktor
|
||||||
|
implementation(libs.ktor.core)
|
||||||
|
implementation(libs.ktor.logging)
|
||||||
|
implementation(libs.ktor.okhttp)
|
||||||
|
implementation(libs.ktor.content.negotiation)
|
||||||
|
implementation(libs.ktor.serialization)
|
||||||
|
|
||||||
|
// Markdown
|
||||||
|
implementation(libs.markdown.renderer)
|
||||||
|
|
||||||
|
// Fading Edges
|
||||||
|
implementation(libs.fading.edges)
|
||||||
|
|
||||||
|
// Scrollbars
|
||||||
|
implementation(libs.scrollbars)
|
||||||
|
|
||||||
|
// EnumUtil
|
||||||
|
implementation(libs.enumutil)
|
||||||
|
ksp(libs.enumutil.ksp)
|
||||||
|
|
||||||
|
// Reorderable lists
|
||||||
|
implementation(libs.reorderable)
|
||||||
|
|
||||||
|
// Compose Icons
|
||||||
|
implementation(libs.compose.icons.fontawesome)
|
||||||
|
}
|
63
app/proguard-rules.pro
vendored
Normal file
63
app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.kts.kts.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
-dontobfuscate
|
||||||
|
|
||||||
|
# Required for serialization to work properly
|
||||||
|
-if @kotlinx.serialization.Serializable class **
|
||||||
|
-keepclassmembers class <1> {
|
||||||
|
static <1>$Companion Companion;
|
||||||
|
}
|
||||||
|
-if @kotlinx.serialization.Serializable class ** {
|
||||||
|
static **$* *;
|
||||||
|
}
|
||||||
|
-keepclassmembers class <2>$<3> {
|
||||||
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
|
}
|
||||||
|
-if @kotlinx.serialization.Serializable class ** {
|
||||||
|
public static ** INSTANCE;
|
||||||
|
}
|
||||||
|
-keepclassmembers class <1> {
|
||||||
|
public static <1> INSTANCE;
|
||||||
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
|
}
|
||||||
|
|
||||||
|
# This required for the process runtime.
|
||||||
|
-keep class app.revanced.manager.patcher.runtime.process.* {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
# Required for the patcher to function correctly
|
||||||
|
-keep class app.revanced.patcher.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
-keep class brut.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
-keep class org.xmlpull.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
-keep class kotlin.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
-keep class org.jf.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
-keep class com.android.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
-keep class app.revanced.manager.plugin.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
|
||||||
|
-dontwarn com.google.auto.value.**
|
||||||
|
-dontwarn java.awt.**
|
||||||
|
-dontwarn javax.**
|
||||||
|
-dontwarn org.slf4j.**
|
||||||
|
-dontwarn it.skrape.fetcher.*
|
||||||
|
-dontwarn com.google.j2objc.annotations.*
|
||||||
|
|
||||||
|
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
|
429
app/schemas/app.revanced.manager.data.room.AppDatabase/1.json
Normal file
429
app/schemas/app.revanced.manager.data.room.AppDatabase/1.json
Normal file
@ -0,0 +1,429 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 1,
|
||||||
|
"identityHash": "d0119047505da435972c5247181de675",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "patch_bundles",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `name` TEXT NOT NULL, `version` TEXT, `source` TEXT NOT NULL, `auto_update` INTEGER NOT NULL, PRIMARY KEY(`uid`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "source",
|
||||||
|
"columnName": "source",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "autoUpdate",
|
||||||
|
"columnName": "auto_update",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "patch_selections",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `patch_bundle` INTEGER NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`uid`), FOREIGN KEY(`patch_bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "patchBundle",
|
||||||
|
"columnName": "patch_bundle",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "packageName",
|
||||||
|
"columnName": "package_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_patch_selections_patch_bundle_package_name",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"patch_bundle",
|
||||||
|
"package_name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_patch_selections_patch_bundle_package_name` ON `${TABLE_NAME}` (`patch_bundle`, `package_name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "patch_bundles",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"patch_bundle"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "selected_patches",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`selection` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`selection`, `patch_name`), FOREIGN KEY(`selection`) REFERENCES `patch_selections`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "selection",
|
||||||
|
"columnName": "selection",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "patchName",
|
||||||
|
"columnName": "patch_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"selection",
|
||||||
|
"patch_name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "patch_selections",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"selection"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "downloaded_app",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `directory` TEXT NOT NULL, `last_used` INTEGER NOT NULL, PRIMARY KEY(`package_name`, `version`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "packageName",
|
||||||
|
"columnName": "package_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "directory",
|
||||||
|
"columnName": "directory",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUsed",
|
||||||
|
"columnName": "last_used",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"package_name",
|
||||||
|
"version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "installed_app",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`current_package_name` TEXT NOT NULL, `original_package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `install_type` TEXT NOT NULL, PRIMARY KEY(`current_package_name`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "currentPackageName",
|
||||||
|
"columnName": "current_package_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "originalPackageName",
|
||||||
|
"columnName": "original_package_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "installType",
|
||||||
|
"columnName": "install_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"current_package_name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "applied_patch",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `bundle` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`package_name`, `bundle`, `patch_name`), FOREIGN KEY(`package_name`) REFERENCES `installed_app`(`current_package_name`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "packageName",
|
||||||
|
"columnName": "package_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bundle",
|
||||||
|
"columnName": "bundle",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "patchName",
|
||||||
|
"columnName": "patch_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"package_name",
|
||||||
|
"bundle",
|
||||||
|
"patch_name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_applied_patch_bundle",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"bundle"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_applied_patch_bundle` ON `${TABLE_NAME}` (`bundle`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "installed_app",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"package_name"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"current_package_name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "patch_bundles",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"bundle"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "option_groups",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `patch_bundle` INTEGER NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`uid`), FOREIGN KEY(`patch_bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "patchBundle",
|
||||||
|
"columnName": "patch_bundle",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "packageName",
|
||||||
|
"columnName": "package_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_option_groups_patch_bundle_package_name",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"patch_bundle",
|
||||||
|
"package_name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_option_groups_patch_bundle_package_name` ON `${TABLE_NAME}` (`patch_bundle`, `package_name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "patch_bundles",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"patch_bundle"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "options",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, `key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`group`, `patch_name`, `key`), FOREIGN KEY(`group`) REFERENCES `option_groups`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "group",
|
||||||
|
"columnName": "group",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "patchName",
|
||||||
|
"columnName": "patch_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "key",
|
||||||
|
"columnName": "key",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "value",
|
||||||
|
"columnName": "value",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"group",
|
||||||
|
"patch_name",
|
||||||
|
"key"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "option_groups",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"group"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "trusted_downloader_plugins",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `signature` BLOB NOT NULL, PRIMARY KEY(`package_name`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "packageName",
|
||||||
|
"columnName": "package_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "signature",
|
||||||
|
"columnName": "signature",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"package_name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd0119047505da435972c5247181de675')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
78
app/src/main/AndroidManifest.xml
Normal file
78
app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<permission
|
||||||
|
android:name="app.revanced.manager.permission.PLUGIN_HOST"
|
||||||
|
android:protectionLevel="signature"
|
||||||
|
android:label="@string/plugin_host_permission_label"
|
||||||
|
android:description="@string/plugin_host_permission_description"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<uses-permission android:name="app.revanced.manager.permission.PLUGIN_HOST" />
|
||||||
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
|
||||||
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||||
|
tools:ignore="ScopedStorage" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".ManagerApplication"
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:largeHeap="true"
|
||||||
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.ReVancedManager"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
|
tools:targetApi="34">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:theme="@style/Theme.ReVancedManager">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity android:name=".plugin.downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" />
|
||||||
|
|
||||||
|
<service android:name=".service.InstallService" />
|
||||||
|
<service android:name=".service.UninstallService" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
|
android:foregroundServiceType="specialUse"
|
||||||
|
android:exported="false"
|
||||||
|
tools:node="merge">
|
||||||
|
<property
|
||||||
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
|
android:value="patching"
|
||||||
|
/>
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.startup.InitializationProvider"
|
||||||
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
|
android:exported="false"
|
||||||
|
tools:node="merge">
|
||||||
|
<meta-data
|
||||||
|
android:name="androidx.work.WorkManagerInitializer"
|
||||||
|
android:value="androidx.startup"
|
||||||
|
tools:node="remove" />
|
||||||
|
</provider>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
@ -0,0 +1,8 @@
|
|||||||
|
// IRootService.aidl
|
||||||
|
package app.revanced.manager;
|
||||||
|
|
||||||
|
// Declare any non-default types here with import statements
|
||||||
|
|
||||||
|
interface IRootSystemService {
|
||||||
|
IBinder getFileSystemService();
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
// Parameters.aidl
|
||||||
|
package app.revanced.manager.patcher.runtime.process;
|
||||||
|
|
||||||
|
parcelable Parameters;
|
6
app/src/main/assets/root/module.prop
Normal file
6
app/src/main/assets/root/module.prop
Normal 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
|
40
app/src/main/assets/root/service.sh
Normal file
40
app/src/main/assets/root/service.sh
Normal 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"
|
38
app/src/main/cpp/CMakeLists.txt
Normal file
38
app/src/main/cpp/CMakeLists.txt
Normal 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)
|
62
app/src/main/cpp/prop_override.cpp
Normal file
62
app/src/main/cpp/prop_override.cpp
Normal 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);
|
||||||
|
}
|
336
app/src/main/java/app/revanced/manager/MainActivity.kt
Normal file
336
app/src/main/java/app/revanced/manager/MainActivity.kt
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
package app.revanced.manager
|
||||||
|
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.animation.slideInHorizontally
|
||||||
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.navigation.NavBackStackEntry
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.navigation
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import androidx.navigation.toRoute
|
||||||
|
import app.revanced.manager.ui.model.navigation.AppSelector
|
||||||
|
import app.revanced.manager.ui.model.navigation.ComplexParameter
|
||||||
|
import app.revanced.manager.ui.model.navigation.Dashboard
|
||||||
|
import app.revanced.manager.ui.model.navigation.InstalledApplicationInfo
|
||||||
|
import app.revanced.manager.ui.model.navigation.Patcher
|
||||||
|
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
||||||
|
import app.revanced.manager.ui.model.navigation.Settings
|
||||||
|
import app.revanced.manager.ui.model.navigation.Update
|
||||||
|
import app.revanced.manager.ui.screen.AppSelectorScreen
|
||||||
|
import app.revanced.manager.ui.screen.DashboardScreen
|
||||||
|
import app.revanced.manager.ui.screen.InstalledAppInfoScreen
|
||||||
|
import app.revanced.manager.ui.screen.PatcherScreen
|
||||||
|
import app.revanced.manager.ui.screen.PatchesSelectorScreen
|
||||||
|
import app.revanced.manager.ui.screen.RequiredOptionsScreen
|
||||||
|
import app.revanced.manager.ui.screen.SelectedAppInfoScreen
|
||||||
|
import app.revanced.manager.ui.screen.SettingsScreen
|
||||||
|
import app.revanced.manager.ui.screen.UpdateScreen
|
||||||
|
import app.revanced.manager.ui.screen.settings.AboutSettingsScreen
|
||||||
|
import app.revanced.manager.ui.screen.settings.AdvancedSettingsScreen
|
||||||
|
import app.revanced.manager.ui.screen.settings.ContributorSettingsScreen
|
||||||
|
import app.revanced.manager.ui.screen.settings.DeveloperSettingsScreen
|
||||||
|
import app.revanced.manager.ui.screen.settings.DownloadsSettingsScreen
|
||||||
|
import app.revanced.manager.ui.screen.settings.GeneralSettingsScreen
|
||||||
|
import app.revanced.manager.ui.screen.settings.ImportExportSettingsScreen
|
||||||
|
import app.revanced.manager.ui.screen.settings.LicensesSettingsScreen
|
||||||
|
import app.revanced.manager.ui.screen.settings.update.ChangelogsSettingsScreen
|
||||||
|
import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen
|
||||||
|
import app.revanced.manager.ui.theme.ReVancedManagerTheme
|
||||||
|
import app.revanced.manager.ui.theme.Theme
|
||||||
|
import app.revanced.manager.ui.viewmodel.MainViewModel
|
||||||
|
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
|
||||||
|
import app.revanced.manager.util.EventEffect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
import org.koin.androidx.compose.navigation.koinNavViewModel
|
||||||
|
import org.koin.core.parameter.parametersOf
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
|
||||||
|
|
||||||
|
class MainActivity : ComponentActivity() {
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
enableEdgeToEdge()
|
||||||
|
installSplashScreen()
|
||||||
|
|
||||||
|
val vm: MainViewModel = getActivityViewModel()
|
||||||
|
|
||||||
|
setContent {
|
||||||
|
val launcher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult(),
|
||||||
|
onResult = vm::applyLegacySettings
|
||||||
|
)
|
||||||
|
val theme by vm.prefs.theme.getAsState()
|
||||||
|
val dynamicColor by vm.prefs.dynamicColor.getAsState()
|
||||||
|
|
||||||
|
EventEffect(vm.legacyImportActivityFlow) {
|
||||||
|
try {
|
||||||
|
launcher.launch(it)
|
||||||
|
} catch (_: ActivityNotFoundException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ReVancedManagerTheme(
|
||||||
|
darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK,
|
||||||
|
dynamicColor = dynamicColor
|
||||||
|
) {
|
||||||
|
ReVancedManager(vm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ReVancedManager(vm: MainViewModel) {
|
||||||
|
val navController = rememberNavController()
|
||||||
|
|
||||||
|
EventEffect(vm.appSelectFlow) { app ->
|
||||||
|
navController.navigateComplex(
|
||||||
|
SelectedApplicationInfo,
|
||||||
|
SelectedApplicationInfo.ViewModelParams(app)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
NavHost(
|
||||||
|
navController = navController,
|
||||||
|
startDestination = Dashboard,
|
||||||
|
enterTransition = { slideInHorizontally(initialOffsetX = { it }) },
|
||||||
|
exitTransition = { slideOutHorizontally(targetOffsetX = { -it / 3 }) },
|
||||||
|
popEnterTransition = { slideInHorizontally(initialOffsetX = { -it / 3 }) },
|
||||||
|
popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) },
|
||||||
|
) {
|
||||||
|
composable<Dashboard> {
|
||||||
|
DashboardScreen(
|
||||||
|
onSettingsClick = { navController.navigate(Settings) },
|
||||||
|
onAppSelectorClick = {
|
||||||
|
navController.navigate(AppSelector)
|
||||||
|
},
|
||||||
|
onUpdateClick = {
|
||||||
|
navController.navigate(Update())
|
||||||
|
},
|
||||||
|
onDownloaderPluginClick = {
|
||||||
|
navController.navigate(Settings.Downloads)
|
||||||
|
},
|
||||||
|
onAppClick = { packageName ->
|
||||||
|
navController.navigate(InstalledApplicationInfo(packageName))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<InstalledApplicationInfo> {
|
||||||
|
val data = it.toRoute<InstalledApplicationInfo>()
|
||||||
|
|
||||||
|
InstalledAppInfoScreen(
|
||||||
|
onPatchClick = vm::selectApp,
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
viewModel = koinViewModel { parametersOf(data.packageName) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<AppSelector> {
|
||||||
|
AppSelectorScreen(
|
||||||
|
onSelect = vm::selectApp,
|
||||||
|
onStorageSelect = vm::selectApp,
|
||||||
|
onBackClick = navController::popBackStack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Patcher> {
|
||||||
|
PatcherScreen(
|
||||||
|
onBackClick = {
|
||||||
|
navController.navigate(route = Dashboard) {
|
||||||
|
launchSingleTop = true
|
||||||
|
popUpTo<Dashboard> {
|
||||||
|
inclusive = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
viewModel = koinViewModel { parametersOf(it.getComplexArg<Patcher.ViewModelParams>()) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Update> {
|
||||||
|
val data = it.toRoute<Update>()
|
||||||
|
|
||||||
|
UpdateScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
vm = koinViewModel { parametersOf(data.downloadOnScreenEntry) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
navigation<SelectedApplicationInfo>(startDestination = SelectedApplicationInfo.Main) {
|
||||||
|
composable<SelectedApplicationInfo.Main> {
|
||||||
|
val parentBackStackEntry = navController.navGraphEntry(it)
|
||||||
|
val data =
|
||||||
|
parentBackStackEntry.getComplexArg<SelectedApplicationInfo.ViewModelParams>()
|
||||||
|
val viewModel =
|
||||||
|
koinNavViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
|
||||||
|
parametersOf(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectedAppInfoScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
onPatchClick = {
|
||||||
|
it.lifecycleScope.launch {
|
||||||
|
navController.navigateComplex(
|
||||||
|
Patcher,
|
||||||
|
viewModel.getPatcherParams()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPatchSelectorClick = { app, patches, options ->
|
||||||
|
navController.navigateComplex(
|
||||||
|
SelectedApplicationInfo.PatchesSelector,
|
||||||
|
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
|
||||||
|
app,
|
||||||
|
patches,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onRequiredOptions = { app, patches, options ->
|
||||||
|
navController.navigateComplex(
|
||||||
|
SelectedApplicationInfo.RequiredOptions,
|
||||||
|
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
|
||||||
|
app,
|
||||||
|
patches,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
vm = viewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<SelectedApplicationInfo.PatchesSelector> {
|
||||||
|
val data =
|
||||||
|
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
||||||
|
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
||||||
|
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||||
|
)
|
||||||
|
|
||||||
|
PatchesSelectorScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
onSave = { patches, options ->
|
||||||
|
selectedAppInfoVm.updateConfiguration(patches, options)
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
vm = koinViewModel { parametersOf(data) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<SelectedApplicationInfo.RequiredOptions> {
|
||||||
|
val data =
|
||||||
|
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
||||||
|
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
||||||
|
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||||
|
)
|
||||||
|
|
||||||
|
RequiredOptionsScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
onContinue = { patches, options ->
|
||||||
|
selectedAppInfoVm.updateConfiguration(patches, options)
|
||||||
|
it.lifecycleScope.launch {
|
||||||
|
navController.navigateComplex(
|
||||||
|
Patcher,
|
||||||
|
selectedAppInfoVm.getPatcherParams()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
vm = koinViewModel { parametersOf(data) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
navigation<Settings>(startDestination = Settings.Main) {
|
||||||
|
composable<Settings.Main> {
|
||||||
|
SettingsScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
navigate = navController::navigate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.General> {
|
||||||
|
GeneralSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.Advanced> {
|
||||||
|
AdvancedSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.Developer> {
|
||||||
|
DeveloperSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.Updates> {
|
||||||
|
UpdatesSettingsScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
onChangelogClick = { navController.navigate(Settings.Changelogs) },
|
||||||
|
onUpdateClick = { navController.navigate(Update()) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.Downloads> {
|
||||||
|
DownloadsSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.ImportExport> {
|
||||||
|
ImportExportSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.About> {
|
||||||
|
AboutSettingsScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
navigate = navController::navigate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.Changelogs> {
|
||||||
|
ChangelogsSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.Contributors> {
|
||||||
|
ContributorSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.Licenses> {
|
||||||
|
LicensesSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NavController.navGraphEntry(entry: NavBackStackEntry) =
|
||||||
|
remember(entry) { getBackStackEntry(entry.destination.parent!!.id) }
|
||||||
|
|
||||||
|
// Androidx Navigation does not support storing complex types in route objects, so we have to store them inside the saved state handle of the back stack entry instead.
|
||||||
|
private fun <T : Parcelable, R : ComplexParameter<T>> NavController.navigateComplex(
|
||||||
|
route: R,
|
||||||
|
data: T
|
||||||
|
) {
|
||||||
|
navigate(route)
|
||||||
|
getBackStackEntry(route).savedStateHandle["args"] = data
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T : Parcelable> NavBackStackEntry.getComplexArg() = savedStateHandle.get<T>("args")!!
|
110
app/src/main/java/app/revanced/manager/ManagerApplication.kt
Normal file
110
app/src/main/java/app/revanced/manager/ManagerApplication.kt
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
package app.revanced.manager
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.Application
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import app.revanced.manager.data.platform.Filesystem
|
||||||
|
import app.revanced.manager.di.*
|
||||||
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
|
import app.revanced.manager.domain.repository.DownloaderPluginRepository
|
||||||
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
|
import app.revanced.manager.util.tag
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import coil.Coil
|
||||||
|
import coil.ImageLoader
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import com.topjohnwu.superuser.internal.BuilderImpl
|
||||||
|
import kotlinx.coroutines.MainScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
|
||||||
|
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.android.ext.koin.androidLogger
|
||||||
|
import org.koin.androidx.workmanager.koin.workManagerFactory
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
|
||||||
|
class ManagerApplication : Application() {
|
||||||
|
private val scope = MainScope()
|
||||||
|
private val prefs: PreferencesManager by inject()
|
||||||
|
private val patchBundleRepository: PatchBundleRepository by inject()
|
||||||
|
private val downloaderPluginRepository: DownloaderPluginRepository by inject()
|
||||||
|
private val fs: Filesystem by inject()
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
startKoin {
|
||||||
|
androidContext(this@ManagerApplication)
|
||||||
|
androidLogger()
|
||||||
|
workManagerFactory()
|
||||||
|
modules(
|
||||||
|
httpModule,
|
||||||
|
preferencesModule,
|
||||||
|
repositoryModule,
|
||||||
|
serviceModule,
|
||||||
|
managerModule,
|
||||||
|
workerModule,
|
||||||
|
viewModelModule,
|
||||||
|
databaseModule,
|
||||||
|
rootModule
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val pixels = 512
|
||||||
|
Coil.setImageLoader(
|
||||||
|
ImageLoader.Builder(this)
|
||||||
|
.components {
|
||||||
|
add(AppIconKeyer())
|
||||||
|
add(AppIconFetcher.Factory(pixels, true, this@ManagerApplication))
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
val shellBuilder = BuilderImpl.create().setFlags(Shell.FLAG_MOUNT_MASTER)
|
||||||
|
Shell.setDefaultBuilder(shellBuilder)
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
prefs.preload()
|
||||||
|
}
|
||||||
|
scope.launch(Dispatchers.Default) {
|
||||||
|
downloaderPluginRepository.reload()
|
||||||
|
}
|
||||||
|
scope.launch(Dispatchers.Default) {
|
||||||
|
with(patchBundleRepository) {
|
||||||
|
reload()
|
||||||
|
updateCheck()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
|
||||||
|
private var firstActivityCreated = false
|
||||||
|
|
||||||
|
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||||
|
if (firstActivityCreated) return
|
||||||
|
firstActivityCreated = true
|
||||||
|
|
||||||
|
// We do not want to call onFreshProcessStart() if there is state to restore.
|
||||||
|
// This can happen on system-initiated process death.
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
Log.d(tag, "Fresh process created")
|
||||||
|
onFreshProcessStart()
|
||||||
|
} else Log.d(tag, "System-initiated process death detected")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityStarted(activity: Activity) {}
|
||||||
|
override fun onActivityResumed(activity: Activity) {}
|
||||||
|
override fun onActivityPaused(activity: Activity) {}
|
||||||
|
override fun onActivityStopped(activity: Activity) {}
|
||||||
|
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
|
||||||
|
override fun onActivityDestroyed(activity: Activity) {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onFreshProcessStart() {
|
||||||
|
fs.uiTempDir.apply {
|
||||||
|
deleteRecursively()
|
||||||
|
mkdirs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
package app.revanced.manager.data.platform
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import app.revanced.manager.util.RequestManageStorageContract
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
class Filesystem(private val app: Application) {
|
||||||
|
val contentResolver = app.contentResolver // TODO: move Content Resolver operations to here.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A directory that gets cleared when the app restarts.
|
||||||
|
* Do not store paths to this directory in a parcel.
|
||||||
|
*/
|
||||||
|
val tempDir: File = app.getDir("ephemeral", Context.MODE_PRIVATE).apply {
|
||||||
|
deleteRecursively()
|
||||||
|
mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A directory for storing temporary files related to UI.
|
||||||
|
* This is the same as [tempDir], but does not get cleared on system-initiated process death.
|
||||||
|
* Paths to this directory can be safely stored in parcels.
|
||||||
|
*/
|
||||||
|
val uiTempDir: File = app.getDir("ui_ephemeral", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
fun externalFilesDir(): Path = Environment.getExternalStorageDirectory().toPath()
|
||||||
|
|
||||||
|
private fun usesManagePermission() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
|
|
||||||
|
private val storagePermissionName =
|
||||||
|
if (usesManagePermission()) Manifest.permission.MANAGE_EXTERNAL_STORAGE else Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
|
|
||||||
|
fun permissionContract(): Pair<ActivityResultContract<String, Boolean>, String> {
|
||||||
|
val contract =
|
||||||
|
if (usesManagePermission()) RequestManageStorageContract() else ActivityResultContracts.RequestPermission()
|
||||||
|
return contract to storagePermissionName
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasStoragePermission() =
|
||||||
|
if (usesManagePermission()) Environment.isExternalStorageManager() else app.checkSelfPermission(
|
||||||
|
storagePermissionName
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package app.revanced.manager.data.room
|
||||||
|
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
import app.revanced.manager.data.room.apps.downloaded.DownloadedAppDao
|
||||||
|
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
|
||||||
|
import app.revanced.manager.data.room.apps.installed.AppliedPatch
|
||||||
|
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
||||||
|
import app.revanced.manager.data.room.apps.installed.InstalledAppDao
|
||||||
|
import app.revanced.manager.data.room.selection.PatchSelection
|
||||||
|
import app.revanced.manager.data.room.selection.SelectedPatch
|
||||||
|
import app.revanced.manager.data.room.selection.SelectionDao
|
||||||
|
import app.revanced.manager.data.room.bundles.PatchBundleDao
|
||||||
|
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||||
|
import app.revanced.manager.data.room.options.Option
|
||||||
|
import app.revanced.manager.data.room.options.OptionDao
|
||||||
|
import app.revanced.manager.data.room.options.OptionGroup
|
||||||
|
import app.revanced.manager.data.room.plugins.TrustedDownloaderPlugin
|
||||||
|
import app.revanced.manager.data.room.plugins.TrustedDownloaderPluginDao
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
@Database(
|
||||||
|
entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, OptionGroup::class, Option::class, TrustedDownloaderPlugin::class],
|
||||||
|
version = 1
|
||||||
|
)
|
||||||
|
@TypeConverters(Converters::class)
|
||||||
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
abstract fun patchBundleDao(): PatchBundleDao
|
||||||
|
abstract fun selectionDao(): SelectionDao
|
||||||
|
abstract fun downloadedAppDao(): DownloadedAppDao
|
||||||
|
abstract fun installedAppDao(): InstalledAppDao
|
||||||
|
abstract fun optionDao(): OptionDao
|
||||||
|
abstract fun trustedDownloaderPluginDao(): TrustedDownloaderPluginDao
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun generateUid() = Random.Default.nextInt()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package app.revanced.manager.data.room
|
||||||
|
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import app.revanced.manager.data.room.bundles.Source
|
||||||
|
import app.revanced.manager.data.room.options.Option.SerializedValue
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class Converters {
|
||||||
|
@TypeConverter
|
||||||
|
fun sourceFromString(value: String) = Source.from(value)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun sourceToString(value: Source) = value.toString()
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fileFromString(value: String) = File(value)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fileToString(file: File): String = file.path
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun serializedOptionFromString(value: String) = SerializedValue.fromJsonString(value)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun serializedOptionToString(value: SerializedValue) = value.toJsonString()
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package app.revanced.manager.data.room.apps.downloaded
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "downloaded_app",
|
||||||
|
primaryKeys = ["package_name", "version"]
|
||||||
|
)
|
||||||
|
data class DownloadedApp(
|
||||||
|
@ColumnInfo(name = "package_name") val packageName: String,
|
||||||
|
@ColumnInfo(name = "version") val version: String,
|
||||||
|
@ColumnInfo(name = "directory") val directory: File,
|
||||||
|
@ColumnInfo(name = "last_used") val lastUsed: Long = System.currentTimeMillis()
|
||||||
|
)
|
@ -0,0 +1,26 @@
|
|||||||
|
package app.revanced.manager.data.room.apps.downloaded
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Upsert
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface DownloadedAppDao {
|
||||||
|
@Query("SELECT * FROM downloaded_app")
|
||||||
|
fun getAllApps(): Flow<List<DownloadedApp>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM downloaded_app WHERE package_name = :packageName AND version = :version")
|
||||||
|
suspend fun get(packageName: String, version: String): DownloadedApp?
|
||||||
|
|
||||||
|
@Upsert
|
||||||
|
suspend fun upsert(downloadedApp: DownloadedApp)
|
||||||
|
|
||||||
|
@Query("UPDATE downloaded_app SET last_used = :newValue WHERE package_name = :packageName AND version = :version")
|
||||||
|
suspend fun markUsed(packageName: String, version: String, newValue: Long = System.currentTimeMillis())
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun delete(downloadedApps: Collection<DownloadedApp>)
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package app.revanced.manager.data.room.apps.installed
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.Index
|
||||||
|
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
@Entity(
|
||||||
|
tableName = "applied_patch",
|
||||||
|
primaryKeys = ["package_name", "bundle", "patch_name"],
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
InstalledApp::class,
|
||||||
|
parentColumns = ["current_package_name"],
|
||||||
|
childColumns = ["package_name"],
|
||||||
|
onDelete = ForeignKey.CASCADE
|
||||||
|
),
|
||||||
|
ForeignKey(
|
||||||
|
PatchBundleEntity::class,
|
||||||
|
parentColumns = ["uid"],
|
||||||
|
childColumns = ["bundle"],
|
||||||
|
onDelete = ForeignKey.CASCADE
|
||||||
|
)
|
||||||
|
],
|
||||||
|
indices = [Index(value = ["bundle"], unique = false)]
|
||||||
|
)
|
||||||
|
data class AppliedPatch(
|
||||||
|
@ColumnInfo(name = "package_name") val packageName: String,
|
||||||
|
@ColumnInfo(name = "bundle") val bundle: Int,
|
||||||
|
@ColumnInfo(name = "patch_name") val patchName: String
|
||||||
|
) : Parcelable
|
@ -0,0 +1,20 @@
|
|||||||
|
package app.revanced.manager.data.room.apps.installed
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import app.revanced.manager.R
|
||||||
|
|
||||||
|
enum class InstallType(val stringResource: Int) {
|
||||||
|
DEFAULT(R.string.default_install),
|
||||||
|
MOUNT(R.string.mount_install)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity(tableName = "installed_app")
|
||||||
|
data class InstalledApp(
|
||||||
|
@PrimaryKey
|
||||||
|
@ColumnInfo(name = "current_package_name") val currentPackageName: String,
|
||||||
|
@ColumnInfo(name = "original_package_name") val originalPackageName: String,
|
||||||
|
@ColumnInfo(name = "version") val version: String,
|
||||||
|
@ColumnInfo(name = "install_type") val installType: InstallType
|
||||||
|
)
|
@ -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)
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
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, auto_update FROM patch_bundles WHERE uid = :uid")
|
||||||
|
fun getPropsById(uid: Int): Flow<BundleProperties?>
|
||||||
|
|
||||||
|
@Query("UPDATE patch_bundles SET version = :patches WHERE uid = :uid")
|
||||||
|
suspend fun updateVersion(uid: Int, patches: String?)
|
||||||
|
|
||||||
|
@Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid")
|
||||||
|
suspend fun setAutoUpdate(uid: Int, value: Boolean)
|
||||||
|
|
||||||
|
@Query("UPDATE patch_bundles SET name = :value WHERE uid = :uid")
|
||||||
|
suspend fun setName(uid: Int, value: String)
|
||||||
|
|
||||||
|
@Query("DELETE FROM patch_bundles WHERE uid != 0")
|
||||||
|
suspend fun purgeCustomBundles()
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
suspend fun reset() {
|
||||||
|
purgeCustomBundles()
|
||||||
|
updateVersion(0, null) // Reset the main source
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("DELETE FROM patch_bundles WHERE uid = :uid")
|
||||||
|
suspend fun remove(uid: Int)
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
suspend fun add(source: PatchBundleEntity)
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
package app.revanced.manager.data.room.bundles
|
||||||
|
|
||||||
|
import androidx.room.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
|
||||||
|
sealed class Source {
|
||||||
|
object Local : Source() {
|
||||||
|
const val SENTINEL = "local"
|
||||||
|
|
||||||
|
override fun toString() = SENTINEL
|
||||||
|
}
|
||||||
|
|
||||||
|
object API : Source() {
|
||||||
|
const val SENTINEL = "api"
|
||||||
|
|
||||||
|
override fun toString() = SENTINEL
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Remote(val url: Url) : Source() {
|
||||||
|
override fun toString() = url.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(value: String) = when (value) {
|
||||||
|
Local.SENTINEL -> Local
|
||||||
|
API.SENTINEL -> API
|
||||||
|
else -> Remote(Url(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity(tableName = "patch_bundles")
|
||||||
|
data class PatchBundleEntity(
|
||||||
|
@PrimaryKey val uid: Int,
|
||||||
|
@ColumnInfo(name = "name") val name: String,
|
||||||
|
@ColumnInfo(name = "version") val version: String? = null,
|
||||||
|
@ColumnInfo(name = "source") val source: Source,
|
||||||
|
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BundleProperties(
|
||||||
|
@ColumnInfo(name = "version") val version: String? = null,
|
||||||
|
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
|
||||||
|
)
|
@ -0,0 +1,116 @@
|
|||||||
|
package app.revanced.manager.data.room.options
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import app.revanced.manager.patcher.patch.Option
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.SerializationException
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import kotlinx.serialization.json.JsonNull
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import kotlinx.serialization.json.add
|
||||||
|
import kotlinx.serialization.json.boolean
|
||||||
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
|
import kotlinx.serialization.json.float
|
||||||
|
import kotlinx.serialization.json.int
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.long
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
import kotlin.reflect.KType
|
||||||
|
import kotlin.reflect.typeOf
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "options",
|
||||||
|
primaryKeys = ["group", "patch_name", "key"],
|
||||||
|
foreignKeys = [ForeignKey(
|
||||||
|
OptionGroup::class,
|
||||||
|
parentColumns = ["uid"],
|
||||||
|
childColumns = ["group"],
|
||||||
|
onDelete = ForeignKey.CASCADE
|
||||||
|
)]
|
||||||
|
)
|
||||||
|
data class Option(
|
||||||
|
@ColumnInfo(name = "group") val group: Int,
|
||||||
|
@ColumnInfo(name = "patch_name") val patchName: String,
|
||||||
|
@ColumnInfo(name = "key") val key: String,
|
||||||
|
// Encoded as Json.
|
||||||
|
@ColumnInfo(name = "value") val value: SerializedValue,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class SerializedValue(val raw: JsonElement) {
|
||||||
|
fun toJsonString() = json.encodeToString(raw)
|
||||||
|
fun deserializeFor(option: Option<*>): Any? {
|
||||||
|
if (raw is JsonNull) return null
|
||||||
|
|
||||||
|
val errorMessage = "Cannot deserialize value as ${option.type}"
|
||||||
|
try {
|
||||||
|
if (option.type.classifier == List::class) {
|
||||||
|
val elementType = option.type.arguments.first().type!!
|
||||||
|
return raw.jsonArray.map { deserializeBasicType(elementType, it.jsonPrimitive) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return deserializeBasicType(option.type, raw.jsonPrimitive)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
throw SerializationException(errorMessage, e)
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
throw SerializationException(errorMessage, e)
|
||||||
|
} catch (e: kotlinx.serialization.SerializationException) {
|
||||||
|
throw SerializationException(errorMessage, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val json = Json {
|
||||||
|
// Patcher does not forbid the use of these values, so we should support them.
|
||||||
|
allowSpecialFloatingPointValues = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deserializeBasicType(type: KType, value: JsonPrimitive) = when (type) {
|
||||||
|
typeOf<Boolean>() -> value.boolean
|
||||||
|
typeOf<Int>() -> value.int
|
||||||
|
typeOf<Long>() -> value.long
|
||||||
|
typeOf<Float>() -> value.float
|
||||||
|
typeOf<String>() -> value.content.also {
|
||||||
|
if (!value.isString) throw SerializationException(
|
||||||
|
"Expected value to be a string: $value"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> throw SerializationException("Unknown type: $type")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromJsonString(value: String) = SerializedValue(json.decodeFromString(value))
|
||||||
|
fun fromValue(value: Any?) = SerializedValue(when (value) {
|
||||||
|
null -> JsonNull
|
||||||
|
is Number -> JsonPrimitive(value)
|
||||||
|
is Boolean -> JsonPrimitive(value)
|
||||||
|
is String -> JsonPrimitive(value)
|
||||||
|
is List<*> -> buildJsonArray {
|
||||||
|
var elementClass: KClass<out Any>? = null
|
||||||
|
|
||||||
|
value.forEach {
|
||||||
|
when (it) {
|
||||||
|
null -> throw SerializationException("List elements must not be null")
|
||||||
|
is Number -> add(it)
|
||||||
|
is Boolean -> add(it)
|
||||||
|
is String -> add(it)
|
||||||
|
else -> throw SerializationException("Unknown element type: ${it::class.simpleName}")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elementClass == null) elementClass = it::class
|
||||||
|
else if (elementClass != it::class) throw SerializationException("List elements must have the same type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> throw SerializationException("Unknown type: ${value::class.simpleName}")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SerializationException(message: String, cause: Throwable? = null) :
|
||||||
|
Exception(message, cause)
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
)
|
@ -0,0 +1,11 @@
|
|||||||
|
package app.revanced.manager.data.room.plugins
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(tableName = "trusted_downloader_plugins")
|
||||||
|
class TrustedDownloaderPlugin(
|
||||||
|
@PrimaryKey @ColumnInfo(name = "package_name") val packageName: String,
|
||||||
|
@ColumnInfo(name = "signature") val signature: ByteArray
|
||||||
|
)
|
@ -0,0 +1,22 @@
|
|||||||
|
package app.revanced.manager.data.room.plugins
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Upsert
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface TrustedDownloaderPluginDao {
|
||||||
|
@Query("SELECT signature FROM trusted_downloader_plugins WHERE package_name = :packageName")
|
||||||
|
suspend fun getTrustedSignature(packageName: String): ByteArray?
|
||||||
|
|
||||||
|
@Upsert
|
||||||
|
suspend fun upsertTrust(plugin: TrustedDownloaderPlugin)
|
||||||
|
|
||||||
|
@Query("DELETE FROM trusted_downloader_plugins WHERE package_name = :packageName")
|
||||||
|
suspend fun remove(packageName: String)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("DELETE FROM trusted_downloader_plugins WHERE package_name IN (:packageNames)")
|
||||||
|
suspend fun removeAll(packageNames: Set<String>)
|
||||||
|
}
|
@ -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
|
||||||
|
)
|
@ -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
|
||||||
|
)
|
@ -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) })
|
||||||
|
}
|
||||||
|
}
|
15
app/src/main/java/app/revanced/manager/di/DatabaseModule.kt
Normal file
15
app/src/main/java/app/revanced/manager/di/DatabaseModule.kt
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
60
app/src/main/java/app/revanced/manager/di/HttpModule.kt
Normal file
60
app/src/main/java/app/revanced/manager/di/HttpModule.kt
Normal 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)
|
||||||
|
}
|
11
app/src/main/java/app/revanced/manager/di/ManagerModule.kt
Normal file
11
app/src/main/java/app/revanced/manager/di/ManagerModule.kt
Normal 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)
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
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(::DownloaderPluginRepository)
|
||||||
|
singleOf(::WorkerRepository)
|
||||||
|
singleOf(::DownloadedAppRepository)
|
||||||
|
singleOf(::InstalledAppRepository)
|
||||||
|
}
|
9
app/src/main/java/app/revanced/manager/di/RootModule.kt
Normal file
9
app/src/main/java/app/revanced/manager/di/RootModule.kt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package app.revanced.manager.di
|
||||||
|
|
||||||
|
import app.revanced.manager.domain.installer.RootInstaller
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val rootModule = module {
|
||||||
|
singleOf(::RootInstaller)
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package app.revanced.manager.di
|
||||||
|
|
||||||
|
import app.revanced.manager.network.service.HttpService
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val serviceModule = module {
|
||||||
|
singleOf(::HttpService)
|
||||||
|
}
|
26
app/src/main/java/app/revanced/manager/di/ViewModelModule.kt
Normal file
26
app/src/main/java/app/revanced/manager/di/ViewModelModule.kt
Normal 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(::GeneralSettingsViewModel)
|
||||||
|
viewModelOf(::AdvancedSettingsViewModel)
|
||||||
|
viewModelOf(::AppSelectorViewModel)
|
||||||
|
viewModelOf(::PatcherViewModel)
|
||||||
|
viewModelOf(::UpdateViewModel)
|
||||||
|
viewModelOf(::ChangelogsViewModel)
|
||||||
|
viewModelOf(::ImportExportViewModel)
|
||||||
|
viewModelOf(::AboutViewModel)
|
||||||
|
viewModelOf(::DeveloperOptionsViewModel)
|
||||||
|
viewModelOf(::ContributorViewModel)
|
||||||
|
viewModelOf(::DownloadsViewModel)
|
||||||
|
viewModelOf(::InstalledAppsViewModel)
|
||||||
|
viewModelOf(::InstalledAppInfoViewModel)
|
||||||
|
viewModelOf(::UpdatesSettingsViewModel)
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package app.revanced.manager.domain.bundles
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
class LocalPatchBundle(name: String, id: Int, directory: File) :
|
||||||
|
PatchBundleSource(name, id, directory) {
|
||||||
|
suspend fun replace(patches: InputStream) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
patchBundleOutputStream().use { outputStream ->
|
||||||
|
patches.copyTo(outputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reload()?.also {
|
||||||
|
saveVersion(it.readManifestAttribute("Version"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,114 @@
|
|||||||
|
package app.revanced.manager.domain.bundles
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.domain.repository.PatchBundlePersistenceRepository
|
||||||
|
import app.revanced.manager.patcher.patch.PatchBundle
|
||||||
|
import app.revanced.manager.util.tag
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
import java.io.File
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [PatchBundle] source.
|
||||||
|
*/
|
||||||
|
@Stable
|
||||||
|
sealed class PatchBundleSource(initialName: String, val uid: Int, directory: File) : KoinComponent {
|
||||||
|
protected val configRepository: PatchBundlePersistenceRepository by inject()
|
||||||
|
private val app: Application by inject()
|
||||||
|
protected val patchesFile = directory.resolve("patches.jar")
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow(load())
|
||||||
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
private val _nameFlow = MutableStateFlow(initialName)
|
||||||
|
val nameFlow =
|
||||||
|
_nameFlow.map { it.ifEmpty { app.getString(if (isDefault) R.string.bundle_name_default else R.string.bundle_name_fallback) } }
|
||||||
|
|
||||||
|
suspend fun getName() = nameFlow.first()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 load(): State {
|
||||||
|
if (!hasInstalled()) return State.Missing
|
||||||
|
|
||||||
|
return try {
|
||||||
|
State.Loaded(PatchBundle(patchesFile))
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Log.e(tag, "Failed to load patch bundle with UID $uid", t)
|
||||||
|
State.Failed(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun reload(): PatchBundle? {
|
||||||
|
val newState = load()
|
||||||
|
_state.value = newState
|
||||||
|
|
||||||
|
val bundle = newState.patchBundleOrNull()
|
||||||
|
// Try to read the name from the patch bundle manifest if the bundle does not have a name.
|
||||||
|
if (bundle != null && _nameFlow.value.isEmpty()) {
|
||||||
|
bundle.readManifestAttribute("Name")?.let { setName(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a flow that emits the [app.revanced.manager.data.room.bundles.BundleProperties] of this [PatchBundleSource].
|
||||||
|
* The flow will emit null if the associated [PatchBundleSource] is deleted.
|
||||||
|
*/
|
||||||
|
fun propsFlow() = configRepository.getProps(uid).flowOn(Dispatchers.Default)
|
||||||
|
suspend fun getProps() = propsFlow().first()!!
|
||||||
|
|
||||||
|
suspend fun currentVersion() = getProps().version
|
||||||
|
protected suspend fun saveVersion(version: String?) =
|
||||||
|
configRepository.updateVersion(uid, version)
|
||||||
|
|
||||||
|
suspend fun setName(name: String) {
|
||||||
|
configRepository.setName(uid, name)
|
||||||
|
_nameFlow.value = name
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface State {
|
||||||
|
fun patchBundleOrNull(): PatchBundle? = null
|
||||||
|
|
||||||
|
data object Missing : State
|
||||||
|
data class Failed(val throwable: Throwable) : State
|
||||||
|
data class Loaded(val bundle: PatchBundle) : State {
|
||||||
|
override fun patchBundleOrNull() = bundle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object Extensions {
|
||||||
|
val PatchBundleSource.isDefault inline get() = uid == 0
|
||||||
|
val PatchBundleSource.asRemoteOrNull inline get() = this as? RemotePatchBundle
|
||||||
|
val PatchBundleSource.nameState
|
||||||
|
@Composable inline get() = nameFlow.collectAsStateWithLifecycle(
|
||||||
|
""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
package app.revanced.manager.domain.bundles
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import app.revanced.manager.network.api.ReVancedAPI
|
||||||
|
import app.revanced.manager.network.dto.ReVancedAsset
|
||||||
|
import app.revanced.manager.network.service.HttpService
|
||||||
|
import app.revanced.manager.network.utils.getOrThrow
|
||||||
|
import io.ktor.client.request.url
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
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) {
|
||||||
|
protected val http: HttpService by inject()
|
||||||
|
|
||||||
|
protected abstract suspend fun getLatestInfo(): ReVancedAsset
|
||||||
|
|
||||||
|
private suspend fun download(info: ReVancedAsset) = withContext(Dispatchers.IO) {
|
||||||
|
patchBundleOutputStream().use {
|
||||||
|
http.streamTo(it) {
|
||||||
|
url(info.downloadUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveVersion(info.version)
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun downloadLatest() {
|
||||||
|
download(getLatestInfo())
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun update(): Boolean = withContext(Dispatchers.IO) {
|
||||||
|
val info = getLatestInfo()
|
||||||
|
if (hasInstalled() && info.version == currentVersion())
|
||||||
|
return@withContext false
|
||||||
|
|
||||||
|
download(info)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteLocalFiles() = withContext(Dispatchers.Default) {
|
||||||
|
patchesFile.delete()
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
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<ReVancedAsset> {
|
||||||
|
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() = api.getPatchesUpdate().getOrThrow()
|
||||||
|
}
|
@ -0,0 +1,181 @@
|
|||||||
|
package app.revanced.manager.domain.installer
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.os.IBinder
|
||||||
|
import app.revanced.manager.IRootSystemService
|
||||||
|
import app.revanced.manager.service.ManagerRootService
|
||||||
|
import app.revanced.manager.util.PM
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import com.topjohnwu.superuser.ipc.RootService
|
||||||
|
import com.topjohnwu.superuser.nio.FileSystemManager
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.time.withTimeoutOrNull
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
class RootInstaller(
|
||||||
|
private val app: Application,
|
||||||
|
private val pm: PM
|
||||||
|
) : ServiceConnection {
|
||||||
|
private var remoteFS = CompletableDeferred<FileSystemManager>()
|
||||||
|
|
||||||
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||||
|
val ipc = IRootSystemService.Stub.asInterface(service)
|
||||||
|
val binder = ipc.fileSystemService
|
||||||
|
|
||||||
|
remoteFS.complete(FileSystemManager.getRemote(binder))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
remoteFS = CompletableDeferred()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun awaitRemoteFS(): FileSystemManager {
|
||||||
|
if (remoteFS.isActive) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val intent = Intent(app, ManagerRootService::class.java)
|
||||||
|
RootService.bind(intent, this@RootInstaller)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return withTimeoutOrNull(Duration.ofSeconds(20L)) {
|
||||||
|
remoteFS.await()
|
||||||
|
} ?: throw RootServiceException()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getShell() = with(CompletableDeferred<Shell>()) {
|
||||||
|
Shell.getShell(::complete)
|
||||||
|
|
||||||
|
await()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun execute(vararg commands: String) = getShell().newJob().add(*commands).exec()
|
||||||
|
|
||||||
|
fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false
|
||||||
|
|
||||||
|
fun isDeviceRooted() = System.getenv("PATH")?.split(":")?.any { path ->
|
||||||
|
File(path, "su").canExecute()
|
||||||
|
} ?: false
|
||||||
|
|
||||||
|
suspend fun isAppInstalled(packageName: String) =
|
||||||
|
awaitRemoteFS().getFile("$modulesPath/$packageName-revanced").exists()
|
||||||
|
|
||||||
|
suspend fun isAppMounted(packageName: String) = withContext(Dispatchers.IO) {
|
||||||
|
pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir?.let {
|
||||||
|
execute("mount | grep \"$it\"").isSuccess
|
||||||
|
} ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun mount(packageName: String) {
|
||||||
|
if (isAppMounted(packageName)) return
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir
|
||||||
|
?: throw Exception("Failed to load application info")
|
||||||
|
val patchedAPK = "$modulesPath/$packageName-revanced/$packageName.apk"
|
||||||
|
|
||||||
|
execute("mount -o bind \"$patchedAPK\" \"$stockAPK\"").assertSuccess("Failed to mount APK")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun unmount(packageName: String) {
|
||||||
|
if (!isAppMounted(packageName)) return
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir
|
||||||
|
?: throw Exception("Failed to load application info")
|
||||||
|
|
||||||
|
execute("umount -l \"$stockAPK\"").assertSuccess("Failed to unmount APK")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun install(
|
||||||
|
patchedAPK: File,
|
||||||
|
stockAPK: File?,
|
||||||
|
packageName: String,
|
||||||
|
version: String,
|
||||||
|
label: String
|
||||||
|
) = withContext(Dispatchers.IO) {
|
||||||
|
val remoteFS = awaitRemoteFS()
|
||||||
|
val assets = app.assets
|
||||||
|
val modulePath = "$modulesPath/$packageName-revanced"
|
||||||
|
|
||||||
|
unmount(packageName)
|
||||||
|
|
||||||
|
stockAPK?.let { stockApp ->
|
||||||
|
pm.getPackageInfo(packageName)?.let { packageInfo ->
|
||||||
|
// TODO: get user id programmatically
|
||||||
|
if (pm.getVersionCode(packageInfo) <= pm.getVersionCode(
|
||||||
|
pm.getPackageInfo(patchedAPK)
|
||||||
|
?: error("Failed to get package info for patched app")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
execute("pm uninstall -k --user 0 $packageName").assertSuccess("Failed to uninstall stock app")
|
||||||
|
}
|
||||||
|
|
||||||
|
execute("pm install \"${stockApp.absolutePath}\"").assertSuccess("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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(
|
||||||
|
"chmod 644 $apkPath",
|
||||||
|
"chown system:system $apkPath",
|
||||||
|
"chcon u:object_r:apk_data_file:s0 $apkPath",
|
||||||
|
"chmod +x $modulePath/service.sh"
|
||||||
|
).assertSuccess("Failed to set file permissions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun uninstall(packageName: String) {
|
||||||
|
val remoteFS = awaitRemoteFS()
|
||||||
|
if (isAppMounted(packageName))
|
||||||
|
unmount(packageName)
|
||||||
|
|
||||||
|
remoteFS.getFile("$modulesPath/$packageName-revanced").deleteRecursively()
|
||||||
|
.also { if (!it) throw Exception("Failed to delete files") }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val modulesPath = "/data/adb/modules"
|
||||||
|
|
||||||
|
private fun Shell.Result.assertSuccess(errorMessage: String) {
|
||||||
|
if (!isSuccess) throw Exception(errorMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RootServiceException : Exception("Root not available")
|
@ -0,0 +1,95 @@
|
|||||||
|
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
|
||||||
|
import java.util.Date
|
||||||
|
import kotlin.time.Duration.Companion.days
|
||||||
|
|
||||||
|
class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
|
||||||
|
companion object Constants {
|
||||||
|
/**
|
||||||
|
* Default alias and password for the keystore.
|
||||||
|
*/
|
||||||
|
const val DEFAULT = "ReVanced"
|
||||||
|
private val eightYearsFromNow get() = Date(System.currentTimeMillis() + (365.days * 8).inWholeMilliseconds * 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 signingDetails(path: File = keystorePath) = ApkUtils.KeyStoreDetails(
|
||||||
|
keyStore = path,
|
||||||
|
keyStorePassword = null,
|
||||||
|
alias = prefs.keystoreCommonName.get(),
|
||||||
|
password = prefs.keystorePass.get()
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun sign(input: File, output: File) = withContext(Dispatchers.Default) {
|
||||||
|
ApkUtils.signApk(input, output, prefs.keystoreCommonName.get(), signingDetails())
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun regenerate() = withContext(Dispatchers.Default) {
|
||||||
|
val keyCertPair = ApkSigner.newPrivateKeyCertificatePair(
|
||||||
|
prefs.keystoreCommonName.get(),
|
||||||
|
eightYearsFromNow
|
||||||
|
)
|
||||||
|
val ks = ApkSigner.newKeyStore(
|
||||||
|
setOf(
|
||||||
|
ApkSigner.KeyStoreEntry(
|
||||||
|
DEFAULT, DEFAULT, keyCertPair
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
keystorePath.outputStream().use {
|
||||||
|
ks.store(it, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePrefs(DEFAULT, DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun import(cn: String, pass: String, keystore: InputStream): Boolean {
|
||||||
|
val keystoreData = withContext(Dispatchers.IO) { keystore.readBytes() }
|
||||||
|
|
||||||
|
try {
|
||||||
|
val ks = ApkSigner.readKeyStore(ByteArrayInputStream(keystoreData), null)
|
||||||
|
|
||||||
|
ApkSigner.readPrivateKeyCertificatePair(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
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
|
||||||
|
import app.revanced.manager.util.isDebuggable
|
||||||
|
|
||||||
|
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 useProcessRuntime = booleanPreference("use_process_runtime", false)
|
||||||
|
val patcherProcessMemoryLimit = intPreference("process_runtime_memory_limit", 700)
|
||||||
|
|
||||||
|
val keystoreCommonName = stringPreference("keystore_cn", KeystoreManager.DEFAULT)
|
||||||
|
val keystorePass = stringPreference("keystore_pass", KeystoreManager.DEFAULT)
|
||||||
|
|
||||||
|
val firstLaunch = booleanPreference("first_launch", true)
|
||||||
|
val managerAutoUpdates = booleanPreference("manager_auto_updates", false)
|
||||||
|
val showManagerUpdateDialogOnLaunch = booleanPreference("show_manager_update_dialog_on_launch", true)
|
||||||
|
|
||||||
|
val disablePatchVersionCompatCheck = booleanPreference("disable_patch_version_compatibility_check", false)
|
||||||
|
val disableSelectionWarning = booleanPreference("disable_selection_warning", false)
|
||||||
|
val disableUniversalPatchWarning = booleanPreference("disable_universal_patch_warning", false)
|
||||||
|
val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true)
|
||||||
|
|
||||||
|
val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet())
|
||||||
|
|
||||||
|
val showDeveloperSettings = booleanPreference("show_developer_settings", context.isDebuggable)
|
||||||
|
}
|
@ -0,0 +1,150 @@
|
|||||||
|
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 stringSetPreference(key: String, default: Set<String>) =
|
||||||
|
StringSetPreference(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) }
|
||||||
|
|
||||||
|
operator fun Preference<Set<String>>.plusAssign(value: String) = prefs.run {
|
||||||
|
write(read() + value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class Preference<T>(
|
||||||
|
private val dataStore: DataStore<Preferences>,
|
||||||
|
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 StringSetPreference(
|
||||||
|
dataStore: DataStore<Preferences>,
|
||||||
|
key: String,
|
||||||
|
default: Set<String>
|
||||||
|
) : BasePreference<Set<String>>(dataStore, default) {
|
||||||
|
override val key = stringSetPreferencesKey(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)
|
||||||
|
}
|
@ -0,0 +1,133 @@
|
|||||||
|
package app.revanced.manager.domain.repository
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Parcelable
|
||||||
|
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.LoadedDownloaderPlugin
|
||||||
|
import app.revanced.manager.plugin.downloader.OutputDownloadScope
|
||||||
|
import app.revanced.manager.util.PM
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
|
import kotlinx.coroutines.flow.conflate
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FilterOutputStream
|
||||||
|
import java.nio.file.StandardOpenOption
|
||||||
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
|
import kotlin.io.path.outputStream
|
||||||
|
|
||||||
|
class DownloadedAppRepository(
|
||||||
|
private val app: Application,
|
||||||
|
db: AppDatabase,
|
||||||
|
private val pm: PM
|
||||||
|
) {
|
||||||
|
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(
|
||||||
|
plugin: LoadedDownloaderPlugin,
|
||||||
|
data: Parcelable,
|
||||||
|
expectedPackageName: String,
|
||||||
|
expectedVersion: String?,
|
||||||
|
onDownload: suspend (downloadProgress: Pair<Long, Long?>) -> Unit,
|
||||||
|
): File {
|
||||||
|
// Converted integers cannot contain / or .. unlike the package name or version, so they are safer to use here.
|
||||||
|
val relativePath = File(generateUid().toString())
|
||||||
|
val saveDir = dir.resolve(relativePath).also { it.mkdirs() }
|
||||||
|
val targetFile = saveDir.resolve("base.apk").toPath()
|
||||||
|
|
||||||
|
try {
|
||||||
|
val downloadSize = AtomicLong(0)
|
||||||
|
val downloadedBytes = AtomicLong(0)
|
||||||
|
|
||||||
|
channelFlow {
|
||||||
|
val scope = object : OutputDownloadScope {
|
||||||
|
override val pluginPackageName = plugin.packageName
|
||||||
|
override val hostPackageName = app.packageName
|
||||||
|
override suspend fun reportSize(size: Long) {
|
||||||
|
require(size > 0) { "Size must be greater than zero" }
|
||||||
|
require(
|
||||||
|
downloadSize.compareAndSet(
|
||||||
|
0,
|
||||||
|
size
|
||||||
|
)
|
||||||
|
) { "Download size has already been set" }
|
||||||
|
send(downloadedBytes.get() to size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun emitProgress(bytes: Long) {
|
||||||
|
val newValue = downloadedBytes.addAndGet(bytes)
|
||||||
|
val totalSize = downloadSize.get()
|
||||||
|
if (totalSize < 1) return
|
||||||
|
trySend(newValue to totalSize).getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
targetFile.outputStream(StandardOpenOption.CREATE_NEW).buffered().use {
|
||||||
|
val stream = object : FilterOutputStream(it) {
|
||||||
|
override fun write(b: Int) = out.write(b).also { emitProgress(1) }
|
||||||
|
|
||||||
|
override fun write(b: ByteArray?, off: Int, len: Int) =
|
||||||
|
out.write(b, off, len).also {
|
||||||
|
emitProgress(
|
||||||
|
(len - off).toLong()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plugin.download(scope, data, stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.conflate()
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.collect { (downloaded, size) -> onDownload(downloaded to size) }
|
||||||
|
|
||||||
|
if (downloadedBytes.get() < 1) error("Downloader did not download anything.")
|
||||||
|
val pkgInfo =
|
||||||
|
pm.getPackageInfo(targetFile.toFile()) ?: error("Downloaded APK file is invalid")
|
||||||
|
if (pkgInfo.packageName != expectedPackageName) error("Downloaded APK has the wrong package name. Expected: $expectedPackageName, Actual: ${pkgInfo.packageName}")
|
||||||
|
if (expectedVersion != null && pkgInfo.versionName != expectedVersion) error("Downloaded APK has the wrong version. Expected: $expectedVersion, Actual: ${pkgInfo.versionName}")
|
||||||
|
|
||||||
|
// Delete the previous copy (if present).
|
||||||
|
dao.get(pkgInfo.packageName, pkgInfo.versionName!!)?.directory?.let {
|
||||||
|
if (!dir.resolve(it).deleteRecursively()) throw Exception("Failed to delete existing directory")
|
||||||
|
}
|
||||||
|
dao.upsert(
|
||||||
|
DownloadedApp(
|
||||||
|
packageName = pkgInfo.packageName,
|
||||||
|
version = pkgInfo.versionName!!,
|
||||||
|
directory = relativePath,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
saveDir.deleteRecursively()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the Apk file.
|
||||||
|
return getApkFileForDir(saveDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun get(packageName: String, version: String, markUsed: Boolean = false) =
|
||||||
|
dao.get(packageName, version)?.also {
|
||||||
|
if (markUsed) dao.markUsed(packageName, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun delete(downloadedApps: Collection<DownloadedApp>) {
|
||||||
|
downloadedApps.forEach {
|
||||||
|
dir.resolve(it.directory).deleteRecursively()
|
||||||
|
}
|
||||||
|
|
||||||
|
dao.delete(downloadedApps)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,168 @@
|
|||||||
|
package app.revanced.manager.domain.repository
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.Log
|
||||||
|
import app.revanced.manager.data.room.AppDatabase
|
||||||
|
import app.revanced.manager.data.room.plugins.TrustedDownloaderPlugin
|
||||||
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
|
import app.revanced.manager.network.downloader.DownloaderPluginState
|
||||||
|
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||||
|
import app.revanced.manager.network.downloader.ParceledDownloaderData
|
||||||
|
import app.revanced.manager.plugin.downloader.DownloaderBuilder
|
||||||
|
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||||
|
import app.revanced.manager.plugin.downloader.Scope
|
||||||
|
import app.revanced.manager.util.PM
|
||||||
|
import app.revanced.manager.util.tag
|
||||||
|
import dalvik.system.PathClassLoader
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.lang.reflect.Modifier
|
||||||
|
|
||||||
|
@OptIn(PluginHostApi::class)
|
||||||
|
class DownloaderPluginRepository(
|
||||||
|
private val pm: PM,
|
||||||
|
private val prefs: PreferencesManager,
|
||||||
|
private val app: Application,
|
||||||
|
db: AppDatabase
|
||||||
|
) {
|
||||||
|
private val trustDao = db.trustedDownloaderPluginDao()
|
||||||
|
private val _pluginStates = MutableStateFlow(emptyMap<String, DownloaderPluginState>())
|
||||||
|
val pluginStates = _pluginStates.asStateFlow()
|
||||||
|
val loadedPluginsFlow = pluginStates.map { states ->
|
||||||
|
states.values.filterIsInstance<DownloaderPluginState.Loaded>().map { it.plugin }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val acknowledgedDownloaderPlugins = prefs.acknowledgedDownloaderPlugins
|
||||||
|
private val installedPluginPackageNames = MutableStateFlow(emptySet<String>())
|
||||||
|
val newPluginPackageNames = combine(
|
||||||
|
installedPluginPackageNames,
|
||||||
|
acknowledgedDownloaderPlugins.flow
|
||||||
|
) { installed, acknowledged ->
|
||||||
|
installed subtract acknowledged
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun reload() {
|
||||||
|
val plugins =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
pm.getPackagesWithFeature(PLUGIN_FEATURE)
|
||||||
|
.associate { it.packageName to loadPlugin(it.packageName) }
|
||||||
|
}
|
||||||
|
|
||||||
|
_pluginStates.value = plugins
|
||||||
|
installedPluginPackageNames.value = plugins.keys
|
||||||
|
|
||||||
|
val acknowledgedPlugins = acknowledgedDownloaderPlugins.get()
|
||||||
|
val uninstalledPlugins = acknowledgedPlugins subtract installedPluginPackageNames.value
|
||||||
|
if (uninstalledPlugins.isNotEmpty()) {
|
||||||
|
Log.d(tag, "Uninstalled plugins: ${uninstalledPlugins.joinToString(", ")}")
|
||||||
|
acknowledgedDownloaderPlugins.update(acknowledgedPlugins subtract uninstalledPlugins)
|
||||||
|
trustDao.removeAll(uninstalledPlugins)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unwrapParceledData(data: ParceledDownloaderData): Pair<LoadedDownloaderPlugin, Parcelable> {
|
||||||
|
val plugin =
|
||||||
|
(_pluginStates.value[data.pluginPackageName] as? DownloaderPluginState.Loaded)?.plugin
|
||||||
|
?: throw Exception("Downloader plugin with name ${data.pluginPackageName} is not available")
|
||||||
|
|
||||||
|
return plugin to data.unwrapWith(plugin)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadPlugin(packageName: String): DownloaderPluginState {
|
||||||
|
try {
|
||||||
|
if (!verify(packageName)) return DownloaderPluginState.Untrusted
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(tag, "Got exception while verifying plugin $packageName", e)
|
||||||
|
return DownloaderPluginState.Failed(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val packageInfo = pm.getPackageInfo(packageName, flags = PackageManager.GET_META_DATA)!!
|
||||||
|
val className = packageInfo.applicationInfo!!.metaData.getString(METADATA_PLUGIN_CLASS)
|
||||||
|
?: throw Exception("Missing metadata attribute $METADATA_PLUGIN_CLASS")
|
||||||
|
|
||||||
|
val classLoader =
|
||||||
|
PathClassLoader(packageInfo.applicationInfo!!.sourceDir, app.classLoader)
|
||||||
|
val pluginContext = app.createPackageContext(packageName, 0)
|
||||||
|
|
||||||
|
val downloader = classLoader
|
||||||
|
.loadClass(className)
|
||||||
|
.getDownloaderBuilder()
|
||||||
|
.build(
|
||||||
|
scopeImpl = object : Scope {
|
||||||
|
override val hostPackageName = app.packageName
|
||||||
|
override val pluginPackageName = pluginContext.packageName
|
||||||
|
},
|
||||||
|
context = pluginContext
|
||||||
|
)
|
||||||
|
|
||||||
|
DownloaderPluginState.Loaded(
|
||||||
|
LoadedDownloaderPlugin(
|
||||||
|
packageName,
|
||||||
|
with(pm) { packageInfo.label() },
|
||||||
|
packageInfo.versionName!!,
|
||||||
|
downloader.get,
|
||||||
|
downloader.download,
|
||||||
|
classLoader
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Log.e(tag, "Failed to load plugin $packageName", t)
|
||||||
|
DownloaderPluginState.Failed(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun trustPackage(packageName: String) {
|
||||||
|
trustDao.upsertTrust(
|
||||||
|
TrustedDownloaderPlugin(
|
||||||
|
packageName,
|
||||||
|
pm.getSignature(packageName).toByteArray()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
reload()
|
||||||
|
prefs.edit {
|
||||||
|
acknowledgedDownloaderPlugins += packageName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun revokeTrustForPackage(packageName: String) =
|
||||||
|
trustDao.remove(packageName).also { reload() }
|
||||||
|
|
||||||
|
suspend fun acknowledgeAllNewPlugins() =
|
||||||
|
acknowledgedDownloaderPlugins.update(installedPluginPackageNames.value)
|
||||||
|
|
||||||
|
private suspend fun verify(packageName: String): Boolean {
|
||||||
|
val expectedSignature =
|
||||||
|
trustDao.getTrustedSignature(packageName) ?: return false
|
||||||
|
|
||||||
|
return pm.hasSignature(packageName, expectedSignature)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val PLUGIN_FEATURE = "app.revanced.manager.plugin.downloader"
|
||||||
|
const val METADATA_PLUGIN_CLASS = "app.revanced.manager.plugin.downloader.class"
|
||||||
|
|
||||||
|
const val PUBLIC_STATIC = Modifier.PUBLIC or Modifier.STATIC
|
||||||
|
val Int.isPublicStatic get() = (this and PUBLIC_STATIC) == PUBLIC_STATIC
|
||||||
|
val Class<*>.isDownloaderBuilder get() = DownloaderBuilder::class.java.isAssignableFrom(this)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun Class<*>.getDownloaderBuilder() =
|
||||||
|
declaredMethods
|
||||||
|
.firstOrNull { it.modifiers.isPublicStatic && it.returnType.isDownloaderBuilder && it.parameterTypes.isEmpty() }
|
||||||
|
?.let { it(null) as DownloaderBuilder<Parcelable> }
|
||||||
|
?: throw Exception("Could not find a valid downloader implementation in class $canonicalName")
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user