mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2025-07-03 23:46:45 +08:00
Compare commits
979 Commits
feat/compo
...
ci/update-
Author | SHA1 | Date | |
---|---|---|---|
0de45e2d79 | |||
5fc54eb53b | |||
88b0b8c078 | |||
7959c36e71 | |||
e9542c6cf0 | |||
0992e63c28 | |||
aebad0b0e2 | |||
bdb0317a9e | |||
c0d7cf7c2c | |||
2c1ff4d2cd | |||
7c5552f93f | |||
1c5373ff61 | |||
e629d2df0c | |||
7ca003a30d | |||
70a695017e | |||
9b2c99da05 | |||
07158ae1d1 | |||
2b380b0d7c | |||
5153e5e0cb | |||
a1f5dd3c26 | |||
2b0784865a | |||
d7c0913277 | |||
18199bb968 | |||
9d329e0f54 | |||
e9fcb4a383 | |||
68f74f1651 | |||
6264800a05 | |||
658699dd81 | |||
222089a7ec | |||
eff5c4860b | |||
d4e60acbaa | |||
1ab74acf1d | |||
28aad879ba | |||
9f44541bbd | |||
02d2153195 | |||
39e821738f | |||
7863fbb604 | |||
94ab6996ae | |||
1319a03651 | |||
f93085f782 | |||
40a4317993 | |||
8095a1f963 | |||
90c7600586 | |||
105492bfa5 | |||
ce63b799e6 | |||
2fe2d46c72 | |||
bc3888da79 | |||
52b982d81f | |||
ab48672621 | |||
0027c90ed3 | |||
b6ad686a26 | |||
f59d57499d | |||
5f65c12ec1 | |||
65e44dc5a8 | |||
e6ed4a88c9 | |||
390e3533c9 | |||
c1ff2f9924 | |||
d084925c0f | |||
3cf540f190 | |||
5514c75061 | |||
ca147cc6dc | |||
3c3e995f31 | |||
67809700c7 | |||
32c7eddb48 | |||
756e3a815f | |||
f8f915563e | |||
2733ce4915 | |||
04a78fabff | |||
94e26ba053 | |||
1704947c52 | |||
e027f8cc9c | |||
37e612febc | |||
374531237f | |||
abe5a20c4a | |||
0a29ff48ca | |||
95cffcc0a0 | |||
45ff64f26e | |||
7973b367ec | |||
641f6af6da | |||
6d142e72a6 | |||
e812f69740 | |||
df79e3d13a | |||
207b005d56 | |||
4a1695a766 | |||
737e709287 | |||
4727e8243c | |||
5e0ba77f4a | |||
a76a58d6ee | |||
4ebc33cd2a | |||
89a1a3026e | |||
16f16e859b | |||
83eb1a9fd7 | |||
9a336aa3ef | |||
d0b8cba2bf | |||
7436d99532 | |||
c982babaeb | |||
211f7d2fa2 | |||
d432ffbbe0 | |||
500cd63507 | |||
9404c3c297 | |||
f2f89aa185 | |||
d6e931a876 | |||
260964c633 | |||
87addbff55 | |||
5ff5298e0e | |||
0bb08c7afc | |||
a0e67a42e0 | |||
585d54a8a8 | |||
abdae89434 | |||
c7c4da54fb | |||
2dacfce61d | |||
f8da11e684 | |||
f384c66dd6 | |||
1a9031193c | |||
dc743021c3 | |||
a81913c2f7 | |||
494197b5dd | |||
285c55228d | |||
aedc6e9970 | |||
3ed2c87f45 | |||
d5b22258a6 | |||
e6361118a7 | |||
edf2f28eca | |||
b5abe1bbc3 | |||
8654da0dfe | |||
c48698334c | |||
f53299b2a6 | |||
8c1b8e1ee1 | |||
500e0ad9b7 | |||
0d6ee98609 | |||
231cf52f30 | |||
994cb6c4b0 | |||
7365fc241a | |||
e2f02ebf22 | |||
5150adeaff | |||
92612f9aec | |||
44b5f7b3bc | |||
46bd2f48a8 | |||
3ac2062992 | |||
95be465b39 | |||
226d9c9c23 | |||
dabf16a436 | |||
ff0bf43c7d | |||
016de8bb0d | |||
b6c02b7be1 | |||
9e7b26b1c8 | |||
05eb0d7457 | |||
4b0706f8b0 | |||
9c6f0c324b | |||
c98ca70e08 | |||
424fe25dfb | |||
666deda0b5 | |||
5e4510eed5 | |||
a9147ed0c0 | |||
bd7c4aa554 | |||
9d0f3a3605 | |||
093a4ebf49 | |||
17cc9f9e9e | |||
bd85b254e4 | |||
1460fd7be2 | |||
bc3fe3f0f2 | |||
bdc0fc89c3 | |||
aeefe644c2 | |||
48604804f9 | |||
03ccea46e2 | |||
b4bc14e4ed | |||
6dcbe271a7 | |||
3317fd5649 | |||
c9eb3ffa14 | |||
fe1e65ce9c | |||
f7426309b4 | |||
7f67a86413 | |||
a22ef4d9b8 | |||
ada99e80f9 | |||
111d8b6543 | |||
15599dbb21 | |||
24cd2dca75 | |||
c48f5b2488 | |||
06d4485032 | |||
73fdf92780 | |||
0fda344952 | |||
9c0665acb2 | |||
6bafa23bb4 | |||
8387ada245 | |||
048ba12703 | |||
1b6a77a463 | |||
ff9d021a2b | |||
6ac4819478 | |||
4b5e2e97f7 | |||
7a4b0bd7c8 | |||
205650865a | |||
39ff42db01 | |||
62f5acee1a | |||
67ecc13a28 | |||
ad10a19acd | |||
a35c62a99d | |||
8450243ddc | |||
d239efcf14 | |||
691b615b02 | |||
3f34407741 | |||
da4153039c | |||
1de59f420b | |||
464aa753f4 | |||
0cf49998e0 | |||
991a8cb5d1 | |||
ac75d1da27 | |||
5907659cc8 | |||
d8d2478d0f | |||
4ad3c3fb72 | |||
0e00b9f526 | |||
88f3701a6c | |||
a15924617e | |||
db04672d72 | |||
977345e5aa | |||
76e5731eb8 | |||
99bfd84e03 | |||
edb387e1a8 | |||
80ff6711f4 | |||
cadbb3f46d | |||
e1742fd4c0 | |||
c2c4895a29 | |||
ffe5c058e0 | |||
59ddd9f393 | |||
57ba3ad374 | |||
6fed17705b | |||
f915b544c4 | |||
11a383a13a | |||
4b178d947c | |||
f2e7661b5c | |||
e33862f436 | |||
51dc429330 | |||
5a41cc1162 | |||
28ab79d962 | |||
8f2c18585f | |||
2f533d12b9 | |||
44cec48a7f | |||
395da595a2 | |||
18ea6adb20 | |||
daeb534692 | |||
0b2ddbe0bf | |||
3c3ff64b18 | |||
43befa8713 | |||
3c820405a8 | |||
d65e830467 | |||
154b23202c | |||
91e0d48721 | |||
ae5eef0f2c | |||
382c068a03 | |||
cfaf874326 | |||
97d25b5602 | |||
cde470f867 | |||
1ad3e3423c | |||
d62f0a96fb | |||
d4ee3334e0 | |||
384fb19ddf | |||
ab04ef99c3 | |||
c812ce2011 | |||
fa8f154d65 | |||
af03eec4b5 | |||
da66b43497 | |||
e302ea9f9e | |||
6aa3b6c4b0 | |||
5cb887ebe6 | |||
4cd00c122d | |||
5744bdda80 | |||
89e373f98c | |||
ecd4b01108 | |||
08686252bb | |||
ab1dd8862d | |||
c4abf8a324 | |||
05adb78932 | |||
3ae1d3374a | |||
067f8adf4b | |||
83f6d287b3 | |||
3dde82fc18 | |||
de0af4c489 | |||
f98386dcc8 | |||
49209ca562 | |||
d68e7f71e9 | |||
fffdb314a1 | |||
ee41e315fb | |||
cd3d654318 | |||
3bd1ef3de7 | |||
ba1a152231 | |||
55573eb94f | |||
80e78f544b | |||
7572944c9e | |||
f5e9826dfb | |||
c8ac94d82d | |||
d9ff833100 | |||
7150fb4435 | |||
34c331f39b | |||
1ff76cf584 | |||
1de0e87983 | |||
93b2dd6176 | |||
f3e2435fef | |||
b42d8842d5 | |||
c052a0c0f5 | |||
34cf91d4b6 | |||
f99504d3e4 | |||
3ec1df9650 | |||
871a34df23 | |||
b65ec4560f | |||
0eaeb5d5ea | |||
060f39fb9b | |||
722dfadb3c | |||
6567be40cb | |||
9539d23c12 | |||
d0d0a17a55 | |||
d2e965f056 | |||
cda0e127d9 | |||
fea11dfef6 | |||
dcc4477e3e | |||
6eb21e1fab | |||
b8902d04d7 | |||
99efdb130f | |||
5177cd3083 | |||
ff4b9ab960 | |||
ad998ac22d | |||
881d2430c3 | |||
b07ae90c86 | |||
8e6519cfb0 | |||
bb90cc6e81 | |||
fd02e0799c | |||
f07204460c | |||
66be0f96e0 | |||
a1ca19b289 | |||
af779153d5 | |||
78966e13c4 | |||
8bdcf76832 | |||
05ecbde6c2 | |||
e558a47204 | |||
61de7568cb | |||
2e7f8457d3 | |||
332bad699d | |||
0b5ab33b3e | |||
5b4242d28b | |||
0c76ed3af0 | |||
39d698e545 | |||
18e91e7cbc | |||
14dfe07795 | |||
8e011a5d6b | |||
fc5f97e54b | |||
78728c1f2a | |||
90c95c0669 | |||
fbd1e221da | |||
c35c776ce2 | |||
f275f57c11 | |||
520b86df0a | |||
8991827ac7 | |||
0871180dcc | |||
7103bd2ec1 | |||
e5029c7d2c | |||
a512af50b5 | |||
cc59d60dfd | |||
4d894e908e | |||
77b499ef29 | |||
0142b85ede | |||
d9633906f5 | |||
3dd14fd34b | |||
0b19a9865d | |||
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 |
61
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
61
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
@ -1,61 +0,0 @@
|
||||
name: 🐞 Bug report
|
||||
description: Create a new bug report.
|
||||
title: 'bug: <title>'
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# ReVanced Manager bug report
|
||||
|
||||
Please check for existing issues [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Bug description
|
||||
description: |
|
||||
- Describe your bug in detail
|
||||
- Add steps to reproduce the bug if possible (Step 1. Download some files. Step 2. ...)
|
||||
- Add images and videos if possible
|
||||
- List selected patches if applicable
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Version of ReVanced Manager and version & name of application you tried to patch
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Installation type
|
||||
options:
|
||||
- Non-root
|
||||
- Root
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Device logs
|
||||
description: Export logs in ReVanced Manager settings.
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Patcher logs
|
||||
description: Export logs in "Patcher" screen.
|
||||
render: shell
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your issue will be closed if you don't follow the checklist below!
|
||||
options:
|
||||
- label: This request is not a duplicate of an existing issue.
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
- label: All requested information has been provided properly.
|
||||
required: true
|
||||
- label: The issue is solely related to the ReVanced Manager
|
||||
required: true
|
109
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
109
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,109 @@
|
||||
name: 🐞 Bug report
|
||||
description: Report a bug or an issue.
|
||||
title: 'bug: '
|
||||
labels: ['Bug report']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="256px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="256px"
|
||||
src="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-logo/revanced-logo.svg" />
|
||||
<img height="24px" src="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-logo/revanced-logo.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="http://revanced.app/discord">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/revancedapp">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://t.me/app_revanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://x.com/revancedapp">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# ReVanced Manager bug report
|
||||
|
||||
Before creating a new bug report, please keep the following in mind:
|
||||
|
||||
- **Do not submit a duplicate bug report**: Search for existing bug reports [here](https://github.com/ReVanced/revanced-manager/issues?q=label%3A%22Bug+report%22).
|
||||
- **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-manager/blob/main/CONTRIBUTING.md).
|
||||
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Bug description
|
||||
description: |
|
||||
- Describe your bug in detail
|
||||
- Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...)
|
||||
- Add images and videos if possible
|
||||
- List used patches, downloader and settings if applicable
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Patch logs
|
||||
description: Patch logs can be exported by clicking on the "Logs" button in the "Patcher" screen, when patching finishes.
|
||||
render: shell
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Debug logs
|
||||
description: Debug logs can be exported by clicking on "Export debug logs" in "Settings" > "Advanced".
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your bug report will be closed if you don't follow the checklist below.
|
||||
options:
|
||||
- label: I have checked all open and closed bug reports and this is not a duplicate.
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
- label: All requested information has been provided properly.
|
||||
required: true
|
||||
- label: The bug is only related to ReVanced Manager.
|
||||
required: true
|
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 🗨 Discussions
|
||||
url: https://github.com/revanced/revanced-suggestions/discussions
|
||||
about: Have something unspecific to ReVanced Manager in mind? Search for or start a new discussion!
|
42
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
42
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
@ -1,42 +0,0 @@
|
||||
name: ⭐ Feature request
|
||||
description: Create a new feature request.
|
||||
title: 'feat: <title>'
|
||||
labels: [feature request]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# ReVanced Manager feature request
|
||||
|
||||
Please check for existing feature requests [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Feature description
|
||||
description: Describe your feature in detail.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Motivation
|
||||
description: Explain why the lack of it is a problem.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: In case there is something else you want to add.
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your issue will be closed if you don't follow the checklist below!
|
||||
options:
|
||||
- label: This request is not a duplicate of an existing issue.
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
- label: All requested information has been provided properly.
|
||||
required: true
|
||||
- label: The issue is solely related to the ReVanced Manager
|
||||
required: true
|
103
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
103
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@ -0,0 +1,103 @@
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="256px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="256px"
|
||||
src="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-logo/revanced-logo.svg" />
|
||||
<img height="24px" src="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-logo/revanced-logo.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="http://revanced.app/discord">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/revancedapp">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://t.me/app_revanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://x.com/revancedapp">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# ReVanced Manager feature request
|
||||
|
||||
Before creating a new feature request, please keep the following in mind:
|
||||
|
||||
- **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/ReVanced/revanced-manager/issues?q=label%3A%22Feature+request%22).
|
||||
- **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-manager/blob/main/CONTRIBUTING.md).
|
||||
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Feature description
|
||||
description: |
|
||||
- Describe your feature in detail
|
||||
- Add images, videos, links, examples, references, etc. if possible
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Motivation
|
||||
description: |
|
||||
A strong motivation is necessary for a feature request to be considered.
|
||||
|
||||
- Why should this feature be implemented?
|
||||
- What is the explicit use case?
|
||||
- What are the benefits?
|
||||
- What makes this feature important?
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your feature request will be closed if you don't follow the checklist below.
|
||||
options:
|
||||
- label: I have checked all open and closed feature requests and this is not a duplicate
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
- label: All requested information has been provided properly.
|
||||
required: true
|
||||
- label: The feature request is only related to ReVanced Manager.
|
||||
required: true
|
2
.github/config.yaml
vendored
2
.github/config.yaml
vendored
@ -1,2 +1,2 @@
|
||||
firstPRMergeComment: >
|
||||
❤️ Thank you for contributing to ReVanced Manager. Join us on [Discord](https://revanced.app/discord) if you want to receive a contributor role.
|
||||
Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) to receive a role for your contribution.
|
||||
|
31
.github/workflows/build_pull_request.yml
vendored
Normal file
31
.github/workflows/build_pull_request.yml
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
name: Build pull request
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: burrunan/gradle-cache-action@v1
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew assembleRelease --no-daemon
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: revanced-manager
|
||||
path: |
|
||||
app/build/outputs/apk/release/revanced-manager*.apk
|
||||
app/build/outputs/apk/release/revanced-manager*.apk.asc
|
26
.github/workflows/open_pull_request.yml
vendored
Normal file
26
.github/workflows/open_pull_request.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
name: Open a PR to main
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
MESSAGE: Merge branch `${{ github.head_ref || github.ref_name }}` to `main`
|
||||
|
||||
jobs:
|
||||
pull-request:
|
||||
name: Open pull request
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Open pull request
|
||||
uses: repo-sync/pull-request@v2
|
||||
with:
|
||||
destination_branch: 'main'
|
||||
pr_title: 'chore: ${{ env.MESSAGE }}'
|
||||
pr_body: 'This pull request will ${{ env.MESSAGE }}.'
|
||||
pr_draft: true
|
40
.github/workflows/pr-build.yml
vendored
40
.github/workflows/pr-build.yml
vendored
@ -1,40 +0,0 @@
|
||||
name: Build pull request
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/pr-build.yml"
|
||||
- "app/**"
|
||||
- "gradle/**"
|
||||
- "*.properties"
|
||||
- ".kts"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get commit hash
|
||||
id: get_commit_hash
|
||||
run: echo "hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew assembleRelease --no-daemon -PnoProguard -Psuffix=${{ steps.get_commit_hash.outputs.hash }}
|
||||
|
||||
- name: Upload build
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: revanced-manager-${{ steps.get_commit_hash.outputs.hash }}
|
||||
path: app/build/outputs/apk/release/*.apk
|
52
.github/workflows/release-build.yml
vendored
52
.github/workflows/release-build.yml
vendored
@ -1,52 +0,0 @@
|
||||
name: Release Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Set up Node.js 20
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Cache Node modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
key: npm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Setup semantic-release
|
||||
run: npm install
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
with:
|
||||
cache-disabled: true
|
||||
|
||||
- name: Build with Gradle
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
signingKey: "keystore.jks"
|
||||
keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
|
||||
keyAlias: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
keyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||
run: |
|
||||
echo "${{ secrets.SIGNING_KEYSTORE }}" | base64 --decode > app/keystore.jks
|
||||
npx semantic-release
|
70
.github/workflows/release.yml
vendored
Normal file
70
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,70 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: burrunan/gradle-cache-action@v1
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew assembleRelease
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@v6
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
fingerprint: ${{ vars.GPG_FINGERPRINT }}
|
||||
|
||||
- name: Setup keystore
|
||||
run: |
|
||||
echo "${{ secrets.KEYSTORE }}" | base64 --decode > "app/keystore.jks"
|
||||
|
||||
- name: Semantic Release
|
||||
uses: cycjimmy/semantic-release-action@v4
|
||||
id: semantic
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||
KEYSTORE_ENTRY_ALIAS: ${{ secrets.KEYSTORE_ENTRY_ALIAS }}
|
||||
KEYSTORE_ENTRY_PASSWORD: ${{ secrets.KEYSTORE_ENTRY_PASSWORD }}
|
||||
|
||||
- name: Attest
|
||||
if: steps.semantic.outputs.new_release_published == 'true'
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-name: 'ReVanced Manager ${{ steps.release.outputs.new_release_git_tag }}'
|
||||
subject-path: app/build/outputs/apk/release/revanced-manager*.apk
|
@ -11,9 +11,9 @@ jobs:
|
||||
name: Dispatch event to documentation repository
|
||||
if: github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- uses: peter-evans/repository-dispatch@v2
|
||||
- uses: peter-evans/repository-dispatch@v3
|
||||
with:
|
||||
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 }}"}'
|
137
.gitignore
vendored
137
.gitignore
vendored
@ -1,11 +1,132 @@
|
||||
### Java template
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
.idea/artifacts
|
||||
.idea/compiler.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/modules.xml
|
||||
.idea/*.iml
|
||||
.idea/modules
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### Gradle template
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
**/build/
|
||||
!src/**/build/
|
||||
|
||||
# Ignore Gradle GUI config
|
||||
gradle-app.setting
|
||||
|
||||
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
|
||||
!gradle-wrapper.jar
|
||||
|
||||
# Cache of project
|
||||
.gradletasknamecache
|
||||
|
||||
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
|
||||
# gradle/wrapper/gradle-wrapper.properties
|
||||
|
||||
# Potentially copyrighted test APK
|
||||
*.apk
|
||||
|
||||
# Ignore vscode config
|
||||
.vscode/
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
|
||||
# Ignore IDEA files
|
||||
.idea/
|
||||
|
||||
.kotlin/
|
||||
|
||||
local.properties
|
||||
/node_modules
|
||||
|
||||
.cxx
|
18
.releaserc
18
.releaserc
@ -14,15 +14,17 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"@semantic-release/changelog",
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/changelog",
|
||||
"gradle-semantic-release-plugin",
|
||||
[
|
||||
"@semantic-release/git",
|
||||
{
|
||||
"assets": [
|
||||
"gradle.properties"
|
||||
]
|
||||
"CHANGELOG.md",
|
||||
"gradle.properties",
|
||||
],
|
||||
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
||||
}
|
||||
],
|
||||
[
|
||||
@ -30,17 +32,17 @@
|
||||
{
|
||||
"assets": [
|
||||
{
|
||||
"path": "app/build/outputs/apk/release/revanced-manager*.apk"
|
||||
}
|
||||
"path": "app/build/outputs/apk/release/revanced-manager*.apk?(.asc)"
|
||||
},
|
||||
],
|
||||
"successComment": false
|
||||
successComment: false
|
||||
}
|
||||
],
|
||||
[
|
||||
"@saithodev/semantic-release-backmerge",
|
||||
{
|
||||
"backmergeBranches": [{"from": "main", "to": "dev"}],
|
||||
"clearWorkspace": true
|
||||
backmergeBranches: [{"from": "main", "to": "dev"}],
|
||||
clearWorkspace: true
|
||||
}
|
||||
]
|
||||
]
|
||||
|
@ -13,8 +13,8 @@
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo-round.svg" />
|
||||
<img height="24px" src="assets/revanced-logo/revanced-logo-round.svg" />
|
||||
<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">
|
||||
@ -58,21 +58,46 @@
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# 🔒 Security Policy
|
||||
# 👋 Contribution guidelines
|
||||
|
||||
This document describes how to report security vulnerabilities for ReVanced Manager.
|
||||
This document describes how to contribute to ReVanced Manager.
|
||||
|
||||
## 🚨 Reporting a Vulnerability
|
||||
## 📖 Resources to help you get started
|
||||
|
||||
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).
|
||||
* The [documentation](/docs/README.md) provides steps to build ReVanced Manager from source
|
||||
* Our [backlog](https://github.com/orgs/ReVanced/projects/12) is where we keep track of what we're working on
|
||||
* [Issues](https://github.com/ReVanced/revanced-manager/issues) are where we keep track of bugs and feature requests
|
||||
|
||||
If a vulnerability is confirmed and accepted, you can join our [Discord](https://discord.gg/revanced) server to receive a special contributor role.
|
||||
## 🙏 Submitting a feature request
|
||||
|
||||
### ⏳ Supported Versions
|
||||
Features can be requested by opening an issue using the
|
||||
[Feature request issue template](https://github.com/ReVanced/revanced-manager/issues/new?assignees=&labels=Feature+request&projects=&template=feature_request.yml&title=feat%3A+).
|
||||
|
||||
| Version | Branch | Supported |
|
||||
| ------- | ------------|------------------- |
|
||||
| v1.18.0 | main | :white_check_mark: |
|
||||
| latest | dev | :white_check_mark: |
|
||||
| latest | compose-dev | :white_check_mark: |
|
||||
> **Note**
|
||||
> Requests can be accepted or rejected at the discretion of maintainers of ReVanced Manager.
|
||||
> Good motivation has to be provided for a request to be accepted.
|
||||
|
||||
## 🐞 Submitting a bug report
|
||||
|
||||
If you encounter a bug while using ReVanced Manager, open an issue using the
|
||||
[Bug report issue template](https://github.com/ReVanced/revanced-manager/issues/new?assignees=&labels=Bug+report&projects=&template=bug_report.yml&title=bug%3A+).
|
||||
|
||||
## 📝 How to contribute
|
||||
|
||||
1. Before contributing, it is recommended to open an issue to discuss your change
|
||||
with the maintainers of ReVanced Manager. This will help you determine whether your change is acceptable
|
||||
and whether it is worth your time to implement it
|
||||
2. Development happens on the `dev` branch. Fork the repository and create your branch from `dev`
|
||||
3. Commit your changes
|
||||
4. Submit a pull request to the `dev` branch of the repository and reference issues
|
||||
that your pull request closes in the description of your pull request
|
||||
5. Our team will review your pull request and provide feedback. Once your pull request is approved,
|
||||
it will be merged into the `dev` branch and will be included in the next release of ReVanced Manager
|
||||
|
||||
## 🤚 I want to contribute but don't know how to code
|
||||
|
||||
Even if you don't know how to code, you can still contribute by
|
||||
translating ReVanced Manager on [Crowdin](https://translate.revanced.app/).
|
||||
|
||||
❤️ Thank you for considering contributing to ReVanced Manager,
|
||||
ReVanced
|
119
README.md
119
README.md
@ -1,55 +1,104 @@
|
||||
# ReVanced Manager (Compose Rewrite)
|
||||
<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>
|
||||
|
||||
[](../../blob/main/LICENSE)
|
||||
[](https://github.com/ReVanced/revanced-manager/commits/compose-dev)
|
||||
[](https://github.com/ReVanced/revanced-manager/commits/compose-dev)
|
||||
# 💊 ReVanced Manager
|
||||
|
||||
_(Yet another)_ rewrite of the ReVanced Manager using Kotlin and Jetpack Compose.
|
||||

|
||||

|
||||
|
||||
## Design system
|
||||
Application to use ReVanced on Android
|
||||
|
||||
In this rewrite, we are adopting the latest Material Design principles and guidelines by using Material 3 and Material You.
|
||||
## ❓ About
|
||||
|
||||
Material Design is a design system developed by Google that provides a unified visual language for building beautiful and consistent user interfaces across all platforms and devices. Material You is an extension of Material Design that provides even more customization options for users, making it possible for them to personalize their device and create a unique look and feel.
|
||||
ReVanced Manager is an application that uses [ReVanced Patcher](https://github.com/revanced/revanced-patcher) to patch Android apps.
|
||||
|
||||
### Why Material 3?
|
||||
## 💪 Features
|
||||
|
||||
* **Consistent design language**
|
||||
* **Improved accessibility**
|
||||
* **Better user experience**
|
||||
Some of the features ReVanced Manager provides are:
|
||||
|
||||
By using Material 3 and Material You, we are ensuring that the app's user interface is consistent, customizable, accessible, and engaging for our users. This will help to improve the overall user experience and increase user satisfaction with the the manager.
|
||||
|
||||
## Technology stack
|
||||
|
||||
* Kotlin: Kotlin is a modern and concise programming language that is fully interoperable with Java and provides improved safety, readability, and maintainability compared to Java.
|
||||
* Jetpack Compose: Jetpack Compose is a modern UI toolkit for Android development that allows developers to build beautiful and performant user interfaces using declarative programming. It provides a unified and efficient way of building UI that is well-integrated with the Android framework.
|
||||
|
||||
## Why Kotlin and Compose?
|
||||
|
||||
* **Improved safety:** Kotlin provides improved safety compared to Java, which reduces the likelihood of common programming mistakes that can cause security vulnerabilities or crashes.
|
||||
* **Concise and readable code:** Kotlin's concise syntax and expressive type system make the code more readable, which makes it easier for developers to understand and maintain the codebase.
|
||||
* **Better performance:** Jetpack Compose uses the power of the Android framework to provide smooth and fast performance, which enhances the user experience.
|
||||
* **Modern and efficient UI development:** Jetpack Compose provides a modern and efficient way of building UI, which makes it easier for developers to create beautiful and performant user interfaces.
|
||||
- ⬇️ **Download**: Automatically download apps using the ReVanced Manager downloader plugin system
|
||||
- 💉 **Patch**: Select and apply patches to any Android app
|
||||
- 🛠️ **Customize**: Manage patches, apps, signing, themes, updates, and many more settings
|
||||
|
||||
## 🔽 Download
|
||||
|
||||
You can obtain ReVanced Manager by downloading it from either [revanced.app/download](https://revanced.app/download) or [GitHub Releases](https://github.com/ReVanced/revanced-manager/releases)
|
||||
You can download the most recent version of ReVanced Manager at [revanced.app/download](https://revanced.app/download) or from [GitHub releases](https://github.com/ReVanced/revanced-manager/releases/latest).
|
||||
Learn how to use ReVanced Manager by following the [documentation](/docs).
|
||||
|
||||
## 📝 Prerequisites
|
||||
## 📚 Everything else
|
||||
|
||||
For a list of prerequisites, refer to [docs/0_prerequisites.md](docs/0_prerequisites.md)
|
||||
### 📙 Contributing
|
||||
|
||||
## 🔴 Issues
|
||||
Thank you for considering contributing to ReVanced Manager.
|
||||
You can find the contribution guidelines [here](CONTRIBUTING.md).
|
||||
|
||||
For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose).
|
||||
### 🛠️ Building
|
||||
|
||||
## 🌐 Translation
|
||||
To build a ReVanced Manager, you can follow the [documentation](/docs).
|
||||
|
||||
[](https://crowdin.com/project/revanced)
|
||||
### 📄 Documentation
|
||||
|
||||
We're accepting translations on [Crowdin](https://translate.revanced.app)
|
||||
You can find the documentation for ReVanced Manager [here](/docs).
|
||||
|
||||
## 🛠️ Building Manager from source
|
||||
## ⚖️ License
|
||||
|
||||
For instructions on how to build ReVanced Manager from source, refer to [docs/4_building.md](docs/4_building.md)
|
||||
ReVanced Manager is licensed under the GPLv3 license. Please see the [license file](LICENSE) for more information.
|
||||
[tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3) you may copy, distribute and modify ReVanced Manager as long as you track changes/dates in source files.
|
||||
Any modifications to ReVanced Manager must also be made available under the GPL, along with build & install instructions.
|
||||
|
182
api/api/api.api
Normal file
182
api/api/api.api
Normal file
@ -0,0 +1,182 @@
|
||||
public abstract interface class app/revanced/manager/plugin/downloader/BaseDownloadScope : app/revanced/manager/plugin/downloader/Scope {
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/ConstantsKt {
|
||||
public static final field PLUGIN_HOST_PERMISSION Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/DownloadUrl : android/os/Parcelable {
|
||||
public static final field $stable I
|
||||
public static final field CREATOR Landroid/os/Parcelable$Creator;
|
||||
public fun <init> (Ljava/lang/String;Ljava/util/Map;)V
|
||||
public synthetic fun <init> (Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public final fun component1 ()Ljava/lang/String;
|
||||
public final fun component2 ()Ljava/util/Map;
|
||||
public final fun copy (Ljava/lang/String;Ljava/util/Map;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
|
||||
public static synthetic fun copy$default (Lapp/revanced/manager/plugin/downloader/DownloadUrl;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
|
||||
public final fun describeContents ()I
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public final fun getHeaders ()Ljava/util/Map;
|
||||
public final fun getUrl ()Ljava/lang/String;
|
||||
public fun hashCode ()I
|
||||
public final fun toDownloadResult ()Lkotlin/Pair;
|
||||
public fun toString ()Ljava/lang/String;
|
||||
public final fun writeToParcel (Landroid/os/Parcel;I)V
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/DownloadUrl$Creator : android/os/Parcelable$Creator {
|
||||
public fun <init> ()V
|
||||
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
|
||||
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
|
||||
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/DownloadUrl;
|
||||
public synthetic fun newArray (I)[Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/Downloader {
|
||||
public static final field $stable I
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/DownloaderBuilder {
|
||||
public static final field $stable I
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/DownloaderKt {
|
||||
public static final fun Downloader (Lkotlin/jvm/functions/Function1;)Lapp/revanced/manager/plugin/downloader/DownloaderBuilder;
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/DownloaderScope : app/revanced/manager/plugin/downloader/Scope {
|
||||
public static final field $stable I
|
||||
public final fun download (Lkotlin/jvm/functions/Function3;)V
|
||||
public final fun get (Lkotlin/jvm/functions/Function4;)V
|
||||
public fun getHostPackageName ()Ljava/lang/String;
|
||||
public fun getPluginPackageName ()Ljava/lang/String;
|
||||
public final fun useService (Landroid/content/Intent;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/ExtensionsKt {
|
||||
public static final fun download (Lapp/revanced/manager/plugin/downloader/DownloaderScope;Lkotlin/jvm/functions/Function4;)V
|
||||
}
|
||||
|
||||
public abstract interface class app/revanced/manager/plugin/downloader/GetScope : app/revanced/manager/plugin/downloader/Scope {
|
||||
public abstract fun requestStartActivity (Landroid/content/Intent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public abstract interface class app/revanced/manager/plugin/downloader/InputDownloadScope : app/revanced/manager/plugin/downloader/BaseDownloadScope {
|
||||
}
|
||||
|
||||
public abstract interface class app/revanced/manager/plugin/downloader/OutputDownloadScope : app/revanced/manager/plugin/downloader/BaseDownloadScope {
|
||||
public abstract fun reportSize (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/Package : android/os/Parcelable {
|
||||
public static final field $stable I
|
||||
public static final field CREATOR Landroid/os/Parcelable$Creator;
|
||||
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
|
||||
public final fun component1 ()Ljava/lang/String;
|
||||
public final fun component2 ()Ljava/lang/String;
|
||||
public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/manager/plugin/downloader/Package;
|
||||
public static synthetic fun copy$default (Lapp/revanced/manager/plugin/downloader/Package;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lapp/revanced/manager/plugin/downloader/Package;
|
||||
public final fun describeContents ()I
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public final fun getName ()Ljava/lang/String;
|
||||
public final fun getVersion ()Ljava/lang/String;
|
||||
public fun hashCode ()I
|
||||
public fun toString ()Ljava/lang/String;
|
||||
public final fun writeToParcel (Landroid/os/Parcel;I)V
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/Package$Creator : android/os/Parcelable$Creator {
|
||||
public fun <init> ()V
|
||||
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/Package;
|
||||
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
|
||||
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/Package;
|
||||
public synthetic fun newArray (I)[Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public abstract interface annotation class app/revanced/manager/plugin/downloader/PluginHostApi : java/lang/annotation/Annotation {
|
||||
}
|
||||
|
||||
public abstract interface class app/revanced/manager/plugin/downloader/Scope {
|
||||
public abstract fun getHostPackageName ()Ljava/lang/String;
|
||||
public abstract fun getPluginPackageName ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
public abstract class app/revanced/manager/plugin/downloader/UserInteractionException : java/lang/Exception {
|
||||
public static final field $stable I
|
||||
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
}
|
||||
|
||||
public abstract class app/revanced/manager/plugin/downloader/UserInteractionException$Activity : app/revanced/manager/plugin/downloader/UserInteractionException {
|
||||
public static final field $stable I
|
||||
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$Cancelled : app/revanced/manager/plugin/downloader/UserInteractionException$Activity {
|
||||
public static final field $stable I
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$NotCompleted : app/revanced/manager/plugin/downloader/UserInteractionException$Activity {
|
||||
public static final field $stable I
|
||||
public final fun getIntent ()Landroid/content/Intent;
|
||||
public final fun getResultCode ()I
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/UserInteractionException$RequestDenied : app/revanced/manager/plugin/downloader/UserInteractionException {
|
||||
public static final field $stable I
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/webview/APIKt {
|
||||
public static final fun WebViewDownloader (Lkotlin/jvm/functions/Function4;)Lapp/revanced/manager/plugin/downloader/DownloaderBuilder;
|
||||
public static final fun runWebView (Lapp/revanced/manager/plugin/downloader/GetScope;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public class app/revanced/manager/plugin/downloader/webview/IWebView$Default : app/revanced/manager/plugin/downloader/webview/IWebView {
|
||||
public fun <init> ()V
|
||||
public fun asBinder ()Landroid/os/IBinder;
|
||||
public fun finish ()V
|
||||
public fun load (Ljava/lang/String;)V
|
||||
}
|
||||
|
||||
public abstract class app/revanced/manager/plugin/downloader/webview/IWebView$Stub : android/os/Binder, app/revanced/manager/plugin/downloader/webview/IWebView {
|
||||
public fun <init> ()V
|
||||
public fun asBinder ()Landroid/os/IBinder;
|
||||
public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/plugin/downloader/webview/IWebView;
|
||||
public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
|
||||
}
|
||||
|
||||
public class app/revanced/manager/plugin/downloader/webview/IWebViewEvents$Default : app/revanced/manager/plugin/downloader/webview/IWebViewEvents {
|
||||
public fun <init> ()V
|
||||
public fun asBinder ()Landroid/os/IBinder;
|
||||
public fun download (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
|
||||
public fun pageLoad (Ljava/lang/String;)V
|
||||
public fun ready (Lapp/revanced/manager/plugin/downloader/webview/IWebView;)V
|
||||
}
|
||||
|
||||
public abstract class app/revanced/manager/plugin/downloader/webview/IWebViewEvents$Stub : android/os/Binder, app/revanced/manager/plugin/downloader/webview/IWebViewEvents {
|
||||
public fun <init> ()V
|
||||
public fun asBinder ()Landroid/os/IBinder;
|
||||
public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/plugin/downloader/webview/IWebViewEvents;
|
||||
public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters$Creator : android/os/Parcelable$Creator {
|
||||
public fun <init> ()V
|
||||
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters;
|
||||
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
|
||||
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters;
|
||||
public synthetic fun newArray (I)[Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public abstract interface class app/revanced/manager/plugin/downloader/webview/WebViewCallbackScope : app/revanced/manager/plugin/downloader/Scope {
|
||||
public abstract fun finish (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
public abstract fun load (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/webview/WebViewScope : app/revanced/manager/plugin/downloader/Scope {
|
||||
public static final field $stable I
|
||||
public final fun download (Lkotlin/jvm/functions/Function5;)V
|
||||
public fun getHostPackageName ()Ljava/lang/String;
|
||||
public fun getPluginPackageName ()Ljava/lang/String;
|
||||
public final fun pageLoad (Lkotlin/jvm/functions/Function3;)V
|
||||
}
|
||||
|
150
api/build.gradle.kts
Normal file
150
api/build.gradle.kts
Normal file
@ -0,0 +1,150 @@
|
||||
import java.io.IOException
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.parcelize)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
alias(libs.plugins.binary.compatibility.validator)
|
||||
`maven-publish`
|
||||
signing
|
||||
}
|
||||
|
||||
group = "app.revanced"
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.ktx)
|
||||
implementation(libs.runtime.ktx)
|
||||
implementation(libs.activity.compose)
|
||||
implementation(libs.appcompat)
|
||||
}
|
||||
|
||||
fun String.runCommand(): String {
|
||||
val process = ProcessBuilder(split("\\s".toRegex()))
|
||||
.redirectErrorStream(true)
|
||||
.directory(rootDir)
|
||||
.start()
|
||||
|
||||
val output = StringBuilder()
|
||||
val reader = process.inputStream.bufferedReader()
|
||||
|
||||
val thread = Thread {
|
||||
reader.forEachLine {
|
||||
output.appendLine(it)
|
||||
}
|
||||
}
|
||||
thread.start()
|
||||
|
||||
if (!process.waitFor(10, TimeUnit.SECONDS)) {
|
||||
process.destroy()
|
||||
throw IOException("Command timed out: $this")
|
||||
}
|
||||
|
||||
thread.join()
|
||||
return output.toString().trim()
|
||||
}
|
||||
|
||||
val projectPath: String = projectDir.relativeTo(rootDir).path
|
||||
val lastTag = "git describe --tags --abbrev=0".runCommand()
|
||||
val hasChangesInThisModule = "git diff --name-only $lastTag..HEAD".runCommand().lineSequence().any {
|
||||
it.startsWith(projectPath)
|
||||
}
|
||||
|
||||
tasks.matching { it.name.startsWith("publish") }.configureEach {
|
||||
onlyIf {
|
||||
hasChangesInThisModule
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.manager.plugin.downloader"
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
aidl = true
|
||||
}
|
||||
}
|
||||
|
||||
apiValidation {
|
||||
nonPublicMarkers += "app.revanced.manager.plugin.downloader.PluginHostApi"
|
||||
}
|
||||
|
||||
publishing {
|
||||
repositories {
|
||||
maven {
|
||||
name = "GitHubPackages"
|
||||
url = uri("https://maven.pkg.github.com/revanced/revanced-manager")
|
||||
credentials {
|
||||
username = System.getenv("GITHUB_ACTOR") ?: extra["gpr.user"] as String?
|
||||
password = System.getenv("GITHUB_TOKEN") ?: extra["gpr.key"] as String?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
publications {
|
||||
create<MavenPublication>("Api") {
|
||||
afterEvaluate {
|
||||
from(components["release"])
|
||||
}
|
||||
|
||||
groupId = "app.revanced"
|
||||
artifactId = "revanced-manager-api"
|
||||
version = project.version.toString()
|
||||
|
||||
pom {
|
||||
name = "ReVanced Manager API"
|
||||
description = "API for ReVanced Manager."
|
||||
url = "https://revanced.app"
|
||||
|
||||
licenses {
|
||||
license {
|
||||
name = "GNU General Public License v3.0"
|
||||
url = "https://www.gnu.org/licenses/gpl-3.0.en.html"
|
||||
}
|
||||
}
|
||||
developers {
|
||||
developer {
|
||||
id = "ReVanced"
|
||||
name = "ReVanced"
|
||||
email = "contact@revanced.app"
|
||||
}
|
||||
}
|
||||
scm {
|
||||
connection = "scm:git:git://github.com/revanced/revanced-manager.git"
|
||||
developerConnection = "scm:git:git@github.com:revanced/revanced-manager.git"
|
||||
url = "https://github.com/revanced/revanced-manager"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signing {
|
||||
useGpgCmd()
|
||||
sign(publishing.publications["Api"])
|
||||
}
|
3
api/src/main/AndroidManifest.xml
Normal file
3
api/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
</manifest>
|
@ -0,0 +1,8 @@
|
||||
// IWebView.aidl
|
||||
package app.revanced.manager.plugin.downloader.webview;
|
||||
|
||||
@JavaPassthrough(annotation="@app.revanced.manager.plugin.downloader.PluginHostApi")
|
||||
oneway interface IWebView {
|
||||
void load(String url);
|
||||
void finish();
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
// IWebViewEvents.aidl
|
||||
package app.revanced.manager.plugin.downloader.webview;
|
||||
|
||||
import app.revanced.manager.plugin.downloader.webview.IWebView;
|
||||
|
||||
@JavaPassthrough(annotation="@app.revanced.manager.plugin.downloader.PluginHostApi")
|
||||
oneway interface IWebViewEvents {
|
||||
void ready(IWebView iface);
|
||||
void pageLoad(String url);
|
||||
void download(String url, String mimetype, String userAgent);
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package app.revanced.manager.plugin.downloader
|
||||
|
||||
/**
|
||||
* The permission ID of the special plugin host permission. Only ReVanced Manager will have this permission.
|
||||
* Plugin UI activities and internal services can be protected using this permission.
|
||||
*/
|
||||
const val PLUGIN_HOST_PERMISSION = "app.revanced.manager.permission.PLUGIN_HOST"
|
@ -0,0 +1,165 @@
|
||||
package app.revanced.manager.plugin.downloader
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import android.app.Activity
|
||||
import android.os.Parcelable
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
@RequiresOptIn(
|
||||
level = RequiresOptIn.Level.ERROR,
|
||||
message = "This API is only intended for plugin hosts, don't use it in a plugin.",
|
||||
)
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class PluginHostApi
|
||||
|
||||
/**
|
||||
* The base interface for all DSL scopes.
|
||||
*/
|
||||
interface Scope {
|
||||
/**
|
||||
* The package name of ReVanced Manager.
|
||||
*/
|
||||
val hostPackageName: String
|
||||
|
||||
/**
|
||||
* The package name of the plugin.
|
||||
*/
|
||||
val pluginPackageName: String
|
||||
}
|
||||
|
||||
/**
|
||||
* The scope of [DownloaderScope.get].
|
||||
*/
|
||||
interface GetScope : Scope {
|
||||
/**
|
||||
* Ask the user to perform some required interaction in the activity specified by the provided [Intent].
|
||||
* This function returns normally with the resulting [Intent] when the activity finishes with code [Activity.RESULT_OK].
|
||||
*
|
||||
* @throws UserInteractionException.RequestDenied User decided to skip this plugin.
|
||||
* @throws UserInteractionException.Activity.Cancelled The activity was cancelled.
|
||||
* @throws UserInteractionException.Activity.NotCompleted The activity finished with an unknown result code.
|
||||
*/
|
||||
suspend fun requestStartActivity(intent: Intent): Intent?
|
||||
}
|
||||
|
||||
interface BaseDownloadScope : Scope
|
||||
|
||||
/**
|
||||
* The scope for [DownloaderScope.download].
|
||||
*/
|
||||
interface InputDownloadScope : BaseDownloadScope
|
||||
|
||||
typealias Size = Long
|
||||
typealias DownloadResult = Pair<InputStream, Size?>
|
||||
|
||||
typealias Version = String
|
||||
typealias GetResult<T> = Pair<T, Version?>
|
||||
|
||||
class DownloaderScope<T : Parcelable> internal constructor(
|
||||
private val scopeImpl: Scope,
|
||||
internal val context: Context
|
||||
) : Scope by scopeImpl {
|
||||
// Returning an InputStream is the primary way for plugins to implement the download function, but we also want to offer an OutputStream API since using InputStream might not be convenient in all cases.
|
||||
// It is much easier to implement the main InputStream API on top of OutputStreams compared to doing it the other way around, which is why we are using OutputStream here. This detail is not visible to plugins.
|
||||
internal var download: (suspend OutputDownloadScope.(T, OutputStream) -> Unit)? = null
|
||||
internal var get: (suspend GetScope.(String, String?) -> GetResult<T>?)? = null
|
||||
private val inputDownloadScopeImpl = object : InputDownloadScope, Scope by scopeImpl {}
|
||||
|
||||
/**
|
||||
* Define the download block of the plugin.
|
||||
*/
|
||||
fun download(block: suspend InputDownloadScope.(data: T) -> DownloadResult) {
|
||||
download = { app, outputStream ->
|
||||
val (inputStream, size) = inputDownloadScopeImpl.block(app)
|
||||
|
||||
inputStream.use {
|
||||
if (size != null) reportSize(size)
|
||||
it.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the get block of the plugin.
|
||||
* The block should return null if the app cannot be found. The version in the result must match the version argument unless it is null.
|
||||
*/
|
||||
fun get(block: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?) {
|
||||
get = block
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilize the service specified by the provided [Intent]. The service will be unbound when the scope ends.
|
||||
*/
|
||||
suspend fun <R : Any?> useService(intent: Intent, block: suspend (IBinder) -> R): R {
|
||||
var onBind: ((IBinder) -> Unit)? = null
|
||||
val serviceConn = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) =
|
||||
onBind!!(service!!)
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {}
|
||||
}
|
||||
|
||||
return try {
|
||||
val binder = withTimeout(10000L) {
|
||||
suspendCoroutine { continuation ->
|
||||
onBind = continuation::resume
|
||||
context.bindService(intent, serviceConn, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
}
|
||||
block(binder)
|
||||
} finally {
|
||||
onBind = null
|
||||
context.unbindService(serviceConn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DownloaderBuilder<T : Parcelable> internal constructor(private val block: DownloaderScope<T>.() -> Unit) {
|
||||
@PluginHostApi
|
||||
fun build(scopeImpl: Scope, context: Context) =
|
||||
with(DownloaderScope<T>(scopeImpl, context)) {
|
||||
block()
|
||||
|
||||
Downloader(
|
||||
download = download!!,
|
||||
get = get!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Downloader<T : Parcelable> internal constructor(
|
||||
@property:PluginHostApi val get: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?,
|
||||
@property:PluginHostApi val download: suspend OutputDownloadScope.(data: T, outputStream: OutputStream) -> Unit
|
||||
)
|
||||
|
||||
/**
|
||||
* Define a downloader plugin.
|
||||
*/
|
||||
fun <T : Parcelable> Downloader(block: DownloaderScope<T>.() -> Unit) = DownloaderBuilder(block)
|
||||
|
||||
/**
|
||||
* @see GetScope.requestStartActivity
|
||||
*/
|
||||
sealed class UserInteractionException(message: String) : Exception(message) {
|
||||
class RequestDenied @PluginHostApi constructor() :
|
||||
UserInteractionException("Request denied by user")
|
||||
|
||||
sealed class Activity(message: String) : UserInteractionException(message) {
|
||||
class Cancelled @PluginHostApi constructor() : Activity("Interaction cancelled")
|
||||
|
||||
/**
|
||||
* @param resultCode The result code of the activity.
|
||||
* @param intent The [Intent] of the activity.
|
||||
*/
|
||||
class NotCompleted @PluginHostApi constructor(val resultCode: Int, val intent: Intent?) :
|
||||
Activity("Unexpected activity result code: $resultCode")
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package app.revanced.manager.plugin.downloader
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import android.os.Parcelable
|
||||
import java.io.OutputStream
|
||||
|
||||
/**
|
||||
* The scope of the [OutputStream] version of [DownloaderScope.download].
|
||||
*/
|
||||
interface OutputDownloadScope : BaseDownloadScope {
|
||||
suspend fun reportSize(size: Long)
|
||||
}
|
||||
|
||||
/**
|
||||
* A replacement for [DownloaderScope.download] that uses [OutputStream].
|
||||
* The provided [OutputStream] does not need to be closed manually.
|
||||
*/
|
||||
fun <T : Parcelable> DownloaderScope<T>.download(block: suspend OutputDownloadScope.(T, OutputStream) -> Unit) {
|
||||
download = block
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs [GetScope.requestStartActivity] with an [Intent] created using the type information of [ACTIVITY].
|
||||
* @see [GetScope.requestStartActivity]
|
||||
*/
|
||||
suspend inline fun <reified ACTIVITY : Activity> GetScope.requestStartActivity() =
|
||||
requestStartActivity(
|
||||
Intent().apply { setClassName(pluginPackageName, ACTIVITY::class.qualifiedName!!) }
|
||||
)
|
||||
|
||||
/**
|
||||
* Performs [DownloaderScope.useService] with an [Intent] created using the type information of [SERVICE].
|
||||
* @see [DownloaderScope.useService]
|
||||
*/
|
||||
suspend inline fun <reified SERVICE : Service, R : Any?> DownloaderScope<*>.useService(
|
||||
noinline block: suspend (IBinder) -> R
|
||||
) = useService(
|
||||
Intent().apply { setClassName(pluginPackageName, SERVICE::class.qualifiedName!!) }, block
|
||||
)
|
@ -0,0 +1,39 @@
|
||||
package app.revanced.manager.plugin.downloader
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URI
|
||||
|
||||
/**
|
||||
* A simple parcelable data class for storing a package name and version.
|
||||
* This can be used as the data type for plugins that only need a name and version to implement their [DownloaderScope.download] function.
|
||||
*
|
||||
* @param name The package name.
|
||||
* @param version The version.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Package(val name: String, val version: String) : Parcelable
|
||||
|
||||
/**
|
||||
* A data class for storing a download URL.
|
||||
*
|
||||
* @param url The download URL.
|
||||
* @param headers The headers to use for the request.
|
||||
*/
|
||||
@Parcelize
|
||||
data class DownloadUrl(val url: String, val headers: Map<String, String> = emptyMap()) : Parcelable {
|
||||
/**
|
||||
* Converts this into a [DownloadResult].
|
||||
*/
|
||||
fun toDownloadResult(): DownloadResult = with(URI.create(url).toURL().openConnection() as HttpURLConnection) {
|
||||
useCaches = false
|
||||
allowUserInteraction = false
|
||||
headers.forEach(::setRequestProperty)
|
||||
|
||||
connectTimeout = 10_000
|
||||
connect()
|
||||
|
||||
inputStream to getHeaderField("Content-Length").toLong()
|
||||
}
|
||||
}
|
@ -0,0 +1,176 @@
|
||||
package app.revanced.manager.plugin.downloader.webview
|
||||
|
||||
import android.content.Intent
|
||||
import app.revanced.manager.plugin.downloader.DownloadUrl
|
||||
import app.revanced.manager.plugin.downloader.DownloaderScope
|
||||
import app.revanced.manager.plugin.downloader.GetScope
|
||||
import app.revanced.manager.plugin.downloader.Scope
|
||||
import app.revanced.manager.plugin.downloader.Downloader
|
||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
typealias InitialUrl = String
|
||||
typealias PageLoadCallback<T> = suspend WebViewCallbackScope<T>.(url: String) -> Unit
|
||||
typealias DownloadCallback<T> = suspend WebViewCallbackScope<T>.(url: String, mimeType: String, userAgent: String) -> Unit
|
||||
|
||||
interface WebViewCallbackScope<T> : Scope {
|
||||
/**
|
||||
* Finishes the activity and returns the [result].
|
||||
*/
|
||||
suspend fun finish(result: T)
|
||||
|
||||
/**
|
||||
* Tells the WebView to load the specified [url].
|
||||
*/
|
||||
suspend fun load(url: String)
|
||||
}
|
||||
|
||||
@OptIn(PluginHostApi::class)
|
||||
class WebViewScope<T> internal constructor(
|
||||
coroutineScope: CoroutineScope,
|
||||
private val scopeImpl: Scope,
|
||||
setResult: (T) -> Unit
|
||||
) : Scope by scopeImpl {
|
||||
private var onPageLoadCallback: PageLoadCallback<T> = {}
|
||||
private var onDownloadCallback: DownloadCallback<T> = { _, _, _ -> }
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val dispatcher = Dispatchers.Default.limitedParallelism(1)
|
||||
private lateinit var webView: IWebView
|
||||
internal lateinit var initialUrl: String
|
||||
|
||||
internal val binder = object : IWebViewEvents.Stub() {
|
||||
override fun ready(iface: IWebView?) {
|
||||
coroutineScope.launch(dispatcher) {
|
||||
webView = iface!!.also {
|
||||
it.load(initialUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageLoad(url: String?) {
|
||||
coroutineScope.launch(dispatcher) { onPageLoadCallback(callbackScope, url!!) }
|
||||
}
|
||||
|
||||
override fun download(url: String?, mimetype: String?, userAgent: String?) {
|
||||
coroutineScope.launch(dispatcher) {
|
||||
onDownloadCallback(
|
||||
callbackScope,
|
||||
url!!,
|
||||
mimetype!!,
|
||||
userAgent!!
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val callbackScope = object : WebViewCallbackScope<T>, Scope by scopeImpl {
|
||||
override suspend fun finish(result: T) {
|
||||
setResult(result)
|
||||
// Tell the WebViewActivity to finish
|
||||
webView.let { withContext(Dispatchers.IO) { it.finish() } }
|
||||
}
|
||||
|
||||
override suspend fun load(url: String) {
|
||||
webView.let { withContext(Dispatchers.IO) { it.load(url) } }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the WebView attempts to download a file to disk.
|
||||
*/
|
||||
fun download(block: DownloadCallback<T>) {
|
||||
onDownloadCallback = block
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the WebView finishes loading a page.
|
||||
*/
|
||||
fun pageLoad(block: PageLoadCallback<T>) {
|
||||
onPageLoadCallback = block
|
||||
}
|
||||
}
|
||||
|
||||
@JvmInline
|
||||
private value class Container<U>(val value: U)
|
||||
|
||||
/**
|
||||
* Run a [android.webkit.WebView] Activity controlled by the provided code block.
|
||||
* The activity will keep running until it is cancelled or an event handler calls [WebViewCallbackScope.finish].
|
||||
* The [block] defines the event handlers and returns the initial URL.
|
||||
*
|
||||
* @param title The string displayed in the action bar.
|
||||
* @param block The control block.
|
||||
*/
|
||||
@OptIn(PluginHostApi::class)
|
||||
suspend fun <T> GetScope.runWebView(
|
||||
title: String,
|
||||
block: suspend WebViewScope<T>.() -> InitialUrl
|
||||
) = supervisorScope {
|
||||
var result by Delegates.notNull<Container<T>>()
|
||||
|
||||
val scope = WebViewScope<T>(this@supervisorScope, this@runWebView) { result = Container(it) }
|
||||
scope.initialUrl = scope.block()
|
||||
|
||||
// Start the webview activity and wait until it finishes.
|
||||
requestStartActivity(Intent().apply {
|
||||
putExtra(
|
||||
WebViewActivity.KEY,
|
||||
WebViewActivity.Parameters(title, scope.binder)
|
||||
)
|
||||
setClassName(
|
||||
hostPackageName,
|
||||
WebViewActivity::class.qualifiedName!!
|
||||
)
|
||||
})
|
||||
|
||||
// Return the result and cancel any leftover coroutines.
|
||||
coroutineContext.cancelChildren()
|
||||
result.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement a downloader using [runWebView] and [DownloadUrl]. This function will automatically define a handler for download events unlike [runWebView].
|
||||
* Returning null inside the [block] is equivalent to returning null inside [DownloaderScope.get].
|
||||
*
|
||||
* @see runWebView
|
||||
*/
|
||||
fun WebViewDownloader(block: suspend WebViewScope<DownloadUrl>.(packageName: String, version: String?) -> InitialUrl?) =
|
||||
Downloader<DownloadUrl> {
|
||||
val label = context.applicationInfo.loadLabel(
|
||||
context.packageManager
|
||||
).toString()
|
||||
|
||||
get { packageName, version ->
|
||||
class ReturnNull : Exception()
|
||||
|
||||
try {
|
||||
runWebView(label) {
|
||||
download { url, _, userAgent ->
|
||||
finish(
|
||||
DownloadUrl(
|
||||
url,
|
||||
mapOf("User-Agent" to userAgent)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
block(this@runWebView, packageName, version) ?: throw ReturnNull()
|
||||
} to version
|
||||
} catch (_: ReturnNull) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
download {
|
||||
it.toDownloadResult()
|
||||
}
|
||||
}
|
@ -0,0 +1,161 @@
|
||||
package app.revanced.manager.plugin.downloader.webview
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.os.Parcelable
|
||||
import android.view.MenuItem
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||
import app.revanced.manager.plugin.downloader.R
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@OptIn(PluginHostApi::class)
|
||||
@PluginHostApi
|
||||
class WebViewActivity : ComponentActivity() {
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val vm by viewModels<WebViewModel>()
|
||||
enableEdgeToEdge()
|
||||
setContentView(R.layout.activity_webview)
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
insets
|
||||
}
|
||||
val webView = findViewById<WebView>(R.id.webview)
|
||||
onBackPressedDispatcher.addCallback {
|
||||
if (webView.canGoBack()) webView.goBack()
|
||||
else cancelActivity()
|
||||
}
|
||||
|
||||
val params = intent.getParcelableExtra<Parameters>(KEY)!!
|
||||
actionBar?.apply {
|
||||
title = params.title
|
||||
setHomeAsUpIndicator(android.R.drawable.ic_menu_close_clear_cancel)
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
val events = IWebViewEvents.Stub.asInterface(params.events)!!
|
||||
vm.setup(events)
|
||||
|
||||
webView.apply {
|
||||
settings.apply {
|
||||
cacheMode = WebSettings.LOAD_NO_CACHE
|
||||
allowContentAccess = false
|
||||
domStorageEnabled = true
|
||||
javaScriptEnabled = true
|
||||
}
|
||||
|
||||
webViewClient = vm.webViewClient
|
||||
setDownloadListener { url, userAgent, _, mimetype, _ ->
|
||||
vm.onDownload(url, mimetype, userAgent)
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
vm.commands.collect {
|
||||
when (it) {
|
||||
is WebViewModel.Command.Finish -> {
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
|
||||
is WebViewModel.Command.Load -> webView.loadUrl(it.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelActivity() {
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = if (item.itemId == android.R.id.home) {
|
||||
cancelActivity()
|
||||
|
||||
true
|
||||
} else super.onOptionsItemSelected(item)
|
||||
|
||||
@Parcelize
|
||||
internal class Parameters(
|
||||
val title: String, val events: IBinder
|
||||
) : Parcelable
|
||||
|
||||
internal companion object {
|
||||
const val KEY = "params"
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(PluginHostApi::class)
|
||||
internal class WebViewModel : ViewModel() {
|
||||
init {
|
||||
CookieManager.getInstance().apply {
|
||||
removeAllCookies(null)
|
||||
setAcceptCookie(true)
|
||||
}
|
||||
}
|
||||
|
||||
private val commandChannel = Channel<Command>()
|
||||
val commands = commandChannel.receiveAsFlow()
|
||||
|
||||
private var eventBinder: IWebViewEvents? = null
|
||||
private val ctrlBinder = object : IWebView.Stub() {
|
||||
override fun load(url: String?) {
|
||||
viewModelScope.launch {
|
||||
commandChannel.send(Command.Load(url!!))
|
||||
}
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
viewModelScope.launch {
|
||||
commandChannel.send(Command.Finish)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val webViewClient = object : WebViewClient() {
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
eventBinder!!.pageLoad(url)
|
||||
}
|
||||
}
|
||||
|
||||
fun onDownload(url: String, mimeType: String, userAgent: String) {
|
||||
eventBinder!!.download(url, mimeType, userAgent)
|
||||
}
|
||||
|
||||
fun setup(binder: IWebViewEvents) {
|
||||
if (eventBinder != null) return
|
||||
eventBinder = binder
|
||||
binder.ready(ctrlBinder)
|
||||
}
|
||||
|
||||
sealed interface Command {
|
||||
data class Load(val url: String) : Command
|
||||
data object Finish : Command
|
||||
}
|
||||
}
|
11
api/src/main/res/layout/activity_webview.xml
Normal file
11
api/src/main/res/layout/activity_webview.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/main">
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</LinearLayout>
|
1
api/src/main/res/values/strings.xml
Normal file
1
api/src/main/res/values/strings.xml
Normal file
@ -0,0 +1 @@
|
||||
<resources></resources>
|
7
api/src/main/res/values/themes.xml
Normal file
7
api/src/main/res/values/themes.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.WebViewActivity" parent="Theme.AppCompat.DayNight">
|
||||
<item name="android:windowActionBar">true</item>
|
||||
<item name="android:windowNoTitle">false</item>
|
||||
</style>
|
||||
</resources>
|
@ -1,132 +1,28 @@
|
||||
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)
|
||||
id("kotlin-parcelize")
|
||||
kotlin("plugin.serialization") version "1.9.10"
|
||||
signing
|
||||
}
|
||||
|
||||
val (majorVersion, minorVersion, patchVersion, devVersion) = "${project.version}.0".replace("-dev","").split(".")
|
||||
android {
|
||||
namespace = "app.revanced.manager"
|
||||
compileSdk = 34
|
||||
buildToolsVersion = "34.0.0"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "app.revanced.manager"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionName = project.version.toString()
|
||||
versionCode = (majorVersion.toInt() * 100000000) + (minorVersion.toInt() * 100000) + (patchVersion.toInt() * 100) + devVersion.toInt()
|
||||
resourceConfigurations.addAll(listOf(
|
||||
"en",
|
||||
))
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
resValue("string", "app_name", "ReVanced Manager Debug")
|
||||
}
|
||||
|
||||
release {
|
||||
if (System.getenv("signingKey") != null) {
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
storeFile = file(System.getenv("signingKey"))
|
||||
storePassword = System.getenv("keyStorePassword")
|
||||
keyAlias = System.getenv("keyAlias")
|
||||
keyPassword = System.getenv("keyPassword")
|
||||
}
|
||||
}
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
} else {
|
||||
applicationIdSuffix = ".debug"
|
||||
resValue("string", "app_name", "ReVanced Manager Debug")
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
if (!project.hasProperty("noProguard")) {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
var suffix = "v${project.version}"
|
||||
if (project.hasProperty("suffix")) {
|
||||
suffix = "${project.property("suffix")}"
|
||||
}
|
||||
applicationVariants.all {
|
||||
this.outputs
|
||||
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
|
||||
.forEach { output ->
|
||||
output.outputFileName = "revanced-manager-${suffix}.apk"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
includeInApk = false
|
||||
includeInBundle = false
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources.excludes.addAll(listOf(
|
||||
"/prebuilt/**",
|
||||
"META-INF/DEPENDENCIES",
|
||||
"META-INF/**.version",
|
||||
"DebugProbesKt.bin",
|
||||
"kotlin-tooling-metadata.json",
|
||||
"org/bouncycastle/pqc/**.properties",
|
||||
"org/bouncycastle/x509/**.properties",
|
||||
))
|
||||
jniLibs {
|
||||
useLegacyPackaging = true
|
||||
}
|
||||
}
|
||||
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
buildFeatures.compose = true
|
||||
buildFeatures.aidl = true
|
||||
buildFeatures.buildConfig=true
|
||||
|
||||
composeOptions.kotlinCompilerExtensionVersion = "1.5.3"
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
tasks.register("publish") {
|
||||
group = "Build"
|
||||
description = "Assemble main outputs for all the variants."
|
||||
dependsOn("assembleRelease")
|
||||
}
|
||||
val outputApkFileName = "${rootProject.name}-$version.apk"
|
||||
|
||||
dependencies {
|
||||
|
||||
// AndroidX Core
|
||||
implementation(libs.androidx.ktx)
|
||||
implementation(libs.runtime.ktx)
|
||||
implementation(libs.runtime.compose)
|
||||
implementation(libs.splash.screen)
|
||||
implementation(libs.compose.activity)
|
||||
implementation(libs.paging.common.ktx)
|
||||
implementation(libs.activity.compose)
|
||||
implementation(libs.work.runtime.ktx)
|
||||
implementation(libs.preferences.datastore)
|
||||
implementation(libs.appcompat)
|
||||
|
||||
// Compose
|
||||
implementation(platform(libs.compose.bom))
|
||||
@ -136,11 +32,13 @@ dependencies {
|
||||
implementation(libs.compose.livedata)
|
||||
implementation(libs.compose.material.icons.extended)
|
||||
implementation(libs.compose.material3)
|
||||
implementation(libs.navigation.compose)
|
||||
|
||||
// Accompanist
|
||||
implementation(libs.accompanist.drawablepainter)
|
||||
implementation(libs.accompanist.webview)
|
||||
implementation(libs.accompanist.placeholder)
|
||||
|
||||
// Placeholder
|
||||
implementation(libs.placeholder.material3)
|
||||
|
||||
// HTML Scraper
|
||||
implementation(libs.skrapeit.dsl)
|
||||
@ -153,6 +51,7 @@ dependencies {
|
||||
// KotlinX
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.kotlinx.collection.immutable)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
|
||||
// Room
|
||||
implementation(libs.room.runtime)
|
||||
@ -164,6 +63,16 @@ dependencies {
|
||||
implementation(libs.revanced.patcher)
|
||||
implementation(libs.revanced.library)
|
||||
|
||||
// Downloader plugins
|
||||
implementation(project(":api"))
|
||||
|
||||
// Native processes
|
||||
implementation(libs.kotlin.process)
|
||||
|
||||
// HiddenAPI
|
||||
compileOnly(libs.hidden.api.stub)
|
||||
|
||||
// LibSU
|
||||
implementation(libs.libsu.core)
|
||||
implementation(libs.libsu.service)
|
||||
implementation(libs.libsu.nio)
|
||||
@ -171,11 +80,9 @@ dependencies {
|
||||
// Koin
|
||||
implementation(libs.koin.android)
|
||||
implementation(libs.koin.compose)
|
||||
implementation(libs.koin.compose.navigation)
|
||||
implementation(libs.koin.workmanager)
|
||||
|
||||
// Compose Navigation
|
||||
implementation(libs.reimagined.navigation)
|
||||
|
||||
// Licenses
|
||||
implementation(libs.about.libraries)
|
||||
|
||||
@ -194,4 +101,156 @@ dependencies {
|
||||
|
||||
// Scrollbars
|
||||
implementation(libs.scrollbars)
|
||||
|
||||
// EnumUtil
|
||||
implementation(libs.enumutil)
|
||||
ksp(libs.enumutil.ksp)
|
||||
|
||||
// Reorderable lists
|
||||
implementation(libs.reorderable)
|
||||
|
||||
// Compose Icons
|
||||
implementation(libs.compose.icons.fontawesome)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.manager"
|
||||
compileSdk = 35
|
||||
buildToolsVersion = "35.0.1"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "app.revanced.manager"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "0.0.1"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
resValue("string", "app_name", "ReVanced Manager (Debug)")
|
||||
isPseudoLocalesEnabled = true
|
||||
|
||||
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
|
||||
}
|
||||
|
||||
release {
|
||||
if (!project.hasProperty("noProguard")) {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
|
||||
val keystoreFile = file("keystore.jks")
|
||||
|
||||
if (project.hasProperty("signAsDebug") || !keystoreFile.exists()) {
|
||||
applicationIdSuffix = ".debug_signed"
|
||||
resValue("string", "app_name", "ReVanced Manager (Debug signed)")
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
|
||||
isPseudoLocalesEnabled = true
|
||||
} else {
|
||||
signingConfig = signingConfigs.create("release") {
|
||||
storeFile = keystoreFile
|
||||
storePassword = System.getenv("KEYSTORE_PASSWORD")
|
||||
keyAlias = System.getenv("KEYSTORE_ENTRY_ALIAS")
|
||||
keyPassword = System.getenv("KEYSTORE_ENTRY_PASSWORD")
|
||||
}
|
||||
}
|
||||
|
||||
buildConfigField("long", "BUILD_ID", "0L")
|
||||
}
|
||||
}
|
||||
|
||||
applicationVariants.all {
|
||||
outputs.all {
|
||||
this as com.android.build.gradle.internal.api.ApkVariantOutputImpl
|
||||
|
||||
outputFileName = outputApkFileName
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
includeInApk = false
|
||||
includeInBundle = false
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources.excludes.addAll(
|
||||
listOf(
|
||||
"/prebuilt/**",
|
||||
"META-INF/DEPENDENCIES",
|
||||
"META-INF/**.version",
|
||||
"DebugProbesKt.bin",
|
||||
"kotlin-tooling-metadata.json",
|
||||
"org/bouncycastle/pqc/**.properties",
|
||||
"org/bouncycastle/x509/**.properties",
|
||||
)
|
||||
)
|
||||
jniLibs {
|
||||
useLegacyPackaging = true
|
||||
}
|
||||
}
|
||||
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
aidl = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
android {
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path = file("src/main/cpp/CMakeLists.txt")
|
||||
version = "3.22.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
tasks {
|
||||
// Needed by gradle-semantic-release-plugin.
|
||||
// Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435.
|
||||
val publish by registering {
|
||||
group = "publishing"
|
||||
description = "Build the release APK"
|
||||
|
||||
dependsOn("assembleRelease")
|
||||
|
||||
val apk = project.layout.buildDirectory.file("outputs/apk/release/${outputApkFileName}")
|
||||
val ascFile = apk.map { it.asFile.resolveSibling("${it.asFile.name}.asc") }
|
||||
|
||||
inputs.file(apk).withPropertyName("inputApk")
|
||||
outputs.file(ascFile).withPropertyName("outputAsc")
|
||||
|
||||
doLast {
|
||||
signing {
|
||||
useGpgCmd()
|
||||
sign(apk.get().asFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
9
app/proguard-rules.pro
vendored
9
app/proguard-rules.pro
vendored
@ -26,6 +26,10 @@
|
||||
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.** {
|
||||
*;
|
||||
@ -45,6 +49,11 @@
|
||||
-keep class com.android.** {
|
||||
*;
|
||||
}
|
||||
-keep class app.revanced.manager.plugin.** {
|
||||
*;
|
||||
}
|
||||
|
||||
-dontwarn com.google.auto.value.**
|
||||
-dontwarn java.awt.**
|
||||
-dontwarn javax.**
|
||||
-dontwarn org.slf4j.**
|
||||
|
@ -2,11 +2,11 @@
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "802fa2fda94b930bf0ebb85d195f1022",
|
||||
"identityHash": "d0119047505da435972c5247181de675",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "patch_bundles",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `name` TEXT NOT NULL, `source` TEXT NOT NULL, `auto_update` INTEGER NOT NULL, `version` TEXT, `integrations_version` TEXT, PRIMARY KEY(`uid`))",
|
||||
"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",
|
||||
@ -20,6 +20,12 @@
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "version",
|
||||
"columnName": "version",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "source",
|
||||
"columnName": "source",
|
||||
@ -31,18 +37,6 @@
|
||||
"columnName": "auto_update",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "versionInfo.patches",
|
||||
"columnName": "version",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "versionInfo.integrations",
|
||||
"columnName": "integrations_version",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
@ -51,17 +45,7 @@
|
||||
"uid"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_patch_bundles_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_patch_bundles_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
@ -160,7 +144,7 @@
|
||||
},
|
||||
{
|
||||
"tableName": "downloaded_app",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `directory` TEXT NOT NULL, PRIMARY KEY(`package_name`, `version`))",
|
||||
"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",
|
||||
@ -179,6 +163,12 @@
|
||||
"columnName": "directory",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastUsed",
|
||||
"columnName": "last_used",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
@ -231,7 +221,7 @@
|
||||
},
|
||||
{
|
||||
"tableName": "applied_patch",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `bundle` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`package_name`, `bundle`, `patch_name`), FOREIGN KEY(`package_name`) REFERENCES `installed_app`(`current_package_name`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"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",
|
||||
@ -285,7 +275,7 @@
|
||||
},
|
||||
{
|
||||
"table": "patch_bundles",
|
||||
"onDelete": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"bundle"
|
||||
@ -402,12 +392,38 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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, '802fa2fda94b930bf0ebb85d195f1022')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd0119047505da435972c5247181de675')"
|
||||
]
|
||||
}
|
||||
}
|
@ -2,9 +2,16 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="ReservedSystemPermission" />
|
||||
<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" />
|
||||
@ -16,12 +23,7 @@
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent>
|
||||
</queries>
|
||||
<uses-permission android:name="android.permission.ENFORCE_UPDATE_OWNERSHIP" />
|
||||
|
||||
<application
|
||||
android:name=".ManagerApplication"
|
||||
@ -47,6 +49,8 @@
|
||||
</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" />
|
||||
|
||||
|
@ -0,0 +1,11 @@
|
||||
// IPatcherEvents.aidl
|
||||
package app.revanced.manager.patcher.runtime.process;
|
||||
|
||||
// Interface for sending events back to the main app process.
|
||||
oneway interface IPatcherEvents {
|
||||
void log(String level, String msg);
|
||||
void patchSucceeded();
|
||||
void progress(String name, String state, String msg);
|
||||
// The patching process has ended. The exceptionStackTrace is null if it finished successfully.
|
||||
void finished(String exceptionStackTrace);
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
// IPatcherProcess.aidl
|
||||
package app.revanced.manager.patcher.runtime.process;
|
||||
|
||||
import app.revanced.manager.patcher.runtime.process.Parameters;
|
||||
import app.revanced.manager.patcher.runtime.process.IPatcherEvents;
|
||||
|
||||
interface IPatcherProcess {
|
||||
// Returns BuildConfig.BUILD_ID, which is used to ensure the main app and runner process are running the same code.
|
||||
long buildId();
|
||||
// Makes the patcher process exit with code 0
|
||||
oneway void exit();
|
||||
// Starts patching.
|
||||
oneway void start(in Parameters parameters, IPatcherEvents events);
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
// Parameters.aidl
|
||||
package app.revanced.manager.patcher.runtime.process;
|
||||
|
||||
parcelable Parameters;
|
38
app/src/main/cpp/CMakeLists.txt
Normal file
38
app/src/main/cpp/CMakeLists.txt
Normal file
@ -0,0 +1,38 @@
|
||||
|
||||
# For more information about using CMake with Android Studio, read the
|
||||
# documentation: https://d.android.com/studio/projects/add-native-code.html.
|
||||
# For more examples on how to use CMake, see https://github.com/android/ndk-samples.
|
||||
|
||||
# Sets the minimum CMake version required for this project.
|
||||
cmake_minimum_required(VERSION 3.22.1)
|
||||
|
||||
# Declares the project name. The project name can be accessed via ${ PROJECT_NAME},
|
||||
# Since this is the top level CMakeLists.txt, the project name is also accessible
|
||||
# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
|
||||
# build script scope).
|
||||
project("prop_override")
|
||||
|
||||
# Creates and names a library, sets it as either STATIC
|
||||
# or SHARED, and provides the relative paths to its source code.
|
||||
# You can define multiple libraries, and CMake builds them for you.
|
||||
# Gradle automatically packages shared libraries with your APK.
|
||||
#
|
||||
# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
|
||||
# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
|
||||
# is preferred for the same purpose.
|
||||
#
|
||||
# In order to load a library into your app from Java/Kotlin, you must call
|
||||
# System.loadLibrary() and pass the name of the library defined here;
|
||||
# for GameActivity/NativeActivity derived applications, the same library name must be
|
||||
# used in the AndroidManifest.xml file.
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
# List C/C++ source files with relative paths to this CMakeLists.txt.
|
||||
prop_override.cpp)
|
||||
|
||||
# Specifies libraries CMake should link to your target library. You
|
||||
# can link libraries from various origins, such as libraries defined in this
|
||||
# build script, prebuilt third-party libraries, or Android system libraries.
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}
|
||||
# List libraries link to the target library
|
||||
android
|
||||
log)
|
62
app/src/main/cpp/prop_override.cpp
Normal file
62
app/src/main/cpp/prop_override.cpp
Normal file
@ -0,0 +1,62 @@
|
||||
// Library for overriding Android system properties via environment variables.
|
||||
//
|
||||
// Usage: LD_PRELOAD=prop_override.so PROP_dalvik.vm.heapsize=123M getprop dalvik.vm.heapsize
|
||||
// Output: 123M
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <dlfcn.h>
|
||||
|
||||
// Source: https://android.googlesource.com/platform/system/core/+/100b08a848d018eeb1caa5d5e7c7c2aaac65da15/libcutils/include/cutils/properties.h
|
||||
#define PROP_VALUE_MAX 92
|
||||
// This is the mangled name of "android::base::GetProperty".
|
||||
#define GET_PROPERTY_MANGLED_NAME "_ZN7android4base11GetPropertyERKNSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEES9_"
|
||||
|
||||
extern "C" typedef int (*property_get_ptr)(const char *, char *, const char *);
|
||||
typedef std::string (*GetProperty_ptr)(const std::string &, const std::string &);
|
||||
|
||||
char *GetPropOverride(const std::string &key) {
|
||||
auto envKey = "PROP_" + key;
|
||||
|
||||
return getenv(envKey.c_str());
|
||||
}
|
||||
|
||||
// See: https://android.googlesource.com/platform/system/core/+/100b08a848d018eeb1caa5d5e7c7c2aaac65da15/libcutils/properties.cpp
|
||||
extern "C" int property_get(const char *key, char *value, const char *default_value) {
|
||||
auto replacement = GetPropOverride(std::string(key));
|
||||
if (replacement) {
|
||||
int len = strnlen(replacement, PROP_VALUE_MAX);
|
||||
|
||||
strncpy(value, replacement, len);
|
||||
return len;
|
||||
}
|
||||
|
||||
static property_get_ptr original = NULL;
|
||||
if (!original) {
|
||||
// Get the address of the original function.
|
||||
original = reinterpret_cast<property_get_ptr>(dlsym(RTLD_NEXT, "property_get"));
|
||||
}
|
||||
|
||||
return original(key, value, default_value);
|
||||
}
|
||||
|
||||
// Defining android::base::GetProperty ourselves won't work because std::string has a slightly different "path" in the NDK version of the C++ standard library.
|
||||
// We can get around this by forcing the function to adopt a specific name using the asm keyword.
|
||||
std::string GetProperty(const std::string &, const std::string &) asm(GET_PROPERTY_MANGLED_NAME);
|
||||
|
||||
|
||||
// See: https://android.googlesource.com/platform/system/libbase/+/1a34bb67c4f3ba0a1ea6f4f20ac9fe117ba4fe64/properties.cpp
|
||||
// This isn't used for the properties we want to override, but property_get is deprecated so that could change in the future.
|
||||
std::string GetProperty(const std::string &key, const std::string &default_value) {
|
||||
auto replacement = GetPropOverride(key);
|
||||
if (replacement) {
|
||||
return std::string(replacement);
|
||||
}
|
||||
|
||||
static GetProperty_ptr original = NULL;
|
||||
if (!original) {
|
||||
original = reinterpret_cast<GetProperty_ptr>(dlsym(RTLD_NEXT, GET_PROPERTY_MANGLED_NAME));
|
||||
}
|
||||
|
||||
return original(key, default_value);
|
||||
}
|
@ -1,185 +1,336 @@
|
||||
package app.revanced.manager
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Update
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import app.revanced.manager.ui.component.AutoUpdatesDialog
|
||||
import app.revanced.manager.ui.destination.Destination
|
||||
import app.revanced.manager.ui.destination.SettingsDestination
|
||||
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.VersionSelectorScreen
|
||||
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 dev.olshevski.navigation.reimagined.AnimatedNavHost
|
||||
import dev.olshevski.navigation.reimagined.NavBackHandler
|
||||
import dev.olshevski.navigation.reimagined.navigate
|
||||
import dev.olshevski.navigation.reimagined.pop
|
||||
import dev.olshevski.navigation.reimagined.popUpTo
|
||||
import dev.olshevski.navigation.reimagined.rememberNavController
|
||||
import 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.compose.getViewModel as getComposeViewModel
|
||||
import org.koin.androidx.viewmodel.ext.android.getViewModel as getAndroidViewModel
|
||||
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 = getAndroidViewModel()
|
||||
|
||||
vm.importLegacySettings(this)
|
||||
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
|
||||
) {
|
||||
val navController =
|
||||
rememberNavController<Destination>(startDestination = Destination.Dashboard)
|
||||
|
||||
NavBackHandler(navController)
|
||||
|
||||
val firstLaunch by vm.prefs.firstLaunch.getAsState()
|
||||
|
||||
if (firstLaunch) AutoUpdatesDialog(vm::applyAutoUpdatePrefs)
|
||||
|
||||
vm.updatedManagerVersion?.let {
|
||||
AlertDialog(
|
||||
onDismissRequest = vm::dismissUpdateDialog,
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
vm.dismissUpdateDialog()
|
||||
navController.navigate(Destination.Settings(SettingsDestination.Update(false)))
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.update))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = vm::dismissUpdateDialog) {
|
||||
Text(stringResource(R.string.dismiss_temporary))
|
||||
}
|
||||
},
|
||||
icon = { Icon(Icons.Outlined.Update, null) },
|
||||
title = { Text(stringResource(R.string.update_available_dialog_title)) },
|
||||
text = { Text(stringResource(R.string.update_available_dialog_description, it)) }
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedNavHost(
|
||||
controller = navController
|
||||
) { destination ->
|
||||
when (destination) {
|
||||
is Destination.Dashboard -> DashboardScreen(
|
||||
onSettingsClick = { navController.navigate(Destination.Settings()) },
|
||||
onAppSelectorClick = { navController.navigate(Destination.AppSelector) },
|
||||
onAppClick = { installedApp ->
|
||||
navController.navigate(
|
||||
Destination.InstalledApplicationInfo(
|
||||
installedApp
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
is Destination.InstalledApplicationInfo -> InstalledAppInfoScreen(
|
||||
onPatchClick = { packageName, patchSelection ->
|
||||
navController.navigate(
|
||||
Destination.VersionSelector(
|
||||
packageName,
|
||||
patchSelection
|
||||
)
|
||||
)
|
||||
},
|
||||
onBackClick = { navController.pop() },
|
||||
viewModel = getComposeViewModel { parametersOf(destination.installedApp) }
|
||||
)
|
||||
|
||||
is Destination.Settings -> SettingsScreen(
|
||||
onBackClick = { navController.pop() },
|
||||
startDestination = destination.startDestination
|
||||
)
|
||||
|
||||
is Destination.AppSelector -> AppSelectorScreen(
|
||||
onAppClick = { navController.navigate(Destination.VersionSelector(it)) },
|
||||
onStorageClick = {
|
||||
navController.navigate(
|
||||
Destination.SelectedApplicationInfo(
|
||||
it
|
||||
)
|
||||
)
|
||||
},
|
||||
onBackClick = { navController.pop() }
|
||||
)
|
||||
|
||||
is Destination.VersionSelector -> VersionSelectorScreen(
|
||||
onBackClick = { navController.pop() },
|
||||
onAppClick = { selectedApp ->
|
||||
navController.navigate(
|
||||
Destination.SelectedApplicationInfo(
|
||||
selectedApp,
|
||||
destination.patchSelection,
|
||||
)
|
||||
)
|
||||
},
|
||||
viewModel = getComposeViewModel {
|
||||
parametersOf(
|
||||
destination.packageName,
|
||||
destination.patchSelection
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
is Destination.SelectedApplicationInfo -> SelectedAppInfoScreen(
|
||||
onPatchClick = { app, patches, options ->
|
||||
navController.navigate(
|
||||
Destination.Patcher(
|
||||
app, patches, options
|
||||
)
|
||||
)
|
||||
},
|
||||
onBackClick = navController::pop,
|
||||
vm = getComposeViewModel {
|
||||
parametersOf(
|
||||
SelectedAppInfoViewModel.Params(
|
||||
destination.selectedApp,
|
||||
destination.patchSelection
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
is Destination.Patcher -> PatcherScreen(
|
||||
onBackClick = { navController.popUpTo { it is Destination.Dashboard } },
|
||||
vm = getComposeViewModel { parametersOf(destination) }
|
||||
)
|
||||
}
|
||||
}
|
||||
ReVancedManager(vm)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReVancedManager(vm: MainViewModel) {
|
||||
val navController = rememberNavController()
|
||||
|
||||
EventEffect(vm.appSelectFlow) { app ->
|
||||
navController.navigateComplex(
|
||||
SelectedApplicationInfo,
|
||||
SelectedApplicationInfo.ViewModelParams(app)
|
||||
)
|
||||
}
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Dashboard,
|
||||
enterTransition = { slideInHorizontally(initialOffsetX = { it }) },
|
||||
exitTransition = { slideOutHorizontally(targetOffsetX = { -it / 3 }) },
|
||||
popEnterTransition = { slideInHorizontally(initialOffsetX = { -it / 3 }) },
|
||||
popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) },
|
||||
) {
|
||||
composable<Dashboard> {
|
||||
DashboardScreen(
|
||||
onSettingsClick = { navController.navigate(Settings) },
|
||||
onAppSelectorClick = {
|
||||
navController.navigate(AppSelector)
|
||||
},
|
||||
onUpdateClick = {
|
||||
navController.navigate(Update())
|
||||
},
|
||||
onDownloaderPluginClick = {
|
||||
navController.navigate(Settings.Downloads)
|
||||
},
|
||||
onAppClick = { packageName ->
|
||||
navController.navigate(InstalledApplicationInfo(packageName))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable<InstalledApplicationInfo> {
|
||||
val data = it.toRoute<InstalledApplicationInfo>()
|
||||
|
||||
InstalledAppInfoScreen(
|
||||
onPatchClick = vm::selectApp,
|
||||
onBackClick = navController::popBackStack,
|
||||
viewModel = koinViewModel { parametersOf(data.packageName) }
|
||||
)
|
||||
}
|
||||
|
||||
composable<AppSelector> {
|
||||
AppSelectorScreen(
|
||||
onSelect = vm::selectApp,
|
||||
onStorageSelect = vm::selectApp,
|
||||
onBackClick = navController::popBackStack
|
||||
)
|
||||
}
|
||||
|
||||
composable<Patcher> {
|
||||
PatcherScreen(
|
||||
onBackClick = {
|
||||
navController.navigate(route = Dashboard) {
|
||||
launchSingleTop = true
|
||||
popUpTo<Dashboard> {
|
||||
inclusive = false
|
||||
}
|
||||
}
|
||||
},
|
||||
viewModel = koinViewModel { parametersOf(it.getComplexArg<Patcher.ViewModelParams>()) }
|
||||
)
|
||||
}
|
||||
|
||||
composable<Update> {
|
||||
val data = it.toRoute<Update>()
|
||||
|
||||
UpdateScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
vm = koinViewModel { parametersOf(data.downloadOnScreenEntry) }
|
||||
)
|
||||
}
|
||||
|
||||
navigation<SelectedApplicationInfo>(startDestination = SelectedApplicationInfo.Main) {
|
||||
composable<SelectedApplicationInfo.Main> {
|
||||
val parentBackStackEntry = navController.navGraphEntry(it)
|
||||
val data =
|
||||
parentBackStackEntry.getComplexArg<SelectedApplicationInfo.ViewModelParams>()
|
||||
val viewModel =
|
||||
koinNavViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
|
||||
parametersOf(data)
|
||||
}
|
||||
|
||||
SelectedAppInfoScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
onPatchClick = {
|
||||
it.lifecycleScope.launch {
|
||||
navController.navigateComplex(
|
||||
Patcher,
|
||||
viewModel.getPatcherParams()
|
||||
)
|
||||
}
|
||||
},
|
||||
onPatchSelectorClick = { app, patches, options ->
|
||||
navController.navigateComplex(
|
||||
SelectedApplicationInfo.PatchesSelector,
|
||||
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
|
||||
app,
|
||||
patches,
|
||||
options
|
||||
)
|
||||
)
|
||||
},
|
||||
onRequiredOptions = { app, patches, options ->
|
||||
navController.navigateComplex(
|
||||
SelectedApplicationInfo.RequiredOptions,
|
||||
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
|
||||
app,
|
||||
patches,
|
||||
options
|
||||
)
|
||||
)
|
||||
},
|
||||
vm = viewModel
|
||||
)
|
||||
}
|
||||
|
||||
composable<SelectedApplicationInfo.PatchesSelector> {
|
||||
val data =
|
||||
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
||||
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
||||
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||
)
|
||||
|
||||
PatchesSelectorScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
onSave = { patches, options ->
|
||||
selectedAppInfoVm.updateConfiguration(patches, options)
|
||||
navController.popBackStack()
|
||||
},
|
||||
viewModel = koinViewModel { parametersOf(data) }
|
||||
)
|
||||
}
|
||||
|
||||
composable<SelectedApplicationInfo.RequiredOptions> {
|
||||
val data =
|
||||
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
||||
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
||||
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||
)
|
||||
|
||||
RequiredOptionsScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
onContinue = { patches, options ->
|
||||
selectedAppInfoVm.updateConfiguration(patches, options)
|
||||
it.lifecycleScope.launch {
|
||||
navController.navigateComplex(
|
||||
Patcher,
|
||||
selectedAppInfoVm.getPatcherParams()
|
||||
)
|
||||
}
|
||||
},
|
||||
vm = koinViewModel { parametersOf(data) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
navigation<Settings>(startDestination = Settings.Main) {
|
||||
composable<Settings.Main> {
|
||||
SettingsScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
navigate = navController::navigate
|
||||
)
|
||||
}
|
||||
|
||||
composable<Settings.General> {
|
||||
GeneralSettingsScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
|
||||
composable<Settings.Advanced> {
|
||||
AdvancedSettingsScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
|
||||
composable<Settings.Developer> {
|
||||
DeveloperSettingsScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
|
||||
composable<Settings.Updates> {
|
||||
UpdatesSettingsScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
onChangelogClick = { navController.navigate(Settings.Changelogs) },
|
||||
onUpdateClick = { navController.navigate(Update()) }
|
||||
)
|
||||
}
|
||||
|
||||
composable<Settings.Downloads> {
|
||||
DownloadsSettingsScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
|
||||
composable<Settings.ImportExport> {
|
||||
ImportExportSettingsScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
|
||||
composable<Settings.About> {
|
||||
AboutSettingsScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
navigate = navController::navigate
|
||||
)
|
||||
}
|
||||
|
||||
composable<Settings.Changelogs> {
|
||||
ChangelogsSettingsScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
|
||||
composable<Settings.Contributors> {
|
||||
ContributorSettingsScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
|
||||
composable<Settings.Licenses> {
|
||||
LicensesSettingsScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NavController.navGraphEntry(entry: NavBackStackEntry) =
|
||||
remember(entry) { getBackStackEntry(entry.destination.parent!!.id) }
|
||||
|
||||
// Androidx Navigation does not support storing complex types in route objects, so we have to store them inside the saved state handle of the back stack entry instead.
|
||||
private fun <T : Parcelable, R : ComplexParameter<T>> NavController.navigateComplex(
|
||||
route: R,
|
||||
data: T
|
||||
) {
|
||||
navigate(route)
|
||||
getBackStackEntry(route).savedStateHandle["args"] = data
|
||||
}
|
||||
|
||||
private fun <T : Parcelable> NavBackStackEntry.getComplexArg() = savedStateHandle.get<T>("args")!!
|
@ -1,23 +1,24 @@
|
||||
package app.revanced.manager
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
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.service.ManagerRootService
|
||||
import app.revanced.manager.service.RootConnection
|
||||
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 com.topjohnwu.superuser.ipc.RootService
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
|
||||
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.android.ext.koin.androidLogger
|
||||
@ -28,6 +29,9 @@ 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()
|
||||
|
||||
@ -61,17 +65,46 @@ class ManagerApplication : Application() {
|
||||
val shellBuilder = BuilderImpl.create().setFlags(Shell.FLAG_MOUNT_MASTER)
|
||||
Shell.setDefaultBuilder(shellBuilder)
|
||||
|
||||
val intent = Intent(this, ManagerRootService::class.java)
|
||||
RootService.bind(intent, get<RootConnection>())
|
||||
|
||||
scope.launch {
|
||||
prefs.preload()
|
||||
}
|
||||
scope.launch(Dispatchers.Default) {
|
||||
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,13 +1,16 @@
|
||||
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 android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import app.revanced.manager.util.RequestManageStorageContract
|
||||
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.
|
||||
@ -16,21 +19,33 @@ class Filesystem(private val app: Application) {
|
||||
* A directory that gets cleared when the app restarts.
|
||||
* Do not store paths to this directory in a parcel.
|
||||
*/
|
||||
val tempDir = app.cacheDir.resolve("ephemeral").apply {
|
||||
val tempDir: File = app.getDir("ephemeral", Context.MODE_PRIVATE).apply {
|
||||
deleteRecursively()
|
||||
mkdirs()
|
||||
}
|
||||
|
||||
fun externalFilesDir() = Environment.getExternalStorageDirectory().toPath()
|
||||
/**
|
||||
* 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
|
||||
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()
|
||||
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
|
||||
fun hasStoragePermission() =
|
||||
if (usesManagePermission()) Environment.isExternalStorageManager() else app.checkSelfPermission(
|
||||
storagePermissionName
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
@ -16,9 +16,14 @@ 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], version = 1)
|
||||
@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
|
||||
@ -26,6 +31,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun downloadedAppDao(): DownloadedAppDao
|
||||
abstract fun installedAppDao(): InstalledAppDao
|
||||
abstract fun optionDao(): OptionDao
|
||||
abstract fun trustedDownloaderPluginDao(): TrustedDownloaderPluginDao
|
||||
|
||||
companion object {
|
||||
fun generateUid() = Random.Default.nextInt()
|
||||
|
@ -2,7 +2,7 @@ package app.revanced.manager.data.room
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import app.revanced.manager.data.room.bundles.Source
|
||||
import io.ktor.http.*
|
||||
import app.revanced.manager.data.room.options.Option.SerializedValue
|
||||
import java.io.File
|
||||
|
||||
class Converters {
|
||||
@ -17,4 +17,10 @@ class Converters {
|
||||
|
||||
@TypeConverter
|
||||
fun fileToString(file: File): String = file.path
|
||||
|
||||
@TypeConverter
|
||||
fun serializedOptionFromString(value: String) = SerializedValue.fromJsonString(value)
|
||||
|
||||
@TypeConverter
|
||||
fun serializedOptionToString(value: SerializedValue) = value.toJsonString()
|
||||
}
|
@ -12,4 +12,5 @@ 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()
|
||||
)
|
@ -4,6 +4,7 @@ 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
|
||||
@ -14,8 +15,11 @@ interface DownloadedAppDao {
|
||||
@Query("SELECT * FROM downloaded_app WHERE package_name = :packageName AND version = :version")
|
||||
suspend fun get(packageName: String, version: String): DownloadedApp?
|
||||
|
||||
@Insert
|
||||
suspend fun insert(downloadedApp: DownloadedApp)
|
||||
@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>)
|
||||
|
@ -22,7 +22,8 @@ import kotlinx.parcelize.Parcelize
|
||||
ForeignKey(
|
||||
PatchBundleEntity::class,
|
||||
parentColumns = ["uid"],
|
||||
childColumns = ["bundle"]
|
||||
childColumns = ["bundle"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
],
|
||||
indices = [Index(value = ["bundle"], unique = false)]
|
||||
|
@ -1,18 +1,15 @@
|
||||
package app.revanced.manager.data.room.apps.installed
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import app.revanced.manager.R
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
enum class InstallType(val stringResource: Int) {
|
||||
DEFAULT(R.string.default_install),
|
||||
ROOT(R.string.root_install)
|
||||
MOUNT(R.string.mount_install)
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
@Entity(tableName = "installed_app")
|
||||
data class InstalledApp(
|
||||
@PrimaryKey
|
||||
@ -20,4 +17,4 @@ data class InstalledApp(
|
||||
@ColumnInfo(name = "original_package_name") val originalPackageName: String,
|
||||
@ColumnInfo(name = "version") val version: String,
|
||||
@ColumnInfo(name = "install_type") val installType: InstallType
|
||||
) : Parcelable
|
||||
)
|
@ -3,7 +3,7 @@ package app.revanced.manager.data.room.apps.installed
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.MapInfo
|
||||
import androidx.room.MapColumn
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Upsert
|
||||
@ -17,12 +17,13 @@ interface InstalledAppDao {
|
||||
@Query("SELECT * FROM installed_app WHERE current_package_name = :packageName")
|
||||
suspend fun get(packageName: String): InstalledApp?
|
||||
|
||||
@MapInfo(keyColumn = "bundle", valueColumn = "patch_name")
|
||||
@Query(
|
||||
"SELECT bundle, patch_name FROM applied_patch" +
|
||||
" WHERE package_name = :packageName"
|
||||
)
|
||||
suspend fun getPatchesSelection(packageName: String): Map<Int, List<String>>
|
||||
suspend fun getPatchesSelection(packageName: String): Map<@MapColumn("bundle") Int, List<@MapColumn(
|
||||
"patch_name"
|
||||
) String>>
|
||||
|
||||
@Transaction
|
||||
suspend fun upsertApp(installedApp: InstalledApp, appliedPatches: List<AppliedPatch>) {
|
||||
|
@ -8,22 +8,25 @@ interface PatchBundleDao {
|
||||
@Query("SELECT * FROM patch_bundles")
|
||||
suspend fun all(): List<PatchBundleEntity>
|
||||
|
||||
@Query("SELECT version, integrations_version, auto_update FROM patch_bundles WHERE uid = :uid")
|
||||
fun getPropsById(uid: Int): Flow<BundleProperties>
|
||||
@Query("SELECT version, auto_update FROM patch_bundles WHERE uid = :uid")
|
||||
fun getPropsById(uid: Int): Flow<BundleProperties?>
|
||||
|
||||
@Query("UPDATE patch_bundles SET version = :patches, integrations_version = :integrations WHERE uid = :uid")
|
||||
suspend fun updateVersion(uid: Int, patches: String?, integrations: String?)
|
||||
@Query("UPDATE patch_bundles SET 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, null) // Reset the main source
|
||||
updateVersion(0, null) // Reset the main source
|
||||
}
|
||||
|
||||
@Query("DELETE FROM patch_bundles WHERE uid = :uid")
|
||||
|
@ -21,7 +21,7 @@ sealed class Source {
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(value: String) = when(value) {
|
||||
fun from(value: String) = when (value) {
|
||||
Local.SENTINEL -> Local
|
||||
API.SENTINEL -> API
|
||||
else -> Remote(Url(value))
|
||||
@ -29,21 +29,16 @@ sealed class Source {
|
||||
}
|
||||
}
|
||||
|
||||
data class VersionInfo(
|
||||
@ColumnInfo(name = "version") val patches: String? = null,
|
||||
@ColumnInfo(name = "integrations_version") val integrations: String? = null,
|
||||
)
|
||||
|
||||
@Entity(tableName = "patch_bundles", indices = [Index(value = ["name"], unique = true)])
|
||||
@Entity(tableName = "patch_bundles")
|
||||
data class PatchBundleEntity(
|
||||
@PrimaryKey val uid: Int,
|
||||
@ColumnInfo(name = "name") val name: String,
|
||||
@Embedded val versionInfo: VersionInfo,
|
||||
@ColumnInfo(name = "version") val version: String? = null,
|
||||
@ColumnInfo(name = "source") val source: Source,
|
||||
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
|
||||
)
|
||||
|
||||
data class BundleProperties(
|
||||
@Embedded val versionInfo: VersionInfo,
|
||||
@ColumnInfo(name = "version") val version: String? = null,
|
||||
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
|
||||
)
|
@ -3,6 +3,25 @@ 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",
|
||||
@ -19,5 +38,79 @@ data class Option(
|
||||
@ColumnInfo(name = "patch_name") val patchName: String,
|
||||
@ColumnInfo(name = "key") val key: String,
|
||||
// Encoded as Json.
|
||||
@ColumnInfo(name = "value") val value: String,
|
||||
)
|
||||
@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)
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ package app.revanced.manager.data.room.options
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.MapInfo
|
||||
import androidx.room.MapColumn
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@ -10,13 +10,12 @@ import kotlinx.coroutines.flow.Flow
|
||||
@Dao
|
||||
abstract class OptionDao {
|
||||
@Transaction
|
||||
@MapInfo(keyColumn = "patch_bundle")
|
||||
@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<Int, List<Option>>
|
||||
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?
|
||||
@ -28,10 +27,10 @@ abstract class OptionDao {
|
||||
abstract suspend fun createOptionGroup(group: OptionGroup)
|
||||
|
||||
@Query("DELETE FROM option_groups WHERE patch_bundle = :uid")
|
||||
abstract suspend fun clearForPatchBundle(uid: Int)
|
||||
abstract suspend fun resetOptionsForPatchBundle(uid: Int)
|
||||
|
||||
@Query("DELETE FROM option_groups WHERE package_name = :packageName")
|
||||
abstract suspend fun clearForPackage(packageName: String)
|
||||
abstract suspend fun resetOptionsForPackage(packageName: String)
|
||||
|
||||
@Query("DELETE FROM option_groups")
|
||||
abstract suspend fun reset()
|
||||
|
@ -0,0 +1,11 @@
|
||||
package app.revanced.manager.data.room.plugins
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "trusted_downloader_plugins")
|
||||
class TrustedDownloaderPlugin(
|
||||
@PrimaryKey @ColumnInfo(name = "package_name") val packageName: String,
|
||||
@ColumnInfo(name = "signature") val signature: ByteArray
|
||||
)
|
@ -0,0 +1,22 @@
|
||||
package app.revanced.manager.data.room.plugins
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Upsert
|
||||
|
||||
@Dao
|
||||
interface TrustedDownloaderPluginDao {
|
||||
@Query("SELECT signature FROM trusted_downloader_plugins WHERE package_name = :packageName")
|
||||
suspend fun getTrustedSignature(packageName: String): ByteArray?
|
||||
|
||||
@Upsert
|
||||
suspend fun upsertTrust(plugin: TrustedDownloaderPlugin)
|
||||
|
||||
@Query("DELETE FROM trusted_downloader_plugins WHERE package_name = :packageName")
|
||||
suspend fun remove(packageName: String)
|
||||
|
||||
@Transaction
|
||||
@Query("DELETE FROM trusted_downloader_plugins WHERE package_name IN (:packageNames)")
|
||||
suspend fun removeAll(packageNames: Set<String>)
|
||||
}
|
@ -2,29 +2,32 @@ package app.revanced.manager.data.room.selection
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.MapInfo
|
||||
import androidx.room.MapColumn
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
abstract class SelectionDao {
|
||||
@Transaction
|
||||
@MapInfo(keyColumn = "patch_bundle", valueColumn = "patch_name")
|
||||
@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<Int, List<String>>
|
||||
abstract suspend fun getSelectedPatches(packageName: String): Map<@MapColumn("patch_bundle") Int, List<@MapColumn(
|
||||
"patch_name"
|
||||
) String>>
|
||||
|
||||
@Transaction
|
||||
@MapInfo(keyColumn = "package_name", valueColumn = "patch_name")
|
||||
@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<String, List<String>>
|
||||
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?
|
||||
@ -32,11 +35,14 @@ abstract class SelectionDao {
|
||||
@Insert
|
||||
abstract suspend fun createSelection(selection: PatchSelection)
|
||||
|
||||
@Query("SELECT package_name FROM patch_selections")
|
||||
abstract fun getPackagesWithSelection(): Flow<List<String>>
|
||||
|
||||
@Query("DELETE FROM patch_selections WHERE patch_bundle = :uid")
|
||||
abstract suspend fun clearForPatchBundle(uid: Int)
|
||||
abstract suspend fun resetForPatchBundle(uid: Int)
|
||||
|
||||
@Query("DELETE FROM patch_selections WHERE package_name = :packageName")
|
||||
abstract suspend fun clearForPackage(packageName: String)
|
||||
abstract suspend fun resetForPackage(packageName: String)
|
||||
|
||||
@Query("DELETE FROM patch_selections")
|
||||
abstract suspend fun reset()
|
||||
|
@ -22,6 +22,7 @@ val repositoryModule = module {
|
||||
// It is best to load patch bundles ASAP
|
||||
createdAtStart()
|
||||
}
|
||||
singleOf(::DownloaderPluginRepository)
|
||||
singleOf(::WorkerRepository)
|
||||
singleOf(::DownloadedAppRepository)
|
||||
singleOf(::InstalledAppRepository)
|
||||
|
@ -1,11 +1,9 @@
|
||||
package app.revanced.manager.di
|
||||
|
||||
import app.revanced.manager.domain.installer.RootInstaller
|
||||
import app.revanced.manager.service.RootConnection
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val rootModule = module {
|
||||
singleOf(::RootConnection)
|
||||
singleOf(::RootInstaller)
|
||||
}
|
@ -1,11 +1,9 @@
|
||||
package app.revanced.manager.di
|
||||
|
||||
import app.revanced.manager.network.service.HttpService
|
||||
import app.revanced.manager.network.service.ReVancedService
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val serviceModule = module {
|
||||
singleOf(::ReVancedService)
|
||||
singleOf(::HttpService)
|
||||
}
|
@ -9,14 +9,15 @@ val viewModelModule = module {
|
||||
viewModelOf(::DashboardViewModel)
|
||||
viewModelOf(::SelectedAppInfoViewModel)
|
||||
viewModelOf(::PatchesSelectorViewModel)
|
||||
viewModelOf(::SettingsViewModel)
|
||||
viewModelOf(::GeneralSettingsViewModel)
|
||||
viewModelOf(::AdvancedSettingsViewModel)
|
||||
viewModelOf(::AppSelectorViewModel)
|
||||
viewModelOf(::VersionSelectorViewModel)
|
||||
viewModelOf(::PatcherViewModel)
|
||||
viewModelOf(::UpdateViewModel)
|
||||
viewModelOf(::ChangelogsViewModel)
|
||||
viewModelOf(::ImportExportViewModel)
|
||||
viewModelOf(::AboutViewModel)
|
||||
viewModelOf(::DeveloperOptionsViewModel)
|
||||
viewModelOf(::ContributorViewModel)
|
||||
viewModelOf(::DownloadsViewModel)
|
||||
viewModelOf(::InstalledAppsViewModel)
|
||||
|
@ -4,22 +4,18 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardCopyOption
|
||||
|
||||
class LocalPatchBundle(name: String, id: Int, directory: File) : PatchBundleSource(name, id, directory) {
|
||||
suspend fun replace(patches: InputStream? = null, integrations: InputStream? = null) {
|
||||
class LocalPatchBundle(name: String, id: Int, directory: File) :
|
||||
PatchBundleSource(name, id, directory) {
|
||||
suspend fun replace(patches: InputStream) {
|
||||
withContext(Dispatchers.IO) {
|
||||
patches?.let { inputStream ->
|
||||
patchBundleOutputStream().use { outputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
integrations?.let {
|
||||
Files.copy(it, this@LocalPatchBundle.integrationsFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
patchBundleOutputStream().use { outputStream ->
|
||||
patches.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
|
||||
reload()
|
||||
reload()?.also {
|
||||
saveVersion(it.readManifestAttribute("Version"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,22 @@
|
||||
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.flowOf
|
||||
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
|
||||
|
||||
@ -14,13 +24,20 @@ import java.io.OutputStream
|
||||
* A [PatchBundle] source.
|
||||
*/
|
||||
@Stable
|
||||
sealed class PatchBundleSource(val name: String, val uid: Int, directory: File) {
|
||||
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")
|
||||
protected val integrationsFile = directory.resolve("integrations.apk")
|
||||
|
||||
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.
|
||||
*/
|
||||
@ -40,15 +57,40 @@ sealed class PatchBundleSource(val name: String, val uid: Int, directory: File)
|
||||
if (!hasInstalled()) return State.Missing
|
||||
|
||||
return try {
|
||||
State.Loaded(PatchBundle(patchesFile, integrationsFile.takeIf(File::exists)))
|
||||
State.Loaded(PatchBundle(patchesFile))
|
||||
} catch (t: Throwable) {
|
||||
Log.e(tag, "Failed to load patch bundle $name", t)
|
||||
Log.e(tag, "Failed to load patch bundle with UID $uid", t)
|
||||
State.Failed(t)
|
||||
}
|
||||
}
|
||||
|
||||
fun reload() {
|
||||
_state.value = load()
|
||||
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 {
|
||||
@ -61,9 +103,12 @@ sealed class PatchBundleSource(val name: String, val uid: Int, directory: File)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val PatchBundleSource.isDefault get() = uid == 0
|
||||
val PatchBundleSource.asRemoteOrNull get() = this as? RemotePatchBundle
|
||||
fun PatchBundleSource.propsOrNullFlow() = asRemoteOrNull?.propsFlow() ?: flowOf(null)
|
||||
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,54 +1,31 @@
|
||||
package app.revanced.manager.domain.bundles
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import app.revanced.manager.data.room.bundles.VersionInfo
|
||||
import app.revanced.manager.domain.repository.PatchBundlePersistenceRepository
|
||||
import app.revanced.manager.network.api.ReVancedAPI
|
||||
import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType
|
||||
import app.revanced.manager.network.dto.BundleAsset
|
||||
import app.revanced.manager.network.dto.BundleInfo
|
||||
import app.revanced.manager.network.dto.ReVancedAsset
|
||||
import app.revanced.manager.network.service.HttpService
|
||||
import app.revanced.manager.network.utils.getOrThrow
|
||||
import app.revanced.manager.util.APK_MIMETYPE
|
||||
import app.revanced.manager.util.JAR_MIMETYPE
|
||||
import io.ktor.client.request.url
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.io.File
|
||||
|
||||
@Stable
|
||||
sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpoint: String) :
|
||||
PatchBundleSource(name, id, directory), KoinComponent {
|
||||
private val configRepository: PatchBundlePersistenceRepository by inject()
|
||||
PatchBundleSource(name, id, directory) {
|
||||
protected val http: HttpService by inject()
|
||||
|
||||
protected abstract suspend fun getLatestInfo(): BundleInfo
|
||||
protected abstract suspend fun getLatestInfo(): ReVancedAsset
|
||||
|
||||
private suspend fun download(info: BundleInfo) = withContext(Dispatchers.IO) {
|
||||
val (patches, integrations) = info
|
||||
coroutineScope {
|
||||
launch {
|
||||
patchBundleOutputStream().use {
|
||||
http.streamTo(it) {
|
||||
url(patches.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
http.download(integrationsFile) {
|
||||
url(integrations.url)
|
||||
}
|
||||
private suspend fun download(info: ReVancedAsset) = withContext(Dispatchers.IO) {
|
||||
patchBundleOutputStream().use {
|
||||
http.streamTo(it) {
|
||||
url(info.downloadUrl)
|
||||
}
|
||||
}
|
||||
|
||||
saveVersion(patches.version, integrations.version)
|
||||
saveVersion(info.version)
|
||||
reload()
|
||||
}
|
||||
|
||||
@ -58,29 +35,18 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
|
||||
|
||||
suspend fun update(): Boolean = withContext(Dispatchers.IO) {
|
||||
val info = getLatestInfo()
|
||||
if (hasInstalled() && VersionInfo(
|
||||
info.patches.version,
|
||||
info.integrations.version
|
||||
) == currentVersion()
|
||||
) {
|
||||
if (hasInstalled() && info.version == currentVersion())
|
||||
return@withContext false
|
||||
}
|
||||
|
||||
download(info)
|
||||
true
|
||||
}
|
||||
|
||||
private suspend fun currentVersion() = configRepository.getProps(uid).first().versionInfo
|
||||
private suspend fun saveVersion(patches: String, integrations: String) =
|
||||
configRepository.updateVersion(uid, patches, integrations)
|
||||
|
||||
suspend fun deleteLocalFiles() = withContext(Dispatchers.Default) {
|
||||
arrayOf(patchesFile, integrationsFile).forEach(File::delete)
|
||||
patchesFile.delete()
|
||||
reload()
|
||||
}
|
||||
|
||||
fun propsFlow() = configRepository.getProps(uid)
|
||||
|
||||
suspend fun setAutoUpdate(value: Boolean) = configRepository.setAutoUpdate(uid, value)
|
||||
|
||||
companion object {
|
||||
@ -91,7 +57,7 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
|
||||
class JsonPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
|
||||
RemotePatchBundle(name, id, directory, endpoint) {
|
||||
override suspend fun getLatestInfo() = withContext(Dispatchers.IO) {
|
||||
http.request<BundleInfo> {
|
||||
http.request<ReVancedAsset> {
|
||||
url(endpoint)
|
||||
}.getOrThrow()
|
||||
}
|
||||
@ -101,22 +67,5 @@ class APIPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
|
||||
RemotePatchBundle(name, id, directory, endpoint) {
|
||||
private val api: ReVancedAPI by inject()
|
||||
|
||||
override suspend fun getLatestInfo() = coroutineScope {
|
||||
fun getAssetAsync(repo: String, mime: String) = async(Dispatchers.IO) {
|
||||
api
|
||||
.getLatestRelease(repo)
|
||||
.getOrThrow()
|
||||
.let {
|
||||
BundleAsset(it.version, it.findAssetByType(mime).downloadUrl)
|
||||
}
|
||||
}
|
||||
|
||||
val patches = getAssetAsync("revanced-patches", JAR_MIMETYPE)
|
||||
val integrations = getAssetAsync("revanced-integrations", APK_MIMETYPE)
|
||||
|
||||
BundleInfo(
|
||||
patches.await(),
|
||||
integrations.await()
|
||||
)
|
||||
}
|
||||
override suspend fun getLatestInfo() = api.getPatchesUpdate().getOrThrow()
|
||||
}
|
@ -1,49 +1,97 @@
|
||||
package app.revanced.manager.domain.installer
|
||||
|
||||
import android.app.Application
|
||||
import app.revanced.manager.service.RootConnection
|
||||
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 rootConnection: RootConnection,
|
||||
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 isAppInstalled(packageName: String) =
|
||||
rootConnection.remoteFS?.getFile("$modulesPath/$packageName-revanced")
|
||||
?.exists() ?: throw RootServiceException()
|
||||
fun isDeviceRooted() = System.getenv("PATH")?.split(":")?.any { path ->
|
||||
File(path, "su").canExecute()
|
||||
} ?: false
|
||||
|
||||
fun isAppMounted(packageName: String): Boolean {
|
||||
return pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir?.let {
|
||||
Shell.cmd("mount | grep \"$it\"").exec().isSuccess
|
||||
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
|
||||
}
|
||||
|
||||
fun mount(packageName: String) {
|
||||
suspend fun mount(packageName: String) {
|
||||
if (isAppMounted(packageName)) return
|
||||
|
||||
val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir
|
||||
?: throw Exception("Failed to load application info")
|
||||
val patchedAPK = "$modulesPath/$packageName-revanced/$packageName.apk"
|
||||
withContext(Dispatchers.IO) {
|
||||
val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir
|
||||
?: throw Exception("Failed to load application info")
|
||||
val patchedAPK = "$modulesPath/$packageName-revanced/$packageName.apk"
|
||||
|
||||
Shell.cmd("mount -o bind \"$patchedAPK\" \"$stockAPK\"").exec()
|
||||
.also { if (!it.isSuccess) throw Exception("Failed to mount APK") }
|
||||
execute("mount -o bind \"$patchedAPK\" \"$stockAPK\"").assertSuccess("Failed to mount APK")
|
||||
}
|
||||
}
|
||||
|
||||
fun unmount(packageName: String) {
|
||||
suspend fun unmount(packageName: String) {
|
||||
if (!isAppMounted(packageName)) return
|
||||
|
||||
val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir
|
||||
?: throw Exception("Failed to load application info")
|
||||
withContext(Dispatchers.IO) {
|
||||
val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir
|
||||
?: throw Exception("Failed to load application info")
|
||||
|
||||
Shell.cmd("umount -l \"$stockAPK\"").exec()
|
||||
.also { if (!it.isSuccess) throw Exception("Failed to unmount APK") }
|
||||
execute("umount -l \"$stockAPK\"").assertSuccess("Failed to unmount APK")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun install(
|
||||
@ -52,80 +100,82 @@ class RootInstaller(
|
||||
packageName: String,
|
||||
version: String,
|
||||
label: String
|
||||
) {
|
||||
withContext(Dispatchers.IO) {
|
||||
rootConnection.remoteFS?.let { remoteFS ->
|
||||
val assets = app.assets
|
||||
val modulePath = "$modulesPath/$packageName-revanced"
|
||||
) = withContext(Dispatchers.IO) {
|
||||
val remoteFS = awaitRemoteFS()
|
||||
val assets = app.assets
|
||||
val modulePath = "$modulesPath/$packageName-revanced"
|
||||
|
||||
unmount(packageName)
|
||||
unmount(packageName)
|
||||
|
||||
stockAPK?.let { stockApp ->
|
||||
pm.getPackageInfo(packageName)?.let { packageInfo ->
|
||||
if (packageInfo.versionName <= version)
|
||||
Shell.cmd("pm uninstall -k --user 0 $packageName").exec()
|
||||
.also { if (!it.isSuccess) throw Exception("Failed to uninstall stock app") }
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Shell.cmd("pm install \"${stockApp.absolutePath}\"").exec()
|
||||
.also { if (!it.isSuccess) throw Exception("Failed to install stock app") }
|
||||
}
|
||||
"$modulePath/$packageName.apk".let { apkPath ->
|
||||
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
"$modulePath/$packageName.apk".let { apkPath ->
|
||||
|
||||
remoteFS.getFile(patchedAPK.absolutePath)
|
||||
.also { if (!it.exists()) throw Exception("File doesn't exist") }
|
||||
.newInputStream().use { inputStream ->
|
||||
remoteFS.getFile(apkPath).newOutputStream().use { outputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
|
||||
Shell.cmd(
|
||||
"chmod 644 $apkPath",
|
||||
"chown system:system $apkPath",
|
||||
"chcon u:object_r:apk_data_file:s0 $apkPath",
|
||||
"chmod +x $modulePath/service.sh"
|
||||
).exec()
|
||||
.let { if (!it.isSuccess) throw Exception("Failed to set file permissions") }
|
||||
}
|
||||
} ?: throw RootServiceException()
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
fun uninstall(packageName: String) {
|
||||
rootConnection.remoteFS?.let { remoteFS ->
|
||||
if (isAppMounted(packageName))
|
||||
unmount(packageName)
|
||||
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") }
|
||||
} ?: throw RootServiceException()
|
||||
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")
|
||||
class RootServiceException : Exception("Root not available")
|
@ -12,6 +12,8 @@ import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.file.Files
|
||||
import java.security.UnrecoverableKeyException
|
||||
import java.util.Date
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
|
||||
companion object Constants {
|
||||
@ -19,6 +21,7 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
|
||||
* Default alias and password for the keystore.
|
||||
*/
|
||||
const val DEFAULT = "ReVanced"
|
||||
private val eightYearsFromNow get() = Date(System.currentTimeMillis() + (365.days * 8).inWholeMilliseconds * 24)
|
||||
}
|
||||
|
||||
private val keystorePath =
|
||||
@ -29,40 +32,45 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
|
||||
prefs.keystorePass.value = pass
|
||||
}
|
||||
|
||||
private suspend fun signingOptions(path: File = keystorePath) = ApkUtils.SigningOptions(
|
||||
private suspend fun signingDetails(path: File = keystorePath) = ApkUtils.KeyStoreDetails(
|
||||
keyStore = path,
|
||||
keyStorePassword = null,
|
||||
alias = prefs.keystoreCommonName.get(),
|
||||
signer = prefs.keystoreCommonName.get(),
|
||||
password = prefs.keystorePass.get()
|
||||
)
|
||||
|
||||
suspend fun sign(input: File, output: File) = withContext(Dispatchers.Default) {
|
||||
ApkUtils.sign(input, output, signingOptions())
|
||||
ApkUtils.signApk(input, output, prefs.keystoreCommonName.get(), signingDetails())
|
||||
}
|
||||
|
||||
suspend fun regenerate() = withContext(Dispatchers.Default) {
|
||||
val keyCertPair = ApkSigner.newPrivateKeyCertificatePair(
|
||||
prefs.keystoreCommonName.get(),
|
||||
eightYearsFromNow
|
||||
)
|
||||
val ks = ApkSigner.newKeyStore(
|
||||
listOf(
|
||||
setOf(
|
||||
ApkSigner.KeyStoreEntry(
|
||||
DEFAULT, DEFAULT
|
||||
DEFAULT, DEFAULT, keyCertPair
|
||||
)
|
||||
)
|
||||
)
|
||||
keystorePath.outputStream().use {
|
||||
ks.store(it, null)
|
||||
withContext(Dispatchers.IO) {
|
||||
keystorePath.outputStream().use {
|
||||
ks.store(it, null)
|
||||
}
|
||||
}
|
||||
|
||||
updatePrefs(DEFAULT, DEFAULT)
|
||||
}
|
||||
|
||||
suspend fun import(cn: String, pass: String, keystore: InputStream): Boolean {
|
||||
val keystoreData = keystore.readBytes()
|
||||
val keystoreData = withContext(Dispatchers.IO) { keystore.readBytes() }
|
||||
|
||||
try {
|
||||
val ks = ApkSigner.readKeyStore(ByteArrayInputStream(keystoreData), null)
|
||||
|
||||
ApkSigner.readKeyCertificatePair(ks, cn, pass)
|
||||
ApkSigner.readPrivateKeyCertificatePair(ks, cn, pass)
|
||||
} catch (_: UnrecoverableKeyException) {
|
||||
return false
|
||||
} catch (_: IllegalArgumentException) {
|
||||
|
@ -3,6 +3,7 @@ package app.revanced.manager.domain.manager
|
||||
import android.content.Context
|
||||
import app.revanced.manager.domain.manager.base.BasePreferencesManager
|
||||
import app.revanced.manager.ui.theme.Theme
|
||||
import app.revanced.manager.util.isDebuggable
|
||||
|
||||
class PreferencesManager(
|
||||
context: Context
|
||||
@ -12,17 +13,22 @@ class PreferencesManager(
|
||||
|
||||
val api = stringPreference("api_url", "https://api.revanced.app")
|
||||
|
||||
val multithreadingDexFileWriter = booleanPreference("multithreading_dex_file_writer", true)
|
||||
val disablePatchVersionCompatCheck = booleanPreference("disable_patch_version_compatibility_check", false)
|
||||
val useProcessRuntime = booleanPreference("use_process_runtime", false)
|
||||
val patcherProcessMemoryLimit = intPreference("process_runtime_memory_limit", 700)
|
||||
|
||||
val keystoreCommonName = stringPreference("keystore_cn", KeystoreManager.DEFAULT)
|
||||
val keystorePass = stringPreference("keystore_pass", KeystoreManager.DEFAULT)
|
||||
|
||||
val preferSplits = booleanPreference("prefer_splits", false)
|
||||
|
||||
val firstLaunch = booleanPreference("first_launch", true)
|
||||
val managerAutoUpdates = booleanPreference("manager_auto_updates", false)
|
||||
val showManagerUpdateDialogOnLaunch = booleanPreference("show_manager_update_dialog_on_launch", true)
|
||||
|
||||
val disablePatchVersionCompatCheck = booleanPreference("disable_patch_version_compatibility_check", false)
|
||||
val disableSelectionWarning = booleanPreference("disable_selection_warning", false)
|
||||
val enableSelectionWarningCountdown = booleanPreference("enable_selection_warning_countdown", true)
|
||||
val disableUniversalPatchCheck = booleanPreference("disable_patch_universal_check", false)
|
||||
val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true)
|
||||
|
||||
val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet())
|
||||
|
||||
val showDeveloperSettings = booleanPreference("show_developer_settings", context.isDebuggable)
|
||||
}
|
||||
|
@ -26,6 +26,9 @@ abstract class BasePreferencesManager(private val context: Context, name: String
|
||||
protected fun stringPreference(key: String, default: String) =
|
||||
StringPreference(dataStore, key, default)
|
||||
|
||||
protected fun stringSetPreference(key: String, default: Set<String>) =
|
||||
StringSetPreference(dataStore, key, default)
|
||||
|
||||
protected fun booleanPreference(key: String, default: Boolean) =
|
||||
BooleanPreference(dataStore, key, default)
|
||||
|
||||
@ -52,11 +55,15 @@ class EditorContext(private val prefs: MutablePreferences) {
|
||||
var <T> Preference<T>.value
|
||||
get() = prefs.run { read() }
|
||||
set(value) = prefs.run { write(value) }
|
||||
|
||||
operator fun Preference<Set<String>>.plusAssign(value: String) = prefs.run {
|
||||
write(read() + value)
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Preference<T>(
|
||||
private val dataStore: DataStore<Preferences>,
|
||||
protected val default: T
|
||||
val default: T
|
||||
) {
|
||||
internal abstract fun Preferences.read(): T
|
||||
internal abstract fun MutablePreferences.write(value: T)
|
||||
@ -65,10 +72,12 @@ abstract class Preference<T>(
|
||||
|
||||
suspend fun get() = flow.first()
|
||||
fun getBlocking() = runBlocking { get() }
|
||||
|
||||
@Composable
|
||||
fun getAsState() = flow.collectAsStateWithLifecycle(initialValue = remember {
|
||||
getBlocking()
|
||||
})
|
||||
|
||||
suspend fun update(value: T) = dataStore.editor {
|
||||
this@Preference.value = value
|
||||
}
|
||||
@ -108,6 +117,14 @@ class StringPreference(
|
||||
override val key = stringPreferencesKey(key)
|
||||
}
|
||||
|
||||
class StringSetPreference(
|
||||
dataStore: DataStore<Preferences>,
|
||||
key: String,
|
||||
default: Set<String>
|
||||
) : BasePreference<Set<String>>(dataStore, default) {
|
||||
override val key = stringSetPreferencesKey(key)
|
||||
}
|
||||
|
||||
class BooleanPreference(
|
||||
dataStore: DataStore<Preferences>,
|
||||
key: String,
|
||||
|
@ -2,56 +2,126 @@ package app.revanced.manager.domain.repository
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import app.revanced.manager.data.room.AppDatabase
|
||||
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
||||
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
|
||||
import app.revanced.manager.network.downloader.AppDownloader
|
||||
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||
import app.revanced.manager.plugin.downloader.OutputDownloadScope
|
||||
import app.revanced.manager.util.PM
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import java.io.File
|
||||
import java.io.FilterOutputStream
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import kotlin.io.path.outputStream
|
||||
|
||||
class DownloadedAppRepository(
|
||||
app: Application,
|
||||
db: AppDatabase
|
||||
private val app: Application,
|
||||
db: AppDatabase,
|
||||
private val pm: PM
|
||||
) {
|
||||
private val dir = app.getDir("downloaded-apps", Context.MODE_PRIVATE)
|
||||
private val dao = db.downloadedAppDao()
|
||||
|
||||
fun getAll() = dao.getAllApps().distinctUntilChanged()
|
||||
|
||||
fun getApkFileForApp(app: DownloadedApp): File = getApkFileForDir(dir.resolve(app.directory))
|
||||
fun getApkFileForApp(app: DownloadedApp): File =
|
||||
getApkFileForDir(dir.resolve(app.directory))
|
||||
|
||||
private fun getApkFileForDir(directory: File) = directory.listFiles()!!.first()
|
||||
|
||||
suspend fun download(
|
||||
app: AppDownloader.App,
|
||||
preferSplits: Boolean,
|
||||
onDownload: suspend (downloadProgress: Pair<Float, Float>?) -> Unit = {},
|
||||
plugin: LoadedDownloaderPlugin,
|
||||
data: Parcelable,
|
||||
expectedPackageName: String,
|
||||
expectedVersion: String?,
|
||||
onDownload: suspend (downloadProgress: Pair<Long, Long?>) -> Unit,
|
||||
): File {
|
||||
this.get(app.packageName, app.version)?.let { downloaded ->
|
||||
return getApkFileForApp(downloaded)
|
||||
}
|
||||
|
||||
// Converted integers cannot contain / or .. unlike the package name or version, so they are safer to use here.
|
||||
val relativePath = File(generateUid().toString())
|
||||
val savePath = dir.resolve(relativePath).also { it.mkdirs() }
|
||||
val saveDir = dir.resolve(relativePath).also { it.mkdirs() }
|
||||
val targetFile = saveDir.resolve("base.apk").toPath()
|
||||
|
||||
try {
|
||||
app.download(savePath, preferSplits, onDownload)
|
||||
val downloadSize = AtomicLong(0)
|
||||
val downloadedBytes = AtomicLong(0)
|
||||
|
||||
dao.insert(DownloadedApp(
|
||||
packageName = app.packageName,
|
||||
version = app.version,
|
||||
directory = relativePath,
|
||||
))
|
||||
channelFlow {
|
||||
val scope = object : OutputDownloadScope {
|
||||
override val pluginPackageName = plugin.packageName
|
||||
override val hostPackageName = app.packageName
|
||||
override suspend fun reportSize(size: Long) {
|
||||
require(size > 0) { "Size must be greater than zero" }
|
||||
require(
|
||||
downloadSize.compareAndSet(
|
||||
0,
|
||||
size
|
||||
)
|
||||
) { "Download size has already been set" }
|
||||
send(downloadedBytes.get() to size)
|
||||
}
|
||||
}
|
||||
|
||||
fun emitProgress(bytes: Long) {
|
||||
val newValue = downloadedBytes.addAndGet(bytes)
|
||||
val totalSize = downloadSize.get()
|
||||
if (totalSize < 1) return
|
||||
trySend(newValue to totalSize).getOrThrow()
|
||||
}
|
||||
|
||||
targetFile.outputStream(StandardOpenOption.CREATE_NEW).buffered().use {
|
||||
val stream = object : FilterOutputStream(it) {
|
||||
override fun write(b: Int) = out.write(b).also { emitProgress(1) }
|
||||
|
||||
override fun write(b: ByteArray?, off: Int, len: Int) =
|
||||
out.write(b, off, len).also {
|
||||
emitProgress(
|
||||
(len - off).toLong()
|
||||
)
|
||||
}
|
||||
}
|
||||
plugin.download(scope, data, stream)
|
||||
}
|
||||
}
|
||||
.conflate()
|
||||
.flowOn(Dispatchers.IO)
|
||||
.collect { (downloaded, size) -> onDownload(downloaded to size) }
|
||||
|
||||
if (downloadedBytes.get() < 1) error("Downloader did not download anything.")
|
||||
val pkgInfo =
|
||||
pm.getPackageInfo(targetFile.toFile()) ?: error("Downloaded APK file is invalid")
|
||||
if (pkgInfo.packageName != expectedPackageName) error("Downloaded APK has the wrong package name. Expected: $expectedPackageName, Actual: ${pkgInfo.packageName}")
|
||||
if (expectedVersion != null && pkgInfo.versionName != expectedVersion) error("Downloaded APK has the wrong version. Expected: $expectedVersion, Actual: ${pkgInfo.versionName}")
|
||||
|
||||
// Delete the previous copy (if present).
|
||||
dao.get(pkgInfo.packageName, pkgInfo.versionName!!)?.directory?.let {
|
||||
if (!dir.resolve(it).deleteRecursively()) throw Exception("Failed to delete existing directory")
|
||||
}
|
||||
dao.upsert(
|
||||
DownloadedApp(
|
||||
packageName = pkgInfo.packageName,
|
||||
version = pkgInfo.versionName!!,
|
||||
directory = relativePath,
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
savePath.deleteRecursively()
|
||||
saveDir.deleteRecursively()
|
||||
throw e
|
||||
}
|
||||
|
||||
// Return the Apk file.
|
||||
return getApkFileForDir(savePath)
|
||||
return getApkFileForDir(saveDir)
|
||||
}
|
||||
|
||||
suspend fun get(packageName: String, version: String) = dao.get(packageName, version)
|
||||
suspend fun get(packageName: String, version: String, markUsed: Boolean = false) =
|
||||
dao.get(packageName, version)?.also {
|
||||
if (markUsed) dao.markUsed(packageName, version)
|
||||
}
|
||||
|
||||
suspend fun delete(downloadedApps: Collection<DownloadedApp>) {
|
||||
downloadedApps.forEach {
|
||||
|
@ -0,0 +1,168 @@
|
||||
package app.revanced.manager.domain.repository
|
||||
|
||||
import android.app.Application
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import app.revanced.manager.data.room.AppDatabase
|
||||
import app.revanced.manager.data.room.plugins.TrustedDownloaderPlugin
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.network.downloader.DownloaderPluginState
|
||||
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||
import app.revanced.manager.network.downloader.ParceledDownloaderData
|
||||
import app.revanced.manager.plugin.downloader.DownloaderBuilder
|
||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||
import app.revanced.manager.plugin.downloader.Scope
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.tag
|
||||
import dalvik.system.PathClassLoader
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.lang.reflect.Modifier
|
||||
|
||||
@OptIn(PluginHostApi::class)
|
||||
class DownloaderPluginRepository(
|
||||
private val pm: PM,
|
||||
private val prefs: PreferencesManager,
|
||||
private val app: Application,
|
||||
db: AppDatabase
|
||||
) {
|
||||
private val trustDao = db.trustedDownloaderPluginDao()
|
||||
private val _pluginStates = MutableStateFlow(emptyMap<String, DownloaderPluginState>())
|
||||
val pluginStates = _pluginStates.asStateFlow()
|
||||
val loadedPluginsFlow = pluginStates.map { states ->
|
||||
states.values.filterIsInstance<DownloaderPluginState.Loaded>().map { it.plugin }
|
||||
}
|
||||
|
||||
private val acknowledgedDownloaderPlugins = prefs.acknowledgedDownloaderPlugins
|
||||
private val installedPluginPackageNames = MutableStateFlow(emptySet<String>())
|
||||
val newPluginPackageNames = combine(
|
||||
installedPluginPackageNames,
|
||||
acknowledgedDownloaderPlugins.flow
|
||||
) { installed, acknowledged ->
|
||||
installed subtract acknowledged
|
||||
}
|
||||
|
||||
suspend fun reload() {
|
||||
val plugins =
|
||||
withContext(Dispatchers.IO) {
|
||||
pm.getPackagesWithFeature(PLUGIN_FEATURE)
|
||||
.associate { it.packageName to loadPlugin(it.packageName) }
|
||||
}
|
||||
|
||||
_pluginStates.value = plugins
|
||||
installedPluginPackageNames.value = plugins.keys
|
||||
|
||||
val acknowledgedPlugins = acknowledgedDownloaderPlugins.get()
|
||||
val uninstalledPlugins = acknowledgedPlugins subtract installedPluginPackageNames.value
|
||||
if (uninstalledPlugins.isNotEmpty()) {
|
||||
Log.d(tag, "Uninstalled plugins: ${uninstalledPlugins.joinToString(", ")}")
|
||||
acknowledgedDownloaderPlugins.update(acknowledgedPlugins subtract uninstalledPlugins)
|
||||
trustDao.removeAll(uninstalledPlugins)
|
||||
}
|
||||
}
|
||||
|
||||
fun unwrapParceledData(data: ParceledDownloaderData): Pair<LoadedDownloaderPlugin, Parcelable> {
|
||||
val plugin =
|
||||
(_pluginStates.value[data.pluginPackageName] as? DownloaderPluginState.Loaded)?.plugin
|
||||
?: throw Exception("Downloader plugin with name ${data.pluginPackageName} is not available")
|
||||
|
||||
return plugin to data.unwrapWith(plugin)
|
||||
}
|
||||
|
||||
private suspend fun loadPlugin(packageName: String): DownloaderPluginState {
|
||||
try {
|
||||
if (!verify(packageName)) return DownloaderPluginState.Untrusted
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Got exception while verifying plugin $packageName", e)
|
||||
return DownloaderPluginState.Failed(e)
|
||||
}
|
||||
|
||||
return try {
|
||||
val packageInfo = pm.getPackageInfo(packageName, flags = PackageManager.GET_META_DATA)!!
|
||||
val className = packageInfo.applicationInfo!!.metaData.getString(METADATA_PLUGIN_CLASS)
|
||||
?: throw Exception("Missing metadata attribute $METADATA_PLUGIN_CLASS")
|
||||
|
||||
val classLoader =
|
||||
PathClassLoader(packageInfo.applicationInfo!!.sourceDir, app.classLoader)
|
||||
val pluginContext = app.createPackageContext(packageName, 0)
|
||||
|
||||
val downloader = classLoader
|
||||
.loadClass(className)
|
||||
.getDownloaderBuilder()
|
||||
.build(
|
||||
scopeImpl = object : Scope {
|
||||
override val hostPackageName = app.packageName
|
||||
override val pluginPackageName = pluginContext.packageName
|
||||
},
|
||||
context = pluginContext
|
||||
)
|
||||
|
||||
DownloaderPluginState.Loaded(
|
||||
LoadedDownloaderPlugin(
|
||||
packageName,
|
||||
with(pm) { packageInfo.label() },
|
||||
packageInfo.versionName!!,
|
||||
downloader.get,
|
||||
downloader.download,
|
||||
classLoader
|
||||
)
|
||||
)
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (t: Throwable) {
|
||||
Log.e(tag, "Failed to load plugin $packageName", t)
|
||||
DownloaderPluginState.Failed(t)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun trustPackage(packageName: String) {
|
||||
trustDao.upsertTrust(
|
||||
TrustedDownloaderPlugin(
|
||||
packageName,
|
||||
pm.getSignature(packageName).toByteArray()
|
||||
)
|
||||
)
|
||||
|
||||
reload()
|
||||
prefs.edit {
|
||||
acknowledgedDownloaderPlugins += packageName
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun revokeTrustForPackage(packageName: String) =
|
||||
trustDao.remove(packageName).also { reload() }
|
||||
|
||||
suspend fun acknowledgeAllNewPlugins() =
|
||||
acknowledgedDownloaderPlugins.update(installedPluginPackageNames.value)
|
||||
|
||||
private suspend fun verify(packageName: String): Boolean {
|
||||
val expectedSignature =
|
||||
trustDao.getTrustedSignature(packageName) ?: return false
|
||||
|
||||
return pm.hasSignature(packageName, expectedSignature)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val PLUGIN_FEATURE = "app.revanced.manager.plugin.downloader"
|
||||
const val METADATA_PLUGIN_CLASS = "app.revanced.manager.plugin.downloader.class"
|
||||
|
||||
const val PUBLIC_STATIC = Modifier.PUBLIC or Modifier.STATIC
|
||||
val Int.isPublicStatic get() = (this and PUBLIC_STATIC) == PUBLIC_STATIC
|
||||
val Class<*>.isDownloaderBuilder get() = DownloaderBuilder::class.java.isAssignableFrom(this)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun Class<*>.getDownloaderBuilder() =
|
||||
declaredMethods
|
||||
.firstOrNull { it.modifiers.isPublicStatic && it.returnType.isDownloaderBuilder && it.parameterTypes.isEmpty() }
|
||||
?.let { it(null) as DownloaderBuilder<Parcelable> }
|
||||
?: throw Exception("Could not find a valid downloader implementation in class $canonicalName")
|
||||
}
|
||||
}
|
@ -4,8 +4,6 @@ import app.revanced.manager.data.room.AppDatabase
|
||||
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
||||
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||
import app.revanced.manager.data.room.bundles.Source
|
||||
import app.revanced.manager.data.room.bundles.VersionInfo
|
||||
import io.ktor.http.*
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
class PatchBundlePersistenceRepository(db: AppDatabase) {
|
||||
@ -23,12 +21,11 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
|
||||
|
||||
suspend fun reset() = dao.reset()
|
||||
|
||||
|
||||
suspend fun create(name: String, source: Source, autoUpdate: Boolean = false) =
|
||||
PatchBundleEntity(
|
||||
uid = generateUid(),
|
||||
name = name,
|
||||
versionInfo = VersionInfo(),
|
||||
version = null,
|
||||
source = source,
|
||||
autoUpdate = autoUpdate
|
||||
).also {
|
||||
@ -37,18 +34,20 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
|
||||
|
||||
suspend fun delete(uid: Int) = dao.remove(uid)
|
||||
|
||||
suspend fun updateVersion(uid: Int, patches: String, integrations: String) =
|
||||
dao.updateVersion(uid, patches, integrations)
|
||||
suspend fun updateVersion(uid: Int, version: String?) =
|
||||
dao.updateVersion(uid, version)
|
||||
|
||||
suspend fun setAutoUpdate(uid: Int, value: Boolean) = dao.setAutoUpdate(uid, value)
|
||||
|
||||
suspend fun setName(uid: Int, name: String) = dao.setName(uid, name)
|
||||
|
||||
fun getProps(id: Int) = dao.getPropsById(id).distinctUntilChanged()
|
||||
|
||||
private companion object {
|
||||
val defaultSource = PatchBundleEntity(
|
||||
uid = 0,
|
||||
name = "Main",
|
||||
versionInfo = VersionInfo(),
|
||||
name = "",
|
||||
version = null,
|
||||
source = Source.API,
|
||||
autoUpdate = false
|
||||
)
|
||||
|
@ -3,6 +3,7 @@ package app.revanced.manager.domain.repository
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import app.revanced.library.mostCommonCompatibleVersions
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.platform.NetworkInfo
|
||||
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||
@ -12,6 +13,8 @@ import app.revanced.manager.data.room.bundles.Source as SourceInfo
|
||||
import app.revanced.manager.domain.bundles.LocalPatchBundle
|
||||
import app.revanced.manager.domain.bundles.RemotePatchBundle
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.patcher.patch.PatchInfo
|
||||
import app.revanced.manager.util.flatMapLatestAndCombine
|
||||
import app.revanced.manager.util.tag
|
||||
import app.revanced.manager.util.uiSafe
|
||||
@ -29,6 +32,7 @@ class PatchBundleRepository(
|
||||
private val app: Application,
|
||||
private val persistenceRepo: PatchBundlePersistenceRepository,
|
||||
private val networkInfo: NetworkInfo,
|
||||
private val prefs: PreferencesManager,
|
||||
) {
|
||||
private val bundlesDir = app.getDir("patch_bundles", Context.MODE_PRIVATE)
|
||||
|
||||
@ -47,6 +51,37 @@ class PatchBundleRepository(
|
||||
it.state.map { state -> it.uid to state }
|
||||
}
|
||||
|
||||
val suggestedVersions = bundles.map {
|
||||
val allPatches =
|
||||
it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet()
|
||||
|
||||
allPatches.mostCommonCompatibleVersions(countUnusedPatches = true)
|
||||
.mapValues { (_, versions) ->
|
||||
if (versions.keys.size < 2)
|
||||
return@mapValues versions.keys.firstOrNull()
|
||||
|
||||
// The entries are ordered from most compatible to least compatible.
|
||||
// If there are entries with the same number of compatible patches, older versions will be first, which is undesirable.
|
||||
// This means we have to pick the last entry we find that has the highest patch count.
|
||||
// The order may change in future versions of ReVanced Library.
|
||||
var currentHighestPatchCount = -1
|
||||
versions.entries.last { (_, patchCount) ->
|
||||
if (patchCount >= currentHighestPatchCount) {
|
||||
currentHighestPatchCount = patchCount
|
||||
true
|
||||
} else false
|
||||
}.key
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun isVersionAllowed(packageName: String, version: String) =
|
||||
withContext(Dispatchers.Default) {
|
||||
if (!prefs.suggestedVersionSafeguard.get()) return@withContext true
|
||||
|
||||
val suggestedVersion = suggestedVersions.first()[packageName] ?: return@withContext true
|
||||
suggestedVersion == version
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the directory of the [PatchBundleSource] with the specified [uid], creating it if needed.
|
||||
*/
|
||||
@ -102,16 +137,16 @@ class PatchBundleRepository(
|
||||
private fun addBundle(patchBundle: PatchBundleSource) =
|
||||
_sources.update { it.toMutableMap().apply { put(patchBundle.uid, patchBundle) } }
|
||||
|
||||
suspend fun createLocal(name: String, patches: InputStream, integrations: InputStream?) {
|
||||
val id = persistenceRepo.create(name, SourceInfo.Local).uid
|
||||
val bundle = LocalPatchBundle(name, id, directoryOf(id))
|
||||
suspend fun createLocal(patches: InputStream) = withContext(Dispatchers.Default) {
|
||||
val uid = persistenceRepo.create("", SourceInfo.Local).uid
|
||||
val bundle = LocalPatchBundle("", uid, directoryOf(uid))
|
||||
|
||||
bundle.replace(patches, integrations)
|
||||
bundle.replace(patches)
|
||||
addBundle(bundle)
|
||||
}
|
||||
|
||||
suspend fun createRemote(name: String, url: String, autoUpdate: Boolean) {
|
||||
val entity = persistenceRepo.create(name, SourceInfo.from(url), autoUpdate)
|
||||
suspend fun createRemote(url: String, autoUpdate: Boolean) = withContext(Dispatchers.Default) {
|
||||
val entity = persistenceRepo.create("", SourceInfo.from(url), autoUpdate)
|
||||
addBundle(entity.load())
|
||||
}
|
||||
|
||||
@ -139,8 +174,8 @@ class PatchBundleRepository(
|
||||
|
||||
getBundlesByType<RemotePatchBundle>().forEach {
|
||||
launch {
|
||||
if (!it.propsFlow().first().autoUpdate) return@launch
|
||||
Log.d(tag, "Updating patch bundle: ${it.name}")
|
||||
if (!it.getProps().autoUpdate) return@launch
|
||||
Log.d(tag, "Updating patch bundle: ${it.getName()}")
|
||||
it.update()
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,14 @@
|
||||
package app.revanced.manager.domain.repository
|
||||
|
||||
import android.util.Log
|
||||
import app.revanced.manager.data.room.AppDatabase
|
||||
import app.revanced.manager.data.room.options.Option
|
||||
import app.revanced.manager.data.room.options.OptionGroup
|
||||
import app.revanced.manager.patcher.patch.PatchInfo
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.tag
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.booleanOrNull
|
||||
import kotlinx.serialization.json.floatOrNull
|
||||
import kotlinx.serialization.json.intOrNull
|
||||
|
||||
class PatchOptionsRepository(db: AppDatabase) {
|
||||
private val dao = db.optionDao()
|
||||
@ -24,19 +20,37 @@ class PatchOptionsRepository(db: AppDatabase) {
|
||||
packageName = packageName
|
||||
).also { dao.createOptionGroup(it) }.uid
|
||||
|
||||
suspend fun getOptions(packageName: String): Options {
|
||||
suspend fun getOptions(
|
||||
packageName: String,
|
||||
bundlePatches: Map<Int, Map<String, PatchInfo>>
|
||||
): Options {
|
||||
val options = dao.getOptions(packageName)
|
||||
// Bundle -> Patches
|
||||
return buildMap<Int, MutableMap<String, MutableMap<String, Any?>>>(options.size) {
|
||||
options.forEach { (sourceUid, bundlePatchOptionsList) ->
|
||||
// Patches -> Patch options
|
||||
this[sourceUid] = bundlePatchOptionsList.fold(mutableMapOf()) { bundlePatchOptions, option ->
|
||||
val patchOptions = bundlePatchOptions.getOrPut(option.patchName, ::mutableMapOf)
|
||||
this[sourceUid] =
|
||||
bundlePatchOptionsList.fold(mutableMapOf()) { bundlePatchOptions, dbOption ->
|
||||
val deserializedPatchOptions =
|
||||
bundlePatchOptions.getOrPut(dbOption.patchName, ::mutableMapOf)
|
||||
|
||||
patchOptions[option.key] = deserialize(option.value)
|
||||
val option =
|
||||
bundlePatches[sourceUid]?.get(dbOption.patchName)?.options?.find { it.key == dbOption.key }
|
||||
if (option != null) {
|
||||
try {
|
||||
deserializedPatchOptions[option.key] =
|
||||
dbOption.value.deserializeFor(option)
|
||||
} catch (e: Option.SerializationException) {
|
||||
Log.w(
|
||||
tag,
|
||||
"Option ${dbOption.patchName}:${option.key} could not be deserialized",
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
bundlePatchOptions
|
||||
}
|
||||
bundlePatchOptions
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -47,8 +61,12 @@ class PatchOptionsRepository(db: AppDatabase) {
|
||||
|
||||
groupId to bundlePatchOptions.flatMap { (patchName, patchOptions) ->
|
||||
patchOptions.mapNotNull { (key, value) ->
|
||||
val serialized = serialize(value)
|
||||
?: return@mapNotNull null // Don't save options that we can't serialize.
|
||||
val serialized = try {
|
||||
Option.SerializedValue.fromValue(value)
|
||||
} catch (e: Option.SerializationException) {
|
||||
Log.e(tag, "Option $patchName:$key could not be serialized", e)
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
Option(groupId, patchName, key, serialized)
|
||||
}
|
||||
@ -58,32 +76,7 @@ class PatchOptionsRepository(db: AppDatabase) {
|
||||
fun getPackagesWithSavedOptions() =
|
||||
dao.getPackagesWithOptions().map(Iterable<String>::toSet).distinctUntilChanged()
|
||||
|
||||
suspend fun clearOptionsForPackage(packageName: String) = dao.clearForPackage(packageName)
|
||||
suspend fun clearOptionsForPatchBundle(uid: Int) = dao.clearForPatchBundle(uid)
|
||||
suspend fun resetOptionsForPackage(packageName: String) = dao.resetOptionsForPackage(packageName)
|
||||
suspend fun resetOptionsForPatchBundle(uid: Int) = dao.resetOptionsForPatchBundle(uid)
|
||||
suspend fun reset() = dao.reset()
|
||||
|
||||
private companion object {
|
||||
fun deserialize(value: String): Any? {
|
||||
val primitive = Json.decodeFromString<JsonPrimitive>(value)
|
||||
|
||||
return when {
|
||||
primitive.isString -> primitive.content
|
||||
primitive is JsonNull -> null
|
||||
else -> primitive.booleanOrNull ?: primitive.intOrNull ?: primitive.floatOrNull
|
||||
}
|
||||
}
|
||||
|
||||
fun serialize(value: Any?): String? {
|
||||
val primitive = when (value) {
|
||||
null -> JsonNull
|
||||
is String -> JsonPrimitive(value)
|
||||
is Int -> JsonPrimitive(value)
|
||||
is Float -> JsonPrimitive(value)
|
||||
is Boolean -> JsonPrimitive(value)
|
||||
else -> return null
|
||||
}
|
||||
|
||||
return Json.encodeToString(primitive)
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,8 @@ package app.revanced.manager.domain.repository
|
||||
import app.revanced.manager.data.room.AppDatabase
|
||||
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
||||
import app.revanced.manager.data.room.selection.PatchSelection
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class PatchSelectionRepository(db: AppDatabase) {
|
||||
private val dao = db.selectionDao()
|
||||
@ -25,8 +27,15 @@ class PatchSelectionRepository(db: AppDatabase) {
|
||||
)
|
||||
})
|
||||
|
||||
suspend fun clearSelection(packageName: String) {
|
||||
dao.clearForPackage(packageName)
|
||||
fun getPackagesWithSavedSelection() =
|
||||
dao.getPackagesWithSelection().map(Iterable<String>::toSet).distinctUntilChanged()
|
||||
|
||||
suspend fun resetSelectionForPackage(packageName: String) {
|
||||
dao.resetForPackage(packageName)
|
||||
}
|
||||
|
||||
suspend fun resetSelectionForPatchBundle(uid: Int) {
|
||||
dao.resetForPatchBundle(uid)
|
||||
}
|
||||
|
||||
suspend fun reset() = dao.reset()
|
||||
@ -34,7 +43,7 @@ class PatchSelectionRepository(db: AppDatabase) {
|
||||
suspend fun export(bundleUid: Int): SerializedSelection = dao.exportSelection(bundleUid)
|
||||
|
||||
suspend fun import(bundleUid: Int, selection: SerializedSelection) {
|
||||
dao.clearForPatchBundle(bundleUid)
|
||||
dao.resetForPatchBundle(bundleUid)
|
||||
dao.updateSelections(selection.entries.associate { (packageName, patches) ->
|
||||
getOrCreateSelection(bundleUid, packageName) to patches.toSet()
|
||||
})
|
||||
|
@ -2,34 +2,41 @@ package app.revanced.manager.network.api
|
||||
|
||||
import android.os.Build
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.network.dto.ReVancedRelease
|
||||
import app.revanced.manager.network.service.ReVancedService
|
||||
import app.revanced.manager.network.dto.ReVancedAsset
|
||||
import app.revanced.manager.network.dto.ReVancedGitRepository
|
||||
import app.revanced.manager.network.dto.ReVancedInfo
|
||||
import app.revanced.manager.network.service.HttpService
|
||||
import app.revanced.manager.network.utils.APIResponse
|
||||
import app.revanced.manager.network.utils.getOrThrow
|
||||
import app.revanced.manager.network.utils.transform
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import io.ktor.client.request.url
|
||||
|
||||
class ReVancedAPI(
|
||||
private val service: ReVancedService,
|
||||
private val client: HttpService,
|
||||
private val prefs: PreferencesManager
|
||||
) {
|
||||
private suspend fun apiUrl() = prefs.api.get()
|
||||
|
||||
suspend fun getContributors() = service.getContributors(apiUrl()).transform { it.repositories }
|
||||
private suspend inline fun <reified T> request(api: String, route: String): APIResponse<T> =
|
||||
withContext(
|
||||
Dispatchers.IO
|
||||
) {
|
||||
client.request {
|
||||
url("$api/v4/$route")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getLatestRelease(name: String) =
|
||||
service.getLatestRelease(apiUrl(), name).transform { it.release }
|
||||
|
||||
suspend fun getReleases(name: String) =
|
||||
service.getReleases(apiUrl(), name).transform { it.releases }
|
||||
private suspend inline fun <reified T> request(route: String) = request<T>(apiUrl(), route)
|
||||
|
||||
suspend fun getAppUpdate() =
|
||||
getLatestRelease("revanced-manager")
|
||||
.getOrThrow()
|
||||
.takeIf { it.version != Build.VERSION.RELEASE }
|
||||
getLatestAppInfo().getOrThrow().takeIf { it.version != Build.VERSION.RELEASE }
|
||||
|
||||
companion object Extensions {
|
||||
fun ReVancedRelease.findAssetByType(mime: String) =
|
||||
assets.singleOrNull { it.contentType == mime } ?: throw MissingAssetException(mime)
|
||||
}
|
||||
}
|
||||
suspend fun getLatestAppInfo() = request<ReVancedAsset>("manager")
|
||||
|
||||
class MissingAssetException(type: String) : Exception("No asset with type $type")
|
||||
suspend fun getPatchesUpdate() = request<ReVancedAsset>("patches")
|
||||
|
||||
suspend fun getContributors() = request<List<ReVancedGitRepository>>("contributors")
|
||||
|
||||
suspend fun getInfo(api: String? = null) = request<ReVancedInfo>(api ?: apiUrl(), "about")
|
||||
}
|
@ -1,276 +0,0 @@
|
||||
package app.revanced.manager.network.downloader
|
||||
|
||||
import android.os.Build.SUPPORTED_ABIS
|
||||
import app.revanced.manager.network.service.HttpService
|
||||
import io.ktor.client.plugins.onDownload
|
||||
import io.ktor.client.request.parameter
|
||||
import io.ktor.client.request.url
|
||||
import it.skrape.selects.html5.a
|
||||
import it.skrape.selects.html5.div
|
||||
import it.skrape.selects.html5.form
|
||||
import it.skrape.selects.html5.h5
|
||||
import it.skrape.selects.html5.input
|
||||
import it.skrape.selects.html5.p
|
||||
import it.skrape.selects.html5.span
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.koin.core.component.inject
|
||||
import java.io.File
|
||||
|
||||
class APKMirror : AppDownloader, KoinComponent {
|
||||
private val httpClient: HttpService = get()
|
||||
|
||||
enum class APKType {
|
||||
APK,
|
||||
BUNDLE
|
||||
}
|
||||
|
||||
data class Variant(
|
||||
val apkType: APKType,
|
||||
val arch: String,
|
||||
val link: String
|
||||
)
|
||||
|
||||
private suspend fun getAppLink(packageName: String): String {
|
||||
val searchResults = httpClient.getHtml { url("$apkMirror/?post_type=app_release&searchtype=app&s=$packageName") }
|
||||
.div {
|
||||
withId = "content"
|
||||
findFirst {
|
||||
div {
|
||||
withClass = "listWidget"
|
||||
findAll {
|
||||
|
||||
find {
|
||||
it.children.first().text.contains(packageName)
|
||||
}!!.children.mapNotNull {
|
||||
if (it.classNames.isEmpty()) {
|
||||
it.h5 {
|
||||
withClass = "appRowTitle"
|
||||
findFirst {
|
||||
a {
|
||||
findFirst {
|
||||
attribute("href")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else null
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return searchResults.find { url ->
|
||||
httpClient.getHtml { url(apkMirror + url) }
|
||||
.div {
|
||||
withId = "primary"
|
||||
findFirst {
|
||||
div {
|
||||
withClass = "tab-buttons"
|
||||
findFirst {
|
||||
div {
|
||||
withClass = "tab-button-positioning"
|
||||
findFirst {
|
||||
children.any {
|
||||
it.attribute("href") == "https://play.google.com/store/apps/details?id=$packageName"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} ?: throw Exception("App isn't available for download")
|
||||
}
|
||||
|
||||
override fun getAvailableVersions(packageName: String, versionFilter: Set<String>) = flow<AppDownloader.App> {
|
||||
|
||||
// Vanced music uses the same package name so we have to hardcode...
|
||||
val appCategory = if (packageName == "com.google.android.apps.youtube.music")
|
||||
"youtube-music"
|
||||
else
|
||||
getAppLink(packageName).split("/")[3]
|
||||
|
||||
var page = 1
|
||||
|
||||
val versions = mutableListOf<String>()
|
||||
|
||||
while (
|
||||
if (versionFilter.isNotEmpty())
|
||||
versions.size < versionFilter.size && page <= 7
|
||||
else
|
||||
page <= 1
|
||||
) {
|
||||
httpClient.getHtml {
|
||||
url("$apkMirror/uploads/page/$page/")
|
||||
parameter("appcategory", appCategory)
|
||||
}.div {
|
||||
withClass = "widget_appmanager_recentpostswidget"
|
||||
findFirst {
|
||||
div {
|
||||
withClass = "listWidget"
|
||||
findFirst {
|
||||
children.mapNotNull { element ->
|
||||
if (element.className.isEmpty()) {
|
||||
|
||||
APKMirrorApp(
|
||||
packageName = packageName,
|
||||
version = element.div {
|
||||
withClass = "infoSlide"
|
||||
findFirst {
|
||||
p {
|
||||
findFirst {
|
||||
span {
|
||||
withClass = "infoSlide-value"
|
||||
findFirst {
|
||||
text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.also {
|
||||
if (it in versionFilter)
|
||||
versions.add(it)
|
||||
},
|
||||
downloadLink = element.findFirst {
|
||||
a {
|
||||
withClass = "downloadLink"
|
||||
findFirst {
|
||||
attribute("href")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
} else null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onEach { version -> emit(version) }
|
||||
|
||||
page++
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
private class APKMirrorApp(
|
||||
override val packageName: String,
|
||||
override val version: String,
|
||||
private val downloadLink: String,
|
||||
) : AppDownloader.App, KoinComponent {
|
||||
@IgnoredOnParcel private val httpClient: HttpService by inject()
|
||||
|
||||
override suspend fun download(
|
||||
saveDirectory: File,
|
||||
preferSplit: Boolean,
|
||||
onDownload: suspend (downloadProgress: Pair<Float, Float>?) -> Unit
|
||||
) {
|
||||
val variants = httpClient.getHtml { url(apkMirror + downloadLink) }
|
||||
.div {
|
||||
withClass = "variants-table"
|
||||
findFirst { // list of variants
|
||||
children.drop(1).map {
|
||||
Variant(
|
||||
apkType = it.div {
|
||||
findFirst {
|
||||
span {
|
||||
findFirst {
|
||||
enumValueOf(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
arch = it.div {
|
||||
findSecond {
|
||||
text
|
||||
}
|
||||
},
|
||||
link = it.div {
|
||||
findFirst {
|
||||
a {
|
||||
findFirst {
|
||||
attribute("href")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val orderedAPKTypes = mutableListOf(APKType.APK, APKType.BUNDLE)
|
||||
.also { if (preferSplit) it.reverse() }
|
||||
|
||||
val variant = orderedAPKTypes.firstNotNullOfOrNull { apkType ->
|
||||
supportedArches.firstNotNullOfOrNull { arch ->
|
||||
variants.find { it.arch == arch && it.apkType == apkType }
|
||||
}
|
||||
} ?: throw Exception("No compatible variant found")
|
||||
|
||||
if (variant.apkType == APKType.BUNDLE) throw Exception("Split apks are not supported yet") // TODO
|
||||
|
||||
val downloadPage = httpClient.getHtml { url(apkMirror + variant.link) }
|
||||
.a {
|
||||
withClass = "downloadButton"
|
||||
findFirst {
|
||||
attribute("href")
|
||||
}
|
||||
}
|
||||
|
||||
val downloadLink = httpClient.getHtml { url(apkMirror + downloadPage) }
|
||||
.form {
|
||||
withId = "filedownload"
|
||||
findFirst {
|
||||
val apkLink = attribute("action")
|
||||
val id = input {
|
||||
withAttribute = "name" to "id"
|
||||
findFirst {
|
||||
attribute("value")
|
||||
}
|
||||
}
|
||||
val key = input {
|
||||
withAttribute = "name" to "key"
|
||||
findFirst {
|
||||
attribute("value")
|
||||
}
|
||||
}
|
||||
"$apkLink?id=$id&key=$key"
|
||||
}
|
||||
}
|
||||
|
||||
val targetFile = saveDirectory.resolve("base.apk")
|
||||
|
||||
try {
|
||||
httpClient.download(targetFile) {
|
||||
url(apkMirror + downloadLink)
|
||||
onDownload { bytesSentTotal, contentLength ->
|
||||
onDownload(bytesSentTotal.div(100000).toFloat().div(10) to contentLength.div(100000).toFloat().div(10))
|
||||
}
|
||||
}
|
||||
|
||||
if (variant.apkType == APKType.BUNDLE) {
|
||||
// TODO: Extract temp.zip
|
||||
|
||||
targetFile.delete()
|
||||
}
|
||||
} finally {
|
||||
onDownload(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val apkMirror = "https://www.apkmirror.com"
|
||||
|
||||
val supportedArches = listOf("universal", "noarch") + SUPPORTED_ABIS
|
||||
}
|
||||
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package app.revanced.manager.network.downloader
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.io.File
|
||||
|
||||
interface AppDownloader {
|
||||
|
||||
/**
|
||||
* Returns all downloadable apps.
|
||||
*
|
||||
* @param packageName The package name of the app.
|
||||
* @param versionFilter A set of versions to filter.
|
||||
*/
|
||||
fun getAvailableVersions(packageName: String, versionFilter: Set<String>): Flow<App>
|
||||
|
||||
interface App : Parcelable {
|
||||
val packageName: String
|
||||
val version: String
|
||||
|
||||
suspend fun download(
|
||||
saveDirectory: File,
|
||||
preferSplit: Boolean,
|
||||
onDownload: suspend (downloadProgress: Pair<Float, Float>?) -> Unit = {}
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package app.revanced.manager.network.downloader
|
||||
|
||||
sealed interface DownloaderPluginState {
|
||||
data object Untrusted : DownloaderPluginState
|
||||
|
||||
data class Loaded(val plugin: LoadedDownloaderPlugin) : DownloaderPluginState
|
||||
|
||||
data class Failed(val throwable: Throwable) : DownloaderPluginState
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package app.revanced.manager.network.downloader
|
||||
|
||||
import android.os.Parcelable
|
||||
import app.revanced.manager.plugin.downloader.OutputDownloadScope
|
||||
import app.revanced.manager.plugin.downloader.GetScope
|
||||
import java.io.OutputStream
|
||||
|
||||
class LoadedDownloaderPlugin(
|
||||
val packageName: String,
|
||||
val name: String,
|
||||
val version: String,
|
||||
val get: suspend GetScope.(packageName: String, version: String?) -> Pair<Parcelable, String?>?,
|
||||
val download: suspend OutputDownloadScope.(data: Parcelable, outputStream: OutputStream) -> Unit,
|
||||
val classLoader: ClassLoader
|
||||
)
|
@ -0,0 +1,45 @@
|
||||
package app.revanced.manager.network.downloader
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
/**
|
||||
* A container for [Parcelable] data returned from downloader. Instances of this class can be safely stored in a bundle without needing to set the [ClassLoader].
|
||||
*/
|
||||
class ParceledDownloaderData private constructor(
|
||||
val pluginPackageName: String,
|
||||
private val bundle: Bundle
|
||||
) : Parcelable {
|
||||
constructor(plugin: LoadedDownloaderPlugin, data: Parcelable) : this(
|
||||
plugin.packageName,
|
||||
createBundle(data)
|
||||
)
|
||||
|
||||
fun unwrapWith(plugin: LoadedDownloaderPlugin): Parcelable {
|
||||
bundle.classLoader = plugin.classLoader
|
||||
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val className = bundle.getString(CLASS_NAME_KEY)!!
|
||||
val clazz = plugin.classLoader.loadClass(className)
|
||||
|
||||
bundle.getParcelable(DATA_KEY, clazz)!! as Parcelable
|
||||
} else @Suppress("Deprecation") bundle.getParcelable(DATA_KEY)!!
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val CLASS_NAME_KEY = "class"
|
||||
const val DATA_KEY = "data"
|
||||
|
||||
fun createBundle(data: Parcelable) = Bundle().apply {
|
||||
putParcelable(DATA_KEY, data)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) putString(
|
||||
CLASS_NAME_KEY,
|
||||
data::class.java.canonicalName
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package app.revanced.manager.network.dto
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class BundleInfo(val patches: BundleAsset, val integrations: BundleAsset)
|
||||
|
||||
@Serializable
|
||||
data class BundleAsset(val version: String, val url: String)
|
@ -1,16 +0,0 @@
|
||||
package app.revanced.manager.network.dto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GithubChangelog(
|
||||
@SerialName("tag_name") val version: String,
|
||||
@SerialName("body") val body: String,
|
||||
@SerialName("assets") val assets: List<GithubAsset>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GithubAsset(
|
||||
@SerialName("download_count") val downloadCount: Int,
|
||||
)
|
@ -0,0 +1,18 @@
|
||||
package app.revanced.manager.network.dto
|
||||
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ReVancedAsset (
|
||||
@SerialName("download_url")
|
||||
val downloadUrl: String,
|
||||
@SerialName("created_at")
|
||||
val createdAt: LocalDateTime,
|
||||
@SerialName("signature_download_url")
|
||||
val signatureDownloadUrl: String? = null,
|
||||
val description: String,
|
||||
val version: String,
|
||||
)
|
||||
|
@ -3,19 +3,15 @@ package app.revanced.manager.network.dto
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ReVancedGitRepositories(
|
||||
val repositories: List<ReVancedGitRepository>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReVancedGitRepository(
|
||||
val name: String,
|
||||
val url: String,
|
||||
val contributors: List<ReVancedContributor>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReVancedContributor(
|
||||
@SerialName("login") val username: String,
|
||||
@SerialName("name") val username: String,
|
||||
@SerialName("avatar_url") val avatarUrl: String,
|
||||
)
|
||||
)
|
@ -0,0 +1,53 @@
|
||||
package app.revanced.manager.network.dto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ReVancedInfo(
|
||||
val name: String,
|
||||
val about: String,
|
||||
val branding: ReVancedBranding,
|
||||
val contact: ReVancedContact,
|
||||
val socials: List<ReVancedSocial>,
|
||||
val donations: ReVancedDonation,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReVancedBranding(
|
||||
val logo: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReVancedContact(
|
||||
val email: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReVancedSocial(
|
||||
val name: String,
|
||||
val url: String,
|
||||
val preferred: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReVancedDonation(
|
||||
val wallets: List<ReVancedWallet>,
|
||||
val links: List<ReVancedDonationLink>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReVancedWallet(
|
||||
val network: String,
|
||||
@SerialName("currency_code")
|
||||
val currencyCode: String,
|
||||
val address: String,
|
||||
val preferred: Boolean
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReVancedDonationLink(
|
||||
val name: String,
|
||||
val url: String,
|
||||
val preferred: Boolean,
|
||||
)
|
@ -1,41 +0,0 @@
|
||||
package app.revanced.manager.network.dto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ReVancedLatestRelease(
|
||||
val release: ReVancedRelease,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReVancedReleases(
|
||||
val releases: List<ReVancedRelease>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReVancedRelease(
|
||||
val metadata: ReVancedReleaseMeta,
|
||||
val assets: List<Asset>
|
||||
) {
|
||||
val version get() = metadata.tag
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ReVancedReleaseMeta(
|
||||
@SerialName("tag_name") val tag: String,
|
||||
val name: String,
|
||||
val draft: Boolean,
|
||||
val prerelease: Boolean,
|
||||
@SerialName("created_at") val createdAt: String,
|
||||
@SerialName("published_at") val publishedAt: String,
|
||||
val body: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Asset(
|
||||
val name: String,
|
||||
@SerialName("download_count") val downloadCount: Int,
|
||||
@SerialName("browser_download_url") val downloadUrl: String,
|
||||
@SerialName("content_type") val contentType: String
|
||||
)
|
@ -1,34 +0,0 @@
|
||||
package app.revanced.manager.network.service
|
||||
|
||||
import app.revanced.manager.network.dto.ReVancedLatestRelease
|
||||
import app.revanced.manager.network.dto.ReVancedGitRepositories
|
||||
import app.revanced.manager.network.dto.ReVancedReleases
|
||||
import app.revanced.manager.network.utils.APIResponse
|
||||
import io.ktor.client.request.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ReVancedService(
|
||||
private val client: HttpService,
|
||||
) {
|
||||
suspend fun getLatestRelease(api: String, repo: String): APIResponse<ReVancedLatestRelease> =
|
||||
withContext(Dispatchers.IO) {
|
||||
client.request {
|
||||
url("$api/v2/$repo/releases/latest")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getReleases(api: String, repo: String): APIResponse<ReVancedReleases> =
|
||||
withContext(Dispatchers.IO) {
|
||||
client.request {
|
||||
url("$api/v2/$repo/releases")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getContributors(api: String): APIResponse<ReVancedGitRepositories> =
|
||||
withContext(Dispatchers.IO) {
|
||||
client.request {
|
||||
url("$api/contributors")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package app.revanced.manager.patcher
|
||||
|
||||
import android.content.Context
|
||||
import java.io.File
|
||||
|
||||
abstract class LibraryResolver {
|
||||
protected fun findLibrary(context: Context, searchTerm: String): File? = File(context.applicationInfo.nativeLibraryDir).run {
|
||||
list { _, f -> !File(f).isDirectory && f.contains(searchTerm) }?.firstOrNull()?.let { resolve(it) }
|
||||
}
|
||||
}
|
@ -1,22 +1,20 @@
|
||||
package app.revanced.manager.patcher
|
||||
|
||||
import android.content.Context
|
||||
import app.revanced.library.ApkUtils
|
||||
import app.revanced.library.ApkUtils.applyTo
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.patcher.logger.ManagerLogger
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.patcher.Patcher
|
||||
import app.revanced.patcher.PatcherOptions
|
||||
import app.revanced.patcher.PatcherConfig
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.patch.PatchResult
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.util.logging.Logger
|
||||
|
||||
internal typealias PatchList = List<Patch<*>>
|
||||
|
||||
@ -24,11 +22,10 @@ class Session(
|
||||
cacheDir: String,
|
||||
frameworkDir: String,
|
||||
aaptPath: String,
|
||||
multithreadingDexFileWriter: Boolean,
|
||||
private val androidContext: Context,
|
||||
private val logger: ManagerLogger,
|
||||
private val logger: Logger,
|
||||
private val input: File,
|
||||
private val patchesProgress: MutableStateFlow<Pair<Int, Int>>,
|
||||
private val onPatchCompleted: suspend () -> Unit,
|
||||
private val onProgress: (name: String?, state: State?, message: String?) -> Unit
|
||||
) : Closeable {
|
||||
private fun updateProgress(name: String? = null, state: State? = null, message: String? = null) =
|
||||
@ -36,12 +33,11 @@ class Session(
|
||||
|
||||
private val tempDir = File(cacheDir).resolve("patcher").also { it.mkdirs() }
|
||||
private val patcher = Patcher(
|
||||
PatcherOptions(
|
||||
inputFile = input,
|
||||
resourceCachePath = tempDir.resolve("aapt-resources"),
|
||||
PatcherConfig(
|
||||
apkFile = input,
|
||||
temporaryFilesPath = tempDir,
|
||||
frameworkFileDirectory = frameworkDir,
|
||||
aaptBinaryPath = aaptPath,
|
||||
multithreadingDexFileWriter = multithreadingDexFileWriter,
|
||||
aaptBinaryPath = aaptPath
|
||||
)
|
||||
)
|
||||
|
||||
@ -49,16 +45,16 @@ class Session(
|
||||
var nextPatchIndex = 0
|
||||
|
||||
updateProgress(
|
||||
name = androidContext.getString(R.string.applying_patch, selectedPatches[nextPatchIndex]),
|
||||
name = androidContext.getString(R.string.executing_patch, selectedPatches[nextPatchIndex]),
|
||||
state = State.RUNNING
|
||||
)
|
||||
|
||||
this.apply(true).collect { (patch, exception) ->
|
||||
this().collect { (patch, exception) ->
|
||||
if (patch !in selectedPatches) return@collect
|
||||
|
||||
if (exception != null) {
|
||||
updateProgress(
|
||||
name = androidContext.getString(R.string.failed_to_apply_patch, patch.name),
|
||||
name = androidContext.getString(R.string.failed_to_execute_patch, patch.name),
|
||||
state = State.FAILED,
|
||||
message = exception.stackTraceToString()
|
||||
)
|
||||
@ -70,13 +66,11 @@ class Session(
|
||||
|
||||
nextPatchIndex++
|
||||
|
||||
patchesProgress.value.let {
|
||||
patchesProgress.emit(it.copy(it.first + 1))
|
||||
}
|
||||
onPatchCompleted()
|
||||
|
||||
selectedPatches.getOrNull(nextPatchIndex)?.let { nextPatch ->
|
||||
updateProgress(
|
||||
name = androidContext.getString(R.string.applying_patch, nextPatch.name)
|
||||
name = androidContext.getString(R.string.executing_patch, nextPatch.name)
|
||||
)
|
||||
}
|
||||
|
||||
@ -86,28 +80,28 @@ class Session(
|
||||
updateProgress(
|
||||
state = State.COMPLETED,
|
||||
name = androidContext.resources.getQuantityString(
|
||||
R.plurals.patches_applied,
|
||||
R.plurals.patches_executed,
|
||||
selectedPatches.size,
|
||||
selectedPatches.size
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun run(output: File, selectedPatches: PatchList, integrations: List<File>) {
|
||||
suspend fun run(output: File, selectedPatches: PatchList) {
|
||||
updateProgress(state = State.COMPLETED) // Unpacking
|
||||
Logger.getLogger("").apply {
|
||||
|
||||
java.util.logging.Logger.getLogger("").apply {
|
||||
handlers.forEach {
|
||||
it.close()
|
||||
removeHandler(it)
|
||||
}
|
||||
|
||||
addHandler(logger)
|
||||
addHandler(logger.handler)
|
||||
}
|
||||
|
||||
with(patcher) {
|
||||
logger.info("Merging integrations")
|
||||
acceptIntegrations(integrations)
|
||||
acceptPatches(selectedPatches)
|
||||
updateProgress(state = State.COMPLETED) // Merging
|
||||
this += selectedPatches.toSet()
|
||||
|
||||
logger.info("Applying patches...")
|
||||
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
|
||||
@ -116,13 +110,16 @@ class Session(
|
||||
logger.info("Writing patched files...")
|
||||
val result = patcher.get()
|
||||
|
||||
val aligned = tempDir.resolve("aligned.apk")
|
||||
ApkUtils.copyAligned(input, aligned, result)
|
||||
val patched = tempDir.resolve("result.apk")
|
||||
withContext(Dispatchers.IO) {
|
||||
Files.copy(input.toPath(), patched.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
result.applyTo(patched)
|
||||
|
||||
logger.info("Patched apk saved to $aligned")
|
||||
logger.info("Patched apk saved to $patched")
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
Files.move(aligned.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
Files.move(patched.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
updateProgress(state = State.COMPLETED) // Saving
|
||||
}
|
||||
|
@ -1,18 +1,12 @@
|
||||
package app.revanced.manager.patcher.aapt
|
||||
|
||||
import android.content.Context
|
||||
import app.revanced.manager.patcher.LibraryResolver
|
||||
import android.os.Build.SUPPORTED_ABIS as DEVICE_ABIS
|
||||
import java.io.File
|
||||
|
||||
object Aapt {
|
||||
private val WORKING_ABIS = setOf("arm64-v8a", "x86", "x86_64")
|
||||
object Aapt : LibraryResolver() {
|
||||
private val WORKING_ABIS = setOf("arm64-v8a", "x86", "x86_64", "armeabi-v7a")
|
||||
|
||||
fun supportsDevice() = (DEVICE_ABIS intersect WORKING_ABIS).isNotEmpty()
|
||||
|
||||
fun binary(context: Context): File? {
|
||||
return File(context.applicationInfo.nativeLibraryDir).resolveAapt()
|
||||
}
|
||||
fun binary(context: Context) = findLibrary(context, "aapt")
|
||||
}
|
||||
|
||||
private fun File.resolveAapt() =
|
||||
list { _, f -> !File(f).isDirectory && f.contains("aapt") }?.firstOrNull()?.let { resolve(it) }
|
||||
|
@ -0,0 +1,37 @@
|
||||
package app.revanced.manager.patcher.logger
|
||||
|
||||
import java.util.logging.Handler
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.LogRecord
|
||||
|
||||
abstract class Logger {
|
||||
abstract fun log(level: LogLevel, message: String)
|
||||
|
||||
fun trace(msg: String) = log(LogLevel.TRACE, msg)
|
||||
fun info(msg: String) = log(LogLevel.INFO, msg)
|
||||
fun warn(msg: String) = log(LogLevel.WARN, msg)
|
||||
fun error(msg: String) = log(LogLevel.ERROR, msg)
|
||||
|
||||
val handler = object : Handler() {
|
||||
override fun publish(record: LogRecord) {
|
||||
val msg = record.message
|
||||
|
||||
when (record.level) {
|
||||
Level.INFO -> info(msg)
|
||||
Level.SEVERE -> error(msg)
|
||||
Level.WARNING -> warn(msg)
|
||||
else -> trace(msg)
|
||||
}
|
||||
}
|
||||
|
||||
override fun flush() = Unit
|
||||
override fun close() = Unit
|
||||
}
|
||||
}
|
||||
|
||||
enum class LogLevel {
|
||||
TRACE,
|
||||
INFO,
|
||||
WARN,
|
||||
ERROR,
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
package app.revanced.manager.patcher.logger
|
||||
|
||||
import android.util.Log
|
||||
import java.util.logging.Handler
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.LogRecord
|
||||
|
||||
class ManagerLogger : Handler() {
|
||||
private val logs = mutableListOf<Pair<LogLevel, String>>()
|
||||
private fun log(level: LogLevel, msg: String) {
|
||||
level.androidLog(msg)
|
||||
if (level == LogLevel.TRACE) return
|
||||
logs.add(level to msg)
|
||||
}
|
||||
|
||||
fun export() =
|
||||
logs.asSequence().map { (level, msg) -> "[${level.name}]: $msg" }.joinToString("\n")
|
||||
|
||||
fun trace(msg: String) = log(LogLevel.TRACE, msg)
|
||||
fun info(msg: String) = log(LogLevel.INFO, msg)
|
||||
fun warn(msg: String) = log(LogLevel.WARN, msg)
|
||||
fun error(msg: String) = log(LogLevel.ERROR, msg)
|
||||
override fun publish(record: LogRecord) {
|
||||
val msg = record.message
|
||||
val fn = when (record.level) {
|
||||
Level.INFO -> ::info
|
||||
Level.SEVERE -> ::error
|
||||
Level.WARNING -> ::warn
|
||||
else -> ::trace
|
||||
}
|
||||
|
||||
fn(msg)
|
||||
}
|
||||
|
||||
override fun flush() = Unit
|
||||
|
||||
override fun close() = Unit
|
||||
}
|
||||
|
||||
enum class LogLevel {
|
||||
TRACE {
|
||||
override fun androidLog(msg: String) = Log.v(androidTag, msg)
|
||||
},
|
||||
INFO {
|
||||
override fun androidLog(msg: String) = Log.i(androidTag, msg)
|
||||
},
|
||||
WARN {
|
||||
override fun androidLog(msg: String) = Log.w(androidTag, msg)
|
||||
},
|
||||
ERROR {
|
||||
override fun androidLog(msg: String) = Log.e(androidTag, msg)
|
||||
};
|
||||
|
||||
abstract fun androidLog(msg: String): Int
|
||||
|
||||
private companion object {
|
||||
const val androidTag = "ReVanced Patcher"
|
||||
}
|
||||
}
|
@ -2,23 +2,24 @@ package app.revanced.manager.patcher.patch
|
||||
|
||||
import android.util.Log
|
||||
import app.revanced.manager.util.tag
|
||||
import app.revanced.patcher.PatchBundleLoader
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.patch.PatchLoader
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.jar.JarFile
|
||||
|
||||
class PatchBundle(private val loader: Iterable<Patch<*>>, val integrations: File?) {
|
||||
constructor(bundleJar: File, integrations: File?) : this(
|
||||
object : Iterable<Patch<*>> {
|
||||
private fun load(): Iterable<Patch<*>> {
|
||||
bundleJar.setReadOnly()
|
||||
return PatchBundleLoader.Dex(bundleJar, optimizedDexDirectory = null)
|
||||
}
|
||||
class PatchBundle(val patchesJar: File) {
|
||||
private val loader = object : Iterable<Patch<*>> {
|
||||
private fun load(): Iterable<Patch<*>> {
|
||||
patchesJar.setReadOnly()
|
||||
return PatchLoader.Dex(setOf(patchesJar))
|
||||
}
|
||||
|
||||
override fun iterator(): Iterator<Patch<*>> = load().iterator()
|
||||
},
|
||||
integrations
|
||||
) {
|
||||
Log.d(tag, "Loaded patch bundle: $bundleJar")
|
||||
override fun iterator(): Iterator<Patch<*>> = load().iterator()
|
||||
}
|
||||
|
||||
init {
|
||||
Log.d(tag, "Loaded patch bundle: $patchesJar")
|
||||
}
|
||||
|
||||
/**
|
||||
@ -26,15 +27,26 @@ class PatchBundle(private val loader: Iterable<Patch<*>>, val integrations: File
|
||||
*/
|
||||
val patches = loader.map(::PatchInfo)
|
||||
|
||||
/**
|
||||
* The [java.util.jar.Manifest] of [patchesJar].
|
||||
*/
|
||||
private val manifest = try {
|
||||
JarFile(patchesJar).use { it.manifest }
|
||||
} catch (_: IOException) {
|
||||
null
|
||||
}
|
||||
|
||||
fun readManifestAttribute(name: String) = manifest?.mainAttributes?.getValue(name)
|
||||
|
||||
/**
|
||||
* Load all patches compatible with the specified package.
|
||||
*/
|
||||
fun patchClasses(packageName: String) = loader.filter { patch ->
|
||||
fun patches(packageName: String) = loader.filter { patch ->
|
||||
val compatiblePackages = patch.compatiblePackages
|
||||
?: // The patch has no compatibility constraints, which means it is universal.
|
||||
return@filter true
|
||||
|
||||
if (!compatiblePackages.any { it.name == packageName }) {
|
||||
if (!compatiblePackages.any { (name, _) -> name == packageName }) {
|
||||
// Patch is not compatible with this package.
|
||||
return@filter false
|
||||
}
|
||||
|
@ -2,69 +2,86 @@ package app.revanced.manager.patcher.patch
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.patch.options.PatchOption
|
||||
import app.revanced.patcher.patch.Option as PatchOption
|
||||
import app.revanced.patcher.patch.resourcePatch
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlin.reflect.KType
|
||||
|
||||
data class PatchInfo(
|
||||
val name: String,
|
||||
val description: String?,
|
||||
val include: Boolean,
|
||||
val compatiblePackages: ImmutableList<CompatiblePackage>?,
|
||||
val options: ImmutableList<Option>?
|
||||
val options: ImmutableList<Option<*>>?
|
||||
) {
|
||||
constructor(patch: Patch<*>) : this(
|
||||
patch.name.orEmpty(),
|
||||
patch.description,
|
||||
patch.use,
|
||||
patch.compatiblePackages?.map { CompatiblePackage(it) }?.toImmutableList(),
|
||||
patch.compatiblePackages?.map { (pkgName, versions) ->
|
||||
CompatiblePackage(
|
||||
pkgName,
|
||||
versions?.toImmutableSet()
|
||||
)
|
||||
}?.toImmutableList(),
|
||||
patch.options.map { (_, option) -> Option(option) }.ifEmpty { null }?.toImmutableList()
|
||||
)
|
||||
|
||||
fun compatibleWith(packageName: String) =
|
||||
compatiblePackages?.any { it.packageName == packageName } ?: true
|
||||
|
||||
fun supportsVersion(packageName: String, versionName: String): Boolean {
|
||||
fun supports(packageName: String, versionName: String?): Boolean {
|
||||
val packages = compatiblePackages ?: return true // Universal patch
|
||||
|
||||
return packages.any { pkg ->
|
||||
if (pkg.packageName != packageName) {
|
||||
return@any false
|
||||
}
|
||||
if (pkg.packageName != packageName) return@any false
|
||||
if (pkg.versions == null) return@any true
|
||||
|
||||
pkg.versions == null || pkg.versions.contains(versionName)
|
||||
versionName != null && versionName in pkg.versions
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a fake [Patch] with the same metadata as the [PatchInfo] instance.
|
||||
* The resulting patch cannot be executed.
|
||||
* This is necessary because some functions in ReVanced Library only accept full [Patch] objects.
|
||||
*/
|
||||
fun toPatcherPatch(): Patch<*> =
|
||||
resourcePatch(name = name, description = description, use = include) {
|
||||
compatiblePackages?.let { pkgs ->
|
||||
compatibleWith(*pkgs.map { it.packageName to it.versions }.toTypedArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class CompatiblePackage(
|
||||
val packageName: String,
|
||||
val versions: ImmutableSet<String>?
|
||||
) {
|
||||
constructor(pkg: Patch.CompatiblePackage) : this(
|
||||
pkg.name,
|
||||
pkg.versions?.toImmutableSet()
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class Option(
|
||||
data class Option<T>(
|
||||
val title: String,
|
||||
val key: String,
|
||||
val description: String,
|
||||
val required: Boolean,
|
||||
val type: String,
|
||||
val default: Any?
|
||||
val type: KType,
|
||||
val default: T?,
|
||||
val presets: Map<String, T?>?,
|
||||
val validator: (T?) -> Boolean,
|
||||
) {
|
||||
constructor(option: PatchOption<*>) : this(
|
||||
constructor(option: PatchOption<T>) : this(
|
||||
option.title ?: option.key,
|
||||
option.key,
|
||||
option.description.orEmpty(),
|
||||
option.required,
|
||||
option.valueType,
|
||||
option.type,
|
||||
option.default,
|
||||
option.values,
|
||||
{ option.validator(option, it) },
|
||||
)
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package app.revanced.manager.patcher.runtime
|
||||
|
||||
import android.content.Context
|
||||
import app.revanced.manager.patcher.Session
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Simple [Runtime] implementation that runs the patcher using coroutines.
|
||||
*/
|
||||
class CoroutineRuntime(private val context: Context) : Runtime(context) {
|
||||
override suspend fun execute(
|
||||
inputFile: String,
|
||||
outputFile: String,
|
||||
packageName: String,
|
||||
selectedPatches: PatchSelection,
|
||||
options: Options,
|
||||
logger: Logger,
|
||||
onPatchCompleted: suspend () -> Unit,
|
||||
onProgress: ProgressEventHandler,
|
||||
) {
|
||||
val bundles = bundles()
|
||||
|
||||
val selectedBundles = selectedPatches.keys
|
||||
val allPatches = bundles.filterKeys { selectedBundles.contains(it) }
|
||||
.mapValues { (_, bundle) -> bundle.patches(packageName) }
|
||||
|
||||
val patchList = selectedPatches.flatMap { (bundle, selected) ->
|
||||
allPatches[bundle]?.filter { selected.contains(it.name) }
|
||||
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
||||
}
|
||||
|
||||
// Set all patch options.
|
||||
options.forEach { (bundle, bundlePatchOptions) ->
|
||||
val patches = allPatches[bundle] ?: return@forEach
|
||||
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
|
||||
val patchOptions = patches.single { it.name == patchName }.options
|
||||
configuredPatchOptions.forEach { (key, value) ->
|
||||
patchOptions[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onProgress(null, State.COMPLETED, null) // Loading patches
|
||||
|
||||
Session(
|
||||
cacheDir,
|
||||
frameworkPath,
|
||||
aaptPath,
|
||||
context,
|
||||
logger,
|
||||
File(inputFile),
|
||||
onPatchCompleted = onPatchCompleted,
|
||||
onProgress
|
||||
).use { session ->
|
||||
session.run(
|
||||
File(outputFile),
|
||||
patchList
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,189 @@
|
||||
package app.revanced.manager.patcher.runtime
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import app.revanced.manager.BuildConfig
|
||||
import app.revanced.manager.patcher.runtime.process.IPatcherEvents
|
||||
import app.revanced.manager.patcher.runtime.process.IPatcherProcess
|
||||
import app.revanced.manager.patcher.LibraryResolver
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.runtime.process.Parameters
|
||||
import app.revanced.manager.patcher.runtime.process.PatchConfiguration
|
||||
import app.revanced.manager.patcher.runtime.process.PatcherProcess
|
||||
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import app.revanced.manager.util.tag
|
||||
import com.github.pgreze.process.Redirect
|
||||
import com.github.pgreze.process.process
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.koin.core.component.inject
|
||||
|
||||
/**
|
||||
* Runs the patcher in another process by using the app_process binary and IPC.
|
||||
*/
|
||||
class ProcessRuntime(private val context: Context) : Runtime(context) {
|
||||
private val pm: PM by inject()
|
||||
|
||||
private suspend fun awaitBinderConnection(): IPatcherProcess {
|
||||
val binderFuture = CompletableDeferred<IPatcherProcess>()
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val binder =
|
||||
intent.getBundleExtra(INTENT_BUNDLE_KEY)?.getBinder(BUNDLE_BINDER_KEY)!!
|
||||
|
||||
binderFuture.complete(IPatcherProcess.Stub.asInterface(binder))
|
||||
}
|
||||
}
|
||||
|
||||
ContextCompat.registerReceiver(context, receiver, IntentFilter().apply {
|
||||
addAction(CONNECT_TO_APP_ACTION)
|
||||
}, ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||
|
||||
return try {
|
||||
withTimeout(10000L) {
|
||||
binderFuture.await()
|
||||
}
|
||||
} finally {
|
||||
context.unregisterReceiver(receiver)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun execute(
|
||||
inputFile: String,
|
||||
outputFile: String,
|
||||
packageName: String,
|
||||
selectedPatches: PatchSelection,
|
||||
options: Options,
|
||||
logger: Logger,
|
||||
onPatchCompleted: suspend () -> Unit,
|
||||
onProgress: ProgressEventHandler,
|
||||
) = coroutineScope {
|
||||
// Get the location of our own Apk.
|
||||
val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo!!.sourceDir
|
||||
|
||||
val limit = "${prefs.patcherProcessMemoryLimit.get()}M"
|
||||
val propOverride = resolvePropOverride(context)?.absolutePath
|
||||
?: throw Exception("Couldn't find prop override library")
|
||||
|
||||
val env =
|
||||
System.getenv().toMutableMap().apply {
|
||||
putAll(
|
||||
mapOf(
|
||||
"CLASSPATH" to managerBaseApk,
|
||||
// Override the props used by ART to set the memory limit.
|
||||
"LD_PRELOAD" to propOverride,
|
||||
"PROP_dalvik.vm.heapgrowthlimit" to limit,
|
||||
"PROP_dalvik.vm.heapsize" to limit,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
launch(Dispatchers.IO) {
|
||||
val result = process(
|
||||
APP_PROCESS_BIN_PATH,
|
||||
"-Djava.io.tmpdir=$cacheDir", // The process will use /tmp if this isn't set, which is a problem because that folder is not accessible on Android.
|
||||
"/", // The unused cmd-dir parameter
|
||||
"--nice-name=${context.packageName}:Patcher",
|
||||
PatcherProcess::class.java.name, // The class with the main function.
|
||||
context.packageName,
|
||||
env = env,
|
||||
stdout = Redirect.CAPTURE,
|
||||
stderr = Redirect.CAPTURE,
|
||||
) { line ->
|
||||
// The process shouldn't generally be writing to stdio. Log any lines we get as warnings.
|
||||
logger.warn("[STDIO]: $line")
|
||||
}
|
||||
|
||||
Log.d(tag, "Process finished with exit code ${result.resultCode}")
|
||||
|
||||
if (result.resultCode != 0) throw Exception("Process exited with nonzero exit code ${result.resultCode}")
|
||||
}
|
||||
|
||||
val patching = CompletableDeferred<Unit>()
|
||||
val scope = this
|
||||
|
||||
launch(Dispatchers.IO) {
|
||||
val binder = awaitBinderConnection()
|
||||
|
||||
// Android Studio's fast deployment feature causes an issue where the other process will be running older code compared to the main process.
|
||||
// The patcher process is running outdated code if the randomly generated BUILD_ID numbers don't match.
|
||||
// To fix it, clear the cache in the Android settings or disable fast deployment (Run configurations -> Edit Configurations -> app -> Enable "always deploy with package manager").
|
||||
if (binder.buildId() != BuildConfig.BUILD_ID) throw Exception("app_process is running outdated code. Clear the app cache or disable disable Android 11 deployment optimizations in your IDE")
|
||||
|
||||
val eventHandler = object : IPatcherEvents.Stub() {
|
||||
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg)
|
||||
|
||||
override fun patchSucceeded() {
|
||||
scope.launch { onPatchCompleted() }
|
||||
}
|
||||
|
||||
override fun progress(name: String?, state: String?, msg: String?) =
|
||||
onProgress(name, state?.let { enumValueOf<State>(it) }, msg)
|
||||
|
||||
override fun finished(exceptionStackTrace: String?) {
|
||||
binder.exit()
|
||||
|
||||
exceptionStackTrace?.let {
|
||||
patching.completeExceptionally(RemoteFailureException(it))
|
||||
return
|
||||
}
|
||||
patching.complete(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
val bundles = bundles()
|
||||
|
||||
val parameters = Parameters(
|
||||
aaptPath = aaptPath,
|
||||
frameworkDir = frameworkPath,
|
||||
cacheDir = cacheDir,
|
||||
packageName = packageName,
|
||||
inputFile = inputFile,
|
||||
outputFile = outputFile,
|
||||
configurations = selectedPatches.map { (id, patches) ->
|
||||
val bundle = bundles[id]!!
|
||||
|
||||
PatchConfiguration(
|
||||
bundle.patchesJar.absolutePath,
|
||||
patches,
|
||||
options[id].orEmpty()
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
binder.start(parameters, eventHandler)
|
||||
}
|
||||
|
||||
// Wait until patching finishes.
|
||||
patching.await()
|
||||
}
|
||||
|
||||
companion object : LibraryResolver() {
|
||||
private const val APP_PROCESS_BIN_PATH = "/system/bin/app_process"
|
||||
|
||||
const val CONNECT_TO_APP_ACTION = "CONNECT_TO_APP_ACTION"
|
||||
const val INTENT_BUNDLE_KEY = "BUNDLE"
|
||||
const val BUNDLE_BINDER_KEY = "BINDER"
|
||||
|
||||
private fun resolvePropOverride(context: Context) = findLibrary(context, "prop_override")
|
||||
}
|
||||
|
||||
/**
|
||||
* An [Exception] occurred in the remote process while patching.
|
||||
*
|
||||
* @param originalStackTrace The stack trace of the original [Exception].
|
||||
*/
|
||||
class RemoteFailureException(val originalStackTrace: String) : Exception()
|
||||
}
|
||||
|
@ -0,0 +1,40 @@
|
||||
package app.revanced.manager.patcher.runtime
|
||||
|
||||
import android.content.Context
|
||||
import app.revanced.manager.data.platform.Filesystem
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.patcher.aapt.Aapt
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import kotlinx.coroutines.flow.first
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
sealed class Runtime(context: Context) : KoinComponent {
|
||||
private val fs: Filesystem by inject()
|
||||
private val patchBundlesRepo: PatchBundleRepository by inject()
|
||||
protected val prefs: PreferencesManager by inject()
|
||||
|
||||
protected val cacheDir: String = fs.tempDir.absolutePath
|
||||
protected val aaptPath = Aapt.binary(context)?.absolutePath
|
||||
?: throw FileNotFoundException("Could not resolve aapt.")
|
||||
protected val frameworkPath: String =
|
||||
context.cacheDir.resolve("framework").also { it.mkdirs() }.absolutePath
|
||||
|
||||
protected suspend fun bundles() = patchBundlesRepo.bundles.first()
|
||||
|
||||
abstract suspend fun execute(
|
||||
inputFile: String,
|
||||
outputFile: String,
|
||||
packageName: String,
|
||||
selectedPatches: PatchSelection,
|
||||
options: Options,
|
||||
logger: Logger,
|
||||
onPatchCompleted: suspend () -> Unit,
|
||||
onProgress: ProgressEventHandler,
|
||||
)
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package app.revanced.manager.patcher.runtime.process
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.parcelize.RawValue
|
||||
|
||||
@Parcelize
|
||||
data class Parameters(
|
||||
val cacheDir: String,
|
||||
val aaptPath: String,
|
||||
val frameworkDir: String,
|
||||
val packageName: String,
|
||||
val inputFile: String,
|
||||
val outputFile: String,
|
||||
val configurations: List<PatchConfiguration>,
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class PatchConfiguration(
|
||||
val bundlePath: String,
|
||||
val patches: Set<String>,
|
||||
val options: @RawValue Map<String, Map<String, Any?>>
|
||||
) : Parcelable
|
@ -0,0 +1,138 @@
|
||||
package app.revanced.manager.patcher.runtime.process
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.ActivityThread
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Looper
|
||||
import app.revanced.manager.BuildConfig
|
||||
import app.revanced.manager.patcher.Session
|
||||
import app.revanced.manager.patcher.logger.LogLevel
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.patch.PatchBundle
|
||||
import app.revanced.manager.patcher.runtime.ProcessRuntime
|
||||
import app.revanced.manager.ui.model.State
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
* The main class that runs inside the runner process launched by [ProcessRuntime].
|
||||
*/
|
||||
class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
||||
private var eventBinder: IPatcherEvents? = null
|
||||
|
||||
private val scope =
|
||||
CoroutineScope(Dispatchers.Default + CoroutineExceptionHandler { _, throwable ->
|
||||
// Try to send the exception information to the main app.
|
||||
eventBinder?.let {
|
||||
try {
|
||||
it.finished(throwable.stackTraceToString())
|
||||
return@CoroutineExceptionHandler
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
throwable.printStackTrace()
|
||||
exitProcess(1)
|
||||
})
|
||||
|
||||
override fun buildId() = BuildConfig.BUILD_ID
|
||||
override fun exit() = exitProcess(0)
|
||||
|
||||
override fun start(parameters: Parameters, events: IPatcherEvents) {
|
||||
eventBinder = events
|
||||
|
||||
scope.launch {
|
||||
val logger = object : Logger() {
|
||||
override fun log(level: LogLevel, message: String) =
|
||||
events.log(level.name, message)
|
||||
}
|
||||
|
||||
logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
|
||||
|
||||
val patchList = parameters.configurations.flatMap { config ->
|
||||
val bundle = PatchBundle(File(config.bundlePath))
|
||||
|
||||
val patches =
|
||||
bundle.patches(parameters.packageName).filter { it.name in config.patches }
|
||||
.associateBy { it.name }
|
||||
|
||||
config.options.forEach { (patchName, opts) ->
|
||||
val patchOptions = patches[patchName]?.options
|
||||
?: throw Exception("Patch with name $patchName does not exist.")
|
||||
|
||||
opts.forEach { (key, value) ->
|
||||
patchOptions[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
patches.values
|
||||
}
|
||||
|
||||
events.progress(null, State.COMPLETED.name, null) // Loading patches
|
||||
|
||||
Session(
|
||||
cacheDir = parameters.cacheDir,
|
||||
aaptPath = parameters.aaptPath,
|
||||
frameworkDir = parameters.frameworkDir,
|
||||
androidContext = context,
|
||||
logger = logger,
|
||||
input = File(parameters.inputFile),
|
||||
onPatchCompleted = { events.patchSucceeded() },
|
||||
onProgress = { name, state, message ->
|
||||
events.progress(name, state?.name, message)
|
||||
}
|
||||
).use {
|
||||
it.run(File(parameters.outputFile), patchList)
|
||||
}
|
||||
|
||||
events.finished(null)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val longArrayClass = LongArray::class.java
|
||||
private val emptyLongArray = LongArray(0)
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
Looper.prepare()
|
||||
|
||||
val managerPackageName = args[0]
|
||||
|
||||
// Abuse hidden APIs to get a context.
|
||||
val systemContext = ActivityThread.systemMain().systemContext as Context
|
||||
val appContext = systemContext.createPackageContext(managerPackageName, 0)
|
||||
|
||||
// Avoid annoying logs. See https://github.com/robolectric/robolectric/blob/ad0484c6b32c7d11176c711abeb3cb4a900f9258/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java#L376-L388
|
||||
Class.forName("android.app.AppCompatCallbacks").apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
getDeclaredMethod("install", longArrayClass, longArrayClass).also { it.isAccessible = true }(null, emptyLongArray, emptyLongArray)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
getDeclaredMethod("install", longArrayClass).also { it.isAccessible = true }(null, emptyLongArray)
|
||||
}
|
||||
}
|
||||
|
||||
val ipcInterface = PatcherProcess(appContext)
|
||||
|
||||
appContext.sendBroadcast(Intent().apply {
|
||||
action = ProcessRuntime.CONNECT_TO_APP_ACTION
|
||||
`package` = managerPackageName
|
||||
|
||||
putExtra(ProcessRuntime.INTENT_BUNDLE_KEY, Bundle().apply {
|
||||
putBinder(ProcessRuntime.BUNDLE_BINDER_KEY, ipcInterface.asBinder())
|
||||
})
|
||||
})
|
||||
|
||||
Looper.loop()
|
||||
exitProcess(1) // Shouldn't happen
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package app.revanced.manager.patcher.worker
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
@ -9,9 +10,10 @@ import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import android.os.PowerManager
|
||||
import android.util.Log
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.WorkerParameters
|
||||
@ -22,50 +24,58 @@ import app.revanced.manager.domain.installer.RootInstaller
|
||||
import app.revanced.manager.domain.manager.KeystoreManager
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
||||
import app.revanced.manager.domain.repository.DownloaderPluginRepository
|
||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.domain.worker.Worker
|
||||
import app.revanced.manager.domain.worker.WorkerRepository
|
||||
import app.revanced.manager.patcher.Session
|
||||
import app.revanced.manager.patcher.aapt.Aapt
|
||||
import app.revanced.manager.patcher.logger.ManagerLogger
|
||||
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.runtime.CoroutineRuntime
|
||||
import app.revanced.manager.patcher.runtime.ProcessRuntime
|
||||
import app.revanced.manager.plugin.downloader.GetScope
|
||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||
import app.revanced.manager.plugin.downloader.UserInteractionException
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import app.revanced.manager.util.tag
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
typealias ProgressEventHandler = (name: String?, state: State?, message: String?) -> Unit
|
||||
|
||||
@OptIn(PluginHostApi::class)
|
||||
class PatcherWorker(
|
||||
context: Context,
|
||||
parameters: WorkerParameters
|
||||
) : Worker<PatcherWorker.Args>(context, parameters), KoinComponent {
|
||||
private val patchBundleRepository: PatchBundleRepository by inject()
|
||||
private val workerRepository: WorkerRepository by inject()
|
||||
private val prefs: PreferencesManager by inject()
|
||||
private val keystoreManager: KeystoreManager by inject()
|
||||
private val downloaderPluginRepository: DownloaderPluginRepository by inject()
|
||||
private val downloadedAppRepository: DownloadedAppRepository by inject()
|
||||
private val pm: PM by inject()
|
||||
private val fs: Filesystem by inject()
|
||||
private val installedAppRepository: InstalledAppRepository by inject()
|
||||
private val rootInstaller: RootInstaller by inject()
|
||||
|
||||
data class Args(
|
||||
class Args(
|
||||
val input: SelectedApp,
|
||||
val output: String,
|
||||
val selectedPatches: PatchSelection,
|
||||
val options: Options,
|
||||
val logger: ManagerLogger,
|
||||
val downloadProgress: MutableStateFlow<Pair<Float, Float>?>,
|
||||
val patchesProgress: MutableStateFlow<Pair<Int, Int>>,
|
||||
val setInputFile: (File) -> Unit,
|
||||
val onProgress: (name: String?, state: State?, message: String?) -> Unit
|
||||
val logger: Logger,
|
||||
val onDownloadProgress: suspend (Pair<Long, Long?>?) -> Unit,
|
||||
val onPatchCompleted: suspend () -> Unit,
|
||||
val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult,
|
||||
val setInputFile: suspend (File) -> Unit,
|
||||
val onProgress: ProgressEventHandler
|
||||
) {
|
||||
val packageName get() = input.packageName
|
||||
}
|
||||
@ -111,7 +121,8 @@ class PatcherWorker(
|
||||
|
||||
val wakeLock: PowerManager.WakeLock =
|
||||
(applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager)
|
||||
.newWakeLock(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, "$tag::Patcher").apply {
|
||||
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::Patcher")
|
||||
.apply {
|
||||
acquire(10 * 60 * 1000L)
|
||||
Log.d(tag, "Acquired wakelock.")
|
||||
}
|
||||
@ -133,97 +144,114 @@ class PatcherWorker(
|
||||
val patchedApk = fs.tempDir.resolve("patched.apk")
|
||||
|
||||
return try {
|
||||
val bundles = patchBundleRepository.bundles.first()
|
||||
|
||||
// TODO: consider passing all the classes directly now that the input no longer needs to be serializable.
|
||||
val selectedBundles = args.selectedPatches.keys
|
||||
val allPatches = bundles.filterKeys { selectedBundles.contains(it) }
|
||||
.mapValues { (_, bundle) -> bundle.patchClasses(args.packageName) }
|
||||
|
||||
val selectedPatches = args.selectedPatches.flatMap { (bundle, selected) ->
|
||||
allPatches[bundle]?.filter { selected.contains(it.name) }
|
||||
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
||||
}
|
||||
|
||||
val aaptPath = Aapt.binary(applicationContext)?.absolutePath
|
||||
?: throw FileNotFoundException("Could not resolve aapt.")
|
||||
|
||||
val frameworkPath =
|
||||
applicationContext.cacheDir.resolve("framework").also { it.mkdirs() }.absolutePath
|
||||
|
||||
val integrations = bundles.mapNotNull { (_, bundle) -> bundle.integrations }
|
||||
|
||||
if (args.input is SelectedApp.Installed) {
|
||||
installedAppRepository.get(args.packageName)?.let {
|
||||
if (it.installType == InstallType.ROOT) {
|
||||
if (it.installType == InstallType.MOUNT) {
|
||||
rootInstaller.unmount(args.packageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set all patch options.
|
||||
args.options.forEach { (bundle, bundlePatchOptions) ->
|
||||
val patches = allPatches[bundle] ?: return@forEach
|
||||
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
|
||||
val patchOptions = patches.single { it.name == patchName }.options
|
||||
configuredPatchOptions.forEach { (key, value) ->
|
||||
patchOptions[key] = value
|
||||
}
|
||||
suspend fun download(plugin: LoadedDownloaderPlugin, data: Parcelable) =
|
||||
downloadedAppRepository.download(
|
||||
plugin,
|
||||
data,
|
||||
args.packageName,
|
||||
args.input.version,
|
||||
onDownload = args.onDownloadProgress
|
||||
).also {
|
||||
args.setInputFile(it)
|
||||
updateProgress(state = State.COMPLETED) // Download APK
|
||||
}
|
||||
}
|
||||
|
||||
updateProgress(state = State.COMPLETED) // Loading patches
|
||||
|
||||
val inputFile = when (val selectedApp = args.input) {
|
||||
is SelectedApp.Download -> {
|
||||
downloadedAppRepository.download(
|
||||
selectedApp.app,
|
||||
prefs.preferSplits.get(),
|
||||
onDownload = { args.downloadProgress.emit(it) }
|
||||
).also {
|
||||
args.setInputFile(it)
|
||||
updateProgress(state = State.COMPLETED) // Download APK
|
||||
}
|
||||
val (plugin, data) = downloaderPluginRepository.unwrapParceledData(selectedApp.data)
|
||||
|
||||
download(plugin, data)
|
||||
}
|
||||
|
||||
is SelectedApp.Search -> {
|
||||
downloaderPluginRepository.loadedPluginsFlow.first()
|
||||
.firstNotNullOfOrNull { plugin ->
|
||||
try {
|
||||
val getScope = object : GetScope {
|
||||
override val pluginPackageName = plugin.packageName
|
||||
override val hostPackageName = applicationContext.packageName
|
||||
override suspend fun requestStartActivity(intent: Intent): Intent? {
|
||||
val result = args.handleStartActivityRequest(plugin, intent)
|
||||
return when (result.resultCode) {
|
||||
Activity.RESULT_OK -> result.data
|
||||
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
|
||||
else -> throw UserInteractionException.Activity.NotCompleted(
|
||||
result.resultCode,
|
||||
result.data
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
plugin.get(
|
||||
getScope,
|
||||
selectedApp.packageName,
|
||||
selectedApp.version
|
||||
)
|
||||
}?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
|
||||
} catch (e: UserInteractionException.Activity.NotCompleted) {
|
||||
throw e
|
||||
} catch (_: UserInteractionException) {
|
||||
null
|
||||
}?.let { (data, _) -> download(plugin, data) }
|
||||
} ?: throw Exception("App is not available.")
|
||||
}
|
||||
|
||||
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
|
||||
is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo.sourceDir)
|
||||
is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo!!.sourceDir)
|
||||
}
|
||||
|
||||
Session(
|
||||
fs.tempDir.absolutePath,
|
||||
frameworkPath,
|
||||
aaptPath,
|
||||
prefs.multithreadingDexFileWriter.get(),
|
||||
applicationContext,
|
||||
args.logger,
|
||||
inputFile,
|
||||
args.patchesProgress,
|
||||
args.onProgress
|
||||
).use { session ->
|
||||
session.run(
|
||||
patchedApk,
|
||||
selectedPatches,
|
||||
integrations
|
||||
)
|
||||
val runtime = if (prefs.useProcessRuntime.get()) {
|
||||
ProcessRuntime(applicationContext)
|
||||
} else {
|
||||
CoroutineRuntime(applicationContext)
|
||||
}
|
||||
|
||||
runtime.execute(
|
||||
inputFile.absolutePath,
|
||||
patchedApk.absolutePath,
|
||||
args.packageName,
|
||||
args.selectedPatches,
|
||||
args.options,
|
||||
args.logger,
|
||||
args.onPatchCompleted,
|
||||
args.onProgress
|
||||
)
|
||||
|
||||
keystoreManager.sign(patchedApk, File(args.output))
|
||||
updateProgress(state = State.COMPLETED) // Signing
|
||||
|
||||
Log.i(tag, "Patching succeeded".logFmt())
|
||||
Result.success()
|
||||
} catch (e: ProcessRuntime.RemoteFailureException) {
|
||||
Log.e(
|
||||
tag,
|
||||
"An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt()
|
||||
)
|
||||
updateProgress(state = State.FAILED, message = e.originalStackTrace)
|
||||
Result.failure()
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Exception while patching".logFmt(), e)
|
||||
Log.e(tag, "An exception occurred while patching".logFmt(), e)
|
||||
updateProgress(state = State.FAILED, message = e.stackTraceToString())
|
||||
Result.failure()
|
||||
} finally {
|
||||
patchedApk.delete()
|
||||
if (args.input is SelectedApp.Local && args.input.temporary) {
|
||||
args.input.file.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val logPrefix = "[Worker]:"
|
||||
private fun String.logFmt() = "$logPrefix $this"
|
||||
private const val LOG_PREFIX = "[Worker]"
|
||||
private fun String.logFmt() = "$LOG_PREFIX $this"
|
||||
}
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
package app.revanced.manager.service
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import app.revanced.manager.IRootSystemService
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
@ -14,23 +12,5 @@ class ManagerRootService : RootService() {
|
||||
FileSystemManager.getService()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return RootSystemService()
|
||||
}
|
||||
}
|
||||
|
||||
class RootConnection : ServiceConnection {
|
||||
var remoteFS: FileSystemManager? = null
|
||||
private set
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
val ipc = IRootSystemService.Stub.asInterface(service)
|
||||
val binder = ipc.fileSystemService
|
||||
|
||||
remoteFS = FileSystemManager.getRemote(binder)
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
remoteFS = null
|
||||
}
|
||||
override fun onBind(intent: Intent): IBinder = RootSystemService()
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user