Compare commits
628 Commits
compose/re
...
skia-v3.29
Author | SHA1 | Date | |
---|---|---|---|
a2f6c3836d | |||
73cb27011b | |||
83fc7f131a | |||
8e4a9088ea | |||
7ee2b1a026 | |||
40c99ab4dc | |||
1752fae9d9 | |||
d264a2a363 | |||
a5e909cfc8 | |||
bdb0317a9e | |||
b0464408f1 | |||
767fa77436 | |||
89b48cebcf | |||
dedcb3c51a | |||
3074766ff2 | |||
722f2b645f | |||
f79aa9edd7 | |||
5127c7f599 | |||
23dcbbecb2 | |||
1c84265dc4 | |||
7ccc689bd3 | |||
007dce800e | |||
ba758f3d8b | |||
a1f9a2786f | |||
b11a720621 | |||
aedf475310 | |||
10f35eb727 | |||
5c7d52c8e9 | |||
841d61278b | |||
534e4c2453 | |||
062f28387f | |||
a8a4ffabed | |||
cde3f8d62c | |||
8182228a46 | |||
7fa5daf623 | |||
221e663e47 | |||
915ec0e260 | |||
783f313ed8 | |||
05575cccfb | |||
a8e192b85f | |||
4b591effed | |||
123c51794b | |||
53d64a0636 | |||
64cbb68344 | |||
00950c79f0 | |||
746229120c | |||
133f6ee28f | |||
08a9d2a64f | |||
4b7b05ac0f | |||
5b6426c453 | |||
235489dcdf | |||
b4e5c66f9c | |||
5c58f624de | |||
89ad88c0ef | |||
d3036105bb | |||
3e8862ea5a | |||
0c59bbb7be | |||
f4b279d1fd | |||
c652d196fc | |||
632b9502b2 | |||
dfb3fe3749 | |||
b3ba0fcd6a | |||
8a98b8448b | |||
da604e89ac | |||
624b43733e | |||
4ba7ec1854 | |||
1d6b074856 | |||
695329088b | |||
f2b20e6e7f | |||
a5c3aa3744 | |||
900058a6e5 | |||
4c51ad3650 | |||
a0c2a17bab | |||
851b06b0d2 | |||
57018a65df | |||
b44419133a | |||
8961cf9044 | |||
5139873f79 | |||
5caa79eb0d | |||
9552b2ebc5 | |||
7e3afe0cb2 | |||
7b7d91d661 | |||
44b8d4ceee | |||
aaa97ebb71 | |||
d99e5af384 | |||
c47c7c0a88 | |||
3e32c0fd90 | |||
a45d9598cc | |||
8c8df698d4 | |||
8d0d782ab5 | |||
4db4789a06 | |||
abdd9dc430 | |||
5193042e6b | |||
daba737ecb | |||
bd96701103 | |||
69c20b74cf | |||
7297436ab4 | |||
a329626715 | |||
50a20d0535 | |||
c52edc99c0 | |||
1c6c5d53ae | |||
1b110e5bd5 | |||
8872165a99 | |||
bd79496433 | |||
820cd720b5 | |||
31ff1e0492 | |||
8b429f03c2 | |||
a68a83940d | |||
1f95767aeb | |||
c7298424e5 | |||
d53f8cf130 | |||
a7e2281805 | |||
0ce23d2d60 | |||
d0fe57970f | |||
4dcdc57ffd | |||
1ef1f8d47a | |||
d688f38a63 | |||
582db18d83 | |||
f5a12e01bd | |||
09fd9c4e04 | |||
43d5888182 | |||
646feae0ec | |||
e73ce99f1d | |||
af2d8226de | |||
37c912b598 | |||
3102272a31 | |||
3dc695eafb | |||
6f0721b066 | |||
3dfe6b1080 | |||
02e5641227 | |||
01e4a76caa | |||
59838a51ca | |||
224be29a3d | |||
921052acce | |||
81f05e1b19 | |||
b769a66d16 | |||
5d612fe790 | |||
61251deffa | |||
f2931443d9 | |||
3d8318da14 | |||
f6bfe0d3f9 | |||
51c0f14055 | |||
2a6dc09a9b | |||
f1c2f4146c | |||
2f46b3c84e | |||
5f388abf95 | |||
7acdb8e660 | |||
e063b3d102 | |||
cb722f2634 | |||
0499d2b08a | |||
427928e542 | |||
ceb9d66f17 | |||
295c5a74ea | |||
400df69528 | |||
5bc7b135d5 | |||
6dce353d78 | |||
e707e51719 | |||
1cea9600a2 | |||
d81808ad7b | |||
ea9654edec | |||
ced37f7c76 | |||
4c6e214bf6 | |||
d10abd4829 | |||
25f1640fd6 | |||
5690adc0de | |||
f1b25d09da | |||
96a21a5564 | |||
0b21bb9807 | |||
557ee8d472 | |||
6da3751a8a | |||
cf708de005 | |||
b649c4539f | |||
c63342bc21 | |||
9f56b277ca | |||
73c92b9c72 | |||
c7627ced8e | |||
63c6412736 | |||
a7663524e6 | |||
65feb34242 | |||
23690a98df | |||
7449d4e318 | |||
c6f9e36f4b | |||
e9cee0abe2 | |||
9440f23b55 | |||
c67b4b438c | |||
1bdb820aed | |||
a28d77bc65 | |||
d60f9aa1d8 | |||
3209c0e430 | |||
7ef8f0454b | |||
232b702789 | |||
694f2a9fae | |||
77204087bb | |||
b26760b216 | |||
3c36950aeb | |||
bbeb836923 | |||
a99406f0a9 | |||
73368b58be | |||
ca14e77ba3 | |||
cafdfcda47 | |||
5c68d513a3 | |||
fc52560244 | |||
46f6a49a7a | |||
c13827e8e1 | |||
e0a6de2c2b | |||
afdba00722 | |||
9084c71aa3 | |||
8fc5fb6a80 | |||
5f762c5442 | |||
8b21ec1ea3 | |||
e83fbb864e | |||
f03af17f71 | |||
2c3e2e639f | |||
cc85b393dc | |||
fa6ad214f9 | |||
3ceb63be1d | |||
200483d733 | |||
6b03f3a169 | |||
612a3bab49 | |||
3ac08512f3 | |||
1adc673c44 | |||
1aa1bd84cf | |||
4a72267d41 | |||
7e0f18e3b7 | |||
aab5d4411b | |||
e5d83f424a | |||
bc300d81d9 | |||
5e6cc86c7e | |||
d91ea62903 | |||
f00dc4d3e6 | |||
6c0c961ca0 | |||
a99de99202 | |||
a31ec9d1fe | |||
a23f032fd2 | |||
95a8234e2d | |||
a7e481c827 | |||
975870e254 | |||
b7c838fd25 | |||
3776674eb4 | |||
4293e27681 | |||
8564c1a72e | |||
f9e6ef3fd3 | |||
e80ee09893 | |||
025ff527ee | |||
35fdbb5988 | |||
401646ace4 | |||
a62a8852e7 | |||
04a3df3642 | |||
ecb041187b | |||
1fd44e1cfc | |||
fb29036d0a | |||
aba2ed4378 | |||
076e17d670 | |||
a971cbd519 | |||
c265794d0e | |||
ecadb80113 | |||
9a66357f7a | |||
63d83a43ad | |||
7833a0d552 | |||
966796dfec | |||
2d19f36901 | |||
6f70a07970 | |||
e85ed5a8e3 | |||
ad416387c2 | |||
58d8e7f34f | |||
bb105b5662 | |||
a71a930125 | |||
af7e39b7f0 | |||
c981cb4a41 | |||
3ea6ef0bbf | |||
2772a96727 | |||
50b4a5f1d8 | |||
e52a6ce734 | |||
add49e14fb | |||
ab13895196 | |||
846afb420b | |||
dca8a1dab6 | |||
d1c36c1bcc | |||
c209c32613 | |||
9e5e89ac95 | |||
7cc6b88e4e | |||
5cd1cba668 | |||
ca365bac6e | |||
f1fc48ce5a | |||
316e440d0d | |||
95018814a7 | |||
b52e49d90a | |||
4acd738353 | |||
982249f974 | |||
1f5461fbe5 | |||
adb7e5663a | |||
ffc14f2146 | |||
0c57322051 | |||
a8b7debf8d | |||
7ed9787b58 | |||
eef701615b | |||
97d8519b8b | |||
00210f7f0e | |||
cea4c6c27a | |||
bc83a39b0f | |||
aa0575a637 | |||
4ca7b8a7c1 | |||
6ec6546cc5 | |||
c5e04cc824 | |||
4fc913eae1 | |||
2d7026ac7a | |||
1200360588 | |||
02722fc0be | |||
2e1de94623 | |||
10bae69db6 | |||
6dee3aa1b7 | |||
e76418d48d | |||
52e1020a90 | |||
c170392123 | |||
d414a91f40 | |||
6d6fae1ecd | |||
6d866d4424 | |||
f4dea6e58c | |||
c72d10ac85 | |||
3e696d6847 | |||
28d6ab692e | |||
f7747809f2 | |||
d4b15aee4d | |||
d0689555f1 | |||
82d6e3f105 | |||
244d078b11 | |||
f89c742c90 | |||
ce5088ab53 | |||
0443c8c200 | |||
8a4161753e | |||
f2e7071f6d | |||
25d53ce9a8 | |||
3b58d229da | |||
f5ba84d81e | |||
933c71923e | |||
bfe59ea57a | |||
953209ca13 | |||
b2119ce60e | |||
e0c750d27e | |||
b754a045eb | |||
5a24911fad | |||
0ec6897fda | |||
f57898a471 | |||
3ee29c2256 | |||
3b0fed55e4 | |||
31a32eb11d | |||
64c10b44f5 | |||
34365239c1 | |||
bdfeda23b6 | |||
1aaeac1fe3 | |||
f5578b0fc1 | |||
f82c439b26 | |||
c498cff096 | |||
3559477247 | |||
b9241fa1b2 | |||
9bf5153e6b | |||
eb6d3cd64e | |||
8cda2c164d | |||
c900d09cf8 | |||
edc8ef4f44 | |||
9aeb156d92 | |||
8be07de373 | |||
93482b0041 | |||
5b2c55142e | |||
088a3b7c28 | |||
b115643034 | |||
07fc964f9c | |||
3e51caf111 | |||
deb1ba339f | |||
70a1086edf | |||
c7d975e612 | |||
7104d6d6dd | |||
d933997c89 | |||
bea99bb4c6 | |||
f38a593434 | |||
85950991ab | |||
6fd740f8c0 | |||
ec2c2d8ccb | |||
efb2d5ef32 | |||
acb1e2434b | |||
8e52abda9a | |||
9764326242 | |||
97c33d6c54 | |||
30d5f3ad3f | |||
079c0defaf | |||
cf1afddb9e | |||
b07b9351c4 | |||
7911459817 | |||
ef9b1d5c2d | |||
fd2780624a | |||
4f22e88e42 | |||
0d45fe4a97 | |||
28ae2766f0 | |||
edd86024b9 | |||
f3c78c2c24 | |||
3f96608398 | |||
9ed43efe5d | |||
975180b075 | |||
c94eb7a48e | |||
bb1b0da749 | |||
85c8006977 | |||
850bdc4a4d | |||
ca95e32164 | |||
a141ec85b0 | |||
1298a96b0e | |||
1928b150ad | |||
7426f5484d | |||
11a8f313b0 | |||
69b6ef07a1 | |||
7a1ba9dabf | |||
48a739c94e | |||
9680f0cf12 | |||
5b1c89a0c5 | |||
8a1ab478a3 | |||
2ae8d49526 | |||
a0b673c138 | |||
c56c445fb7 | |||
c23275f2fe | |||
8b28a33b73 | |||
f8d086a743 | |||
c06d15de5f | |||
dcaf1f54e4 | |||
aa91abb022 | |||
67d204eb02 | |||
18e680b298 | |||
6ef1b072e8 | |||
ca9ef544ce | |||
7831a3438d | |||
1fad90441c | |||
7f26c5bd45 | |||
637641cf54 | |||
d9a316abbb | |||
093cfa5269 | |||
61a025de4d | |||
7df1ae7ed8 | |||
eb6e75b156 | |||
8480b3ac3d | |||
6339a31fec | |||
71b5bb3f8f | |||
a9878dbbdf | |||
b1fb9dd7d3 | |||
de51fbd7be | |||
c9412a97d0 | |||
63c29bdd75 | |||
bace26063d | |||
7931eb97b9 | |||
9df89c7b74 | |||
78978276c4 | |||
011eddbbc5 | |||
4e9f3fe1dc | |||
9bd48c19ff | |||
c0516c3665 | |||
06ff36c836 | |||
98c16eb1dc | |||
35d80840e5 | |||
ad3a778fb9 | |||
d25d1efe9c | |||
de58dff423 | |||
ec26e4d8d1 | |||
c24da41505 | |||
f785185e1c | |||
5ed3ed9a2d | |||
a3adace60e | |||
3db4de09e2 | |||
70b2ee0a84 | |||
e7d82850c9 | |||
e6a8f4e6dc | |||
494e268bc5 | |||
e1c6f65b7e | |||
89075c5588 | |||
c7fa9b8ce7 | |||
cb70082d31 | |||
48b9ac8f5b | |||
0be568bbbd | |||
ba44fa620f | |||
dde402afbf | |||
bbe5142ca9 | |||
e74ffac5b0 | |||
cfb8980e3a | |||
e06e1bdcbe | |||
d60ced2f61 | |||
e68689828e | |||
ba932758c8 | |||
ee43fa6311 | |||
ad6b164d51 | |||
4a5510acb2 | |||
970dbc4428 | |||
f8f37325eb | |||
bb999019ef | |||
533b6a155a | |||
4cdc92388c | |||
ccc6be1e71 | |||
b355778a92 | |||
6a12e8f37a | |||
59adb91f5f | |||
53677e2f39 | |||
1c74f43b22 | |||
b4801970e8 | |||
7a3a6b512f | |||
72ea33b6de | |||
d97192e0ee | |||
196d9fe4d2 | |||
e960fcc303 | |||
f334da95ff | |||
5d5f311e36 | |||
d577e97758 | |||
2dc92e26d3 | |||
f4994a36a3 | |||
7a785a8163 | |||
6ad0d860c7 | |||
38a2fa55df | |||
a21b170b52 | |||
44265b2362 | |||
069193342b | |||
54e9a56cda | |||
39bc9227dc | |||
ac636670c3 | |||
2abadc73e4 | |||
377368f6bf | |||
4085c10bfc | |||
657ba11e7e | |||
a9ae45fe63 | |||
61bb39b46f | |||
2ad106f7d7 | |||
8fd4fe0e55 | |||
b1c9aedac3 | |||
a80415be02 | |||
d9acd0d74b | |||
7ae09159ba | |||
a709abd80c | |||
cd07f39b69 | |||
f7c11d07a8 | |||
b07439d402 | |||
d8eadc2a2d | |||
3a88d4d3e6 | |||
012110f008 | |||
4de274bf62 | |||
76b89baee3 | |||
697ae92031 | |||
c87f92b346 | |||
6961bb7fd0 | |||
6e26130744 | |||
123a375a27 | |||
2b4b3ca0a5 | |||
c4a795418f | |||
91837ebade | |||
0492e910ea | |||
36c86e22b1 | |||
6bdc0c7bb2 | |||
1e8d8f749a | |||
2e8e3b0d1e | |||
15b8613d3c | |||
8ce266bc94 | |||
8661d72e45 | |||
62505f2543 | |||
37986c58ec | |||
2968d96fe9 | |||
e7c8d0e78c | |||
83cbb34a5b | |||
7559c7b67e | |||
02822f4b38 | |||
96736afb94 | |||
72ae132fcd | |||
2250e1bcab | |||
d9d5b746c3 | |||
f1ea306291 | |||
378d62395a | |||
99c92069b9 | |||
2a89ef797f | |||
5838550188 | |||
e0e01ae3ee | |||
0983ba8a0f | |||
0bfa776ce7 | |||
d2b09936d1 | |||
68e9f0f7c1 | |||
c3d345de80 | |||
385c0e246a | |||
5ead49a5b7 | |||
c0760b1347 | |||
e01b323aee | |||
6f4866ef63 | |||
1b6d72661c | |||
c59d4aea81 | |||
6260a80738 | |||
e75d3c8273 | |||
b7acb475e9 | |||
42b6bbff7c | |||
4b8542b35b | |||
9ad1d6cbfb | |||
4cdd9acd73 | |||
f4b0a695d6 | |||
b525ea1ba4 | |||
c1fc2c4766 | |||
5c733932c7 | |||
d1218616ec | |||
2bf6a03d56 | |||
b6ee63c1ea | |||
6d08efdcd7 | |||
a0a43a5651 | |||
3af2f5b032 | |||
8f54b226b4 | |||
9f64011b26 | |||
c5fc54e721 | |||
fc8a4fc5b6 | |||
6f9ab232ae | |||
8cb96f1e45 | |||
5733acb77a | |||
e49bcb2a69 | |||
42e41c399f | |||
166a3180d3 | |||
3bf4982f23 | |||
f4e1cccfac | |||
7911a8f49e | |||
64a96fc3ce | |||
8e2cfbddc5 | |||
45fae3f0fd | |||
e45a7824c1 | |||
5d72c48a76 | |||
d6169c6fa2 | |||
9df6d52e2d | |||
239de8e923 | |||
7d553a87f3 | |||
557b42bc56 | |||
8423914748 | |||
07dce23794 | |||
18fd0552db | |||
d537d48f8e | |||
b456512bbb |
35
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -1,7 +1,7 @@
|
||||
name: 🐞 Bug report
|
||||
description: Report a bug or an issue.
|
||||
title: 'bug: '
|
||||
labels: ['Bug report']
|
||||
title: "bug: "
|
||||
labels: ["Bug report"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@ -80,21 +80,38 @@ body:
|
||||
- Describe your bug in detail
|
||||
- Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...)
|
||||
- Add images and videos if possible
|
||||
- List used patches, downloader and settings if applicable
|
||||
- List used patches if applicable
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Version of ReVanced Manager and version & name of app you are patching
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Installation method
|
||||
options:
|
||||
- Regular
|
||||
- Mount
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: ReVanced Manager logs
|
||||
description: Export logs from the ReVanced Manager settings.
|
||||
render: shell
|
||||
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.
|
||||
description: Export logs from the "Patcher" screen.
|
||||
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
|
||||
required: false
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your bug report will be closed if you don't follow the checklist below.
|
||||
|
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -2,4 +2,4 @@ blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 🗨 Discussions
|
||||
url: https://github.com/revanced/revanced-suggestions/discussions
|
||||
about: Have something unspecific to ReVanced Manager in mind? Search for or start a new discussion!
|
||||
about: Have something unspecific to ReVanced Manager in mind? Search for or start a new discussion!
|
||||
|
10
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -1,3 +1,7 @@
|
||||
name: ⭐ Feature request
|
||||
description: Create a detailed request for a new feature.
|
||||
title: "feat: "
|
||||
labels: ["Feature request"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@ -80,7 +84,7 @@ body:
|
||||
label: Motivation
|
||||
description: |
|
||||
A strong motivation is necessary for a feature request to be considered.
|
||||
|
||||
|
||||
- Why should this feature be implemented?
|
||||
- What is the explicit use case?
|
||||
- What are the benefits?
|
||||
@ -93,11 +97,9 @@ body:
|
||||
label: Acknowledgements
|
||||
description: Your feature request will be closed if you don't follow the checklist below.
|
||||
options:
|
||||
- label: I have checked all open and closed feature requests and this is not a duplicate
|
||||
- label: I have checked all open and closed feature requests and this is not a duplicate.
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
- label: All requested information has been provided properly.
|
||||
required: true
|
||||
- label: The feature request is only related to ReVanced Manager.
|
||||
required: true
|
||||
|
78
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,78 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
labels: []
|
||||
directory: /
|
||||
target-branch: dev
|
||||
schedule:
|
||||
interval: monthly
|
||||
groups:
|
||||
gh-actions:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
- package-ecosystem: npm
|
||||
labels: []
|
||||
directory: /
|
||||
target-branch: dev
|
||||
schedule:
|
||||
interval: monthly
|
||||
groups:
|
||||
npm:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
# ReVanced Manager Flutter
|
||||
- package-ecosystem: pub
|
||||
labels: []
|
||||
directory: /
|
||||
target-branch: dev
|
||||
schedule:
|
||||
interval: monthly
|
||||
groups:
|
||||
pubspec:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
- package-ecosystem: gradle
|
||||
labels: []
|
||||
directory: /android
|
||||
target-branch: dev
|
||||
schedule:
|
||||
interval: monthly
|
||||
groups:
|
||||
gradle:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
# ReVanced Manager Compose
|
||||
- package-ecosystem: gradle
|
||||
labels: [ "ReVanced Manager Compose" ]
|
||||
directory: /
|
||||
target-branch: compose-dev
|
||||
schedule:
|
||||
interval: monthly
|
||||
groups:
|
||||
gradle-compose:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
60
.github/workflows/build_pull_request.yml
vendored
@ -2,24 +2,70 @@ name: Build pull request
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
inputs:
|
||||
pr-number:
|
||||
description: PR number
|
||||
required: true
|
||||
app-flavor:
|
||||
description: App flavor
|
||||
default: release
|
||||
type: choice
|
||||
options:
|
||||
- release
|
||||
- debug
|
||||
- profile
|
||||
|
||||
jobs:
|
||||
release:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: refs/pull/${{ inputs.pr-number }}/merge
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Set up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version: 3.29.x
|
||||
cache: true
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: burrunan/gradle-cache-action@v1
|
||||
uses: burrunan/gradle-cache-action@v3
|
||||
with:
|
||||
build-root-directory: ${{ github.workspace }}/android
|
||||
|
||||
- name: Get dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Generate translations
|
||||
run: dart run slang
|
||||
|
||||
- name: Generate code files
|
||||
run: dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
- name: Build
|
||||
id: flutter-build
|
||||
run: flutter build apk --${{ inputs.app-flavor }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew build --no-daemon
|
||||
|
||||
- name: Upload artifacts
|
||||
if: steps.flutter-build.outcome == 'success'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: revanced-manager-(${{ env.COMMIT_HASH }}
|
||||
path: |
|
||||
build/app/outputs/flutter-apk/app-*.apk
|
||||
|
5
.github/workflows/open_pull_request.yml
vendored
@ -12,6 +12,8 @@ env:
|
||||
jobs:
|
||||
pull-request:
|
||||
name: Open pull request
|
||||
permissions:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@ -22,5 +24,6 @@ jobs:
|
||||
with:
|
||||
destination_branch: 'main'
|
||||
pr_title: 'chore: ${{ env.MESSAGE }}'
|
||||
pr_body: 'This pull request will ${{ env.MESSAGE }}.'
|
||||
pr_body: |
|
||||
This pull request will ${{ env.MESSAGE }}.
|
||||
pr_draft: true
|
||||
|
37
.github/workflows/release.yml
vendored
@ -27,26 +27,39 @@ jobs:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: burrunan/gradle-cache-action@v1
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew assembleRelease
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: 'npm'
|
||||
|
||||
- name: Set up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version: 3.29.x
|
||||
cache: true
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: burrunan/gradle-cache-action@v3
|
||||
with:
|
||||
build-root-directory: ${{ github.workspace }}/android
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
run: npm i
|
||||
|
||||
- name: Get dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Generate translations
|
||||
run: dart run slang
|
||||
|
||||
- name: Generate code files
|
||||
run: dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
- name: Setup keystore
|
||||
run: |
|
||||
echo "${{ secrets.KEYSTORE }}" | base64 --decode > "app/keystore.jks"
|
||||
echo "${{ secrets.KEYSTORE }}" | base64 --decode > "android/app/keystore.jks"
|
||||
|
||||
- name: Semantic Release
|
||||
uses: cycjimmy/semantic-release-action@v4
|
||||
@ -56,9 +69,9 @@ jobs:
|
||||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||
KEYSTORE_ENTRY_ALIAS: ${{ secrets.KEYSTORE_ENTRY_ALIAS }}
|
||||
KEYSTORE_ENTRY_PASSWORD: ${{ secrets.KEYSTORE_ENTRY_PASSWORD }}
|
||||
|
||||
|
||||
- name: Attest
|
||||
if: steps.semantic.outputs.new_release_published == 'true'
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-path: build/app/outputs/apk/release/revanced-manager-*.apk
|
||||
subject-path: build/app/outputs/apk/release/revanced-manager-*.apk
|
||||
|
82
.github/workflows/sync_crowdin.yml
vendored
Normal file
@ -0,0 +1,82 @@
|
||||
name: Sync Crowdin
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: 00 12 * * 1
|
||||
push:
|
||||
branches: dev
|
||||
paths:
|
||||
- assets/i18n/*.json
|
||||
- assets/i18n/*.dart
|
||||
- .github/workflows/sync_crowdin.yml
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: Sync
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: dev
|
||||
fetch-depth: 0
|
||||
clean: true
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version: 3.29.x
|
||||
cache: true
|
||||
|
||||
- name: Sync translations from Crowdin
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
config: crowdin.yml
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
localization_branch_name: feat/translations
|
||||
skip_ref_checkout: true
|
||||
create_pull_request: true
|
||||
pull_request_title: "chore: Sync translations"
|
||||
pull_request_body: "Sync translations from [crowdin.com/project/revanced](https://crowdin.com/project/revanced)"
|
||||
pull_request_base_branch_name: "dev"
|
||||
commit_message: "chore: Sync translations"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
||||
- name: Validation of synced translations
|
||||
run: |
|
||||
dart pub get
|
||||
dart run slang validate
|
||||
|
||||
- name: Normalization of Translation Strings
|
||||
run: |
|
||||
sudo chmod 766 assets/i18n/*.i18n.json
|
||||
|
||||
dart run slang analyze
|
||||
dart run slang clean
|
||||
dart run slang normalize
|
||||
|
||||
dart run slang
|
||||
|
||||
cd assets/i18n
|
||||
dart nuke.dart >> $GITHUB_STEP_SUMMARY
|
||||
cd ../..
|
||||
|
||||
flutter analyze lib/gen/strings.g.dart --no-fatal-infos --no-fatal-warnings
|
||||
|
||||
- name: Commit translations
|
||||
run: |
|
||||
git config --global user.name 'github-actions[bot]'
|
||||
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
sudo chown -R $USER:$USER .git
|
||||
git commit -m "chore: Remove empty values from JSON" assets/i18n/*.i18n.json
|
||||
git push origin HEAD:feat/translations
|
2
.github/workflows/update_documentation.yml
vendored
@ -16,4 +16,4 @@ jobs:
|
||||
token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }}
|
||||
repository: revanced/revanced-documentation
|
||||
event-type: update-documentation
|
||||
client-payload: '{"repo": "${{ github.event.repository.name }}", "ref": "${{ github.ref }}"}'
|
||||
client-payload: '{"repo": "${{ github.event.repository.name }}", "ref": "${{ github.ref }}"}'
|
||||
|
74
.gitignore
vendored
@ -1,12 +1,64 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.lock
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.buildlog/
|
||||
.history
|
||||
|
||||
# packages file containing multi-root paths
|
||||
.packages.generated
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
**/generated_plugin_registrant.dart
|
||||
.packages
|
||||
.pub-preload-cache/
|
||||
.pub/
|
||||
build/
|
||||
flutter_*.png
|
||||
linked_*.ds
|
||||
unlinked.ds
|
||||
unlinked_spec.ds
|
||||
|
||||
# Android related
|
||||
**/android/**/gradle-wrapper.jar
|
||||
.gradle/
|
||||
**/android/captures/
|
||||
**/android/gradlew
|
||||
**/android/gradlew.bat
|
||||
**/android/local.properties
|
||||
**/android/**/GeneratedPluginRegistrant.java
|
||||
**/android/key.properties
|
||||
*.jks
|
||||
|
||||
# Coverage
|
||||
coverage/
|
||||
|
||||
# Symbols
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Generated files
|
||||
android/app/.cxx
|
||||
**/*.g.dart
|
||||
**/*.locator.dart
|
||||
**/*.router.dart
|
||||
|
||||
# Project specific
|
||||
node_modules/
|
||||
.vscode/
|
||||
|
||||
|
||||
.kotlin/
|
||||
|
30
.metadata
Normal file
@ -0,0 +1,30 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "2663184aa79047d0a33a14a3b607954f8fdd8730"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
|
||||
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
|
||||
- platform: android
|
||||
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
|
||||
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
62
.releaserc
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
"branches": [
|
||||
"main",
|
||||
{
|
||||
"name": "dev",
|
||||
"prerelease": true
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"@semantic-release/commit-analyzer", {
|
||||
"releaseRules": [
|
||||
{ "type": "build", "scope": "Needs bump", "release": "patch" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"@semantic-release/changelog",
|
||||
"@semantic-release/release-notes-generator",
|
||||
[
|
||||
"semantic-release-pub",
|
||||
{
|
||||
"publishPub": false,
|
||||
"updateBuildNumber": true
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/exec",
|
||||
{
|
||||
"prepareCmd": "flutter build apk"
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/git",
|
||||
{
|
||||
"assets": [
|
||||
"pubspec.yaml"
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/github",
|
||||
{
|
||||
"assets": [
|
||||
{
|
||||
"path": "build/app/outputs/apk/release/revanced-manager*.apk"
|
||||
},
|
||||
],
|
||||
"commits": [
|
||||
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
||||
],
|
||||
"successComment": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"@saithodev/semantic-release-backmerge",
|
||||
{
|
||||
"backmergeBranches": [{"from": "main", "to": "dev"}],
|
||||
"clearWorkspace": true
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
@ -96,7 +96,7 @@ If you encounter a bug while using ReVanced Manager, open an issue using the
|
||||
|
||||
## 🤚 I want to contribute but don't know how to code
|
||||
|
||||
Even if you don't know how to code, you can still contribute by
|
||||
Even if you don't know how to code, you can still contribute by
|
||||
translating ReVanced Manager on [Crowdin](https://translate.revanced.app/).
|
||||
|
||||
❤️ Thank you for considering contributing to ReVanced Manager,
|
||||
|
@ -73,9 +73,10 @@ ReVanced Manager is an application that uses [ReVanced Patcher](https://github.c
|
||||
|
||||
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
|
||||
- 💉 **Patch apps**: Apply any patch of your choice to Android apps
|
||||
- 📱 **Portable**: ReVanced Patcher that fits in your pocket
|
||||
- 🤗 **Simple UI**: Quickly understand the ins and outs of ReVanced Manager
|
||||
- 🛠️ **Customization**: Configurable API, custom sources, language, signing keystore, theme and more
|
||||
|
||||
## 🔽 Download
|
||||
|
||||
|
77
SECURITY.md
Normal file
@ -0,0 +1,77 @@
|
||||
<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>
|
||||
|
||||
# 🔒 Security Policy
|
||||
|
||||
This document describes how to report security vulnerabilities for ReVanced Manager.
|
||||
|
||||
## 🚨 Reporting a Vulnerability
|
||||
|
||||
Please open an issue in our [advisory tracker](https://github.com/ReVanced/revanced-manager/security/advisories/new) or reach out privately to us on [Discord](https://discord.gg/revanced).
|
||||
|
||||
If a vulnerability is confirmed and accepted, you can join our [Discord](https://discord.gg/revanced) server to receive a special contributor role.
|
||||
|
||||
### ⏳ Supported Versions
|
||||
|
||||
| Version | Branch | Supported |
|
||||
| --------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------ |
|
||||
|  | main | :white_check_mark: |
|
||||
|  | dev | :white_check_mark: |
|
||||
|  | compose-dev | :white_check_mark: |
|
156
analysis_options.yaml
Normal file
@ -0,0 +1,156 @@
|
||||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
- lib/app/app.locator.dart
|
||||
- lib/app/app.router.dart
|
||||
- lib/models/patch.g.dart
|
||||
- lib/models/patched_application.g.dart
|
||||
- lib/gen/
|
||||
|
||||
linter:
|
||||
rules:
|
||||
- always_declare_return_types
|
||||
- require_trailing_commas
|
||||
- always_put_control_body_on_new_line
|
||||
- always_use_package_imports # we do this commonly
|
||||
- annotate_overrides
|
||||
- avoid_bool_literals_in_conditional_expressions
|
||||
- avoid_double_and_int_checks
|
||||
- avoid_empty_else
|
||||
- avoid_equals_and_hash_code_on_mutable_classes
|
||||
- avoid_escaping_inner_quotes
|
||||
- avoid_field_initializers_in_const_classes
|
||||
- avoid_function_literals_in_foreach_calls
|
||||
- avoid_implementing_value_types
|
||||
- avoid_init_to_null
|
||||
- avoid_js_rounded_ints
|
||||
- avoid_null_checks_in_equality_operators
|
||||
- avoid_print
|
||||
- avoid_redundant_argument_values
|
||||
- avoid_relative_lib_imports
|
||||
- avoid_renaming_method_parameters
|
||||
- avoid_return_types_on_setters
|
||||
- avoid_returning_null_for_void
|
||||
- avoid_setters_without_getters
|
||||
- avoid_shadowing_type_parameters
|
||||
- avoid_single_cascade_in_expression_statements
|
||||
- avoid_type_to_string
|
||||
- avoid_types_as_parameter_names
|
||||
- avoid_unnecessary_containers
|
||||
- avoid_void_async
|
||||
- avoid_web_libraries_in_flutter # we use web libraries in web-specific code, and our tests prevent us from using them elsewhere
|
||||
- await_only_futures
|
||||
- camel_case_extensions
|
||||
- camel_case_types
|
||||
- cancel_subscriptions
|
||||
- cast_nullable_to_non_nullable
|
||||
- close_sinks # not reliable enough
|
||||
- control_flow_in_finally
|
||||
- curly_braces_in_flow_control_structures
|
||||
- depend_on_referenced_packages
|
||||
- deprecated_consistency
|
||||
- directives_ordering
|
||||
- empty_catches
|
||||
- empty_constructor_bodies
|
||||
- empty_statements
|
||||
- eol_at_end_of_file
|
||||
- exhaustive_cases
|
||||
- file_names
|
||||
- flutter_style_todos
|
||||
- hash_and_equals
|
||||
- implementation_imports
|
||||
- collection_methods_unrelated_type
|
||||
- leading_newlines_in_multiline_strings
|
||||
- library_prefixes
|
||||
- library_private_types_in_public_api
|
||||
- missing_whitespace_between_adjacent_strings
|
||||
- no_adjacent_strings_in_list
|
||||
- no_duplicate_case_values
|
||||
- no_logic_in_create_state
|
||||
- non_constant_identifier_names
|
||||
- noop_primitive_operations
|
||||
- null_check_on_nullable_type_parameter
|
||||
- null_closures
|
||||
- overridden_fields
|
||||
- package_names
|
||||
- prefer_adjacent_string_concatenation
|
||||
- prefer_asserts_in_initializer_lists
|
||||
- prefer_collection_literals
|
||||
- prefer_conditional_assignment
|
||||
- prefer_const_constructors
|
||||
- prefer_const_constructors_in_immutables
|
||||
- prefer_const_declarations
|
||||
- prefer_const_literals_to_create_immutables
|
||||
- prefer_contains
|
||||
- prefer_final_fields
|
||||
- prefer_final_in_for_each
|
||||
- prefer_final_locals
|
||||
- prefer_for_elements_to_map_fromIterable
|
||||
- prefer_foreach
|
||||
- prefer_function_declarations_over_variables
|
||||
- prefer_generic_function_type_aliases
|
||||
- prefer_if_elements_to_conditional_expressions
|
||||
- prefer_if_null_operators
|
||||
- prefer_initializing_formals
|
||||
- prefer_inlined_adds
|
||||
- prefer_interpolation_to_compose_strings
|
||||
- prefer_is_empty
|
||||
- prefer_is_not_empty
|
||||
- prefer_is_not_operator
|
||||
- prefer_iterable_whereType
|
||||
- prefer_null_aware_method_calls # "call()" is confusing to people new to the language since it's not documented anywhere
|
||||
- prefer_null_aware_operators
|
||||
- prefer_single_quotes
|
||||
- prefer_spread_collections
|
||||
- prefer_typing_uninitialized_variables
|
||||
- provide_deprecation_message
|
||||
- recursive_getters
|
||||
- sized_box_for_whitespace
|
||||
- slash_for_doc_comments
|
||||
- sort_child_properties_last
|
||||
- sort_constructors_first
|
||||
- sort_pub_dependencies
|
||||
- sort_unnamed_constructors_first
|
||||
- test_types_in_equals
|
||||
- throw_in_finally
|
||||
- tighten_type_of_initializing_formals
|
||||
- type_init_formals
|
||||
- unnecessary_brace_in_string_interps
|
||||
- unnecessary_const
|
||||
- unnecessary_getters_setters
|
||||
- unnecessary_new
|
||||
- unnecessary_null_aware_assignments
|
||||
- unnecessary_null_checks
|
||||
- unnecessary_null_in_if_null_operators
|
||||
- unnecessary_nullable_for_final_variable_declarations
|
||||
- unnecessary_overrides
|
||||
- unnecessary_parenthesis
|
||||
- unnecessary_statements
|
||||
- unnecessary_string_escapes
|
||||
- unnecessary_string_interpolations
|
||||
- unnecessary_this
|
||||
- unrelated_type_equality_checks
|
||||
- use_build_context_synchronously
|
||||
- use_full_hex_values_for_flutter_colors
|
||||
- use_function_type_syntax_for_parameters
|
||||
- use_if_null_to_convert_nulls_to_bools
|
||||
- use_is_even_rather_than_modulo
|
||||
- use_key_in_widget_constructors
|
||||
- use_late_for_private_fields_and_variables
|
||||
- use_named_constants
|
||||
- use_raw_strings
|
||||
- use_rethrow_when_possible
|
||||
- use_setters_to_change_properties
|
||||
- use_test_throws_matchers
|
||||
- valid_regexps
|
||||
- void_checks
|
13
android/.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
104
android/app/build.gradle.kts
Normal file
@ -0,0 +1,104 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.manager.flutter"
|
||||
compileSdk = 35
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "app.revanced.manager.flutter"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
|
||||
resValue("string", "app_name", "ReVanced Manager")
|
||||
}
|
||||
|
||||
applicationVariants.all {
|
||||
outputs.all {
|
||||
this as com.android.build.gradle.internal.api.ApkVariantOutputImpl
|
||||
|
||||
outputFileName = "revanced-manager-$versionName.apk"
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
configureEach {
|
||||
isShrinkResources = false
|
||||
isMinifyEnabled = false
|
||||
|
||||
signingConfig = signingConfigs["debug"]
|
||||
|
||||
ndk.abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86_64")
|
||||
}
|
||||
|
||||
release {
|
||||
isShrinkResources = true
|
||||
isMinifyEnabled = true
|
||||
|
||||
val keystoreFile = file("keystore.jks")
|
||||
if (keystoreFile.exists()) {
|
||||
signingConfig = signingConfigs.create("release") {
|
||||
storeFile = keystoreFile
|
||||
storePassword = System.getenv("KEYSTORE_PASSWORD")
|
||||
keyAlias = System.getenv("KEYSTORE_ENTRY_ALIAS")
|
||||
keyPassword = System.getenv("KEYSTORE_ENTRY_PASSWORD")
|
||||
}
|
||||
|
||||
resValue("string", "app_name", "ReVanced Manager")
|
||||
} else {
|
||||
applicationIdSuffix = ".development"
|
||||
resValue("string", "app_name", "ReVanced Manager (Development)")
|
||||
signingConfig = signingConfigs["debug"]
|
||||
}
|
||||
}
|
||||
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
resValue("string", "app_name", "ReVanced Manager (Debug)")
|
||||
}
|
||||
|
||||
named("profile") {
|
||||
initWith(getByName("debug"))
|
||||
applicationIdSuffix = ".profile"
|
||||
resValue("string", "app_name", "ReVanced Manager (Profile)")
|
||||
}
|
||||
}
|
||||
|
||||
packaging {
|
||||
jniLibs {
|
||||
useLegacyPackaging = true
|
||||
excludes.add("/prebuilt/**")
|
||||
}
|
||||
|
||||
resources {
|
||||
excludes.add("/prebuilt/**")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring(libs.desugar.jdk.libs) // https://pub.dev/packages/flutter_local_notifications#gradle-setup
|
||||
implementation(libs.revanced.patcher)
|
||||
implementation(libs.revanced.library)
|
||||
}
|
17
android/app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
-dontobfuscate
|
||||
|
||||
-keep class app.revanced.** { *; }
|
||||
-keep class com.android.tools.smali.** { *; }
|
||||
-keep class kotlin.** { *; }
|
||||
-keep class com.google.auto.value.** { *; }
|
||||
-keep class com.android.apksig.internal.** { *; }
|
||||
-keepnames class com.google.common.collect.**
|
||||
-keepnames class org.xmlpull.** { *; }
|
||||
|
||||
-dontwarn com.google.auto.value.**
|
||||
-dontwarn com.google.j2objc.annotations.*
|
||||
-dontwarn java.awt.**
|
||||
-dontwarn javax.**
|
||||
|
||||
# Required for Share Plus, ref: ReVanced/revanced-manager#2474
|
||||
-keep interface android.content.res.XmlResourceParser { *; }
|
7
android/app/src/debug/AndroidManifest.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
88
android/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,88 @@
|
||||
<manifest xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<application
|
||||
android:label="@string/app_name"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:largeHeap="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:enableOnBackInvokedCallback="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme" />
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.EnableImpeller"
|
||||
android:value="false" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service
|
||||
android:name="de.julianassmann.flutter_background.IsolateHolderService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="shortService" />
|
||||
<activity
|
||||
android:name=".ExportSettingsActivity"
|
||||
android:exported="true">
|
||||
</activity>
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileProvider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<receiver
|
||||
android:name=".utils.packageInstaller.InstallerReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="APP_INSTALL_ACTION" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".utils.packageInstaller.UninstallerReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="APP_UNINSTALL_ACTION" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
</manifest>
|
@ -0,0 +1,81 @@
|
||||
package app.revanced.manager.flutter
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.util.Base64
|
||||
import org.json.JSONObject
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import java.security.MessageDigest
|
||||
|
||||
class ExportSettingsActivity : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (getFingerprint(callingPackage!!) == getFingerprint(packageName)) {
|
||||
// Create JSON Object
|
||||
val json = JSONObject()
|
||||
|
||||
// Default Data
|
||||
json.put("keystorePassword", "s3cur3p@ssw0rd")
|
||||
|
||||
// Load Shared Preferences
|
||||
val sharedPreferences = getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
|
||||
val allEntries: Map<String, *> = sharedPreferences.getAll()
|
||||
for ((key, value) in allEntries.entries) {
|
||||
json.put(key.replace("flutter.", ""), value)
|
||||
}
|
||||
|
||||
// Load keystore
|
||||
val keystoreFile = File(getExternalFilesDir(null), "/revanced-manager.keystore")
|
||||
if (keystoreFile.exists()) {
|
||||
val keystoreBytes = keystoreFile.readBytes()
|
||||
val keystoreBase64 = Base64.encodeToString(keystoreBytes, Base64.DEFAULT)
|
||||
json.put("keystore", keystoreBase64)
|
||||
}
|
||||
|
||||
// Load saved patches
|
||||
val storedPatchesFile = File(filesDir.parentFile.absolutePath, "/app_flutter/selected-patches.json")
|
||||
if (storedPatchesFile.exists()) {
|
||||
val patchesBytes = storedPatchesFile.readBytes()
|
||||
val patches = String(patchesBytes, Charsets.UTF_8)
|
||||
json.put("patches", JSONObject(patches))
|
||||
}
|
||||
|
||||
// Send data back
|
||||
val resultIntent = Intent()
|
||||
resultIntent.putExtra("data", json.toString())
|
||||
setResult(Activity.RESULT_OK, resultIntent)
|
||||
finish()
|
||||
} else {
|
||||
val resultIntent = Intent()
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
fun getFingerprint(packageName: String): String {
|
||||
// Get the signature of the app that matches the package name
|
||||
val packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
|
||||
val signature = packageInfo.signatures!![0]
|
||||
|
||||
// Get the raw certificate data
|
||||
val rawCert = signature.toByteArray()
|
||||
|
||||
// Generate an X509Certificate from the data
|
||||
val certFactory = CertificateFactory.getInstance("X509")
|
||||
val x509Cert = certFactory.generateCertificate(ByteArrayInputStream(rawCert)) as X509Certificate
|
||||
|
||||
// Get the SHA256 fingerprint
|
||||
val fingerprint = MessageDigest.getInstance("SHA256").digest(x509Cert.encoded).joinToString("") {
|
||||
"%02x".format(it)
|
||||
}
|
||||
|
||||
return fingerprint
|
||||
}
|
||||
}
|
@ -0,0 +1,427 @@
|
||||
package app.revanced.manager.flutter
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.app.SearchManager
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import app.revanced.library.ApkUtils
|
||||
import app.revanced.library.ApkUtils.applyTo
|
||||
import app.revanced.manager.flutter.utils.Aapt
|
||||
import app.revanced.manager.flutter.utils.packageInstaller.InstallerReceiver
|
||||
import app.revanced.manager.flutter.utils.packageInstaller.UninstallerReceiver
|
||||
import app.revanced.patcher.Patcher
|
||||
import app.revanced.patcher.PatcherConfig
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.patch.PatchResult
|
||||
import app.revanced.patcher.patch.loadPatchesFromDex
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import java.util.logging.LogRecord
|
||||
import java.util.logging.Logger
|
||||
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private lateinit var installerChannel: MethodChannel
|
||||
private var cancel: Boolean = false
|
||||
private var stopResult: MethodChannel.Result? = null
|
||||
|
||||
private lateinit var patches: Set<Patch<*>>
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
val patcherChannel = "app.revanced.manager.flutter/patcher"
|
||||
val installerChannel = "app.revanced.manager.flutter/installer"
|
||||
val openBrowserChannel = "app.revanced.manager.flutter/browser"
|
||||
|
||||
MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
openBrowserChannel
|
||||
).setMethodCallHandler { call, result ->
|
||||
if (call.method == "openBrowser") {
|
||||
val searchQuery = call.argument<String>("query")
|
||||
openBrowser(searchQuery)
|
||||
result.success(null)
|
||||
} else {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
val mainChannel =
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, patcherChannel)
|
||||
|
||||
this.installerChannel =
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, installerChannel)
|
||||
|
||||
mainChannel.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"runPatcher" -> {
|
||||
val inFilePath = call.argument<String>("inFilePath")
|
||||
val outFilePath = call.argument<String>("outFilePath")
|
||||
val selectedPatches = call.argument<List<String>>("selectedPatches")
|
||||
val options = call.argument<Map<String, Map<String, Any>>>("options")
|
||||
val tmpDirPath = call.argument<String>("tmpDirPath")
|
||||
val keyStoreFilePath = call.argument<String>("keyStoreFilePath")
|
||||
val keystorePassword = call.argument<String>("keystorePassword")
|
||||
|
||||
if (
|
||||
inFilePath != null &&
|
||||
outFilePath != null &&
|
||||
selectedPatches != null &&
|
||||
options != null &&
|
||||
tmpDirPath != null &&
|
||||
keyStoreFilePath != null &&
|
||||
keystorePassword != null
|
||||
) {
|
||||
cancel = false
|
||||
runPatcher(
|
||||
result,
|
||||
inFilePath,
|
||||
outFilePath,
|
||||
selectedPatches,
|
||||
options,
|
||||
tmpDirPath,
|
||||
keyStoreFilePath,
|
||||
keystorePassword
|
||||
)
|
||||
} else result.error(
|
||||
"INVALID_ARGUMENTS",
|
||||
"Invalid arguments",
|
||||
"One or more arguments are missing"
|
||||
)
|
||||
}
|
||||
|
||||
"stopPatcher" -> {
|
||||
cancel = true
|
||||
stopResult = result
|
||||
}
|
||||
|
||||
"getPatches" -> {
|
||||
val patchBundleFilePath = call.argument<String>("patchBundleFilePath")!!
|
||||
|
||||
try {
|
||||
val patchBundleFile = File(patchBundleFilePath)
|
||||
patchBundleFile.setWritable(false)
|
||||
patches = loadPatchesFromDex(
|
||||
setOf(patchBundleFile),
|
||||
optimizedDexDirectory = codeCacheDir
|
||||
)
|
||||
} catch (t: Throwable) {
|
||||
return@setMethodCallHandler result.error(
|
||||
"PATCH_BUNDLE_ERROR",
|
||||
"Failed to load patch bundle",
|
||||
t.stackTraceToString()
|
||||
)
|
||||
}
|
||||
|
||||
JSONArray().apply {
|
||||
patches.forEach {
|
||||
JSONObject().apply {
|
||||
put("name", it.name)
|
||||
put("description", it.description)
|
||||
put("excluded", !it.use)
|
||||
put("compatiblePackages", JSONArray().apply {
|
||||
it.compatiblePackages?.forEach { (name, versions) ->
|
||||
val compatiblePackageJson = JSONObject().apply {
|
||||
put("name", name)
|
||||
put(
|
||||
"versions",
|
||||
JSONArray().apply {
|
||||
versions?.forEach { version ->
|
||||
put(version)
|
||||
}
|
||||
})
|
||||
}
|
||||
put(compatiblePackageJson)
|
||||
}
|
||||
})
|
||||
put("options", JSONArray().apply {
|
||||
it.options.values.forEach { option ->
|
||||
JSONObject().apply {
|
||||
put("key", option.key)
|
||||
put("title", option.title)
|
||||
put("description", option.description)
|
||||
put("required", option.required)
|
||||
|
||||
fun JSONObject.putValue(
|
||||
value: Any?,
|
||||
key: String = "value"
|
||||
) = if (value is Array<*>) put(
|
||||
key,
|
||||
JSONArray().apply {
|
||||
value.forEach { put(it) }
|
||||
})
|
||||
else put(key, value)
|
||||
|
||||
putValue(option.default)
|
||||
|
||||
option.values?.let { values ->
|
||||
put(
|
||||
"values",
|
||||
JSONObject().apply {
|
||||
values.forEach { (key, value) ->
|
||||
putValue(value, key)
|
||||
}
|
||||
})
|
||||
} ?: put("values", null)
|
||||
put("type", option.type)
|
||||
}.let(::put)
|
||||
}
|
||||
})
|
||||
}.let(::put)
|
||||
}
|
||||
}.toString().let(result::success)
|
||||
}
|
||||
|
||||
"installApk" -> {
|
||||
val apkPath = call.argument<String>("apkPath")!!
|
||||
PackageInstallerManager.result = result
|
||||
installApk(apkPath)
|
||||
}
|
||||
|
||||
"uninstallApp" -> {
|
||||
val packageName = call.argument<String>("packageName")!!
|
||||
uninstallApp(packageName)
|
||||
PackageInstallerManager.result = result
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openBrowser(query: String?) {
|
||||
val intent = Intent(Intent.ACTION_WEB_SEARCH).apply {
|
||||
putExtra(SearchManager.QUERY, query)
|
||||
}
|
||||
if (intent.resolveActivity(packageManager) != null) {
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun runPatcher(
|
||||
result: MethodChannel.Result,
|
||||
inFilePath: String,
|
||||
outFilePath: String,
|
||||
selectedPatches: List<String>,
|
||||
options: Map<String, Map<String, Any>>,
|
||||
tmpDirPath: String,
|
||||
keyStoreFilePath: String,
|
||||
keystorePassword: String
|
||||
) {
|
||||
val inFile = File(inFilePath)
|
||||
// Necessary because the file is copied from a nonwriteable location.
|
||||
inFile.setWritable(true)
|
||||
inFile.setReadable(true)
|
||||
val outFile = File(outFilePath)
|
||||
val keyStoreFile = File(keyStoreFilePath)
|
||||
val tmpDir = File(tmpDirPath)
|
||||
|
||||
Thread {
|
||||
fun updateProgress(progress: Double, header: String, log: String) {
|
||||
handler.post {
|
||||
installerChannel.invokeMethod(
|
||||
"update",
|
||||
mapOf(
|
||||
"progress" to progress,
|
||||
"header" to header,
|
||||
"log" to log
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun postStop() = handler.post { stopResult!!.success(null) }
|
||||
|
||||
fun cancel(block: () -> Unit = {}): Boolean {
|
||||
if (cancel) {
|
||||
block()
|
||||
postStop()
|
||||
}
|
||||
|
||||
return cancel
|
||||
}
|
||||
|
||||
|
||||
// Setup logger
|
||||
Logger.getLogger("").apply {
|
||||
handlers.forEach { handler ->
|
||||
handler.close()
|
||||
removeHandler(handler)
|
||||
}
|
||||
|
||||
object : java.util.logging.Handler() {
|
||||
override fun publish(record: LogRecord) {
|
||||
if (cancel) return
|
||||
if (
|
||||
record.loggerName?.startsWith("app.revanced") == true ||
|
||||
// Logger in class brut.util.OS.
|
||||
record.loggerName == ""
|
||||
) updateProgress(-1.0, "", record.message)
|
||||
}
|
||||
|
||||
override fun flush() = Unit
|
||||
override fun close() = flush()
|
||||
}.let(::addHandler)
|
||||
}
|
||||
|
||||
try {
|
||||
updateProgress(0.0, "Reading APK...", "Reading APK")
|
||||
|
||||
val patcher = Patcher(
|
||||
PatcherConfig(
|
||||
inFile,
|
||||
tmpDir,
|
||||
Aapt.binary(applicationContext).absolutePath,
|
||||
tmpDir.path,
|
||||
)
|
||||
)
|
||||
|
||||
if (cancel(patcher::close)) return@Thread
|
||||
updateProgress(0.02, "Loading patches...", "Loading patches")
|
||||
|
||||
val patches = patches.filter { patch ->
|
||||
val isCompatible = patch.compatiblePackages?.any { (name, _) ->
|
||||
name == patcher.context.packageMetadata.packageName
|
||||
} ?: false
|
||||
|
||||
val compatibleOrUniversal =
|
||||
isCompatible || patch.compatiblePackages.isNullOrEmpty()
|
||||
|
||||
compatibleOrUniversal && selectedPatches.any { it == patch.name }
|
||||
}.onEach { patch ->
|
||||
options[patch.name]?.forEach { (key, value) ->
|
||||
patch.options[key] = value
|
||||
}
|
||||
}.toSet()
|
||||
|
||||
if (cancel(patcher::close)) return@Thread
|
||||
updateProgress(0.05, "Executing...", "")
|
||||
|
||||
val patcherResult = patcher.use {
|
||||
it += patches
|
||||
|
||||
runBlocking {
|
||||
// Update the progress bar every time a patch is executed from 0.15 to 0.7
|
||||
val totalPatchesCount = patches.size
|
||||
val progressStep = 0.55 / totalPatchesCount
|
||||
var progress = 0.05
|
||||
|
||||
patcher().collect(FlowCollector { patchResult: PatchResult ->
|
||||
if (cancel(patcher::close)) return@FlowCollector
|
||||
|
||||
val msg = patchResult.exception?.let {
|
||||
val writer = StringWriter()
|
||||
it.printStackTrace(PrintWriter(writer))
|
||||
"${patchResult.patch.name} failed: $writer"
|
||||
} ?: run {
|
||||
"${patchResult.patch.name} succeeded"
|
||||
}
|
||||
|
||||
updateProgress(progress, "", msg)
|
||||
progress += progressStep
|
||||
})
|
||||
}
|
||||
|
||||
if (cancel(patcher::close)) return@Thread
|
||||
updateProgress(0.75, "Building...", "")
|
||||
|
||||
patcher.get()
|
||||
}
|
||||
|
||||
if (cancel(patcher::close)) return@Thread
|
||||
|
||||
patcherResult.applyTo(inFile)
|
||||
|
||||
if (cancel(patcher::close)) return@Thread
|
||||
|
||||
ApkUtils.signApk(
|
||||
inFile,
|
||||
outFile,
|
||||
"ReVanced",
|
||||
ApkUtils.KeyStoreDetails(
|
||||
keyStoreFile,
|
||||
keystorePassword,
|
||||
"alias",
|
||||
keystorePassword
|
||||
)
|
||||
)
|
||||
|
||||
updateProgress(.85, "Patched", "Patched APK")
|
||||
} catch (ex: Throwable) {
|
||||
if (!cancel) {
|
||||
val stack = ex.stackTraceToString()
|
||||
updateProgress(
|
||||
-100.0,
|
||||
"Failed",
|
||||
"An error occurred:\n$stack"
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
inFile.delete()
|
||||
tmpDir.deleteRecursively()
|
||||
}
|
||||
|
||||
handler.post { result.success(null) }
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun installApk(apkPath: String) {
|
||||
val packageInstaller: PackageInstaller = applicationContext.packageManager.packageInstaller
|
||||
val sessionParams =
|
||||
PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
||||
val sessionId: Int = packageInstaller.createSession(sessionParams)
|
||||
val session: PackageInstaller.Session = packageInstaller.openSession(sessionId)
|
||||
session.use { activeSession ->
|
||||
val sessionOutputStream = activeSession.openWrite(applicationContext.packageName, 0, -1)
|
||||
sessionOutputStream.use { outputStream ->
|
||||
val apkFile = File(apkPath)
|
||||
apkFile.inputStream().use { inputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
val receiverIntent = Intent(applicationContext, InstallerReceiver::class.java).apply {
|
||||
action = "APP_INSTALL_ACTION"
|
||||
}
|
||||
val receiverPendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
sessionId,
|
||||
receiverIntent,
|
||||
PackageInstallerManager.flags
|
||||
)
|
||||
session.commit(receiverPendingIntent.intentSender)
|
||||
session.close()
|
||||
}
|
||||
|
||||
private fun uninstallApp(packageName: String) {
|
||||
val packageInstaller: PackageInstaller = applicationContext.packageManager.packageInstaller
|
||||
val receiverIntent = Intent(applicationContext, UninstallerReceiver::class.java).apply {
|
||||
action = "APP_UNINSTALL_ACTION"
|
||||
}
|
||||
val receiverPendingIntent =
|
||||
PendingIntent.getBroadcast(context, 0, receiverIntent, PackageInstallerManager.flags)
|
||||
packageInstaller.uninstall(packageName, receiverPendingIntent.intentSender)
|
||||
}
|
||||
|
||||
object PackageInstallerManager {
|
||||
var result: MethodChannel.Result? = null
|
||||
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
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())
|
@ -0,0 +1,32 @@
|
||||
package app.revanced.manager.flutter.utils.packageInstaller
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import app.revanced.manager.flutter.MainActivity
|
||||
|
||||
class InstallerReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)) {
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
|
||||
if (confirmationIntent != null) {
|
||||
context.startActivity(confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
val packageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
|
||||
val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
||||
val otherPackageName = intent.getStringExtra(PackageInstaller.EXTRA_OTHER_PACKAGE_NAME)
|
||||
MainActivity.PackageInstallerManager.result!!.success(mapOf(
|
||||
"status" to status,
|
||||
"packageName" to packageName,
|
||||
"message" to message,
|
||||
"otherPackageName" to otherPackageName
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package app.revanced.manager.flutter.utils.packageInstaller
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import app.revanced.manager.flutter.MainActivity
|
||||
|
||||
class UninstallerReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)) {
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
|
||||
if (confirmationIntent != null) {
|
||||
context.startActivity(confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
MainActivity.PackageInstallerManager.result!!.success(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
12
android/app/src/main/res/drawable-v21/launch_background.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?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>
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
12
android/app/src/main/res/drawable/launch_background.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?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>
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
3
android/app/src/main/res/raw/revanced_manager_keep.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:keep="@drawable/ic_notification" />
|
19
android/app/src/main/res/values-night-v31/styles.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?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>
|
18
android/app/src/main/res/values-night/styles.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?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>
|
20
android/app/src/main/res/values-v31/styles.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<?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>
|
||||
|
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#1B1B1B</color>
|
||||
</resources>
|
18
android/app/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?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>
|
4
android/app/src/main/res/xml/file_paths.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<cache-path name="cache" path="." />
|
||||
</paths>
|
7
android/app/src/profile/AndroidManifest.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
40
android/build.gradle.kts
Normal file
@ -0,0 +1,40 @@
|
||||
import com.android.build.api.dsl.CommonExtension
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven {
|
||||
name = "GitHubPackages"
|
||||
url = uri("https://maven.pkg.github.com/revanced/registry")
|
||||
credentials {
|
||||
username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
|
||||
password = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layout.buildDirectory = File("../build")
|
||||
|
||||
project(":screenshot_callback") {
|
||||
tasks.withType<KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
afterEvaluate {
|
||||
extensions.findByName("android")?.let {
|
||||
it as CommonExtension<*, *, *, *, *, *>
|
||||
if (it.compileSdk != null && it.compileSdk!! < 31)
|
||||
it.compileSdk = 34
|
||||
}
|
||||
}
|
||||
|
||||
layout.buildDirectory = rootProject.layout.buildDirectory.file(name).get().asFile
|
||||
evaluationDependsOn(":app")
|
||||
}
|
7
android/gradle.properties
Normal file
@ -0,0 +1,7 @@
|
||||
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
org.gradle.parallel=true
|
||||
org.gradle.daemon=true
|
||||
org.gradle.caching=true
|
||||
android.nonTransitiveRClass=false
|
||||
android.nonFinalResIds=false
|
9
android/gradle/libs.versions.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[versions]
|
||||
revanced-patcher = "21.0.0"
|
||||
revanced-library = "3.1.0"
|
||||
desugar_jdk_libs = "2.1.4"
|
||||
|
||||
[libraries]
|
||||
revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" }
|
||||
revanced-library = { module = "app.revanced:revanced-library", version.ref = "revanced-library" }
|
||||
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
|
@ -1,7 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=61ad310d3c7d3e5da131b76bbf22b5a4c0786e9d892dae8c1658d4b484de3caa
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
|
||||
distributionSha256Sum=8d97a97984f6cbd2b85fe4c60a743440a347544bf18818048e611f5288d46c94
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
24
android/settings.gradle.kts
Normal file
@ -0,0 +1,24 @@
|
||||
pluginManagement {
|
||||
val properties = java.util.Properties().apply {
|
||||
load(file("local.properties").inputStream())
|
||||
}
|
||||
|
||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.9.0" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.1.10" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
1
app/.gitignore
vendored
@ -1 +0,0 @@
|
||||
/build
|
@ -1,222 +0,0 @@
|
||||
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
@ -1,63 +0,0 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
-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
|
@ -1,429 +0,0 @@
|
||||
{
|
||||
"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')"
|
||||
]
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
<?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>
|
@ -1,8 +0,0 @@
|
||||
// IRootService.aidl
|
||||
package app.revanced.manager;
|
||||
|
||||
// Declare any non-default types here with import statements
|
||||
|
||||
interface IRootSystemService {
|
||||
IBinder getFileSystemService();
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
// 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);
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
// 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);
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
// Parameters.aidl
|
||||
package app.revanced.manager.patcher.runtime.process;
|
||||
|
||||
parcelable Parameters;
|
@ -1,6 +0,0 @@
|
||||
id=__PKG_NAME__-ReVanced
|
||||
name=__LABEL__ ReVanced
|
||||
version=__VERSION__
|
||||
versionCode=0
|
||||
author=ReVanced
|
||||
description=Mounts the patched APK on top of the original one
|
@ -1,40 +0,0 @@
|
||||
#!/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"
|
@ -1,38 +0,0 @@
|
||||
|
||||
# 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)
|
@ -1,62 +0,0 @@
|
||||
// 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);
|
||||
}
|
@ -1,336 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
vm = 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")!!
|
@ -1,110 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
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()
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
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()
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
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()
|
||||
)
|
@ -1,26 +0,0 @@
|
||||
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>)
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
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
|
@ -1,20 +0,0 @@
|
||||
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
|
||||
)
|
@ -1,46 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
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
|
||||
)
|
@ -1,116 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
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
|
||||
)
|
@ -1,11 +0,0 @@
|
||||
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
|
||||
)
|
@ -1,22 +0,0 @@
|
||||
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>)
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
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
|
||||
)
|
@ -1,20 +0,0 @@
|
||||
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
|
||||
)
|
@ -1,58 +0,0 @@
|
||||
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) })
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
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())
|
||||
}
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
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"))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,114 +0,0 @@
|
||||
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(
|
||||
""
|
||||
)
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
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()
|
||||
}
|
@ -1,181 +0,0 @@
|
||||
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")
|