mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-08-14 09:48:31 +00:00
Compare commits
762 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2c6adbc69b | ||
![]() |
5280982363 | ||
![]() |
18c45ae289 | ||
![]() |
41fbd2a7be | ||
![]() |
5e45884af4 | ||
![]() |
d78ee171bc | ||
![]() |
356ee1febd | ||
![]() |
cc044ccc4c | ||
![]() |
9c638cc463 | ||
![]() |
df786eb2b6 | ||
![]() |
8e7186eebb | ||
![]() |
74b7b84561 | ||
![]() |
308c9999fa | ||
![]() |
930bb8687f | ||
![]() |
f2c4288d2d | ||
![]() |
b44141ae39 | ||
![]() |
86e0020964 | ||
![]() |
94d3daeadf | ||
![]() |
79334b7702 | ||
![]() |
df66458db6 | ||
![]() |
97705704e2 | ||
![]() |
1206179580 | ||
![]() |
a0b8aa4da6 | ||
![]() |
65207f96c8 | ||
![]() |
062e498bdd | ||
![]() |
1057cb3e3c | ||
![]() |
2dd23b2518 | ||
![]() |
8cab12998c | ||
![]() |
48b1c26dc8 | ||
![]() |
f1e0bc3e4a | ||
![]() |
38527cd58f | ||
![]() |
e94d65b4b2 | ||
![]() |
27ece3c7df | ||
![]() |
06687abffc | ||
![]() |
deedb462a0 | ||
![]() |
c48962bdf7 | ||
![]() |
1ef3f6e13b | ||
![]() |
83a34a9004 | ||
![]() |
e30bda6c8d | ||
![]() |
00e9d76a5a | ||
![]() |
6cda6c2fae | ||
![]() |
6dfda6dc39 | ||
![]() |
f41994cb52 | ||
![]() |
a003336497 | ||
![]() |
401090d6fe | ||
![]() |
90dcc1cd30 | ||
![]() |
2ac464b186 | ||
![]() |
8b7fae278b | ||
![]() |
d73c2daf6d | ||
![]() |
ca25935de3 | ||
![]() |
d7750b7220 | ||
![]() |
98861f0b5a | ||
![]() |
e35925d520 | ||
![]() |
685a2d2101 | ||
![]() |
f7e471616d | ||
![]() |
c013a349af | ||
![]() |
61ea59a27b | ||
![]() |
e55f338367 | ||
![]() |
1425cf4105 | ||
![]() |
b493a985b0 | ||
![]() |
1fe9ede940 | ||
![]() |
1fd49e4987 | ||
![]() |
d49b02b274 | ||
![]() |
d47e70cfaa | ||
![]() |
40cb031af5 | ||
![]() |
1dcf325547 | ||
![]() |
4e99997013 | ||
![]() |
334554697d | ||
![]() |
e77cbd0c15 | ||
![]() |
46ba008b9d | ||
![]() |
58aded31c2 | ||
![]() |
6f6b0ade06 | ||
![]() |
d9b67a207b | ||
![]() |
c7083659aa | ||
![]() |
a6d1803105 | ||
![]() |
66eef75673 | ||
![]() |
367f5f7b44 | ||
![]() |
0edcb03c45 | ||
![]() |
63eef153de | ||
![]() |
68442f38ac | ||
![]() |
8d5b9e5329 | ||
![]() |
6c0966b795 | ||
![]() |
7c2e93d266 | ||
![]() |
1ff7b9055f | ||
![]() |
49f241b77c | ||
![]() |
cfb20b0f86 | ||
![]() |
6d6f14fcb3 | ||
![]() |
977c981265 | ||
![]() |
ef48abf19d | ||
![]() |
65c18f9c09 | ||
![]() |
ecb31eed40 | ||
![]() |
a80cadf587 | ||
![]() |
fce1bf2365 | ||
![]() |
cbc6d40b2c | ||
![]() |
9fbd079560 | ||
![]() |
42eb928054 | ||
![]() |
577483fde1 | ||
![]() |
aa6c7c15cc | ||
![]() |
6c807d35b2 | ||
![]() |
8ca8cdae97 | ||
![]() |
75e37be6f3 | ||
![]() |
4985314ca6 | ||
![]() |
ac5ceb18c8 | ||
![]() |
72b39594d3 | ||
![]() |
16ae4aedf1 | ||
![]() |
3ba00858e6 | ||
![]() |
489100c755 | ||
![]() |
da766f2a4e | ||
![]() |
c81d7ff76c | ||
![]() |
a6e50d3648 | ||
![]() |
a177846044 | ||
![]() |
19a4e11645 | ||
![]() |
67cc36268e | ||
![]() |
28770b9a32 | ||
![]() |
9f92e1bf15 | ||
![]() |
23fe5d5a19 | ||
![]() |
9088b584f6 | ||
![]() |
beaf636415 | ||
![]() |
09bb2fe8dc | ||
![]() |
1d6747d90e | ||
![]() |
efadd94de3 | ||
![]() |
8c0b4e444a | ||
![]() |
32c7106e40 | ||
![]() |
d2f2a9e4c8 | ||
![]() |
985454afd4 | ||
![]() |
9e1322de25 | ||
![]() |
4e4ec73d94 | ||
![]() |
bb39a524d0 | ||
![]() |
196d9af099 | ||
![]() |
1eeb2a34a1 | ||
![]() |
cf43c56218 | ||
![]() |
e6c1aec443 | ||
![]() |
43fd1c4c1b | ||
![]() |
022caca979 | ||
![]() |
0352ea2cca | ||
![]() |
e483d6befe | ||
![]() |
678c07fff5 | ||
![]() |
91c92051f1 | ||
![]() |
4b8a0388e7 | ||
![]() |
66788dc58c | ||
![]() |
dd8c28b1cb | ||
![]() |
32c5153e8e | ||
![]() |
36de62873a | ||
![]() |
51e37880c6 | ||
![]() |
4b83c1e76c | ||
![]() |
b0b04690d5 | ||
![]() |
6d1e8d86cb | ||
![]() |
eda8c70a80 | ||
![]() |
587b6cfd41 | ||
![]() |
e774408782 | ||
![]() |
187f583c95 | ||
![]() |
f5d3a71478 | ||
![]() |
d868ff3080 | ||
![]() |
f80198a669 | ||
![]() |
6076b52c48 | ||
![]() |
79a1c39b30 | ||
![]() |
5c92d39498 | ||
![]() |
6e7a995716 | ||
![]() |
a55d570213 | ||
![]() |
5d07d0b964 | ||
![]() |
ec115cd7e3 | ||
![]() |
9b3896fd3d | ||
![]() |
a3f5918d25 | ||
![]() |
b28326198c | ||
![]() |
46275b90c2 | ||
![]() |
15e13a8d8b | ||
![]() |
b750c89c87 | ||
![]() |
8d7c7c3dfb | ||
![]() |
8e1a91509c | ||
![]() |
927cd571f8 | ||
![]() |
5fbd3e5c65 | ||
![]() |
877aeb66cb | ||
![]() |
8a88d8465a | ||
![]() |
dda8cc85c9 | ||
![]() |
6a59939d9a | ||
![]() |
4745e86c1b | ||
![]() |
9aa466c773 | ||
![]() |
0243610c09 | ||
![]() |
0a2a590ab7 | ||
![]() |
89aee6ffa7 | ||
![]() |
4eaf701cb7 | ||
![]() |
4fff2aa7d8 | ||
![]() |
35b3c8ba5c | ||
![]() |
1d7cff7703 | ||
![]() |
8d81bd0e33 | ||
![]() |
7826d7527f | ||
![]() |
d4e552d08b | ||
![]() |
5a16418543 | ||
![]() |
7297aba15a | ||
![]() |
bc5d5f9502 | ||
![]() |
1761986c1b | ||
![]() |
1e034e3e0e | ||
![]() |
bbf9756bfa | ||
![]() |
96e559fb0e | ||
![]() |
4c45775131 | ||
![]() |
c072b4254d | ||
![]() |
2dbb812126 | ||
![]() |
be50f17f55 | ||
![]() |
6f77f190f2 | ||
![]() |
6bdc57cbe4 | ||
![]() |
de00f1d5a9 | ||
![]() |
e9b9bf987b | ||
![]() |
f4b6385f9f | ||
![]() |
75d905a56d | ||
![]() |
b1363ee479 | ||
![]() |
51afe43a30 | ||
![]() |
189c03c047 | ||
![]() |
ae9d270a32 | ||
![]() |
e47e869f6b | ||
![]() |
c39038a439 | ||
![]() |
69174e2c13 | ||
![]() |
474268a0af | ||
![]() |
eadb0307fa | ||
![]() |
5a5d0d5d72 | ||
![]() |
a1273bc467 | ||
![]() |
0c28a916be | ||
![]() |
0ba573b789 | ||
![]() |
ec42ee152c | ||
![]() |
abcb487361 | ||
![]() |
d12d9e82f1 | ||
![]() |
275208e81b | ||
![]() |
41226c12b8 | ||
![]() |
f86c66c99d | ||
![]() |
93876b5fd3 | ||
![]() |
b5b14ce343 | ||
![]() |
350d0d600c | ||
![]() |
f924ffcbf3 | ||
![]() |
0f5963f231 | ||
![]() |
1961ff2c40 | ||
![]() |
40003691d6 | ||
![]() |
8290358241 | ||
![]() |
ee34f775c3 | ||
![]() |
feb47cd88c | ||
![]() |
c6efb51f61 | ||
![]() |
a5acf33ccd | ||
![]() |
ab9ee449e4 | ||
![]() |
9571b6f9be | ||
![]() |
207d7fd3f6 | ||
![]() |
bcdcfa1104 | ||
![]() |
e0a4230dac | ||
![]() |
17ba5cba3e | ||
![]() |
f2e109ad7d | ||
![]() |
c83e141a1c | ||
![]() |
6089cc36de | ||
![]() |
9638dc0a66 | ||
![]() |
b191a14a23 | ||
![]() |
cf1bc82537 | ||
![]() |
6141bb5bb3 | ||
![]() |
4d2b62da0d | ||
![]() |
39383229d1 | ||
![]() |
08bfbb154a | ||
![]() |
d390ca2fdf | ||
![]() |
7ad77a14ae | ||
![]() |
f33343b4e6 | ||
![]() |
af65d07456 | ||
![]() |
16d728f379 | ||
![]() |
c97ab690b6 | ||
![]() |
4caed73fe0 | ||
![]() |
4856da1584 | ||
![]() |
0a07018fec | ||
![]() |
64c82e1f2c | ||
![]() |
e8e8afa6c2 | ||
![]() |
af2207433d | ||
![]() |
75ba62d588 | ||
![]() |
606d97ae4d | ||
![]() |
d778b0b0a7 | ||
![]() |
5ee6daf126 | ||
![]() |
43b9a09c9b | ||
![]() |
8475a2bb94 | ||
![]() |
d8692de2f4 | ||
![]() |
33a9abc946 | ||
![]() |
ee943afbc9 | ||
![]() |
1f7c3e9f14 | ||
![]() |
46770db18b | ||
![]() |
92f980601c | ||
![]() |
d0b8c16651 | ||
![]() |
a470ee6f93 | ||
![]() |
ff1c56683d | ||
![]() |
4ee4cbada6 | ||
![]() |
dbc2236dd2 | ||
![]() |
a8c4a33e91 | ||
![]() |
279f955a84 | ||
![]() |
fbd1dbb20c | ||
![]() |
6c09fc2e64 | ||
![]() |
f3304b482c | ||
![]() |
0a85ef61c3 | ||
![]() |
dc26ad7125 | ||
![]() |
24b1c607f3 | ||
![]() |
732a161b67 | ||
![]() |
9c7cf340a1 | ||
![]() |
399b9e5eba | ||
![]() |
5805573625 | ||
![]() |
a6b1149b9f | ||
![]() |
51e985ae7f | ||
![]() |
9929b25339 | ||
![]() |
2359cfc480 | ||
![]() |
81493475f9 | ||
![]() |
0493829231 | ||
![]() |
e2d1952ad9 | ||
![]() |
7450965458 | ||
![]() |
f45384685b | ||
![]() |
8abcccc262 | ||
![]() |
a9c89cbbbb | ||
![]() |
d2eaa6e6c1 | ||
![]() |
53257b6ea1 | ||
![]() |
c874391be4 | ||
![]() |
7e8e013832 | ||
![]() |
037f46f7f0 | ||
![]() |
d3e1c496ca | ||
![]() |
d7d0a44693 | ||
![]() |
9d6f6764cb | ||
![]() |
cb3ab63815 | ||
![]() |
caae932117 | ||
![]() |
e9cf27eb5a | ||
![]() |
6ee6685f4c | ||
![]() |
d15017b777 | ||
![]() |
a9387e63e1 | ||
![]() |
23c1f0111b | ||
![]() |
866386e21f | ||
![]() |
bf10496fa9 | ||
![]() |
607e6547a7 | ||
![]() |
6b21091fe2 | ||
![]() |
e58f98e844 | ||
![]() |
b8cb9cd84d | ||
![]() |
c1038ac6f9 | ||
![]() |
c556dd0aac | ||
![]() |
d2fbcd07b7 | ||
![]() |
bf6359abaa | ||
![]() |
d1621845b8 | ||
![]() |
f33f1d25d0 | ||
![]() |
40f25f4d56 | ||
![]() |
e13775ec2c | ||
![]() |
ee4dad7a13 | ||
![]() |
5e2ef1b7f4 | ||
![]() |
f8c38eab74 | ||
![]() |
305e8b3d14 | ||
![]() |
2a654e5d7f | ||
![]() |
57afae3425 | ||
![]() |
feb44f875e | ||
![]() |
7eebe62bb6 | ||
![]() |
9ea9f01933 | ||
![]() |
665c6bdc4b | ||
![]() |
c79bc83275 | ||
![]() |
c30fbdf145 | ||
![]() |
f12951bd1d | ||
![]() |
52f2e8c4a0 | ||
![]() |
1b2af1ed6d | ||
![]() |
0f9b2a7df8 | ||
![]() |
f2846694e1 | ||
![]() |
e668dbf6f7 | ||
![]() |
d77a368176 | ||
![]() |
ad0da08610 | ||
![]() |
0c52385ad4 | ||
![]() |
5b8b48ccc1 | ||
![]() |
659b9c6fee | ||
![]() |
ec31cab5a7 | ||
![]() |
dd93556ad8 | ||
![]() |
533aeadd38 | ||
![]() |
18d0cedbe2 | ||
![]() |
5a94ef9106 | ||
![]() |
8e8f01f8b5 | ||
![]() |
7087badf87 | ||
![]() |
47d2d4e3a5 | ||
![]() |
6c47d8f556 | ||
![]() |
8c9d0314fb | ||
![]() |
69144942e3 | ||
![]() |
5627053b74 | ||
![]() |
0f666de5e6 | ||
![]() |
eddc862fa3 | ||
![]() |
4327682120 | ||
![]() |
af5bdee78f | ||
![]() |
0e36e86dbf | ||
![]() |
f95478f1f1 | ||
![]() |
9fe8741a02 | ||
![]() |
a5768e02ea | ||
![]() |
f5aaff2b1e | ||
![]() |
655f778171 | ||
![]() |
2e77a426b2 | ||
![]() |
2bcf2e76f1 | ||
![]() |
57bd450798 | ||
![]() |
582cad1b8d | ||
![]() |
6ca2a3d841 | ||
![]() |
91773c3311 | ||
![]() |
dc61033b2c | ||
![]() |
f8d62a4b6c | ||
![]() |
1d2145b1b7 | ||
![]() |
1f7f84b74a | ||
![]() |
cd7a335d0f | ||
![]() |
17569005a4 | ||
![]() |
f36b21bae5 | ||
![]() |
fe1ca52f6d | ||
![]() |
1be647a279 | ||
![]() |
2ea1a47bec | ||
![]() |
2d799dae0d | ||
![]() |
c6408babac | ||
![]() |
a8c1ed8795 | ||
![]() |
e3cb5f8ddd | ||
![]() |
e160e211dd | ||
![]() |
22d05ca399 | ||
![]() |
bd2651057d | ||
![]() |
1610092ec4 | ||
![]() |
b9e6937996 | ||
![]() |
a207f03952 | ||
![]() |
851153dd7c | ||
![]() |
583ffc8177 | ||
![]() |
7518092ad2 | ||
![]() |
8c2ad3883a | ||
![]() |
d364554425 | ||
![]() |
726ffdcd98 | ||
![]() |
f9d22cf8ee | ||
![]() |
ee50da566f | ||
![]() |
9f7d410959 | ||
![]() |
bc94ea4334 | ||
![]() |
c0c9204848 | ||
![]() |
c0d1bf63bc | ||
![]() |
bbda0cdffe | ||
![]() |
7b5ff99cd1 | ||
![]() |
21ddb26db8 | ||
![]() |
7bf2e3875f | ||
![]() |
b136aba1e2 | ||
![]() |
0d84f80b3c | ||
![]() |
af45aeb771 | ||
![]() |
1c5a435e1f | ||
![]() |
0ea1257dcd | ||
![]() |
4c92677b5a | ||
![]() |
979260bd62 | ||
![]() |
f7de649a36 | ||
![]() |
0cf0d2b821 | ||
![]() |
3733c9a091 | ||
![]() |
e9f32e4f68 | ||
![]() |
68c2817d40 | ||
![]() |
83d837d868 | ||
![]() |
093eb15ee1 | ||
![]() |
c6412c1b1b | ||
![]() |
1151393d74 | ||
![]() |
468f3efb13 | ||
![]() |
d6b19b9d4c | ||
![]() |
709f25f600 | ||
![]() |
4b16e4b026 | ||
![]() |
cdfbc02922 | ||
![]() |
d0c9384233 | ||
![]() |
2488668b06 | ||
![]() |
52a98cbd51 | ||
![]() |
1840c4c486 | ||
![]() |
34080f3958 | ||
![]() |
e9b76b6aa5 | ||
![]() |
b7799b53d9 | ||
![]() |
1e206515c7 | ||
![]() |
6bb313184d | ||
![]() |
2763992434 | ||
![]() |
18fe0e6442 | ||
![]() |
a70c73bffd | ||
![]() |
b4ae3493a6 | ||
![]() |
1a16004b20 | ||
![]() |
56707b8119 | ||
![]() |
c3f9533ddc | ||
![]() |
3b3abd63cc | ||
![]() |
411d3ed4e9 | ||
![]() |
f29cc26103 | ||
![]() |
1cd595a598 | ||
![]() |
22e023b58d | ||
![]() |
7be958e35d | ||
![]() |
69b66ef637 | ||
![]() |
daf8653c38 | ||
![]() |
e2545e57cf | ||
![]() |
7cb0909c70 | ||
![]() |
cc5ff36165 | ||
![]() |
18b1ef6c29 | ||
![]() |
7fe012347a | ||
![]() |
5c165c9bb0 | ||
![]() |
6c3519923d | ||
![]() |
9ea859810d | ||
![]() |
8dae7b5451 | ||
![]() |
f827755aaf | ||
![]() |
637a8af234 | ||
![]() |
b0fc580860 | ||
![]() |
9279f30e89 | ||
![]() |
b505819ca2 | ||
![]() |
39d1d23909 | ||
![]() |
69529ac59c | ||
![]() |
a18a440236 | ||
![]() |
aa7846c1c0 | ||
![]() |
24ba4ab95b | ||
![]() |
762b70ba9d | ||
![]() |
41b77e4f25 | ||
![]() |
2087e47300 | ||
![]() |
46ce765860 | ||
![]() |
5117dc1a31 | ||
![]() |
620fd7d124 | ||
![]() |
3e991dc003 | ||
![]() |
15cab86152 | ||
![]() |
aa785b5845 | ||
![]() |
97731a519a | ||
![]() |
b696dae808 | ||
![]() |
732a8260c2 | ||
![]() |
4ff60ef9a9 | ||
![]() |
23b1b69110 | ||
![]() |
3a4fe53f27 | ||
![]() |
e48afff5e8 | ||
![]() |
3f4f4598e8 | ||
![]() |
3921e9cb1b | ||
![]() |
0b987dd0b0 | ||
![]() |
1620e15f99 | ||
![]() |
b089511e91 | ||
![]() |
958788c1aa | ||
![]() |
b5a8a27296 | ||
![]() |
98123775ad | ||
![]() |
c7133974be | ||
![]() |
04324a7ebe | ||
![]() |
f54daa3469 | ||
![]() |
07c22ccd39 | ||
![]() |
e893c13cf1 | ||
![]() |
dba5020e4f | ||
![]() |
87e036a190 | ||
![]() |
3dd94672b0 | ||
![]() |
004b193f69 | ||
![]() |
4417997749 | ||
![]() |
2eef542054 | ||
![]() |
a07d4080b6 | ||
![]() |
b9d0a3b3d4 | ||
![]() |
76405bd984 | ||
![]() |
4e2b88b3d0 | ||
![]() |
7048aa1014 | ||
![]() |
1c2fcd14b5 | ||
![]() |
84e1bd7bc3 | ||
![]() |
362eea741f | ||
![]() |
4de93cfd4b | ||
![]() |
03cee0b8d4 | ||
![]() |
54ecc001f4 | ||
![]() |
5c325d9466 | ||
![]() |
0e851cdcf8 | ||
![]() |
af054e4e31 | ||
![]() |
33fb4653f0 | ||
![]() |
d9f0aed571 | ||
![]() |
98813c24fb | ||
![]() |
3cc81bb3fd | ||
![]() |
366dd52419 | ||
![]() |
fe6b658c02 | ||
![]() |
3cf66d1c57 | ||
![]() |
382568bd3c | ||
![]() |
d130aa02a1 | ||
![]() |
1a1646795f | ||
![]() |
d52ea1b068 | ||
![]() |
e14f7b6908 | ||
![]() |
4709a32641 | ||
![]() |
71b7f52663 | ||
![]() |
981ccabbef | ||
![]() |
9e07eb592c | ||
![]() |
9555380818 | ||
![]() |
f80d5d858e | ||
![]() |
a1ce6f5f12 | ||
![]() |
1aade8f8a8 | ||
![]() |
b9213b7043 | ||
![]() |
4af72324f4 | ||
![]() |
b6ea5b8984 | ||
![]() |
c279e08c88 | ||
![]() |
2717feac21 | ||
![]() |
8adf27859d | ||
![]() |
307cf87215 | ||
![]() |
ca31412c05 | ||
![]() |
f59fbd5dca | ||
![]() |
2285f5e888 | ||
![]() |
da36e5bcd5 | ||
![]() |
4ed9f57fdc | ||
![]() |
ea7be6162f | ||
![]() |
3726eb6032 | ||
![]() |
6e918ffd68 | ||
![]() |
4772868d6a | ||
![]() |
78df677a42 | ||
![]() |
85a4b249b3 | ||
![]() |
d06e9a0b51 | ||
![]() |
5eb774a2ad | ||
![]() |
cbbbbab483 | ||
![]() |
e5641d5bdb | ||
![]() |
a721206c6f | ||
![]() |
c7a27481f9 | ||
![]() |
594c304634 | ||
![]() |
d0ec387c28 | ||
![]() |
7dbfba76bf | ||
![]() |
2a4aa95a6f | ||
![]() |
5520f0fbf7 | ||
![]() |
a1a87c9956 | ||
![]() |
2c53356bfd | ||
![]() |
85d9756f62 | ||
![]() |
79586ece4c | ||
![]() |
6851d11a8e | ||
![]() |
996a857096 | ||
![]() |
d7158131e4 | ||
![]() |
3d3082bc82 | ||
![]() |
744ebca206 | ||
![]() |
92077ebe53 | ||
![]() |
78ca682bc5 | ||
![]() |
af01a36296 | ||
![]() |
97ed1b16d0 | ||
![]() |
508a001753 | ||
![]() |
c1909d520b | ||
![]() |
9b1e173373 | ||
![]() |
4ba365565f | ||
![]() |
ae34659b26 | ||
![]() |
79a85f5937 | ||
![]() |
b249832571 | ||
![]() |
577b5912af | ||
![]() |
9e8c68af12 | ||
![]() |
03418ddcbf | ||
![]() |
220a1c84ce | ||
![]() |
9a4458ffac | ||
![]() |
7a9e6d2ad2 | ||
![]() |
9656cf2f86 | ||
![]() |
584bad5314 | ||
![]() |
459088024f | ||
![]() |
d740bbe058 | ||
![]() |
6ecc04a4df | ||
![]() |
15a7e9af57 | ||
![]() |
0329f00129 | ||
![]() |
cd8a2edefb | ||
![]() |
4318ab5cd2 | ||
![]() |
3517e6d752 | ||
![]() |
67845f9c21 | ||
![]() |
f562710438 | ||
![]() |
e836909c50 | ||
![]() |
7769ba5f54 | ||
![]() |
7fe9db90a1 | ||
![]() |
8f7d6dfb77 | ||
![]() |
2839978cc1 | ||
![]() |
e73f87b758 | ||
![]() |
bd0409fd15 | ||
![]() |
babdfe80cb | ||
![]() |
636223b289 | ||
![]() |
aa0a2f77cf | ||
![]() |
e38f35eab2 | ||
![]() |
cb39514705 | ||
![]() |
78a444d601 | ||
![]() |
37b81ad1f6 | ||
![]() |
7871c2f595 | ||
![]() |
57d83635c6 | ||
![]() |
76fbf4634a | ||
![]() |
7ce4bd3330 | ||
![]() |
ad0e6511e1 | ||
![]() |
a4a734458b | ||
![]() |
f989756b93 | ||
![]() |
5763a3d908 | ||
![]() |
1b745ae1a0 | ||
![]() |
b6d50bea2c | ||
![]() |
831a398bf1 | ||
![]() |
a848783b97 | ||
![]() |
4d876f0145 | ||
![]() |
bdfedea4e0 | ||
![]() |
ea0e3a09ef | ||
![]() |
dadae20960 | ||
![]() |
4ed34cd648 | ||
![]() |
0d38c94c9c | ||
![]() |
2a2a452bd4 | ||
![]() |
13c2695e98 | ||
![]() |
3ff60ed49f | ||
![]() |
bbb1786ec3 | ||
![]() |
4bfd2dac54 | ||
![]() |
857c12372a | ||
![]() |
33f5154269 | ||
![]() |
ed37ddd570 | ||
![]() |
cd5384f13e | ||
![]() |
11b2ddbad8 | ||
![]() |
cf9957ce4d | ||
![]() |
44643ad7b3 | ||
![]() |
1e53a5555e | ||
![]() |
616adc22e1 | ||
![]() |
916e373edb | ||
![]() |
021ae15395 | ||
![]() |
52cf72002a | ||
![]() |
68874bf571 | ||
![]() |
a468fd946d | ||
![]() |
e327565434 | ||
![]() |
c3b4678f6e | ||
![]() |
978216eade | ||
![]() |
44cfe94e4d | ||
![]() |
f9e82c9e8a | ||
![]() |
25b4b107d3 | ||
![]() |
db651fa9ec | ||
![]() |
23ad611566 | ||
![]() |
095d821240 | ||
![]() |
e23f23a8b7 | ||
![]() |
48f829b76e | ||
![]() |
0b82fe197c | ||
![]() |
af99c1b843 | ||
![]() |
c6646efe68 | ||
![]() |
66a7ef5615 | ||
![]() |
9474750bdf | ||
![]() |
e86db0bd61 | ||
![]() |
a29fc11798 | ||
![]() |
a66a3b7438 | ||
![]() |
44029875a6 | ||
![]() |
ccf21b0992 | ||
![]() |
4e14dab60a | ||
![]() |
6e299018a4 | ||
![]() |
555a54ec53 | ||
![]() |
1565bf5442 | ||
![]() |
14b830027b | ||
![]() |
38325e708e | ||
![]() |
646260ad6d | ||
![]() |
d1d26f4481 | ||
![]() |
357d913f18 | ||
![]() |
71b0c8b42b | ||
![]() |
cdc66c1ac8 | ||
![]() |
e9af773901 | ||
![]() |
eadf6e8b96 | ||
![]() |
87bec70d9f | ||
![]() |
3668b28f62 | ||
![]() |
933e4bd163 | ||
![]() |
e3ab9e9a1e | ||
![]() |
58ad2c1416 | ||
![]() |
c5291ad33b | ||
![]() |
77d8445bfd | ||
![]() |
f8395a7dc6 | ||
![]() |
727c70005e | ||
![]() |
38ab6858f0 | ||
![]() |
a54114f149 | ||
![]() |
7a4a5c8992 | ||
![]() |
928a16d8cc | ||
![]() |
3f7f6e619a | ||
![]() |
c2f96975ce | ||
![]() |
8bd4760b00 | ||
![]() |
4f4aeb893d | ||
![]() |
fed4f1b50f | ||
![]() |
e11087cd1a | ||
![]() |
e6eb51551c | ||
![]() |
c5c608f0d3 | ||
![]() |
4737c5117a | ||
![]() |
9806b38d8e | ||
![]() |
6bfe34e5a8 | ||
![]() |
34dd9eb7d6 | ||
![]() |
2d8beabbd4 | ||
![]() |
4d9b7e7114 | ||
![]() |
40aab13601 | ||
![]() |
4c0f72f68f | ||
![]() |
dd565a11ea | ||
![]() |
1735a713cb | ||
![]() |
52ba6d11bc | ||
![]() |
7357a35f8d | ||
![]() |
aeb7fd7cb3 | ||
![]() |
1b4a6850b8 | ||
![]() |
07b45f39df | ||
![]() |
1d0b873950 | ||
![]() |
d449f49d73 | ||
![]() |
e8787b5cfd | ||
![]() |
d17ed2b979 | ||
![]() |
b496923cbb | ||
![]() |
759d196aad | ||
![]() |
a7ab8216ce | ||
![]() |
b9e89a1a2d | ||
![]() |
c7c9fb9576 | ||
![]() |
8b095de04d | ||
![]() |
468325b51a | ||
![]() |
e5058bfb8b | ||
![]() |
d4b9ef736d | ||
![]() |
00d3cb0908 | ||
![]() |
d35072d4e6 | ||
![]() |
1a964e78dd | ||
![]() |
4264ae49c0 | ||
![]() |
f08712cd0a | ||
![]() |
3906fe75dc | ||
![]() |
2497e548c9 | ||
![]() |
e4635684e9 | ||
![]() |
9b61bdfc9a |
44
.github/actions/setup/action.yml
vendored
Normal file
44
.github/actions/setup/action.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
name: Magisk Setup
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: "temurin"
|
||||||
|
java-version: "17"
|
||||||
|
|
||||||
|
- name: Set up Python 3
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.x"
|
||||||
|
|
||||||
|
- name: Set up sccache
|
||||||
|
uses: hendrikmuhs/ccache-action@v1.2
|
||||||
|
with:
|
||||||
|
variant: sccache
|
||||||
|
key: ${{ runner.os }}-${{ github.sha }}
|
||||||
|
restore-keys: ${{ runner.os }}
|
||||||
|
max-size: 10000M
|
||||||
|
|
||||||
|
- name: Cache Gradle dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
!~/.gradle/caches/build-cache-*
|
||||||
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
|
||||||
|
restore-keys: ${{ runner.os }}-gradle-
|
||||||
|
|
||||||
|
- name: Cache build cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches/build-cache-*
|
||||||
|
key: ${{ runner.os }}-build-cache-${{ github.sha }}
|
||||||
|
restore-keys: ${{ runner.os }}-build-cache-
|
||||||
|
|
||||||
|
- name: Set up NDK
|
||||||
|
run: python build.py -v ndk
|
||||||
|
shell: bash
|
19
.github/ccache.sh
vendored
19
.github/ccache.sh
vendored
@@ -1,19 +0,0 @@
|
|||||||
OS=$(uname)
|
|
||||||
CCACHE_VER=4.4
|
|
||||||
|
|
||||||
case $OS in
|
|
||||||
Darwin )
|
|
||||||
brew install ccache
|
|
||||||
ln -s $(which ccache) ./ccache
|
|
||||||
;;
|
|
||||||
Linux )
|
|
||||||
sudo apt-get install -y ccache
|
|
||||||
ln -s $(which ccache) ./ccache
|
|
||||||
;;
|
|
||||||
* )
|
|
||||||
curl -OL https://github.com/ccache/ccache/releases/download/v${CCACHE_VER}/ccache-${CCACHE_VER}-windows-64.zip
|
|
||||||
unzip -j ccache-*-windows-64.zip '*/ccache.exe'
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
mkdir ./.ccache
|
|
||||||
./ccache -o compiler_check='%compiler% -dumpmachine; %compiler% -dumpversion'
|
|
150
.github/workflows/build.yml
vendored
150
.github/workflows/build.yml
vendored
@@ -2,90 +2,116 @@ name: Magisk Build
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ master ]
|
branches: [master]
|
||||||
paths:
|
paths:
|
||||||
- 'app/**'
|
- "app/**"
|
||||||
- 'native/**'
|
- "native/**"
|
||||||
- 'stub/**'
|
- "stub/**"
|
||||||
- 'buildSrc/**'
|
- "buildSrc/**"
|
||||||
- 'build.py'
|
- "build.py"
|
||||||
- 'gradle.properties'
|
- "gradle.properties"
|
||||||
- '.github/workflows/build.yml'
|
- ".github/workflows/build.yml"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [master]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build on ${{ matrix.os }}
|
name: Build Magisk artifacts
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
SCCACHE_DIRECT: false
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
|
||||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
|
||||||
env:
|
|
||||||
NDK_CCACHE: ${{ github.workspace }}/ccache
|
|
||||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out
|
- name: Check out
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: 'recursive'
|
submodules: "recursive"
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up JDK 11
|
- name: Setup environment
|
||||||
uses: actions/setup-java@v1
|
uses: ./.github/actions/setup
|
||||||
with:
|
|
||||||
java-version: '11'
|
|
||||||
|
|
||||||
- name: Set up Python 3
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: '3.x'
|
|
||||||
|
|
||||||
- name: Set up ccache
|
|
||||||
run: bash .github/ccache.sh
|
|
||||||
|
|
||||||
- name: Cache Gradle dependencies
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.gradle/caches
|
|
||||||
~/.gradle/wrapper
|
|
||||||
!~/.gradle/caches/build-cache-*
|
|
||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
|
|
||||||
restore-keys: ${{ runner.os }}-gradle-
|
|
||||||
|
|
||||||
- name: Cache build cache
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
${{ github.workspace }}/.ccache
|
|
||||||
~/.gradle/caches/build-cache-*
|
|
||||||
key: ${{ runner.os }}-build-cache-${{ github.sha }}
|
|
||||||
restore-keys: ${{ runner.os }}-build-cache-
|
|
||||||
|
|
||||||
- name: Set up NDK
|
|
||||||
run: python build.py -v ndk
|
|
||||||
|
|
||||||
- name: Build release
|
- name: Build release
|
||||||
run: |
|
run: ./build.py -vr all
|
||||||
./ccache -zp
|
|
||||||
python build.py -vr all
|
|
||||||
|
|
||||||
- name: Build debug
|
- name: Build debug
|
||||||
run: |
|
run: ./build.py -v all
|
||||||
python build.py -v all
|
|
||||||
./ccache -s
|
|
||||||
|
|
||||||
- name: Stop gradle daemon
|
- name: Stop gradle daemon
|
||||||
run: ./gradlew --stop
|
run: ./gradlew --stop
|
||||||
|
|
||||||
# Only upload artifacts built on Linux
|
|
||||||
- name: Upload build artifact
|
- name: Upload build artifact
|
||||||
if: runner.os == 'Linux'
|
uses: actions/upload-artifact@v4
|
||||||
uses: actions/upload-artifact@v2
|
|
||||||
with:
|
with:
|
||||||
name: ${{ github.sha }}
|
name: ${{ github.sha }}
|
||||||
path: out
|
path: out
|
||||||
|
compression-level: 9
|
||||||
|
|
||||||
|
- name: Upload mapping and native debug symbols
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ github.sha }}-symbols
|
||||||
|
path: app/build/outputs
|
||||||
|
compression-level: 9
|
||||||
|
|
||||||
|
test-build:
|
||||||
|
name: Test building on ${{ matrix.os }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
env:
|
||||||
|
SCCACHE_DIRECT: false
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [windows-latest, macos-14]
|
||||||
|
steps:
|
||||||
|
- name: Check out
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: "recursive"
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup environment
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
|
||||||
|
- name: Build debug
|
||||||
|
run: python build.py -v all
|
||||||
|
|
||||||
|
- name: Stop gradle daemon
|
||||||
|
run: ./gradlew --stop
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Test on API ${{ matrix.api }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
api: [23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Python 3
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.x"
|
||||||
|
|
||||||
|
- name: Download build artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ github.sha }}
|
||||||
|
path: out
|
||||||
|
|
||||||
|
- name: Enable KVM group perms
|
||||||
|
run: |
|
||||||
|
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||||
|
sudo udevadm control --reload-rules
|
||||||
|
sudo udevadm trigger --name-match=kvm
|
||||||
|
|
||||||
|
- name: AVD test
|
||||||
|
run: scripts/avd_test.sh ${{ matrix.api }}
|
||||||
|
46
.gitmodules
vendored
46
.gitmodules
vendored
@@ -1,45 +1,39 @@
|
|||||||
[submodule "selinux"]
|
[submodule "selinux"]
|
||||||
path = native/jni/external/selinux
|
path = native/src/external/selinux
|
||||||
url = https://github.com/topjohnwu/selinux.git
|
url = https://github.com/topjohnwu/selinux.git
|
||||||
[submodule "busybox"]
|
[submodule "busybox"]
|
||||||
path = native/jni/external/busybox
|
path = native/src/external/busybox
|
||||||
url = https://github.com/topjohnwu/ndk-busybox.git
|
url = https://github.com/topjohnwu/ndk-busybox.git
|
||||||
[submodule "dtc"]
|
|
||||||
path = native/jni/external/dtc
|
|
||||||
url = https://github.com/dgibson/dtc.git
|
|
||||||
[submodule "lz4"]
|
[submodule "lz4"]
|
||||||
path = native/jni/external/lz4
|
path = native/src/external/lz4
|
||||||
url = https://github.com/lz4/lz4.git
|
url = https://github.com/lz4/lz4.git
|
||||||
[submodule "bzip2"]
|
[submodule "bzip2"]
|
||||||
path = native/jni/external/bzip2
|
path = native/src/external/bzip2
|
||||||
url = https://github.com/nemequ/bzip2.git
|
url = https://github.com/nemequ/bzip2.git
|
||||||
[submodule "xz"]
|
[submodule "xz"]
|
||||||
path = native/jni/external/xz
|
path = native/src/external/xz
|
||||||
url = https://github.com/xz-mirror/xz.git
|
url = https://github.com/xz-mirror/xz.git
|
||||||
[submodule "nanopb"]
|
|
||||||
path = native/jni/external/nanopb
|
|
||||||
url = https://github.com/nanopb/nanopb.git
|
|
||||||
[submodule "mincrypt"]
|
|
||||||
path = native/jni/external/mincrypt
|
|
||||||
url = https://github.com/topjohnwu/mincrypt.git
|
|
||||||
[submodule "pcre"]
|
[submodule "pcre"]
|
||||||
path = native/jni/external/pcre
|
path = native/src/external/pcre
|
||||||
url = https://android.googlesource.com/platform/external/pcre
|
url = https://android.googlesource.com/platform/external/pcre
|
||||||
[submodule "libcxx"]
|
[submodule "libcxx"]
|
||||||
path = native/jni/external/libcxx
|
path = native/src/external/libcxx
|
||||||
url = https://github.com/topjohnwu/libcxx.git
|
url = https://github.com/topjohnwu/libcxx.git
|
||||||
[submodule "zlib"]
|
[submodule "zlib"]
|
||||||
path = native/jni/external/zlib
|
path = native/src/external/zlib
|
||||||
url = https://android.googlesource.com/platform/external/zlib
|
url = https://android.googlesource.com/platform/external/zlib
|
||||||
[submodule "parallel-hashmap"]
|
[submodule "zopfli"]
|
||||||
path = native/jni/external/parallel-hashmap
|
path = native/src/external/zopfli
|
||||||
url = https://github.com/greg7mdp/parallel-hashmap.git
|
url = https://github.com/google/zopfli.git
|
||||||
|
[submodule "cxx-rs"]
|
||||||
|
path = native/src/external/cxx-rs
|
||||||
|
url = https://github.com/topjohnwu/cxx.git
|
||||||
|
[submodule "lsplt"]
|
||||||
|
path = native/src/external/lsplt
|
||||||
|
url = https://github.com/LSPosed/LSPlt.git
|
||||||
|
[submodule "system_properties"]
|
||||||
|
path = native/src/external/system_properties
|
||||||
|
url = https://github.com/topjohnwu/system_properties.git
|
||||||
[submodule "termux-elf-cleaner"]
|
[submodule "termux-elf-cleaner"]
|
||||||
path = tools/termux-elf-cleaner
|
path = tools/termux-elf-cleaner
|
||||||
url = https://github.com/termux/termux-elf-cleaner.git
|
url = https://github.com/termux/termux-elf-cleaner.git
|
||||||
[submodule "zopfli"]
|
|
||||||
path = native/jni/external/zopfli
|
|
||||||
url = https://github.com/google/zopfli.git
|
|
||||||
[submodule "cxx-rs"]
|
|
||||||
path = native/jni/external/cxx-rs
|
|
||||||
url = https://github.com/topjohnwu/cxx.git
|
|
||||||
|
32
README.MD
32
README.MD
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
Magisk is a suite of open source software for customizing Android, supporting devices higher than Android 5.0.<br>
|
Magisk is a suite of open source software for customizing Android, supporting devices higher than Android 6.0.<br>
|
||||||
Some highlight features:
|
Some highlight features:
|
||||||
|
|
||||||
- **MagiskSU**: Provide root access for applications
|
- **MagiskSU**: Provide root access for applications
|
||||||
@@ -18,16 +18,16 @@ Some highlight features:
|
|||||||
|
|
||||||
[Github](https://github.com/topjohnwu/Magisk/) is the only source where you can get official Magisk information and downloads.
|
[Github](https://github.com/topjohnwu/Magisk/) is the only source where you can get official Magisk information and downloads.
|
||||||
|
|
||||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v25.1)
|
[](https://github.com/topjohnwu/Magisk/releases/tag/v26.4)
|
||||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v25.1)
|
[](https://github.com/topjohnwu/Magisk/releases/tag/v26.4)
|
||||||
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-release.apk)
|
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-release.apk)
|
||||||
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-debug.apk)
|
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-debug.apk)
|
||||||
|
|
||||||
## Useful Links
|
## Useful Links
|
||||||
|
|
||||||
- [Installation Instruction](https://topjohnwu.github.io/Magisk/install.html)
|
- [Installation Instruction](https://topjohnwu.github.io/Magisk/install.html)
|
||||||
|
- [Building and Development](https://topjohnwu.github.io/Magisk/build.html)
|
||||||
- [Magisk Documentation](https://topjohnwu.github.io/Magisk/)
|
- [Magisk Documentation](https://topjohnwu.github.io/Magisk/)
|
||||||
- [Magisk Troubleshoot Wiki](https://www.didgeridoohan.com/magisk/HomePage) (by [@Didgeridoohan](https://github.com/Didgeridoohan))
|
|
||||||
|
|
||||||
## Bug Reports
|
## Bug Reports
|
||||||
|
|
||||||
@@ -37,30 +37,6 @@ For installation issues, upload both boot image and install logs.<br>
|
|||||||
For Magisk issues, upload boot logcat or dmesg.<br>
|
For Magisk issues, upload boot logcat or dmesg.<br>
|
||||||
For Magisk app crashes, record and upload the logcat when the crash occurs.
|
For Magisk app crashes, record and upload the logcat when the crash occurs.
|
||||||
|
|
||||||
## Building and Development
|
|
||||||
|
|
||||||
- Magisk builds on any OS Android Studio supports. Install Android Studio and do the initial setups.
|
|
||||||
- Clone sources: `git clone --recurse-submodules https://github.com/topjohnwu/Magisk.git`
|
|
||||||
- Install Python 3.6+ \
|
|
||||||
(Windows only: select **'Add Python to PATH'** in installer, and run `pip install colorama` after install)
|
|
||||||
- Configure to use the JDK bundled in Android Studio:
|
|
||||||
- macOS: `export JAVA_HOME="/Applications/Android Studio.app/Contents/jre/Contents/Home"`
|
|
||||||
- Linux: `export PATH="/path/to/androidstudio/jre/bin:$PATH"`
|
|
||||||
- Windows: Add `C:\Path\To\Android Studio\jre\bin` to environment variable `PATH`
|
|
||||||
- Set environment variable `ANDROID_SDK_ROOT` to the Android SDK folder (can be found in Android Studio settings)
|
|
||||||
- Run `./build.py ndk` to let the script download and install NDK for you
|
|
||||||
- To start building, run `build.py` to see your options. \
|
|
||||||
For each action, use `-h` to access help (e.g. `./build.py all -h`)
|
|
||||||
- To start development, open the project with Android Studio. The IDE can be used for both app (Kotlin/Java) and native sources.
|
|
||||||
- Optionally, set custom configs with `config.prop`. A sample `config.prop.sample` is provided.
|
|
||||||
|
|
||||||
## Signing and Distribution
|
|
||||||
|
|
||||||
- The certificate of the key used to sign the final Magisk APK product is also directly embedded into some executables. In release builds, Magisk's root daemon will enforce this certificate check and reject and forcefully uninstall any non-matching Magisk apps to protect users from malicious and unverified Magisk APKs.
|
|
||||||
- To do any development on Magisk itself, switch to an **official debug build and reinstall Magisk** to bypass the signature check.
|
|
||||||
- To distribute your own Magisk builds signed with your own keys, set your signing configs in `config.prop`.
|
|
||||||
- Check [Google's Documentation](https://developer.android.com/studio/publish/app-signing.html#generate-key) for more details on generating your own key.
|
|
||||||
|
|
||||||
## Translation Contributions
|
## Translation Contributions
|
||||||
|
|
||||||
Default string resources for the Magisk app and its stub APK are located here:
|
Default string resources for the Magisk app and its stub APK are located here:
|
||||||
|
7
app/.gitignore
vendored
7
app/.gitignore
vendored
@@ -3,10 +3,9 @@
|
|||||||
/local.properties
|
/local.properties
|
||||||
.idea/
|
.idea/
|
||||||
/build
|
/build
|
||||||
app/release
|
|
||||||
*.hprof
|
*.hprof
|
||||||
.externalNativeBuild/
|
.externalNativeBuild/
|
||||||
*.apk
|
*.apk
|
||||||
src/main/assets
|
src/*/assets
|
||||||
src/main/jniLibs
|
src/*/jniLibs
|
||||||
src/main/resources
|
src/*/resources
|
||||||
|
@@ -26,7 +26,10 @@ android {
|
|||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
versionName = Config.version
|
versionName = Config.version
|
||||||
versionCode = Config.versionCode
|
versionCode = Config.versionCode
|
||||||
ndk.abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
ndk {
|
||||||
|
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||||
|
debugSymbolLevel = "FULL"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -39,11 +42,13 @@ android {
|
|||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
dataBinding = true
|
dataBinding = true
|
||||||
|
aidl = true
|
||||||
}
|
}
|
||||||
|
|
||||||
packagingOptions {
|
packaging {
|
||||||
resources {
|
resources {
|
||||||
excludes += "/META-INF/*"
|
excludes += "/META-INF/*"
|
||||||
|
excludes += "/META-INF/versions/**"
|
||||||
excludes += "/org/bouncycastle/**"
|
excludes += "/org/bouncycastle/**"
|
||||||
excludes += "/kotlin/**"
|
excludes += "/kotlin/**"
|
||||||
excludes += "/kotlinx/**"
|
excludes += "/kotlinx/**"
|
||||||
@@ -52,9 +57,6 @@ android {
|
|||||||
excludes += "/*.bin"
|
excludes += "/*.bin"
|
||||||
excludes += "/*.json"
|
excludes += "/*.json"
|
||||||
}
|
}
|
||||||
jniLibs {
|
|
||||||
keepDebugSymbols += "**/*.so"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,13 +74,13 @@ dependencies {
|
|||||||
implementation("com.github.topjohnwu:indeterminate-checkbox:1.0.7")
|
implementation("com.github.topjohnwu:indeterminate-checkbox:1.0.7")
|
||||||
implementation("com.github.topjohnwu:lz4-java:1.7.1")
|
implementation("com.github.topjohnwu:lz4-java:1.7.1")
|
||||||
implementation("com.jakewharton.timber:timber:5.0.1")
|
implementation("com.jakewharton.timber:timber:5.0.1")
|
||||||
implementation("org.bouncycastle:bcpkix-jdk18on:1.71")
|
implementation("org.bouncycastle:bcpkix-jdk18on:1.77")
|
||||||
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0")
|
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.3.0")
|
||||||
implementation("dev.rikka.rikkax.insets:insets:1.2.0")
|
implementation("dev.rikka.rikkax.insets:insets:1.3.0")
|
||||||
implementation("dev.rikka.rikkax.recyclerview:recyclerview-ktx:1.3.1")
|
implementation("dev.rikka.rikkax.recyclerview:recyclerview-ktx:1.3.2")
|
||||||
implementation("io.noties.markwon:core:4.6.2")
|
implementation("io.noties.markwon:core:4.6.2")
|
||||||
|
|
||||||
val vLibsu = "5.0.2"
|
val vLibsu = "5.2.2"
|
||||||
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
|
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
|
||||||
implementation("com.github.topjohnwu.libsu:service:${vLibsu}")
|
implementation("com.github.topjohnwu.libsu:service:${vLibsu}")
|
||||||
implementation("com.github.topjohnwu.libsu:nio:${vLibsu}")
|
implementation("com.github.topjohnwu.libsu:nio:${vLibsu}")
|
||||||
@@ -88,33 +90,32 @@ dependencies {
|
|||||||
implementation("com.squareup.retrofit2:converter-moshi:${vRetrofit}")
|
implementation("com.squareup.retrofit2:converter-moshi:${vRetrofit}")
|
||||||
implementation("com.squareup.retrofit2:converter-scalars:${vRetrofit}")
|
implementation("com.squareup.retrofit2:converter-scalars:${vRetrofit}")
|
||||||
|
|
||||||
val vOkHttp = "4.9.3"
|
val vOkHttp = "4.12.0"
|
||||||
implementation("com.squareup.okhttp3:okhttp:${vOkHttp}")
|
implementation("com.squareup.okhttp3:okhttp:${vOkHttp}")
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor:${vOkHttp}")
|
implementation("com.squareup.okhttp3:logging-interceptor:${vOkHttp}")
|
||||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:${vOkHttp}")
|
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:${vOkHttp}")
|
||||||
|
|
||||||
val vMoshi = "1.13.0"
|
val vMoshi = "1.15.0"
|
||||||
implementation("com.squareup.moshi:moshi:${vMoshi}")
|
implementation("com.squareup.moshi:moshi:${vMoshi}")
|
||||||
kapt("com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}")
|
kapt("com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}")
|
||||||
|
|
||||||
val vRoom = "2.5.0-alpha02"
|
val vRoom = "2.6.1"
|
||||||
implementation("androidx.room:room-runtime:${vRoom}")
|
implementation("androidx.room:room-runtime:${vRoom}")
|
||||||
implementation("androidx.room:room-ktx:${vRoom}")
|
implementation("androidx.room:room-ktx:${vRoom}")
|
||||||
kapt("androidx.room:room-compiler:${vRoom}")
|
kapt("androidx.room:room-compiler:${vRoom}")
|
||||||
|
|
||||||
val vNav = "2.5.0-rc01"
|
val vNav = "2.7.6"
|
||||||
implementation("androidx.navigation:navigation-fragment-ktx:${vNav}")
|
implementation("androidx.navigation:navigation-fragment-ktx:${vNav}")
|
||||||
implementation("androidx.navigation:navigation-ui-ktx:${vNav}")
|
implementation("androidx.navigation:navigation-ui-ktx:${vNav}")
|
||||||
|
|
||||||
implementation("androidx.biometric:biometric:1.1.0")
|
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
implementation("androidx.appcompat:appcompat:1.4.2")
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||||
implementation("androidx.preference:preference:1.2.0")
|
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
implementation("androidx.fragment:fragment-ktx:1.6.2")
|
||||||
implementation("androidx.fragment:fragment-ktx:1.4.1")
|
|
||||||
implementation("androidx.transition:transition:1.4.1")
|
implementation("androidx.transition:transition:1.4.1")
|
||||||
implementation("androidx.core:core-ktx:1.8.0")
|
implementation("androidx.core:core-ktx:1.12.0")
|
||||||
implementation("androidx.core:core-splashscreen:1.0.0-rc01")
|
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||||
implementation("com.google.android.material:material:1.6.1")
|
implementation("androidx.profileinstaller:profileinstaller:1.3.1")
|
||||||
|
implementation("com.google.android.material:material:1.11.0")
|
||||||
}
|
}
|
||||||
|
18
app/proguard-rules.pro
vendored
18
app/proguard-rules.pro
vendored
@@ -11,12 +11,15 @@
|
|||||||
-assumenosideeffects class java.util.Objects {
|
-assumenosideeffects class java.util.Objects {
|
||||||
public static ** requireNonNull(...);
|
public static ** requireNonNull(...);
|
||||||
}
|
}
|
||||||
|
-assumenosideeffects public class kotlin.coroutines.jvm.internal.DebugMetadataKt {
|
||||||
|
private static ** getDebugMetadataAnnotation(...) return null;
|
||||||
|
}
|
||||||
|
|
||||||
# Stub
|
# Stub
|
||||||
-keep class com.topjohnwu.magisk.core.App { <init>(java.lang.Object); }
|
-keep class com.topjohnwu.magisk.core.App { <init>(java.lang.Object); }
|
||||||
-keepclassmembers class androidx.appcompat.app.AppCompatDelegateImpl {
|
-keepclassmembers class androidx.appcompat.app.AppCompatDelegateImpl {
|
||||||
boolean mActivityHandlesUiModeChecked;
|
boolean mActivityHandlesConfigFlagsChecked;
|
||||||
boolean mActivityHandlesUiMode;
|
int mActivityHandlesConfigFlags;
|
||||||
}
|
}
|
||||||
|
|
||||||
# main
|
# main
|
||||||
@@ -30,6 +33,17 @@
|
|||||||
public void d(**);
|
public void d(**);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# https://github.com/square/retrofit/issues/3751#issuecomment-1192043644
|
||||||
|
# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items).
|
||||||
|
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
|
||||||
|
-keep,allowobfuscation,allowshrinking class retrofit2.Response
|
||||||
|
|
||||||
|
# With R8 full mode generic signatures are stripped for classes that are not
|
||||||
|
# kept. Suspend functions are wrapped in continuations where the type argument
|
||||||
|
# is used.
|
||||||
|
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
|
||||||
|
|
||||||
|
|
||||||
# Excessive obfuscation
|
# Excessive obfuscation
|
||||||
-repackageclasses 'a'
|
-repackageclasses 'a'
|
||||||
-allowaccessmodification
|
-allowaccessmodification
|
||||||
|
@@ -7,7 +7,3 @@ setupCommon()
|
|||||||
android {
|
android {
|
||||||
namespace = "com.topjohnwu.shared"
|
namespace = "com.topjohnwu.shared"
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
|
||||||
api("io.michaelrocks:paranoid-core:0.3.7")
|
|
||||||
}
|
|
||||||
|
@@ -1,14 +1,18 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.topjohnwu.shared"
|
|
||||||
android:installLocation="internalOnly">
|
android:installLocation="internalOnly">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS" />
|
<uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS" />
|
||||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="29" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="29"
|
android:maxSdkVersion="29"
|
||||||
|
@@ -2,9 +2,6 @@ package com.topjohnwu.magisk;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import io.michaelrocks.paranoid.Obfuscate;
|
|
||||||
|
|
||||||
@Obfuscate
|
|
||||||
public class ProviderInstaller {
|
public class ProviderInstaller {
|
||||||
|
|
||||||
public static boolean install(Context context) {
|
public static boolean install(Context context) {
|
||||||
|
@@ -3,6 +3,7 @@ package com.topjohnwu.magisk;
|
|||||||
import static android.os.Build.VERSION.SDK_INT;
|
import static android.os.Build.VERSION.SDK_INT;
|
||||||
import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
|
import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
@@ -11,6 +12,7 @@ import android.content.res.AssetManager;
|
|||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.content.res.loader.ResourcesLoader;
|
import android.content.res.loader.ResourcesLoader;
|
||||||
import android.content.res.loader.ResourcesProvider;
|
import android.content.res.loader.ResourcesProvider;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -18,9 +20,6 @@ import java.io.IOException;
|
|||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import io.michaelrocks.paranoid.Obfuscate;
|
|
||||||
|
|
||||||
@Obfuscate
|
|
||||||
public class StubApk {
|
public class StubApk {
|
||||||
private static File dynDir;
|
private static File dynDir;
|
||||||
private static Method addAssetPath;
|
private static Method addAssetPath;
|
||||||
@@ -28,7 +27,7 @@ public class StubApk {
|
|||||||
private static File getDynDir(ApplicationInfo info) {
|
private static File getDynDir(ApplicationInfo info) {
|
||||||
if (dynDir == null) {
|
if (dynDir == null) {
|
||||||
final String dataDir;
|
final String dataDir;
|
||||||
if (SDK_INT >= 24) {
|
if (SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
// Use device protected path to allow directBootAware
|
// Use device protected path to allow directBootAware
|
||||||
dataDir = info.deviceProtectedDataDir;
|
dataDir = info.deviceProtectedDataDir;
|
||||||
} else {
|
} else {
|
||||||
@@ -56,12 +55,24 @@ public class StubApk {
|
|||||||
return new File(getDynDir(info), "update.apk");
|
return new File(getDynDir(info), "update.apk");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.R)
|
||||||
|
private static ResourcesLoader getResourcesLoader(File path) throws IOException {
|
||||||
|
var loader = new ResourcesLoader();
|
||||||
|
ResourcesProvider provider;
|
||||||
|
if (path.isDirectory()) {
|
||||||
|
provider = ResourcesProvider.loadFromDirectory(path.getPath(), null);
|
||||||
|
} else {
|
||||||
|
var fd = ParcelFileDescriptor.open(path, MODE_READ_ONLY);
|
||||||
|
provider = ResourcesProvider.loadFromApk(fd);
|
||||||
|
}
|
||||||
|
loader.addProvider(provider);
|
||||||
|
return loader;
|
||||||
|
}
|
||||||
|
|
||||||
public static void addAssetPath(Resources res, String path) {
|
public static void addAssetPath(Resources res, String path) {
|
||||||
if (SDK_INT >= 30) {
|
if (SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
try (var fd = ParcelFileDescriptor.open(new File(path), MODE_READ_ONLY)) {
|
try {
|
||||||
var loader = new ResourcesLoader();
|
res.addLoaders(getResourcesLoader(new File(path)));
|
||||||
loader.addProvider(ResourcesProvider.loadFromApk(fd));
|
|
||||||
res.addLoaders(loader);
|
|
||||||
} catch (IOException ignored) {}
|
} catch (IOException ignored) {}
|
||||||
} else {
|
} else {
|
||||||
AssetManager asset = res.getAssets();
|
AssetManager asset = res.getAssets();
|
||||||
|
@@ -25,9 +25,6 @@ import java.util.UUID;
|
|||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import io.michaelrocks.paranoid.Obfuscate;
|
|
||||||
|
|
||||||
@Obfuscate
|
|
||||||
public final class APKInstall {
|
public final class APKInstall {
|
||||||
|
|
||||||
public static void transfer(InputStream in, OutputStream out) throws IOException {
|
public static void transfer(InputStream in, OutputStream out) throws IOException {
|
||||||
@@ -39,6 +36,16 @@ public final class APKInstall {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void registerReceiver(
|
||||||
|
Context context, BroadcastReceiver receiver, IntentFilter filter) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
// noinspection InlinedApi
|
||||||
|
context.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED);
|
||||||
|
} else {
|
||||||
|
context.registerReceiver(receiver, filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static Session startSession(Context context) {
|
public static Session startSession(Context context) {
|
||||||
return startSession(context, null, null, null);
|
return startSession(context, null, null, null);
|
||||||
}
|
}
|
||||||
@@ -51,9 +58,9 @@ public final class APKInstall {
|
|||||||
// If pkg is not null, look for package added event
|
// If pkg is not null, look for package added event
|
||||||
var filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
|
var filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
|
||||||
filter.addDataScheme("package");
|
filter.addDataScheme("package");
|
||||||
context.registerReceiver(receiver, filter);
|
registerReceiver(context, receiver, filter);
|
||||||
}
|
}
|
||||||
context.registerReceiver(receiver, new IntentFilter(receiver.sessionId));
|
registerReceiver(context, receiver, new IntentFilter(receiver.sessionId));
|
||||||
return receiver;
|
return receiver;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,27 +101,25 @@ public final class APKInstall {
|
|||||||
} else if (sessionId.equals(intent.getAction())) {
|
} else if (sessionId.equals(intent.getAction())) {
|
||||||
int status = intent.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID);
|
int status = intent.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID);
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case STATUS_PENDING_USER_ACTION:
|
case STATUS_PENDING_USER_ACTION ->
|
||||||
userAction = intent.getParcelableExtra(Intent.EXTRA_INTENT);
|
userAction = intent.getParcelableExtra(Intent.EXTRA_INTENT);
|
||||||
break;
|
case STATUS_SUCCESS -> {
|
||||||
case STATUS_SUCCESS:
|
|
||||||
if (packageName == null) {
|
if (packageName == null) {
|
||||||
onSuccess(context);
|
onSuccess(context);
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
default:
|
default -> {
|
||||||
int id = intent.getIntExtra(EXTRA_SESSION_ID, 0);
|
int id = intent.getIntExtra(EXTRA_SESSION_ID, 0);
|
||||||
if (id > 0) {
|
var installer = context.getPackageManager().getPackageInstaller();
|
||||||
var installer = context.getPackageManager().getPackageInstaller();
|
try {
|
||||||
var info = installer.getSessionInfo(id);
|
installer.abandonSession(id);
|
||||||
if (info != null) {
|
} catch (SecurityException ignored) {
|
||||||
installer.abandonSession(info.getSessionId());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (onFailure != null) {
|
if (onFailure != null) {
|
||||||
onFailure.run();
|
onFailure.run();
|
||||||
}
|
}
|
||||||
context.getApplicationContext().unregisterReceiver(this);
|
context.getApplicationContext().unregisterReceiver(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
package com.topjohnwu.magisk.utils;
|
package com.topjohnwu.magisk.utils;
|
||||||
|
|
||||||
|
import android.os.Process;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
@@ -14,8 +16,8 @@ public class DynamicClassLoader extends BaseDexClassLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public DynamicClassLoader(File apk, ClassLoader parent) {
|
public DynamicClassLoader(File apk, ClassLoader parent) {
|
||||||
// Set optimizedDirectory to null to bypass DexFile's security checks
|
// Set optimizedDirectory to null for RootService to bypass DexFile's security checks
|
||||||
super(apk.getPath(), null, null, parent);
|
super(apk.getPath(), Process.myUid() == 0 ? null : apk.getParentFile(), null, parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@@ -2,12 +2,21 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<permission
|
||||||
|
android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
|
||||||
|
android:protectionLevel="signature"
|
||||||
|
tools:node="remove" />
|
||||||
|
|
||||||
|
<uses-permission
|
||||||
|
android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
|
||||||
|
tools:node="remove" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".core.App"
|
android:name=".core.App"
|
||||||
android:extractNativeLibs="true"
|
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:multiArch="true"
|
android:multiArch="true"
|
||||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
|
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning"
|
||||||
|
tools:remove="android:appComponentFactory">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
@@ -53,7 +62,8 @@
|
|||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".core.download.DownloadService"
|
android:name=".core.download.DownloadService"
|
||||||
android:exported="false" />
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".core.JobService"
|
android:name=".core.JobService"
|
||||||
@@ -72,11 +82,15 @@
|
|||||||
android:name="androidx.room.MultiInstanceInvalidationService"
|
android:name="androidx.room.MultiInstanceInvalidationService"
|
||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
|
|
||||||
<!-- We don't need emoji compat -->
|
<!-- We handle initialization ourselves -->
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.startup.InitializationProvider"
|
android:name="androidx.startup.InitializationProvider"
|
||||||
android:authorities="${applicationId}.androidx-startup"
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
android:exported="false"
|
tools:node="remove" />
|
||||||
|
|
||||||
|
<!-- We handle profile installation ourselves -->
|
||||||
|
<receiver
|
||||||
|
android:name="androidx.profileinstaller.ProfileInstallReceiver"
|
||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
@@ -5,13 +5,14 @@ import android.view.KeyEvent
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.databinding.OnRebindCallback
|
import androidx.databinding.OnRebindCallback
|
||||||
import androidx.databinding.ViewDataBinding
|
import androidx.databinding.ViewDataBinding
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
import com.topjohnwu.magisk.ktx.startAnimations
|
|
||||||
|
|
||||||
abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHolder {
|
abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHolder {
|
||||||
|
|
||||||
@@ -37,6 +38,9 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
|||||||
it.setVariable(BR.viewModel, viewModel)
|
it.setVariable(BR.viewModel, viewModel)
|
||||||
it.lifecycleOwner = viewLifecycleOwner
|
it.lifecycleOwner = viewLifecycleOwner
|
||||||
}
|
}
|
||||||
|
if (this is MenuProvider) {
|
||||||
|
activity?.addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.STARTED)
|
||||||
|
}
|
||||||
savedInstanceState?.let { viewModel.onRestoreState(it) }
|
savedInstanceState?.let { viewModel.onRestoreState(it) }
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
@@ -89,5 +93,4 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
|||||||
fun NavDirections.navigate() {
|
fun NavDirections.navigate() {
|
||||||
navigation?.currentDestination?.getAction(actionId)?.let { navigation!!.navigate(this) }
|
navigation?.currentDestination?.getAction(actionId)?.let { navigation!!.navigate(this) }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,18 +1,18 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
package com.topjohnwu.magisk.arch
|
||||||
|
|
||||||
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
import android.Manifest.permission.*
|
||||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.databinding.PropertyChangeRegistry
|
import androidx.databinding.PropertyChangeRegistry
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.databinding.ObservableHost
|
import com.topjohnwu.magisk.databinding.ObservableHost
|
||||||
import com.topjohnwu.magisk.events.BackPressEvent
|
import com.topjohnwu.magisk.events.BackPressEvent
|
||||||
|
import com.topjohnwu.magisk.events.DialogBuilder
|
||||||
|
import com.topjohnwu.magisk.events.DialogEvent
|
||||||
import com.topjohnwu.magisk.events.NavigationEvent
|
import com.topjohnwu.magisk.events.NavigationEvent
|
||||||
import com.topjohnwu.magisk.events.PermissionEvent
|
import com.topjohnwu.magisk.events.PermissionEvent
|
||||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||||
@@ -53,15 +53,25 @@ abstract class BaseViewModel : ViewModel(), ObservableHost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
inline fun withPostNotificationPermission(crossinline callback: () -> Unit) {
|
||||||
|
withPermission(POST_NOTIFICATIONS) {
|
||||||
|
if (!it) {
|
||||||
|
SnackbarEvent(R.string.post_notifications_denied).publish()
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun back() = BackPressEvent().publish()
|
fun back() = BackPressEvent().publish()
|
||||||
|
|
||||||
fun <Event : ViewEvent> Event.publish() {
|
fun ViewEvent.publish() {
|
||||||
_viewEvents.postValue(this)
|
_viewEvents.postValue(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <Event : ViewEventWithScope> Event.publish() {
|
fun DialogBuilder.show() {
|
||||||
scope = viewModelScope
|
DialogEvent(this).publish()
|
||||||
_viewEvents.postValue(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NavDirections.navigate(pop: Boolean = false) {
|
fun NavDirections.navigate(pop: Boolean = false) {
|
||||||
|
@@ -20,12 +20,14 @@ abstract class NavigationActivity<Binding : ViewDataBinding> : UIActivity<Bindin
|
|||||||
val navigation: NavController get() = navHostFragment.navController
|
val navigation: NavController get() = navHostFragment.navController
|
||||||
|
|
||||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||||
return currentFragment?.onKeyEvent(event) == true || super.dispatchKeyEvent(event)
|
return if (binded && currentFragment?.onKeyEvent(event) == true) true else super.dispatchKeyEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
if (currentFragment?.onBackPressed()?.not() == true) {
|
if (binded) {
|
||||||
super.onBackPressed()
|
if (currentFragment?.onBackPressed() == false) {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,16 +5,20 @@ import android.graphics.Color
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.content.res.use
|
import androidx.core.content.res.use
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.databinding.ViewDataBinding
|
import androidx.databinding.ViewDataBinding
|
||||||
|
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||||
|
import androidx.transition.AutoTransition
|
||||||
|
import androidx.transition.TransitionManager
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||||
import com.topjohnwu.magisk.widget.Pre23CardViewBackgroundColorFixLayoutInflaterListener
|
|
||||||
import rikka.insets.WindowInsetsHelper
|
import rikka.insets.WindowInsetsHelper
|
||||||
import rikka.layoutinflater.view.LayoutInflaterFactory
|
import rikka.layoutinflater.view.LayoutInflaterFactory
|
||||||
|
|
||||||
@@ -23,6 +27,8 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
|||||||
protected lateinit var binding: Binding
|
protected lateinit var binding: Binding
|
||||||
protected abstract val layoutRes: Int
|
protected abstract val layoutRes: Int
|
||||||
|
|
||||||
|
protected val binded get() = ::binding.isInitialized
|
||||||
|
|
||||||
open val snackbarView get() = binding.root
|
open val snackbarView get() = binding.root
|
||||||
open val snackbarAnchorView: View? get() = null
|
open val snackbarAnchorView: View? get() = null
|
||||||
|
|
||||||
@@ -33,11 +39,6 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
layoutInflater.factory2 = LayoutInflaterFactory(delegate)
|
layoutInflater.factory2 = LayoutInflaterFactory(delegate)
|
||||||
.addOnViewCreatedListener(WindowInsetsHelper.LISTENER)
|
.addOnViewCreatedListener(WindowInsetsHelper.LISTENER)
|
||||||
.apply {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
|
||||||
this.addOnViewCreatedListener(Pre23CardViewBackgroundColorFixLayoutInflaterListener.getInstance())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -101,3 +102,14 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
|||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ViewGroup.startAnimations() {
|
||||||
|
val transition = AutoTransition()
|
||||||
|
.setInterpolator(FastOutSlowInInterpolator())
|
||||||
|
.setDuration(400)
|
||||||
|
.excludeTarget(R.id.main_toolbar, true)
|
||||||
|
TransitionManager.beginDelayedTransition(
|
||||||
|
this,
|
||||||
|
transition
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
package com.topjohnwu.magisk.arch
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for passing events from ViewModels to Activities/Fragments
|
* Class for passing events from ViewModels to Activities/Fragments
|
||||||
@@ -9,10 +8,6 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
*/
|
*/
|
||||||
abstract class ViewEvent
|
abstract class ViewEvent
|
||||||
|
|
||||||
abstract class ViewEventWithScope: ViewEvent() {
|
|
||||||
lateinit var scope: CoroutineScope
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContextExecutor {
|
interface ContextExecutor {
|
||||||
operator fun invoke(context: Context)
|
operator fun invoke(context: Context)
|
||||||
}
|
}
|
||||||
|
@@ -34,7 +34,8 @@ object VMFactory : ViewModelProvider.Factory {
|
|||||||
HomeViewModel::class.java -> HomeViewModel(ServiceLocator.networkService)
|
HomeViewModel::class.java -> HomeViewModel(ServiceLocator.networkService)
|
||||||
LogViewModel::class.java -> LogViewModel(ServiceLocator.logRepo)
|
LogViewModel::class.java -> LogViewModel(ServiceLocator.logRepo)
|
||||||
SuperuserViewModel::class.java -> SuperuserViewModel(ServiceLocator.policyDB)
|
SuperuserViewModel::class.java -> SuperuserViewModel(ServiceLocator.policyDB)
|
||||||
InstallViewModel::class.java -> InstallViewModel(ServiceLocator.networkService)
|
InstallViewModel::class.java ->
|
||||||
|
InstallViewModel(ServiceLocator.networkService, ServiceLocator.markwon)
|
||||||
SuRequestViewModel::class.java ->
|
SuRequestViewModel::class.java ->
|
||||||
SuRequestViewModel(ServiceLocator.policyDB, ServiceLocator.timeoutPrefs)
|
SuRequestViewModel(ServiceLocator.policyDB, ServiceLocator.timeoutPrefs)
|
||||||
else -> modelClass.newInstance()
|
else -> modelClass.newInstance()
|
||||||
|
@@ -5,14 +5,25 @@ import android.app.Application
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.profileinstaller.ProfileInstaller
|
||||||
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.StubApk
|
import com.topjohnwu.magisk.StubApk
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
import com.topjohnwu.magisk.core.utils.*
|
import com.topjohnwu.magisk.core.utils.DispatcherExecutor
|
||||||
|
import com.topjohnwu.magisk.core.utils.NetworkObserver
|
||||||
|
import com.topjohnwu.magisk.core.utils.ProcessLifecycle
|
||||||
|
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||||
|
import com.topjohnwu.magisk.core.utils.ShellInit
|
||||||
|
import com.topjohnwu.magisk.core.utils.refreshLocale
|
||||||
|
import com.topjohnwu.magisk.core.utils.setConfig
|
||||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
||||||
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
import com.topjohnwu.superuser.ipc.RootService
|
import com.topjohnwu.superuser.ipc.RootService
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
@@ -70,6 +81,18 @@ open class App() : Application() {
|
|||||||
|
|
||||||
refreshLocale()
|
refreshLocale()
|
||||||
resources.patch()
|
resources.patch()
|
||||||
|
Notifications.setup()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
ProcessLifecycle.init(this)
|
||||||
|
NetworkObserver.init(this)
|
||||||
|
if (!BuildConfig.DEBUG && !isRunningAsStub) {
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
ProfileInstaller.writeProfile(this@App)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
@@ -1,21 +1,19 @@
|
|||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.util.Xml
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||||
import com.topjohnwu.magisk.core.repository.BoolDBPropertyNoWrite
|
import com.topjohnwu.magisk.core.repository.BoolDBPropertyNoWrite
|
||||||
import com.topjohnwu.magisk.core.repository.DBConfig
|
import com.topjohnwu.magisk.core.repository.DBConfig
|
||||||
import com.topjohnwu.magisk.core.repository.PreferenceConfig
|
import com.topjohnwu.magisk.core.repository.PreferenceConfig
|
||||||
import com.topjohnwu.magisk.core.utils.refreshLocale
|
import com.topjohnwu.magisk.core.utils.refreshLocale
|
||||||
import com.topjohnwu.magisk.ui.theme.Theme
|
import com.topjohnwu.magisk.ui.theme.Theme
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import org.xmlpull.v1.XmlPullParser
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
object Config : PreferenceConfig, DBConfig {
|
object Config : PreferenceConfig, DBConfig {
|
||||||
|
|
||||||
@@ -24,13 +22,12 @@ object Config : PreferenceConfig, DBConfig {
|
|||||||
override val context get() = ServiceLocator.deContext
|
override val context get() = ServiceLocator.deContext
|
||||||
override val coroutineScope get() = GlobalScope
|
override val coroutineScope get() = GlobalScope
|
||||||
|
|
||||||
@get:SuppressLint("ApplySharedPref")
|
private val prefsFile = File("${context.filesDir.parent}/shared_prefs", "${fileName}.xml")
|
||||||
val prefsFile: File get() {
|
|
||||||
// Flush prefs to disk
|
@SuppressLint("ApplySharedPref")
|
||||||
prefs.edit().apply {
|
fun getPrefsFile(): File {
|
||||||
remove(Key.ASKED_HOME)
|
prefs.edit().remove(Key.ASKED_HOME).commit()
|
||||||
}.commit()
|
return prefsFile
|
||||||
return File("${context.filesDir.parent}/shared_prefs", "${fileName}.xml")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object Key {
|
object Key {
|
||||||
@@ -117,7 +114,6 @@ object Config : PreferenceConfig, DBConfig {
|
|||||||
|
|
||||||
@JvmField var keepVerity = false
|
@JvmField var keepVerity = false
|
||||||
@JvmField var keepEnc = false
|
@JvmField var keepEnc = false
|
||||||
@JvmField var patchVbmeta = false
|
|
||||||
@JvmField var recovery = false
|
@JvmField var recovery = false
|
||||||
|
|
||||||
var bootId by preference(Key.BOOT_ID, "")
|
var bootId by preference(Key.BOOT_ID, "")
|
||||||
@@ -136,7 +132,15 @@ object Config : PreferenceConfig, DBConfig {
|
|||||||
var themeOrdinal by preference(Key.THEME_ORDINAL, Theme.Piplup.ordinal)
|
var themeOrdinal by preference(Key.THEME_ORDINAL, Theme.Piplup.ordinal)
|
||||||
var suReAuth by preference(Key.SU_REAUTH, false)
|
var suReAuth by preference(Key.SU_REAUTH, false)
|
||||||
var suTapjack by preference(Key.SU_TAPJACK, true)
|
var suTapjack by preference(Key.SU_TAPJACK, true)
|
||||||
var checkUpdate by preference(Key.CHECK_UPDATES, true)
|
private var checkUpdatePrefs by preference(Key.CHECK_UPDATES, true)
|
||||||
|
var checkUpdate
|
||||||
|
get() = checkUpdatePrefs
|
||||||
|
set(value) {
|
||||||
|
if (checkUpdatePrefs != value) {
|
||||||
|
checkUpdatePrefs = value
|
||||||
|
JobService.schedule(AppContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
var doh by preference(Key.DOH, false)
|
var doh by preference(Key.DOH, false)
|
||||||
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
|
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
|
||||||
|
|
||||||
@@ -152,7 +156,12 @@ object Config : PreferenceConfig, DBConfig {
|
|||||||
var rootMode by dbSettings(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB)
|
var rootMode by dbSettings(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB)
|
||||||
var suMntNamespaceMode by dbSettings(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER)
|
var suMntNamespaceMode by dbSettings(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER)
|
||||||
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
|
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
|
||||||
var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
|
private var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
|
||||||
|
var userAuth
|
||||||
|
get() = Info.isDeviceSecure && suBiometric
|
||||||
|
set(value) {
|
||||||
|
suBiometric = value
|
||||||
|
}
|
||||||
var zygisk by dbSettings(Key.ZYGISK, false)
|
var zygisk by dbSettings(Key.ZYGISK, false)
|
||||||
var denyList by BoolDBPropertyNoWrite(Key.DENYLIST, false)
|
var denyList by BoolDBPropertyNoWrite(Key.DENYLIST, false)
|
||||||
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
||||||
@@ -163,9 +172,8 @@ object Config : PreferenceConfig, DBConfig {
|
|||||||
fun load(pkg: String?) {
|
fun load(pkg: String?) {
|
||||||
// Only try to load prefs when fresh install and a previous package name is set
|
// Only try to load prefs when fresh install and a previous package name is set
|
||||||
if (pkg != null && prefs.all.isEmpty()) runCatching {
|
if (pkg != null && prefs.all.isEmpty()) runCatching {
|
||||||
context.contentResolver.openInputStream(Provider.preferencesUri(pkg))?.use {
|
context.contentResolver.openInputStream(Provider.preferencesUri(pkg))?.writeTo(prefsFile)
|
||||||
prefs.edit { parsePrefs(it) }
|
return
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
@@ -182,52 +190,4 @@ object Config : PreferenceConfig, DBConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun SharedPreferences.Editor.parsePrefs(input: InputStream) {
|
|
||||||
runCatching {
|
|
||||||
val parser = Xml.newPullParser()
|
|
||||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
|
|
||||||
parser.setInput(input, "UTF-8")
|
|
||||||
parser.nextTag()
|
|
||||||
parser.require(XmlPullParser.START_TAG, null, "map")
|
|
||||||
while (parser.next() != XmlPullParser.END_TAG) {
|
|
||||||
if (parser.eventType != XmlPullParser.START_TAG)
|
|
||||||
continue
|
|
||||||
val key: String = parser.getAttributeValue(null, "name")
|
|
||||||
fun value() = parser.getAttributeValue(null, "value")!!
|
|
||||||
when (parser.name) {
|
|
||||||
"string" -> {
|
|
||||||
parser.require(XmlPullParser.START_TAG, null, "string")
|
|
||||||
putString(key, parser.nextText())
|
|
||||||
parser.require(XmlPullParser.END_TAG, null, "string")
|
|
||||||
}
|
|
||||||
"boolean" -> {
|
|
||||||
parser.require(XmlPullParser.START_TAG, null, "boolean")
|
|
||||||
putBoolean(key, value().toBoolean())
|
|
||||||
parser.nextTag()
|
|
||||||
parser.require(XmlPullParser.END_TAG, null, "boolean")
|
|
||||||
}
|
|
||||||
"int" -> {
|
|
||||||
parser.require(XmlPullParser.START_TAG, null, "int")
|
|
||||||
putInt(key, value().toInt())
|
|
||||||
parser.nextTag()
|
|
||||||
parser.require(XmlPullParser.END_TAG, null, "int")
|
|
||||||
}
|
|
||||||
"long" -> {
|
|
||||||
parser.require(XmlPullParser.START_TAG, null, "long")
|
|
||||||
putLong(key, value().toLong())
|
|
||||||
parser.nextTag()
|
|
||||||
parser.require(XmlPullParser.END_TAG, null, "long")
|
|
||||||
}
|
|
||||||
"float" -> {
|
|
||||||
parser.require(XmlPullParser.START_TAG, null, "int")
|
|
||||||
putFloat(key, value().toFloat())
|
|
||||||
parser.nextTag()
|
|
||||||
parser.require(XmlPullParser.END_TAG, null, "int")
|
|
||||||
}
|
|
||||||
else -> parser.next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -15,8 +15,7 @@ object Const {
|
|||||||
else Build.SUPPORTED_32_BIT_ABIS.firstOrNull()
|
else Build.SUPPORTED_32_BIT_ABIS.firstOrNull()
|
||||||
|
|
||||||
// Paths
|
// Paths
|
||||||
lateinit var MAGISKTMP: String
|
const val MAGISK_PATH = "/data/adb/modules"
|
||||||
val MAGISK_PATH get() = "$MAGISKTMP/modules"
|
|
||||||
const val TMPDIR = "/dev/tmp"
|
const val TMPDIR = "/dev/tmp"
|
||||||
const val MAGISK_LOG = "/cache/magisk.log"
|
const val MAGISK_LOG = "/cache/magisk.log"
|
||||||
|
|
||||||
|
@@ -13,8 +13,8 @@ import android.util.DisplayMetrics
|
|||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.StubApk
|
import com.topjohnwu.magisk.StubApk
|
||||||
import com.topjohnwu.magisk.core.di.AppContext
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
|
import com.topjohnwu.magisk.core.ktx.unwrap
|
||||||
import com.topjohnwu.magisk.core.utils.syncLocale
|
import com.topjohnwu.magisk.core.utils.syncLocale
|
||||||
import com.topjohnwu.magisk.ktx.unwrap
|
|
||||||
|
|
||||||
lateinit var AppApkPath: String
|
lateinit var AppApkPath: String
|
||||||
|
|
||||||
|
@@ -1,14 +1,12 @@
|
|||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import android.os.Build
|
import android.app.KeyguardManager
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.topjohnwu.magisk.StubApk
|
import com.topjohnwu.magisk.StubApk
|
||||||
import com.topjohnwu.magisk.core.di.AppContext
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
|
import com.topjohnwu.magisk.core.ktx.getProperty
|
||||||
import com.topjohnwu.magisk.core.model.UpdateInfo
|
import com.topjohnwu.magisk.core.model.UpdateInfo
|
||||||
import com.topjohnwu.magisk.core.repository.NetworkService
|
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||||
import com.topjohnwu.magisk.core.utils.net.NetworkObserver
|
|
||||||
import com.topjohnwu.magisk.ktx.getProperty
|
|
||||||
import com.topjohnwu.superuser.ShellUtils.fastCmd
|
import com.topjohnwu.superuser.ShellUtils.fastCmd
|
||||||
|
|
||||||
val isRunningAsStub get() = Info.stub != null
|
val isRunningAsStub get() = Info.stub != null
|
||||||
@@ -28,30 +26,31 @@ object Info {
|
|||||||
// Device state
|
// Device state
|
||||||
@JvmStatic val env by lazy { loadState() }
|
@JvmStatic val env by lazy { loadState() }
|
||||||
@JvmField var isSAR = false
|
@JvmField var isSAR = false
|
||||||
|
var legacySAR = false
|
||||||
var isAB = false
|
var isAB = false
|
||||||
@JvmField val isZygiskEnabled = System.getenv("ZYGISK_ENABLED") == "1"
|
@JvmField val isZygiskEnabled = System.getenv("ZYGISK_ENABLED") == "1"
|
||||||
@JvmStatic val isFDE get() = crypto == "block"
|
@JvmStatic val isFDE get() = crypto == "block"
|
||||||
@JvmField var ramdisk = false
|
@JvmField var ramdisk = false
|
||||||
@JvmField var vbmeta = false
|
var patchBootVbmeta = false
|
||||||
var crypto = ""
|
var crypto = ""
|
||||||
var noDataExec = false
|
var noDataExec = false
|
||||||
var isRooted = false
|
var isRooted = false
|
||||||
|
|
||||||
@JvmField var hasGMS = true
|
@JvmField var hasGMS = true
|
||||||
val isSamsung = Build.MANUFACTURER.equals("samsung", ignoreCase = true)
|
|
||||||
@JvmField val isEmulator =
|
@JvmField val isEmulator =
|
||||||
getProperty("ro.kernel.qemu", "0") == "1" ||
|
getProperty("ro.kernel.qemu", "0") == "1" ||
|
||||||
getProperty("ro.boot.qemu", "0") == "1"
|
getProperty("ro.boot.qemu", "0") == "1"
|
||||||
|
|
||||||
val isConnected: LiveData<Boolean> by lazy {
|
val isConnected = MutableLiveData(false)
|
||||||
MutableLiveData(false).also { field ->
|
|
||||||
NetworkObserver.observe(AppContext) {
|
val showSuperUser: Boolean get() {
|
||||||
remote = EMPTY_REMOTE
|
return env.isActive && (Const.USER_ID == 0
|
||||||
field.postValue(it)
|
|| Config.suMultiuserMode == Config.Value.MULTIUSER_MODE_USER)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isDeviceSecure get() =
|
||||||
|
AppContext.getSystemService(KeyguardManager::class.java).isDeviceSecure
|
||||||
|
|
||||||
private fun loadState(): Env {
|
private fun loadState(): Env {
|
||||||
val v = fastCmd("magisk -v").split(":".toRegex())
|
val v = fastCmd("magisk -v").split(":".toRegex())
|
||||||
return Env(
|
return Env(
|
||||||
@@ -67,9 +66,10 @@ object Info {
|
|||||||
) {
|
) {
|
||||||
val versionCode = when {
|
val versionCode = when {
|
||||||
code < Const.Version.MIN_VERCODE -> -1
|
code < Const.Version.MIN_VERCODE -> -1
|
||||||
else -> if (isRooted) code else -1
|
isRooted -> code
|
||||||
|
else -> -1
|
||||||
}
|
}
|
||||||
val isUnsupported = code > 0 && code < Const.Version.MIN_VERCODE
|
val isUnsupported = code > 0 && code < Const.Version.MIN_VERCODE
|
||||||
val isActive = versionCode >= 0
|
val isActive = versionCode > 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -33,7 +33,7 @@ class JobService : BaseJobService() {
|
|||||||
svc.fetchUpdate()?.let {
|
svc.fetchUpdate()?.let {
|
||||||
Info.remote = it
|
Info.remote = it
|
||||||
if (Info.env.isActive && BuildConfig.VERSION_CODE < it.magisk.versionCode)
|
if (Info.env.isActive && BuildConfig.VERSION_CODE < it.magisk.versionCode)
|
||||||
Notifications.updateAvailable(this)
|
Notifications.updateAvailable()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -6,17 +6,23 @@ import android.os.ParcelFileDescriptor
|
|||||||
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
||||||
import com.topjohnwu.magisk.core.base.BaseProvider
|
import com.topjohnwu.magisk.core.base.BaseProvider
|
||||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
||||||
|
import com.topjohnwu.magisk.core.su.TestHandler
|
||||||
|
|
||||||
class Provider : BaseProvider() {
|
class Provider : BaseProvider() {
|
||||||
|
|
||||||
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
|
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
|
||||||
SuCallbackHandler.run(context!!, method, extras)
|
return when (method) {
|
||||||
return Bundle.EMPTY
|
SuCallbackHandler.LOG, SuCallbackHandler.NOTIFY -> {
|
||||||
|
SuCallbackHandler.run(context!!, method, extras)
|
||||||
|
Bundle.EMPTY
|
||||||
|
}
|
||||||
|
else -> TestHandler.run(method)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
|
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
|
||||||
return when (uri.encodedPath ?: return null) {
|
return when (uri.encodedPath ?: return null) {
|
||||||
"/prefs_file" -> ParcelFileDescriptor.open(Config.prefsFile, MODE_READ_ONLY)
|
"/prefs_file" -> ParcelFileDescriptor.open(Config.getPrefsFile(), MODE_READ_ONLY)
|
||||||
else -> super.openFile(uri, mode)
|
else -> super.openFile(uri, mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -51,7 +51,7 @@ open class Receiver : BaseReceiver() {
|
|||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
val installer = context.packageManager.getInstallerPackageName(context.packageName)
|
val installer = context.packageManager.getInstallerPackageName(context.packageName)
|
||||||
if (installer == context.packageName) {
|
if (installer == context.packageName) {
|
||||||
Notifications.updateDone(context)
|
Notifications.updateDone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
package com.topjohnwu.magisk.core.base
|
package com.topjohnwu.magisk.core.base
|
||||||
|
|
||||||
|
import android.Manifest.permission.POST_NOTIFICATIONS
|
||||||
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
||||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
@@ -17,10 +18,11 @@ import androidx.activity.result.contract.ActivityResultContracts.RequestPermissi
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
|
import com.topjohnwu.magisk.core.ktx.reflectField
|
||||||
|
import com.topjohnwu.magisk.core.ktx.toast
|
||||||
|
import com.topjohnwu.magisk.core.utils.RequestAuthentication
|
||||||
import com.topjohnwu.magisk.core.utils.RequestInstall
|
import com.topjohnwu.magisk.core.utils.RequestInstall
|
||||||
import com.topjohnwu.magisk.core.wrap
|
import com.topjohnwu.magisk.core.wrap
|
||||||
import com.topjohnwu.magisk.ktx.reflectField
|
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
|
|
||||||
interface ContentResultCallback: ActivityResultCallback<Uri>, Parcelable {
|
interface ContentResultCallback: ActivityResultCallback<Uri>, Parcelable {
|
||||||
fun onActivityLaunch() {}
|
fun onActivityLaunch() {}
|
||||||
@@ -35,9 +37,17 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
permissionCallback?.invoke(it)
|
permissionCallback?.invoke(it)
|
||||||
permissionCallback = null
|
permissionCallback = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var installCallback: ((Boolean) -> Unit)? = null
|
||||||
private val requestInstall = registerForActivityResult(RequestInstall()) {
|
private val requestInstall = registerForActivityResult(RequestInstall()) {
|
||||||
permissionCallback?.invoke(it)
|
installCallback?.invoke(it)
|
||||||
permissionCallback = null
|
installCallback = null
|
||||||
|
}
|
||||||
|
|
||||||
|
var authenticateCallback: ((Boolean) -> Unit)? = null
|
||||||
|
val requestAuthenticate = registerForActivityResult(RequestAuthentication()) {
|
||||||
|
authenticateCallback?.invoke(it)
|
||||||
|
authenticateCallback = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private var contentCallback: ContentResultCallback? = null
|
private var contentCallback: ContentResultCallback? = null
|
||||||
@@ -52,9 +62,7 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
val realCallingPackage: String? get() {
|
val realCallingPackage: String? get() {
|
||||||
callingPackage?.let { return it }
|
callingPackage?.let { return it }
|
||||||
if (Build.VERSION.SDK_INT >= 22) {
|
mReferrerField.get(this)?.let { return it as String }
|
||||||
mReferrerField.get(this)?.let { return it as String }
|
|
||||||
}
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,8 +75,8 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
// Overwrite private members to avoid nasty "false" stack traces being logged
|
// Overwrite private members to avoid nasty "false" stack traces being logged
|
||||||
val delegate = delegate
|
val delegate = delegate
|
||||||
val clz = delegate.javaClass
|
val clz = delegate.javaClass
|
||||||
clz.reflectField("mActivityHandlesUiModeChecked").set(delegate, true)
|
clz.reflectField("mActivityHandlesConfigFlagsChecked").set(delegate, true)
|
||||||
clz.reflectField("mActivityHandlesUiMode").set(delegate, false)
|
clz.reflectField("mActivityHandlesConfigFlags").set(delegate, 0)
|
||||||
}
|
}
|
||||||
contentCallback = savedInstanceState?.getParcelable(CONTENT_CALLBACK_KEY)
|
contentCallback = savedInstanceState?.getParcelable(CONTENT_CALLBACK_KEY)
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -82,15 +90,23 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
||||||
if (permission == WRITE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= 30) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
|
||||||
// We do not need external rw on 30+
|
permission == WRITE_EXTERNAL_STORAGE) {
|
||||||
|
// We do not need external rw on R+
|
||||||
|
callback(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU &&
|
||||||
|
permission == POST_NOTIFICATIONS) {
|
||||||
|
// All apps have notification permissions before T
|
||||||
callback(true)
|
callback(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
permissionCallback = callback
|
|
||||||
if (permission == REQUEST_INSTALL_PACKAGES) {
|
if (permission == REQUEST_INSTALL_PACKAGES) {
|
||||||
|
installCallback = callback
|
||||||
requestInstall.launch(Unit)
|
requestInstall.launch(Unit)
|
||||||
} else {
|
} else {
|
||||||
|
permissionCallback = callback
|
||||||
requestPermission.launch(permission)
|
requestPermission.launch(permission)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,7 +117,7 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
getContent.launch(type)
|
getContent.launch(type)
|
||||||
callback.onActivityLaunch()
|
callback.onActivityLaunch()
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Utils.toast(R.string.app_not_found, Toast.LENGTH_SHORT)
|
toast(R.string.app_not_found, Toast.LENGTH_SHORT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -12,15 +12,12 @@ private const val FILE = "file"
|
|||||||
|
|
||||||
interface GithubPageServices {
|
interface GithubPageServices {
|
||||||
|
|
||||||
@GET("{$FILE}")
|
@GET
|
||||||
suspend fun fetchUpdateJSON(@Path(FILE) file: String): UpdateInfo
|
suspend fun fetchUpdateJSON(@Url file: String): UpdateInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RawServices {
|
interface RawServices {
|
||||||
|
|
||||||
@GET
|
|
||||||
suspend fun fetchCustomUpdate(@Url url: String): UpdateInfo
|
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Streaming
|
@Streaming
|
||||||
suspend fun fetchFile(@Url url: String): ResponseBody
|
suspend fun fetchFile(@Url url: String): ResponseBody
|
||||||
|
@@ -1,15 +1,27 @@
|
|||||||
package com.topjohnwu.magisk.core.data
|
package com.topjohnwu.magisk.core.data
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
|
import androidx.room.migration.Migration
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
import com.topjohnwu.magisk.core.model.su.SuLog
|
import com.topjohnwu.magisk.core.model.su.SuLog
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@Database(version = 1, entities = [SuLog::class], exportSchema = false)
|
@Database(version = 2, entities = [SuLog::class], exportSchema = false)
|
||||||
abstract class SuLogDatabase : RoomDatabase() {
|
abstract class SuLogDatabase : RoomDatabase() {
|
||||||
|
|
||||||
abstract fun suLogDao(): SuLogDao
|
abstract fun suLogDao(): SuLogDao
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val MIGRATION_1_2 = object : Migration(1, 2) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) = with(database) {
|
||||||
|
execSQL("ALTER TABLE logs ADD COLUMN target INTEGER NOT NULL DEFAULT -1")
|
||||||
|
execSQL("ALTER TABLE logs ADD COLUMN context TEXT NOT NULL DEFAULT ''")
|
||||||
|
execSQL("ALTER TABLE logs ADD COLUMN gids TEXT NOT NULL DEFAULT ''")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
package com.topjohnwu.magisk.core.data.magiskdb
|
package com.topjohnwu.magisk.core.data.magiskdb
|
||||||
|
|
||||||
import com.topjohnwu.magisk.ktx.await
|
import com.topjohnwu.magisk.core.ktx.await
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@@ -9,9 +9,9 @@ import com.topjohnwu.magisk.core.data.SuLogDatabase
|
|||||||
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
||||||
import com.topjohnwu.magisk.core.data.magiskdb.SettingsDao
|
import com.topjohnwu.magisk.core.data.magiskdb.SettingsDao
|
||||||
import com.topjohnwu.magisk.core.data.magiskdb.StringDao
|
import com.topjohnwu.magisk.core.data.magiskdb.StringDao
|
||||||
|
import com.topjohnwu.magisk.core.ktx.deviceProtectedContext
|
||||||
import com.topjohnwu.magisk.core.repository.LogRepository
|
import com.topjohnwu.magisk.core.repository.LogRepository
|
||||||
import com.topjohnwu.magisk.core.repository.NetworkService
|
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||||
import com.topjohnwu.magisk.ktx.deviceProtectedContext
|
|
||||||
import io.noties.markwon.Markwon
|
import io.noties.markwon.Markwon
|
||||||
import io.noties.markwon.utils.NoCopySpannableFactory
|
import io.noties.markwon.utils.NoCopySpannableFactory
|
||||||
|
|
||||||
@@ -39,13 +39,13 @@ object ServiceLocator {
|
|||||||
NetworkService(
|
NetworkService(
|
||||||
createApiService(retrofit, Const.Url.GITHUB_PAGE_URL),
|
createApiService(retrofit, Const.Url.GITHUB_PAGE_URL),
|
||||||
createApiService(retrofit, Const.Url.GITHUB_RAW_URL),
|
createApiService(retrofit, Const.Url.GITHUB_RAW_URL),
|
||||||
createApiService(retrofit, Const.Url.GITHUB_API_URL)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSuLogDatabase(context: Context) =
|
private fun createSuLogDatabase(context: Context) =
|
||||||
Room.databaseBuilder(context, SuLogDatabase::class.java, "sulogs.db")
|
Room.databaseBuilder(context, SuLogDatabase::class.java, "sulogs.db")
|
||||||
|
.addMigrations(SuLogDatabase.MIGRATION_1_2)
|
||||||
.fallbackToDestructiveMigration()
|
.fallbackToDestructiveMigration()
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
package com.topjohnwu.magisk.core.download
|
package com.topjohnwu.magisk.core.download
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.PendingIntent.*
|
import android.app.PendingIntent.*
|
||||||
@@ -13,26 +14,26 @@ import com.topjohnwu.magisk.R
|
|||||||
import com.topjohnwu.magisk.StubApk
|
import com.topjohnwu.magisk.StubApk
|
||||||
import com.topjohnwu.magisk.core.ActivityTracker
|
import com.topjohnwu.magisk.core.ActivityTracker
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||||
import com.topjohnwu.magisk.core.intent
|
import com.topjohnwu.magisk.core.intent
|
||||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
|
import com.topjohnwu.magisk.core.ktx.*
|
||||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||||
import com.topjohnwu.magisk.ktx.copyAndClose
|
|
||||||
import com.topjohnwu.magisk.ktx.forEach
|
|
||||||
import com.topjohnwu.magisk.ktx.withStreams
|
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
|
||||||
import com.topjohnwu.magisk.utils.APKInstall
|
import com.topjohnwu.magisk.utils.APKInstall
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
import java.util.Properties
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipFile
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
@@ -46,14 +47,12 @@ class DownloadService : NotificationService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
|
||||||
job.cancel()
|
job.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun download(subject: Subject) {
|
private fun download(subject: Subject) {
|
||||||
update(subject.notifyId)
|
notifyUpdate(subject.notifyId)
|
||||||
val coroutineScope = CoroutineScope(job + Dispatchers.IO)
|
CoroutineScope(job + Dispatchers.IO).launch {
|
||||||
coroutineScope.launch {
|
|
||||||
try {
|
try {
|
||||||
val stream = service.fetchFile(subject.url).toProgressStream(subject)
|
val stream = service.fetchFile(subject.url).toProgressStream(subject)
|
||||||
when (subject) {
|
when (subject) {
|
||||||
@@ -62,7 +61,7 @@ class DownloadService : NotificationService() {
|
|||||||
}
|
}
|
||||||
val activity = ActivityTracker.foreground
|
val activity = ActivityTracker.foreground
|
||||||
if (activity != null && subject.autoLaunch) {
|
if (activity != null && subject.autoLaunch) {
|
||||||
remove(subject.notifyId)
|
notifyRemove(subject.notifyId)
|
||||||
subject.pendingIntent(activity)?.send()
|
subject.pendingIntent(activity)?.send()
|
||||||
} else {
|
} else {
|
||||||
notifyFinish(subject)
|
notifyFinish(subject)
|
||||||
@@ -77,9 +76,9 @@ class DownloadService : NotificationService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleApp(stream: InputStream, subject: Subject.App) {
|
private fun handleApp(stream: InputStream, subject: Subject.App) {
|
||||||
fun writeTee(output: OutputStream) {
|
fun writeTee(output: OutputStream) {
|
||||||
val uri = MediaStoreUtils.getFile("${subject.title}.apk").uri
|
val uri = MediaStoreUtils.getFile("${subject.title}.apk").uri
|
||||||
val external = uri.outputStream()
|
val external = uri.outputStream()
|
||||||
stream.copyAndClose(TeeOutputStream(external, output))
|
stream.copyAndClose(TeeOutputStream(external, output))
|
||||||
}
|
}
|
||||||
@@ -90,35 +89,34 @@ class DownloadService : NotificationService() {
|
|||||||
// Download full APK to stub update path
|
// Download full APK to stub update path
|
||||||
writeTee(updateApk.outputStream())
|
writeTee(updateApk.outputStream())
|
||||||
|
|
||||||
if (Info.stub!!.version < subject.stub.versionCode) {
|
val zf = ZipFile(updateApk)
|
||||||
|
val prop = Properties()
|
||||||
|
prop.load(ByteArrayInputStream(zf.comment.toByteArray()))
|
||||||
|
val stubVersion = prop.getProperty("stubVersion").toIntOrNull() ?: -1
|
||||||
|
if (Info.stub!!.version < stubVersion) {
|
||||||
// Also upgrade stub
|
// Also upgrade stub
|
||||||
update(subject.notifyId) {
|
notifyUpdate(subject.notifyId) {
|
||||||
it.setProgress(0, 0, true)
|
it.setProgress(0, 0, true)
|
||||||
.setContentTitle(getString(R.string.hide_app_title))
|
.setContentTitle(getString(R.string.hide_app_title))
|
||||||
.setContentText("")
|
.setContentText("")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download
|
// Extract stub
|
||||||
val apk = subject.file.toFile()
|
val apk = subject.file.toFile()
|
||||||
service.fetchFile(subject.stub.link).byteStream().writeTo(apk)
|
zf.getInputStream(zf.getEntry("assets/stub.apk")).writeTo(apk)
|
||||||
|
zf.close()
|
||||||
|
|
||||||
// Patch and install
|
// Patch and install
|
||||||
val session = APKInstall.startSession(this)
|
subject.intent = HideAPK.upgrade(this, apk)
|
||||||
session.openStream(this).use {
|
?: throw IOException("HideAPK patch error")
|
||||||
val label = applicationInfo.nonLocalizedLabel
|
|
||||||
if (!HideAPK.patch(this, apk, it, packageName, label)) {
|
|
||||||
throw IOException("HideAPK patch error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
apk.delete()
|
apk.delete()
|
||||||
subject.intent = session.waitIntent()
|
|
||||||
} else {
|
} else {
|
||||||
ActivityTracker.foreground?.let {
|
ActivityTracker.foreground?.let {
|
||||||
// Relaunch the process if we are foreground
|
// Relaunch the process if we are foreground
|
||||||
StubApk.restartProcess(it)
|
StubApk.restartProcess(it)
|
||||||
} ?: run {
|
} ?: run {
|
||||||
// Or else kill the current process after posting notification
|
// Or else kill the current process after posting notification
|
||||||
subject.intent = Notifications.selfLaunchIntent(this)
|
subject.intent = selfLaunchIntent()
|
||||||
subject.postDownload = { Runtime.getRuntime().exit(0) }
|
subject.postDownload = { Runtime.getRuntime().exit(0) }
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -199,19 +197,23 @@ class DownloadService : NotificationService() {
|
|||||||
fun getPendingIntent(context: Context, subject: Subject): PendingIntent {
|
fun getPendingIntent(context: Context, subject: Subject): PendingIntent {
|
||||||
val flag = FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT or FLAG_ONE_SHOT
|
val flag = FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT or FLAG_ONE_SHOT
|
||||||
val intent = intent(context, subject)
|
val intent = intent(context, subject)
|
||||||
return if (Build.VERSION.SDK_INT >= 26) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
getForegroundService(context, REQUEST_CODE, intent, flag)
|
getForegroundService(context, REQUEST_CODE, intent, flag)
|
||||||
} else {
|
} else {
|
||||||
getService(context, REQUEST_CODE, intent, flag)
|
getService(context, REQUEST_CODE, intent, flag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start(context: Context, subject: Subject) {
|
@SuppressLint("InlinedApi")
|
||||||
val app = context.applicationContext
|
fun start(activity: BaseActivity, subject: Subject) {
|
||||||
if (Build.VERSION.SDK_INT >= 26) {
|
activity.withPermission(Manifest.permission.POST_NOTIFICATIONS) {
|
||||||
app.startForegroundService(intent(app, subject))
|
// Always download regardless of notification permission status
|
||||||
} else {
|
val app = activity.applicationContext
|
||||||
app.startService(intent(app, subject))
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
app.startForegroundService(intent(app, subject))
|
||||||
|
} else {
|
||||||
|
app.startService(intent(app, subject))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,12 +2,13 @@ package com.topjohnwu.magisk.core.download
|
|||||||
|
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.base.BaseService
|
import com.topjohnwu.magisk.core.base.BaseService
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.core.ktx.synchronized
|
||||||
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
||||||
import com.topjohnwu.magisk.ktx.synchronized
|
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@@ -19,6 +20,8 @@ open class NotificationService : BaseService() {
|
|||||||
|
|
||||||
protected val service get() = ServiceLocator.networkService
|
protected val service get() = ServiceLocator.networkService
|
||||||
|
|
||||||
|
private var attachedNotificationId = 0
|
||||||
|
|
||||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
super.onTaskRemoved(rootIntent)
|
super.onTaskRemoved(rootIntent)
|
||||||
notifications.forEach { Notifications.mgr.cancel(it.key) }
|
notifications.forEach { Notifications.mgr.cancel(it.key) }
|
||||||
@@ -30,11 +33,11 @@ open class NotificationService : BaseService() {
|
|||||||
val total = max.toFloat() / 1048576
|
val total = max.toFloat() / 1048576
|
||||||
val id = subject.notifyId
|
val id = subject.notifyId
|
||||||
|
|
||||||
update(id) { it.setContentTitle(subject.title) }
|
notifyUpdate(id) { it.setContentTitle(subject.title) }
|
||||||
|
|
||||||
return ProgressInputStream(byteStream()) {
|
return ProgressInputStream(byteStream()) {
|
||||||
val progress = it.toFloat() / 1048576
|
val progress = it.toFloat() / 1048576
|
||||||
update(id) { notification ->
|
notifyUpdate(id) { notification ->
|
||||||
if (max > 0) {
|
if (max > 0) {
|
||||||
broadcast(progress / total, subject)
|
broadcast(progress / total, subject)
|
||||||
notification
|
notification
|
||||||
@@ -49,7 +52,7 @@ open class NotificationService : BaseService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
|
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
|
||||||
val notification = remove(id)?.also(editor) ?: return -1
|
val notification = notifyRemove(id)?.also(editor) ?: return -1
|
||||||
val newId = Notifications.nextId()
|
val newId = Notifications.nextId()
|
||||||
Notifications.mgr.notify(newId, notification.build())
|
Notifications.mgr.notify(newId, notification.build())
|
||||||
return newId
|
return newId
|
||||||
@@ -73,29 +76,44 @@ open class NotificationService : BaseService() {
|
|||||||
subject.pendingIntent(this)?.let { intent -> it.setContentIntent(intent) }
|
subject.pendingIntent(this)?.let { intent -> it.setContentIntent(intent) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun create() = Notifications.progress(this, "")
|
private fun attachNotification(id: Int, notification: Notification) {
|
||||||
|
attachedNotificationId = id
|
||||||
|
startForeground(id, notification)
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateForeground() {
|
private fun maybeDetachNotification(id: Int) : Boolean {
|
||||||
|
if (attachedNotificationId != id) return false
|
||||||
if (hasNotifications) {
|
if (hasNotifications) {
|
||||||
val (id, notification) = notifications.entries.first()
|
val (anotherId, notification) = notifications.entries.first()
|
||||||
startForeground(id, notification.build())
|
// Attaching a new notification will remove the current showing one
|
||||||
} else {
|
attachNotification(anotherId, notification.build())
|
||||||
stopForeground(false)
|
return true
|
||||||
}
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
stopForeground(true)
|
||||||
|
}
|
||||||
|
attachedNotificationId = 0
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun update(id: Int, editor: (Notification.Builder) -> Unit = {}) {
|
protected fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit = {}) {
|
||||||
|
fun create() = Notifications.startProgress("")
|
||||||
|
|
||||||
val wasEmpty = !hasNotifications
|
val wasEmpty = !hasNotifications
|
||||||
val notification = notifications.getOrPut(id, ::create).also(editor)
|
val notification = notifications.getOrPut(id, ::create).also(editor).build()
|
||||||
if (wasEmpty)
|
if (wasEmpty)
|
||||||
updateForeground()
|
attachNotification(id, notification)
|
||||||
else
|
else
|
||||||
Notifications.mgr.notify(id, notification.build())
|
Notifications.mgr.notify(id, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun remove(id: Int): Notification.Builder? {
|
protected fun notifyRemove(id: Int): Notification.Builder? {
|
||||||
val n = notifications.remove(id)?.also { updateForeground() }
|
val n = notifications.remove(id)
|
||||||
Notifications.mgr.cancel(id)
|
if (n == null || !maybeDetachNotification(id))
|
||||||
|
Notifications.mgr.cancel(id)
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -9,11 +9,10 @@ import android.os.Parcelable
|
|||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.di.AppContext
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
|
import com.topjohnwu.magisk.core.ktx.cachedFile
|
||||||
import com.topjohnwu.magisk.core.model.MagiskJson
|
import com.topjohnwu.magisk.core.model.MagiskJson
|
||||||
import com.topjohnwu.magisk.core.model.StubJson
|
|
||||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||||
import com.topjohnwu.magisk.ktx.cachedFile
|
|
||||||
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
@@ -59,7 +58,6 @@ sealed class Subject : Parcelable {
|
|||||||
@Parcelize
|
@Parcelize
|
||||||
class App(
|
class App(
|
||||||
private val json: MagiskJson = Info.remote.magisk,
|
private val json: MagiskJson = Info.remote.magisk,
|
||||||
val stub: StubJson = Info.remote.stub,
|
|
||||||
override val notifyId: Int = Notifications.nextId()
|
override val notifyId: Int = Notifications.nextId()
|
||||||
) : Subject() {
|
) : Subject() {
|
||||||
override val title: String get() = "Magisk-${json.version}(${json.versionCode})"
|
override val title: String get() = "Magisk-${json.version}(${json.versionCode})"
|
||||||
|
152
app/src/main/java/com/topjohnwu/magisk/core/ktx/XAndroid.kt
Normal file
152
app/src/main/java/com/topjohnwu/magisk/core/ktx/XAndroid.kt
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
package com.topjohnwu.magisk.core.ktx
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.*
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.drawable.AdaptiveIconDrawable
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.graphics.drawable.LayerDrawable
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Build.VERSION.SDK_INT
|
||||||
|
import android.os.Process
|
||||||
|
import android.view.View
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||||
|
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||||
|
import com.topjohnwu.magisk.utils.APKInstall
|
||||||
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.String
|
||||||
|
|
||||||
|
fun Context.rawResource(id: Int) = resources.openRawResource(id)
|
||||||
|
|
||||||
|
fun Context.getBitmap(id: Int): Bitmap {
|
||||||
|
var drawable = AppCompatResources.getDrawable(this, id)!!
|
||||||
|
if (drawable is BitmapDrawable)
|
||||||
|
return drawable.bitmap
|
||||||
|
if (SDK_INT >= Build.VERSION_CODES.O && drawable is AdaptiveIconDrawable) {
|
||||||
|
drawable = LayerDrawable(arrayOf(drawable.background, drawable.foreground))
|
||||||
|
}
|
||||||
|
val bitmap = Bitmap.createBitmap(
|
||||||
|
drawable.intrinsicWidth, drawable.intrinsicHeight,
|
||||||
|
Bitmap.Config.ARGB_8888
|
||||||
|
)
|
||||||
|
val canvas = Canvas(bitmap)
|
||||||
|
drawable.setBounds(0, 0, canvas.width, canvas.height)
|
||||||
|
drawable.draw(canvas)
|
||||||
|
return bitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
val Context.deviceProtectedContext: Context get() =
|
||||||
|
if (SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
createDeviceProtectedStorageContext()
|
||||||
|
} else { this }
|
||||||
|
|
||||||
|
fun Context.cachedFile(name: String) = File(cacheDir, name)
|
||||||
|
|
||||||
|
fun ApplicationInfo.getLabel(pm: PackageManager): String {
|
||||||
|
runCatching {
|
||||||
|
if (labelRes > 0) {
|
||||||
|
val res = pm.getResourcesForApplication(this)
|
||||||
|
val config = Configuration()
|
||||||
|
config.setLocale(currentLocale)
|
||||||
|
res.updateConfiguration(config, res.displayMetrics)
|
||||||
|
return res.getString(labelRes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadLabel(pm).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.unwrap(): Context {
|
||||||
|
var context = this
|
||||||
|
while (context is ContextWrapper)
|
||||||
|
context = context.baseContext
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Activity.hideKeyboard() {
|
||||||
|
val view = currentFocus ?: return
|
||||||
|
getSystemService<InputMethodManager>()
|
||||||
|
?.hideSoftInputFromWindow(view.windowToken, 0)
|
||||||
|
view.clearFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
val View.activity: Activity get() {
|
||||||
|
var context = context
|
||||||
|
while(true) {
|
||||||
|
if (context !is ContextWrapper)
|
||||||
|
error("View is not attached to activity")
|
||||||
|
if (context is Activity)
|
||||||
|
return context
|
||||||
|
context = context.baseContext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("PrivateApi")
|
||||||
|
fun getProperty(key: String, def: String): String {
|
||||||
|
runCatching {
|
||||||
|
val clazz = Class.forName("android.os.SystemProperties")
|
||||||
|
val get = clazz.getMethod("get", String::class.java, String::class.java)
|
||||||
|
return get.invoke(clazz, key, def) as String
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
@Throws(PackageManager.NameNotFoundException::class)
|
||||||
|
fun PackageManager.getPackageInfo(uid: Int, pid: Int): PackageInfo? {
|
||||||
|
val flag = PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||||
|
val pkgs = getPackagesForUid(uid) ?: throw PackageManager.NameNotFoundException()
|
||||||
|
if (pkgs.size > 1) {
|
||||||
|
if (pid <= 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// Try to find package name from PID
|
||||||
|
val proc = RootUtils.obj?.getAppProcess(pid)
|
||||||
|
if (proc == null) {
|
||||||
|
if (uid == Process.SHELL_UID) {
|
||||||
|
// It is possible that some apps installed are sharing UID with shell.
|
||||||
|
// We will not be able to find a package from the active process list,
|
||||||
|
// because the client is forked from ADB shell, not any app process.
|
||||||
|
return getPackageInfo("com.android.shell", flag)
|
||||||
|
}
|
||||||
|
} else if (uid == proc.uid) {
|
||||||
|
return getPackageInfo(proc.pkgList[0], flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (pkgs.size == 1) {
|
||||||
|
return getPackageInfo(pkgs[0], flag)
|
||||||
|
}
|
||||||
|
throw PackageManager.NameNotFoundException()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.registerRuntimeReceiver(receiver: BroadcastReceiver, filter: IntentFilter) {
|
||||||
|
APKInstall.registerReceiver(this, receiver, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.selfLaunchIntent(): Intent {
|
||||||
|
val pm = packageManager
|
||||||
|
val intent = pm.getLaunchIntentForPackage(packageName)!!
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.toast(msg: CharSequence, duration: Int) {
|
||||||
|
UiThreadHandler.run { Toast.makeText(this, msg, duration).show() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.toast(resId: Int, duration: Int) {
|
||||||
|
UiThreadHandler.run { Toast.makeText(this, resId, duration).show() }
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
package com.topjohnwu.magisk.ktx
|
package com.topjohnwu.magisk.core.ktx
|
||||||
|
|
||||||
import androidx.collection.SparseArrayCompat
|
import androidx.collection.SparseArrayCompat
|
||||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||||
@@ -11,7 +11,7 @@ import java.io.OutputStream
|
|||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.Collections
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
|
|
@@ -1,8 +1,6 @@
|
|||||||
package com.topjohnwu.magisk.ktx
|
package com.topjohnwu.magisk.core.ktx
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.Const
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -15,12 +13,4 @@ fun reboot(reason: String = if (Config.recovery) "recovery" else "") {
|
|||||||
Shell.cmd("/system/bin/svc power reboot $reason || /system/bin/reboot $reason").submit()
|
Shell.cmd("/system/bin/svc power reboot $reason || /system/bin/reboot $reason").submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun relaunchApp(context: Context) {
|
|
||||||
val intent = context.packageManager.getLaunchIntentForPackage(context.packageName) ?: return
|
|
||||||
val args = mutableListOf("am", "start", "--user", Const.USER_ID.toString())
|
|
||||||
val cmd = intent.toCommand(args).joinToString(separator = " ")
|
|
||||||
Shell.cmd("run_delay 1 \"$cmd\"").exec()
|
|
||||||
Runtime.getRuntime().exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun Shell.Job.await() = withContext(Dispatchers.IO) { exec() }
|
suspend fun Shell.Job.await() = withContext(Dispatchers.IO) { exec() }
|
@@ -7,7 +7,6 @@ import kotlinx.parcelize.Parcelize
|
|||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class UpdateInfo(
|
data class UpdateInfo(
|
||||||
val magisk: MagiskJson = MagiskJson(),
|
val magisk: MagiskJson = MagiskJson(),
|
||||||
val stub: StubJson = StubJson()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@@ -19,13 +18,6 @@ data class MagiskJson(
|
|||||||
val note: String = ""
|
val note: String = ""
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class StubJson(
|
|
||||||
val versionCode: Int = -1,
|
|
||||||
val link: String = ""
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class ModuleJson(
|
data class ModuleJson(
|
||||||
val version: String,
|
val version: String,
|
||||||
|
@@ -43,10 +43,10 @@ data class LocalModule(
|
|||||||
set(enable) {
|
set(enable) {
|
||||||
if (enable) {
|
if (enable) {
|
||||||
disableFile.delete()
|
disableFile.delete()
|
||||||
Shell.cmd("copy_sepolicy_rules").submit()
|
Shell.cmd("copy_preinit_files").submit()
|
||||||
} else {
|
} else {
|
||||||
!disableFile.createNewFile()
|
!disableFile.createNewFile()
|
||||||
Shell.cmd("copy_sepolicy_rules").submit()
|
Shell.cmd("copy_preinit_files").submit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,10 +56,10 @@ data class LocalModule(
|
|||||||
if (remove) {
|
if (remove) {
|
||||||
if (updateFile.exists()) return
|
if (updateFile.exists()) return
|
||||||
removeFile.createNewFile()
|
removeFile.createNewFile()
|
||||||
Shell.cmd("copy_sepolicy_rules").submit()
|
Shell.cmd("copy_preinit_files").submit()
|
||||||
} else {
|
} else {
|
||||||
removeFile.delete()
|
removeFile.delete()
|
||||||
Shell.cmd("copy_sepolicy_rules").submit()
|
Shell.cmd("copy_preinit_files").submit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,15 +122,13 @@ data class LocalModule(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private val PERSIST get() = "${Const.MAGISKTMP}/mirror/persist/magisk"
|
|
||||||
|
|
||||||
fun loaded() = RootUtils.fs.getFile(Const.MAGISK_PATH).exists()
|
fun loaded() = RootUtils.fs.getFile(Const.MAGISK_PATH).exists()
|
||||||
|
|
||||||
suspend fun installed() = withContext(Dispatchers.IO) {
|
suspend fun installed() = withContext(Dispatchers.IO) {
|
||||||
RootUtils.fs.getFile(Const.MAGISK_PATH)
|
RootUtils.fs.getFile(Const.MAGISK_PATH)
|
||||||
.listFiles()
|
.listFiles()
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
.filter { !it.isFile }
|
.filter { !it.isFile && !it.isHidden }
|
||||||
.map { LocalModule("${Const.MAGISK_PATH}/${it.name}") }
|
.map { LocalModule("${Const.MAGISK_PATH}/${it.name}") }
|
||||||
.sortedBy { it.name.lowercase(Locale.ROOT) }
|
.sortedBy { it.name.lowercase(Locale.ROOT) }
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@ import android.content.pm.PackageInfo
|
|||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import com.topjohnwu.magisk.ktx.getLabel
|
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||||
|
|
||||||
@Entity(tableName = "logs")
|
@Entity(tableName = "logs")
|
||||||
class SuLog(
|
class SuLog(
|
||||||
@@ -14,7 +14,10 @@ class SuLog(
|
|||||||
val packageName: String,
|
val packageName: String,
|
||||||
val appName: String,
|
val appName: String,
|
||||||
val command: String,
|
val command: String,
|
||||||
val action: Boolean,
|
val action: Int,
|
||||||
|
val target: Int,
|
||||||
|
val context: String,
|
||||||
|
val gids: String,
|
||||||
val time: Long = System.currentTimeMillis()
|
val time: Long = System.currentTimeMillis()
|
||||||
) {
|
) {
|
||||||
@PrimaryKey(autoGenerate = true) var id: Int = 0
|
@PrimaryKey(autoGenerate = true) var id: Int = 0
|
||||||
@@ -25,7 +28,10 @@ fun PackageManager.createSuLog(
|
|||||||
toUid: Int,
|
toUid: Int,
|
||||||
fromPid: Int,
|
fromPid: Int,
|
||||||
command: String,
|
command: String,
|
||||||
policy: Int
|
policy: Int,
|
||||||
|
target: Int,
|
||||||
|
context: String,
|
||||||
|
gids: String,
|
||||||
): SuLog {
|
): SuLog {
|
||||||
val appInfo = info.applicationInfo
|
val appInfo = info.applicationInfo
|
||||||
return SuLog(
|
return SuLog(
|
||||||
@@ -35,7 +41,10 @@ fun PackageManager.createSuLog(
|
|||||||
packageName = getNameForUid(appInfo.uid)!!,
|
packageName = getNameForUid(appInfo.uid)!!,
|
||||||
appName = appInfo.getLabel(this),
|
appName = appInfo.getLabel(this),
|
||||||
command = command,
|
command = command,
|
||||||
action = policy == SuPolicy.ALLOW
|
action = policy,
|
||||||
|
target = target,
|
||||||
|
context = context,
|
||||||
|
gids = gids,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +53,10 @@ fun createSuLog(
|
|||||||
toUid: Int,
|
toUid: Int,
|
||||||
fromPid: Int,
|
fromPid: Int,
|
||||||
command: String,
|
command: String,
|
||||||
policy: Int
|
policy: Int,
|
||||||
|
target: Int,
|
||||||
|
context: String,
|
||||||
|
gids: String,
|
||||||
): SuLog {
|
): SuLog {
|
||||||
return SuLog(
|
return SuLog(
|
||||||
fromUid = fromUid,
|
fromUid = fromUid,
|
||||||
@@ -53,6 +65,9 @@ fun createSuLog(
|
|||||||
packageName = "[UID] $fromUid",
|
packageName = "[UID] $fromUid",
|
||||||
appName = "[UID] $fromUid",
|
appName = "[UID] $fromUid",
|
||||||
command = command,
|
command = command,
|
||||||
action = policy == SuPolicy.ALLOW
|
action = policy,
|
||||||
|
target = target,
|
||||||
|
context = context,
|
||||||
|
gids = gids,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -3,8 +3,8 @@ package com.topjohnwu.magisk.core.repository
|
|||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.data.SuLogDao
|
import com.topjohnwu.magisk.core.data.SuLogDao
|
||||||
|
import com.topjohnwu.magisk.core.ktx.await
|
||||||
import com.topjohnwu.magisk.core.model.su.SuLog
|
import com.topjohnwu.magisk.core.model.su.SuLog
|
||||||
import com.topjohnwu.magisk.ktx.await
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
|
|
||||||
|
|
||||||
|
@@ -8,7 +8,6 @@ import com.topjohnwu.magisk.core.Config.Value.DEBUG_CHANNEL
|
|||||||
import com.topjohnwu.magisk.core.Config.Value.DEFAULT_CHANNEL
|
import com.topjohnwu.magisk.core.Config.Value.DEFAULT_CHANNEL
|
||||||
import com.topjohnwu.magisk.core.Config.Value.STABLE_CHANNEL
|
import com.topjohnwu.magisk.core.Config.Value.STABLE_CHANNEL
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.data.GithubApiServices
|
|
||||||
import com.topjohnwu.magisk.core.data.GithubPageServices
|
import com.topjohnwu.magisk.core.data.GithubPageServices
|
||||||
import com.topjohnwu.magisk.core.data.RawServices
|
import com.topjohnwu.magisk.core.data.RawServices
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
@@ -17,8 +16,7 @@ import java.io.IOException
|
|||||||
|
|
||||||
class NetworkService(
|
class NetworkService(
|
||||||
private val pages: GithubPageServices,
|
private val pages: GithubPageServices,
|
||||||
private val raw: RawServices,
|
private val raw: RawServices
|
||||||
private val api: GithubApiServices
|
|
||||||
) {
|
) {
|
||||||
suspend fun fetchUpdate() = safe {
|
suspend fun fetchUpdate() = safe {
|
||||||
var info = when (Config.updateChannel) {
|
var info = when (Config.updateChannel) {
|
||||||
@@ -42,7 +40,7 @@ class NetworkService(
|
|||||||
private suspend fun fetchBetaUpdate() = pages.fetchUpdateJSON("beta.json")
|
private suspend fun fetchBetaUpdate() = pages.fetchUpdateJSON("beta.json")
|
||||||
private suspend fun fetchCanaryUpdate() = pages.fetchUpdateJSON("canary.json")
|
private suspend fun fetchCanaryUpdate() = pages.fetchUpdateJSON("canary.json")
|
||||||
private suspend fun fetchDebugUpdate() = pages.fetchUpdateJSON("debug.json")
|
private suspend fun fetchDebugUpdate() = pages.fetchUpdateJSON("debug.json")
|
||||||
private suspend fun fetchCustomUpdate(url: String) = raw.fetchCustomUpdate(url)
|
private suspend fun fetchCustomUpdate(url: String) = pages.fetchUpdateJSON(url)
|
||||||
|
|
||||||
private inline fun <T> safe(factory: () -> T): T? {
|
private inline fun <T> safe(factory: () -> T): T? {
|
||||||
return try {
|
return try {
|
||||||
|
@@ -7,11 +7,11 @@ import com.topjohnwu.magisk.BuildConfig
|
|||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||||
|
import com.topjohnwu.magisk.core.ktx.getPackageInfo
|
||||||
|
import com.topjohnwu.magisk.core.ktx.toast
|
||||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||||
import com.topjohnwu.magisk.core.model.su.createSuLog
|
import com.topjohnwu.magisk.core.model.su.createSuLog
|
||||||
import com.topjohnwu.magisk.ktx.getLabel
|
|
||||||
import com.topjohnwu.magisk.ktx.getPackageInfo
|
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@@ -57,17 +57,20 @@ object SuCallbackHandler {
|
|||||||
val toUid = data.getIntComp("to.uid", -1)
|
val toUid = data.getIntComp("to.uid", -1)
|
||||||
val pid = data.getIntComp("pid", -1)
|
val pid = data.getIntComp("pid", -1)
|
||||||
val command = data.getString("command", "")
|
val command = data.getString("command", "")
|
||||||
|
val target = data.getIntComp("target", -1)
|
||||||
|
val seContext = data.getString("context", "")
|
||||||
|
val gids = data.getString("gids", "")
|
||||||
|
|
||||||
val pm = context.packageManager
|
val pm = context.packageManager
|
||||||
|
|
||||||
val log = runCatching {
|
val log = runCatching {
|
||||||
pm.getPackageInfo(fromUid, pid)?.let {
|
pm.getPackageInfo(fromUid, pid)?.let {
|
||||||
pm.createSuLog(it, toUid, pid, command, policy)
|
pm.createSuLog(it, toUid, pid, command, policy, target, seContext, gids)
|
||||||
}
|
}
|
||||||
}.getOrNull() ?: createSuLog(fromUid, toUid, pid, command, policy)
|
}.getOrNull() ?: createSuLog(fromUid, toUid, pid, command, policy, target, seContext, gids)
|
||||||
|
|
||||||
if (notify)
|
if (notify)
|
||||||
notify(context, log.action, log.appName)
|
notify(context, log.action == SuPolicy.ALLOW, log.appName)
|
||||||
|
|
||||||
runBlocking { ServiceLocator.logRepo.insert(log) }
|
runBlocking { ServiceLocator.logRepo.insert(log) }
|
||||||
}
|
}
|
||||||
@@ -93,7 +96,7 @@ object SuCallbackHandler {
|
|||||||
else
|
else
|
||||||
R.string.su_deny_toast
|
R.string.su_deny_toast
|
||||||
|
|
||||||
Utils.toast(context.getString(resId, appName), Toast.LENGTH_SHORT)
|
context.toast(context.getString(resId, appName), Toast.LENGTH_SHORT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,13 +6,14 @@ import android.content.pm.PackageManager
|
|||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
||||||
|
import com.topjohnwu.magisk.core.ktx.getPackageInfo
|
||||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||||
import com.topjohnwu.magisk.ktx.getPackageInfo
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.DataOutputStream
|
import java.io.DataOutputStream
|
||||||
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -22,7 +23,7 @@ class SuRequestHandler(
|
|||||||
private val policyDB: PolicyDao
|
private val policyDB: PolicyDao
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private lateinit var output: DataOutputStream
|
private lateinit var output: File
|
||||||
private lateinit var policy: SuPolicy
|
private lateinit var policy: SuPolicy
|
||||||
lateinit var pkgInfo: PackageInfo
|
lateinit var pkgInfo: PackageInfo
|
||||||
private set
|
private set
|
||||||
@@ -52,37 +53,32 @@ class SuRequestHandler(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun close() {
|
private suspend fun init(intent: Intent): Boolean {
|
||||||
if (::output.isInitialized)
|
val uid = intent.getIntExtra("uid", -1)
|
||||||
runCatching { output.close() }
|
val pid = intent.getIntExtra("pid", -1)
|
||||||
}
|
val fifo = intent.getStringExtra("fifo")
|
||||||
|
if (uid <= 0 || pid <= 0 || fifo == null) {
|
||||||
private suspend fun init(intent: Intent) = withContext(Dispatchers.IO) {
|
Timber.e("Unexpected extras: uid=[${uid}], pid=[${pid}], fifo=[${fifo}]")
|
||||||
try {
|
return false
|
||||||
val fifo = intent.getStringExtra("fifo") ?: throw IOException("fifo == null")
|
|
||||||
output = DataOutputStream(FileOutputStream(fifo))
|
|
||||||
val uid = intent.getIntExtra("uid", -1)
|
|
||||||
if (uid <= 0) {
|
|
||||||
throw IOException("uid == $uid")
|
|
||||||
}
|
|
||||||
policy = SuPolicy(uid)
|
|
||||||
val pid = intent.getIntExtra("pid", -1)
|
|
||||||
try {
|
|
||||||
pkgInfo = pm.getPackageInfo(uid, pid) ?: PackageInfo().apply {
|
|
||||||
val name = pm.getNameForUid(uid) ?: throw PackageManager.NameNotFoundException()
|
|
||||||
// We only fill in sharedUserId and leave other fields uninitialized
|
|
||||||
sharedUserId = name.split(":")[0]
|
|
||||||
}
|
|
||||||
return@withContext true
|
|
||||||
} catch (e: PackageManager.NameNotFoundException) {
|
|
||||||
respond(SuPolicy.DENY, -1)
|
|
||||||
return@withContext false
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Timber.e(e)
|
|
||||||
close()
|
|
||||||
return@withContext false
|
|
||||||
}
|
}
|
||||||
|
output = File(fifo)
|
||||||
|
policy = SuPolicy(uid)
|
||||||
|
try {
|
||||||
|
pkgInfo = pm.getPackageInfo(uid, pid) ?: PackageInfo().apply {
|
||||||
|
val name = pm.getNameForUid(uid) ?: throw PackageManager.NameNotFoundException()
|
||||||
|
// We only fill in sharedUserId and leave other fields uninitialized
|
||||||
|
sharedUserId = name.split(":")[0]
|
||||||
|
}
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
Timber.e(e)
|
||||||
|
respond(SuPolicy.DENY, -1)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!output.canWrite()) {
|
||||||
|
Timber.e("Cannot write to $output")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun respond(action: Int, time: Int) {
|
suspend fun respond(action: Int, time: Int) {
|
||||||
@@ -97,14 +93,15 @@ class SuRequestHandler(
|
|||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
output.writeInt(policy.policy)
|
DataOutputStream(FileOutputStream(output)).use {
|
||||||
output.flush()
|
it.writeInt(policy.policy)
|
||||||
|
it.flush()
|
||||||
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
} finally {
|
}
|
||||||
close()
|
if (until >= 0) {
|
||||||
if (until >= 0)
|
policyDB.update(policy)
|
||||||
policyDB.update(policy)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,68 @@
|
|||||||
|
package com.topjohnwu.magisk.core.su
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.topjohnwu.magisk.core.Config
|
||||||
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
||||||
|
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import com.topjohnwu.superuser.internal.NOPList
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
|
object TestHandler {
|
||||||
|
|
||||||
|
fun run(method: String): Bundle {
|
||||||
|
val r = Bundle()
|
||||||
|
|
||||||
|
fun setup(): Boolean {
|
||||||
|
val nop = NOPList.getInstance()
|
||||||
|
return runBlocking {
|
||||||
|
MagiskInstaller.Emulator(nop, nop).exec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test(): Boolean {
|
||||||
|
// Make sure Zygisk works correctly
|
||||||
|
if (!Info.isZygiskEnabled) {
|
||||||
|
r.putString("reason", "zygisk not enabled")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the Magisk app can get root
|
||||||
|
val shell = Shell.getShell()
|
||||||
|
if (!shell.isRoot) {
|
||||||
|
r.putString("reason", "shell not root")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the root service is running
|
||||||
|
RootUtils.Connection.await()
|
||||||
|
|
||||||
|
// Clear existing grant for ADB shell
|
||||||
|
runBlocking {
|
||||||
|
ServiceLocator.policyDB.delete(2000)
|
||||||
|
Config.suAutoResponse = Config.Value.SU_AUTO_ALLOW
|
||||||
|
Config.prefs.edit().commit()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
val b = runCatching {
|
||||||
|
when (method) {
|
||||||
|
"setup" -> setup()
|
||||||
|
"test" -> test()
|
||||||
|
else -> {
|
||||||
|
r.putString("reason", "unknown method")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.getOrElse {
|
||||||
|
r.putString("reason", it.stackTraceToString())
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
r.putBoolean("result", b)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
}
|
@@ -4,10 +4,10 @@ import android.net.Uri
|
|||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.di.AppContext
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
|
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||||
import com.topjohnwu.magisk.core.utils.unzip
|
import com.topjohnwu.magisk.core.utils.unzip
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@@ -4,22 +4,21 @@ import android.app.Activity
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.StubApk
|
import com.topjohnwu.magisk.StubApk
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.Info
|
|
||||||
import com.topjohnwu.magisk.core.Provider
|
import com.topjohnwu.magisk.core.Provider
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
import com.topjohnwu.magisk.core.ktx.await
|
||||||
|
import com.topjohnwu.magisk.core.ktx.toast
|
||||||
|
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||||
import com.topjohnwu.magisk.core.utils.AXML
|
import com.topjohnwu.magisk.core.utils.AXML
|
||||||
import com.topjohnwu.magisk.core.utils.Keygen
|
import com.topjohnwu.magisk.core.utils.Keygen
|
||||||
import com.topjohnwu.magisk.ktx.await
|
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
|
||||||
import com.topjohnwu.magisk.signing.JarMap
|
import com.topjohnwu.magisk.signing.JarMap
|
||||||
import com.topjohnwu.magisk.signing.SignApk
|
import com.topjohnwu.magisk.signing.SignApk
|
||||||
import com.topjohnwu.magisk.utils.APKInstall
|
import com.topjohnwu.magisk.utils.APKInstall
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Runnable
|
import kotlinx.coroutines.Runnable
|
||||||
@@ -30,6 +29,7 @@ import java.io.FileOutputStream
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
|
import kotlin.random.asKotlinRandom
|
||||||
|
|
||||||
object HideAPK {
|
object HideAPK {
|
||||||
|
|
||||||
@@ -39,8 +39,7 @@ object HideAPK {
|
|||||||
|
|
||||||
// Some arbitrary limit
|
// Some arbitrary limit
|
||||||
const val MAX_LABEL_LENGTH = 32
|
const val MAX_LABEL_LENGTH = 32
|
||||||
|
const val PLACEHOLDER = "COMPONENT_PLACEHOLDER"
|
||||||
private val svc get() = ServiceLocator.networkService
|
|
||||||
|
|
||||||
private fun genPackageName(): String {
|
private fun genPackageName(): String {
|
||||||
val random = SecureRandom()
|
val random = SecureRandom()
|
||||||
@@ -65,20 +64,87 @@ object HideAPK {
|
|||||||
return builder.toString()
|
return builder.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun patch(
|
private fun classNameGenerator() = sequence {
|
||||||
|
val c1 = mutableListOf<String>()
|
||||||
|
val c2 = mutableListOf<String>()
|
||||||
|
val c3 = mutableListOf<String>()
|
||||||
|
val random = SecureRandom()
|
||||||
|
val kRandom = random.asKotlinRandom()
|
||||||
|
|
||||||
|
fun <T> chain(vararg iters: Iterable<T>) = sequence {
|
||||||
|
iters.forEach { it.forEach { v -> yield(v) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
for (a in chain('a'..'z', 'A'..'Z')) {
|
||||||
|
if (a != 'a' && a != 'A') {
|
||||||
|
c1.add("$a")
|
||||||
|
}
|
||||||
|
for (b in chain('a'..'z', 'A'..'Z', '0'..'9')) {
|
||||||
|
c2.add("$a$b")
|
||||||
|
for (c in chain('a'..'z', 'A'..'Z', '0'..'9')) {
|
||||||
|
c3.add("$a$b$c")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c1.shuffle(random)
|
||||||
|
c2.shuffle(random)
|
||||||
|
c3.shuffle(random)
|
||||||
|
|
||||||
|
fun notJavaKeyword(name: String) = when (name) {
|
||||||
|
"do", "if", "for", "int", "new", "try" -> false
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<String>.process() = asSequence().filter(::notJavaKeyword)
|
||||||
|
|
||||||
|
val names = mutableListOf<String>()
|
||||||
|
names.addAll(c1)
|
||||||
|
names.addAll(c2.process().take(30))
|
||||||
|
names.addAll(c3.process().take(30))
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val seg = 2 + random.nextInt(4)
|
||||||
|
val cls = StringBuilder()
|
||||||
|
for (i in 0 until seg) {
|
||||||
|
cls.append(names.random(kRandom))
|
||||||
|
if (i != seg - 1)
|
||||||
|
cls.append('.')
|
||||||
|
}
|
||||||
|
// Old Android does not support capitalized package names
|
||||||
|
// Check Android 7.0.0 PackageParser#buildClassName
|
||||||
|
cls[0] = cls[0].lowercaseChar()
|
||||||
|
yield(cls.toString())
|
||||||
|
}
|
||||||
|
}.distinct().iterator()
|
||||||
|
|
||||||
|
private fun patch(
|
||||||
context: Context,
|
context: Context,
|
||||||
apk: File, out: OutputStream,
|
apk: File, out: OutputStream,
|
||||||
pkg: String, label: CharSequence
|
pkg: String, label: CharSequence
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val info = context.packageManager.getPackageArchiveInfo(apk.path, 0) ?: return false
|
val info = context.packageManager.getPackageArchiveInfo(apk.path, 0) ?: return false
|
||||||
val name = info.applicationInfo.nonLocalizedLabel.toString()
|
val origLabel = info.applicationInfo.nonLocalizedLabel.toString()
|
||||||
try {
|
try {
|
||||||
JarMap.open(apk, true).use { jar ->
|
JarMap.open(apk, true).use { jar ->
|
||||||
val je = jar.getJarEntry(ANDROID_MANIFEST)
|
val je = jar.getJarEntry(ANDROID_MANIFEST)
|
||||||
val xml = AXML(jar.getRawData(je))
|
val xml = AXML(jar.getRawData(je))
|
||||||
|
val generator = classNameGenerator()
|
||||||
|
|
||||||
if (!xml.findAndPatch(APPLICATION_ID to pkg, name to label.toString()))
|
if (!xml.patchStrings {
|
||||||
|
for (i in it.indices) {
|
||||||
|
val s = it[i]
|
||||||
|
if (s.contains(APPLICATION_ID)) {
|
||||||
|
it[i] = s.replace(APPLICATION_ID, pkg)
|
||||||
|
} else if (s.contains(PLACEHOLDER)) {
|
||||||
|
it[i] = generator.next()
|
||||||
|
} else if (s == origLabel) {
|
||||||
|
it[i] = label.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
return false
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Write apk changes
|
// Write apk changes
|
||||||
jar.getOutputStream(je).use { it.write(xml.bytes) }
|
jar.getOutputStream(je).use { it.write(xml.bytes) }
|
||||||
@@ -94,7 +160,6 @@ object HideAPK {
|
|||||||
|
|
||||||
private fun launchApp(activity: Activity, pkg: String) {
|
private fun launchApp(activity: Activity, pkg: String) {
|
||||||
val intent = activity.packageManager.getLaunchIntentForPackage(pkg) ?: return
|
val intent = activity.packageManager.getLaunchIntentForPackage(pkg) ?: return
|
||||||
Config.suManager = if (pkg == APPLICATION_ID) "" else pkg
|
|
||||||
val self = activity.packageName
|
val self = activity.packageName
|
||||||
val flag = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
val flag = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
activity.grantUriPermission(pkg, Provider.preferencesUri(self), flag)
|
activity.grantUriPermission(pkg, Provider.preferencesUri(self), flag)
|
||||||
@@ -103,17 +168,13 @@ object HideAPK {
|
|||||||
activity.finish()
|
activity.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
private fun patchAndHide(activity: Activity, label: String, onFailure: Runnable): Boolean {
|
||||||
private suspend fun patchAndHide(activity: Activity, label: String, onFailure: Runnable): Boolean {
|
|
||||||
val stub = File(activity.cacheDir, "stub.apk")
|
val stub = File(activity.cacheDir, "stub.apk")
|
||||||
try {
|
try {
|
||||||
svc.fetchFile(Info.remote.stub.link).byteStream().writeTo(stub)
|
activity.assets.open("stub.apk").writeTo(stub)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
stub.createNewFile()
|
return false
|
||||||
val cmd = "\$MAGISKBIN/magiskinit -x manager ${stub.path}"
|
|
||||||
if (!Shell.cmd(cmd).exec().isSuccess)
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a new random package name and signature
|
// Generate a new random package name and signature
|
||||||
@@ -129,7 +190,8 @@ object HideAPK {
|
|||||||
launchApp(activity, pkg)
|
launchApp(activity, pkg)
|
||||||
}
|
}
|
||||||
|
|
||||||
val cmd = "adb_pm_install $repack ${activity.applicationInfo.uid}"
|
Config.suManager = pkg
|
||||||
|
val cmd = "adb_pm_install $repack $pkg"
|
||||||
if (Shell.cmd(cmd).exec().isSuccess) return true
|
if (Shell.cmd(cmd).exec().isSuccess) return true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -152,7 +214,7 @@ object HideAPK {
|
|||||||
}
|
}
|
||||||
val onFailure = Runnable {
|
val onFailure = Runnable {
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
Utils.toast(R.string.failure, Toast.LENGTH_LONG)
|
activity.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||||
}
|
}
|
||||||
val success = withContext(Dispatchers.IO) {
|
val success = withContext(Dispatchers.IO) {
|
||||||
patchAndHide(activity, label, onFailure)
|
patchAndHide(activity, label, onFailure)
|
||||||
@@ -170,14 +232,15 @@ object HideAPK {
|
|||||||
}
|
}
|
||||||
val onFailure = Runnable {
|
val onFailure = Runnable {
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
Utils.toast(R.string.failure, Toast.LENGTH_LONG)
|
activity.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||||
}
|
}
|
||||||
val apk = StubApk.current(activity)
|
val apk = StubApk.current(activity)
|
||||||
val session = APKInstall.startSession(activity, APPLICATION_ID, onFailure) {
|
val session = APKInstall.startSession(activity, APPLICATION_ID, onFailure) {
|
||||||
launchApp(activity, APPLICATION_ID)
|
launchApp(activity, APPLICATION_ID)
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
val cmd = "adb_pm_install $apk ${activity.applicationInfo.uid}"
|
Config.suManager = ""
|
||||||
|
val cmd = "adb_pm_install $apk $APPLICATION_ID"
|
||||||
if (Shell.cmd(cmd).await().isSuccess) return
|
if (Shell.cmd(cmd).await().isSuccess) return
|
||||||
val success = withContext(Dispatchers.IO) {
|
val success = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
@@ -191,4 +254,17 @@ object HideAPK {
|
|||||||
}
|
}
|
||||||
if (!success) onFailure.run()
|
if (!success) onFailure.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
fun upgrade(context: Context, apk: File): Intent? {
|
||||||
|
val label = context.applicationInfo.nonLocalizedLabel
|
||||||
|
val pkg = context.packageName
|
||||||
|
val session = APKInstall.startSession(context)
|
||||||
|
session.openStream(context).use {
|
||||||
|
if (!patch(context, apk, it, pkg, label)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return session.waitIntent()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,24 +1,30 @@
|
|||||||
package com.topjohnwu.magisk.core.tasks
|
package com.topjohnwu.magisk.core.tasks
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.system.ErrnoException
|
||||||
import android.system.Os
|
import android.system.Os
|
||||||
|
import android.system.OsConstants
|
||||||
|
import android.system.OsConstants.O_WRONLY
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.core.os.postDelayed
|
import androidx.core.os.postDelayed
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.StubApk
|
import com.topjohnwu.magisk.StubApk
|
||||||
import com.topjohnwu.magisk.core.*
|
import com.topjohnwu.magisk.core.AppApkPath
|
||||||
|
import com.topjohnwu.magisk.core.Config
|
||||||
|
import com.topjohnwu.magisk.core.Const
|
||||||
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
|
import com.topjohnwu.magisk.core.ktx.copyAndClose
|
||||||
|
import com.topjohnwu.magisk.core.ktx.reboot
|
||||||
|
import com.topjohnwu.magisk.core.ktx.toast
|
||||||
|
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||||
import com.topjohnwu.magisk.ktx.reboot
|
|
||||||
import com.topjohnwu.magisk.ktx.withStreams
|
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
|
||||||
import com.topjohnwu.magisk.signing.SignBoot
|
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.ShellUtils
|
import com.topjohnwu.superuser.ShellUtils
|
||||||
import com.topjohnwu.superuser.internal.NOPList
|
import com.topjohnwu.superuser.internal.NOPList
|
||||||
@@ -40,6 +46,7 @@ import java.util.*
|
|||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
abstract class MagiskInstallImpl protected constructor(
|
abstract class MagiskInstallImpl protected constructor(
|
||||||
protected val console: MutableList<String> = NOPList.getInstance(),
|
protected val console: MutableList<String> = NOPList.getInstance(),
|
||||||
@@ -111,7 +118,9 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
val name = n.substring(3, n.length - 3)
|
val name = n.substring(3, n.length - 3)
|
||||||
val dest = File(installDir, name)
|
val dest = File(installDir, name)
|
||||||
zf.getInputStream(it).writeTo(dest)
|
zf.getInputStream(it).writeTo(dest)
|
||||||
|
dest.setExecutable(true)
|
||||||
}
|
}
|
||||||
|
zf.close()
|
||||||
} else {
|
} else {
|
||||||
val info = context.applicationInfo
|
val info = context.applicationInfo
|
||||||
var libs = File(info.nativeLibraryDir).listFiles { _, name ->
|
var libs = File(info.nativeLibraryDir).listFiles { _, name ->
|
||||||
@@ -119,7 +128,8 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
} ?: emptyArray()
|
} ?: emptyArray()
|
||||||
|
|
||||||
// Also symlink magisk32 on non 64-bit only 64-bit devices
|
// Also symlink magisk32 on non 64-bit only 64-bit devices
|
||||||
val lib32 = info.javaClass.getDeclaredField("secondaryNativeLibraryDir").get(info) as String?
|
val lib32 = info.javaClass.getDeclaredField("secondaryNativeLibraryDir")
|
||||||
|
.get(info) as String?
|
||||||
if (lib32 != null) {
|
if (lib32 != null) {
|
||||||
libs += File(lib32, "libmagisk32.so")
|
libs += File(lib32, "libmagisk32.so")
|
||||||
}
|
}
|
||||||
@@ -131,7 +141,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract scripts
|
// Extract scripts
|
||||||
for (script in listOf("util_functions.sh", "boot_patch.sh", "addon.d.sh")) {
|
for (script in listOf("util_functions.sh", "boot_patch.sh", "addon.d.sh", "stub.apk")) {
|
||||||
val dest = File(installDir, script)
|
val dest = File(installDir, script)
|
||||||
context.assets.open(script).writeTo(dest)
|
context.assets.open(script).writeTo(dest)
|
||||||
}
|
}
|
||||||
@@ -164,97 +174,218 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun InputStream.cleanPump(out: OutputStream) = withStreams(this, out) { src, _ ->
|
private fun InputStream.copyAndCloseOut(out: OutputStream) = out.use { copyTo(it) }
|
||||||
src.copyTo(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun newTarEntry(name: String, size: Long): TarEntry {
|
private fun newTarEntry(name: String, size: Long): TarEntry {
|
||||||
console.add("-- Writing: $name")
|
console.add("-- Writing: $name")
|
||||||
return TarEntry(TarHeader.createHeader(name, size, 0, false, 420 /* 0644 */))
|
return TarEntry(TarHeader.createHeader(name, size, 0, false, 420 /* 0644 */))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class NoAvailableStream(s: InputStream) : FilterInputStream(s) {
|
||||||
|
// Make sure available is never called on the actual stream and always return 0
|
||||||
|
// 1. Workaround bug in LZ4FrameInputStream
|
||||||
|
// 2. Reduce max buffer size to prevent OOM
|
||||||
|
override fun available() = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private class NoBootException : IOException()
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun processTar(input: InputStream, output: OutputStream): OutputStream {
|
private fun processTar(tarIn: TarInputStream, tarOut: TarOutputStream): ExtendedFile {
|
||||||
console.add("- Processing tar file")
|
console.add("- Processing tar file")
|
||||||
val tarOut = TarOutputStream(output)
|
lateinit var entry: TarEntry
|
||||||
TarInputStream(input).use { tarIn ->
|
|
||||||
lateinit var entry: TarEntry
|
|
||||||
|
|
||||||
fun decompressedStream(): InputStream {
|
fun decompressedStream(): InputStream {
|
||||||
val src = if (entry.name.endsWith(".lz4")) LZ4FrameInputStream(tarIn) else tarIn
|
val stream = if (entry.name.endsWith(".lz4")) LZ4FrameInputStream(tarIn) else tarIn
|
||||||
return object : FilterInputStream(src) {
|
return NoAvailableStream(stream)
|
||||||
override fun available() = 0 /* Workaround bug in LZ4FrameInputStream */
|
}
|
||||||
override fun close() { /* Never close src stream */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (tarIn.nextEntry?.let { entry = it } != null) {
|
while (tarIn.nextEntry?.let { entry = it } != null) {
|
||||||
if (entry.name.startsWith("boot.img") ||
|
if (entry.name.startsWith("boot.img") ||
|
||||||
(Config.recovery && entry.name.contains("recovery.img"))) {
|
entry.name.startsWith("init_boot.img") ||
|
||||||
val name = entry.name.replace(".lz4", "")
|
(Config.recovery && entry.name.contains("recovery.img"))) {
|
||||||
console.add("-- Extracting: $name")
|
val name = entry.name.replace(".lz4", "")
|
||||||
|
console.add("-- Extracting: $name")
|
||||||
|
|
||||||
val extract = installDir.getChildFile(name)
|
val extract = installDir.getChildFile(name)
|
||||||
decompressedStream().cleanPump(extract.newOutputStream())
|
decompressedStream().copyAndCloseOut(extract.newOutputStream())
|
||||||
} else if (entry.name.contains("vbmeta.img")) {
|
} else if (entry.name.contains("vbmeta.img")) {
|
||||||
val rawData = decompressedStream().readBytes()
|
val rawData = decompressedStream().readBytes()
|
||||||
// Valid vbmeta.img should be at least 256 bytes
|
// Valid vbmeta.img should be at least 256 bytes
|
||||||
if (rawData.size < 256)
|
if (rawData.size < 256)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
// Patch flags to AVB_VBMETA_IMAGE_FLAGS_HASHTREE_DISABLED |
|
// Patch flags to AVB_VBMETA_IMAGE_FLAGS_HASHTREE_DISABLED |
|
||||||
// AVB_VBMETA_IMAGE_FLAGS_VERIFICATION_DISABLED
|
// AVB_VBMETA_IMAGE_FLAGS_VERIFICATION_DISABLED
|
||||||
console.add("-- Patching: vbmeta.img")
|
console.add("-- Patching: vbmeta.img")
|
||||||
ByteBuffer.wrap(rawData).putInt(120, 3)
|
ByteBuffer.wrap(rawData).putInt(120, 3)
|
||||||
tarOut.putNextEntry(newTarEntry("vbmeta.img", rawData.size.toLong()))
|
tarOut.putNextEntry(newTarEntry("vbmeta.img", rawData.size.toLong()))
|
||||||
tarOut.write(rawData)
|
tarOut.write(rawData)
|
||||||
} else {
|
// vbmeta partition exist, disable boot vbmeta patch
|
||||||
console.add("-- Copying: ${entry.name}")
|
Info.patchBootVbmeta = false
|
||||||
tarOut.putNextEntry(entry)
|
} else if (entry.name.contains("userdata.img")) {
|
||||||
tarIn.copyTo(tarOut, bufferSize = 1024 * 1024)
|
continue
|
||||||
}
|
} else {
|
||||||
|
console.add("-- Copying: ${entry.name}")
|
||||||
|
tarOut.putNextEntry(entry)
|
||||||
|
tarIn.copyTo(tarOut, bufferSize = 1024 * 1024)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val boot = installDir.getChildFile("boot.img")
|
val boot = installDir.getChildFile("boot.img")
|
||||||
|
val initBoot = installDir.getChildFile("init_boot.img")
|
||||||
val recovery = installDir.getChildFile("recovery.img")
|
val recovery = installDir.getChildFile("recovery.img")
|
||||||
if (Config.recovery && recovery.exists() && boot.exists()) {
|
|
||||||
// Install to recovery
|
fun ExtendedFile.copyToTar() {
|
||||||
srcBoot = recovery
|
newInputStream().use {
|
||||||
// Repack boot image to prevent auto restore
|
tarOut.putNextEntry(newTarEntry(name, length()))
|
||||||
arrayOf(
|
|
||||||
"cd $installDir",
|
|
||||||
"chmod -R 755 .",
|
|
||||||
"./magiskboot unpack boot.img",
|
|
||||||
"./magiskboot repack boot.img",
|
|
||||||
"cat new-boot.img > boot.img",
|
|
||||||
"./magiskboot cleanup",
|
|
||||||
"rm -f new-boot.img",
|
|
||||||
"cd /").sh()
|
|
||||||
boot.newInputStream().use {
|
|
||||||
tarOut.putNextEntry(newTarEntry("boot.img", boot.length()))
|
|
||||||
it.copyTo(tarOut)
|
it.copyTo(tarOut)
|
||||||
}
|
}
|
||||||
boot.delete()
|
delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch priority: recovery > init_boot > boot
|
||||||
|
return when {
|
||||||
|
recovery.exists() -> {
|
||||||
|
if (boot.exists()) {
|
||||||
|
// Repack boot image to prevent auto restore
|
||||||
|
arrayOf(
|
||||||
|
"cd $installDir",
|
||||||
|
"chmod -R 755 .",
|
||||||
|
"./magiskboot unpack boot.img",
|
||||||
|
"./magiskboot repack boot.img",
|
||||||
|
"cat new-boot.img > boot.img",
|
||||||
|
"./magiskboot cleanup",
|
||||||
|
"rm -f new-boot.img",
|
||||||
|
"cd /").sh()
|
||||||
|
boot.copyToTar()
|
||||||
|
}
|
||||||
|
recovery
|
||||||
|
}
|
||||||
|
initBoot.exists() -> {
|
||||||
|
if (boot.exists())
|
||||||
|
boot.copyToTar()
|
||||||
|
initBoot
|
||||||
|
}
|
||||||
|
boot.exists() -> boot
|
||||||
|
else -> throw NoBootException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun processZip(zipIn: ZipInputStream): ExtendedFile {
|
||||||
|
console.add("- Processing zip file")
|
||||||
|
val boot = installDir.getChildFile("boot.img")
|
||||||
|
val initBoot = installDir.getChildFile("init_boot.img")
|
||||||
|
lateinit var entry: ZipEntry
|
||||||
|
while (zipIn.nextEntry?.also { entry = it } != null) {
|
||||||
|
if (entry.isDirectory) continue
|
||||||
|
when (entry.name.substringAfterLast('/')) {
|
||||||
|
"payload.bin" -> {
|
||||||
|
try {
|
||||||
|
return processPayload(zipIn)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
// No boot image in payload.bin, continue to find boot images
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"init_boot.img" -> {
|
||||||
|
console.add("- Extracting init_boot.img")
|
||||||
|
zipIn.copyAndCloseOut(initBoot.newOutputStream())
|
||||||
|
return initBoot
|
||||||
|
}
|
||||||
|
"boot.img" -> {
|
||||||
|
console.add("- Extracting boot.img")
|
||||||
|
zipIn.copyAndCloseOut(boot.newOutputStream())
|
||||||
|
// Don't return here since there might be an init_boot.img
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (boot.exists()) {
|
||||||
|
return boot
|
||||||
} else {
|
} else {
|
||||||
if (!boot.exists()) {
|
throw NoBootException()
|
||||||
console.add("! No boot image found")
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun processPayload(input: InputStream): ExtendedFile {
|
||||||
|
var fifo: File? = null
|
||||||
|
try {
|
||||||
|
console.add("- Processing payload.bin")
|
||||||
|
fifo = File.createTempFile("payload-fifo-", null, installDir)
|
||||||
|
fifo.delete()
|
||||||
|
Os.mkfifo(fifo.path, 420 /* 0644 */)
|
||||||
|
|
||||||
|
// Enqueue the shell command first, or the subsequent FIFO open will block
|
||||||
|
val future = arrayOf(
|
||||||
|
"cd $installDir",
|
||||||
|
"./magiskboot extract $fifo",
|
||||||
|
"cd /"
|
||||||
|
).eq()
|
||||||
|
|
||||||
|
val fd = Os.open(fifo.path, O_WRONLY, 0)
|
||||||
|
try {
|
||||||
|
val bufSize = 1024 * 1024
|
||||||
|
val buf = ByteBuffer.allocate(bufSize)
|
||||||
|
buf.position(input.read(buf.array()).coerceAtLeast(0)).flip()
|
||||||
|
while (buf.hasRemaining()) {
|
||||||
|
try {
|
||||||
|
Os.write(fd, buf)
|
||||||
|
} catch (e: ErrnoException) {
|
||||||
|
if (e.errno != OsConstants.EPIPE)
|
||||||
|
throw e
|
||||||
|
// If SIGPIPE, then the other side is closed, we're done
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (!buf.hasRemaining()) {
|
||||||
|
buf.limit(bufSize)
|
||||||
|
buf.position(input.read(buf.array()).coerceAtLeast(0)).flip()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Os.close(fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
val success = try { future.get().isSuccess } catch (e: Exception) { false }
|
||||||
|
if (!success) {
|
||||||
|
console.add("! Error while extracting payload.bin")
|
||||||
throw IOException()
|
throw IOException()
|
||||||
}
|
}
|
||||||
srcBoot = boot
|
val boot = installDir.getChildFile("boot.img")
|
||||||
|
val initBoot = installDir.getChildFile("init_boot.img")
|
||||||
|
return when {
|
||||||
|
initBoot.exists() -> {
|
||||||
|
console.add("-- Extract init_boot.img")
|
||||||
|
initBoot
|
||||||
|
}
|
||||||
|
boot.exists() -> {
|
||||||
|
console.add("-- Extract boot.img")
|
||||||
|
boot
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw NoBootException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: ErrnoException) {
|
||||||
|
throw IOException(e)
|
||||||
|
} finally {
|
||||||
|
fifo?.delete()
|
||||||
}
|
}
|
||||||
return tarOut
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleFile(uri: Uri): Boolean {
|
private fun handleFile(uri: Uri): Boolean {
|
||||||
val outStream: OutputStream
|
val outStream: OutputStream
|
||||||
var outFile: MediaStoreUtils.UriFile? = null
|
val outFile: MediaStoreUtils.UriFile
|
||||||
|
|
||||||
// Process input file
|
// Process input file
|
||||||
try {
|
try {
|
||||||
uri.inputStream().buffered().use { src ->
|
uri.inputStream().buffered().use { src ->
|
||||||
src.mark(500)
|
src.mark(500)
|
||||||
val magic = ByteArray(5)
|
val magic = ByteArray(4)
|
||||||
if (src.skip(257) != 257L || src.read(magic) != magic.size) {
|
val tarMagic = ByteArray(5)
|
||||||
|
if (src.read(magic) != magic.size || src.skip(253) != 253L ||
|
||||||
|
src.read(tarMagic) != tarMagic.size
|
||||||
|
) {
|
||||||
console.add("! Invalid input file")
|
console.add("! Invalid input file")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -270,29 +401,52 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
toString()
|
toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
outStream = if (magic.contentEquals("ustar".toByteArray())) {
|
srcBoot = if (tarMagic.contentEquals("ustar".toByteArray())) {
|
||||||
// tar file
|
// tar file
|
||||||
outFile = MediaStoreUtils.getFile("$filename.tar", true)
|
outFile = MediaStoreUtils.getFile("$filename.tar", true)
|
||||||
processTar(src, outFile!!.uri.outputStream())
|
outStream = TarOutputStream(outFile.uri.outputStream())
|
||||||
|
|
||||||
|
try {
|
||||||
|
processTar(TarInputStream(src), outStream)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
outStream.close()
|
||||||
|
outFile.delete()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// raw image
|
// raw image
|
||||||
srcBoot = installDir.getChildFile("boot.img")
|
|
||||||
console.add("- Copying image to cache")
|
|
||||||
src.cleanPump(srcBoot.newOutputStream())
|
|
||||||
outFile = MediaStoreUtils.getFile("$filename.img", true)
|
outFile = MediaStoreUtils.getFile("$filename.img", true)
|
||||||
outFile!!.uri.outputStream()
|
outStream = outFile.uri.outputStream()
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (magic.contentEquals("CrAU".toByteArray())) {
|
||||||
|
processPayload(src)
|
||||||
|
} else if (magic.contentEquals("PK\u0003\u0004".toByteArray())) {
|
||||||
|
processZip(ZipInputStream(src))
|
||||||
|
} else {
|
||||||
|
console.add("- Copying image to cache")
|
||||||
|
installDir.getChildFile("boot.img").also {
|
||||||
|
src.copyAndCloseOut(it.newOutputStream())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
outStream.close()
|
||||||
|
outFile.delete()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
|
if (e is NoBootException)
|
||||||
|
console.add("! No boot image found")
|
||||||
console.add("! Process error")
|
console.add("! Process error")
|
||||||
outFile?.delete()
|
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch file
|
// Patch file
|
||||||
if (!patchBoot()) {
|
if (!patchBoot()) {
|
||||||
outFile!!.delete()
|
outFile.delete()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,10 +454,16 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
try {
|
try {
|
||||||
val newBoot = installDir.getChildFile("new-boot.img")
|
val newBoot = installDir.getChildFile("new-boot.img")
|
||||||
if (outStream is TarOutputStream) {
|
if (outStream is TarOutputStream) {
|
||||||
val name = if (srcBoot.path.contains("recovery")) "recovery.img" else "boot.img"
|
val name = with(srcBoot.path) {
|
||||||
|
when {
|
||||||
|
contains("recovery") -> "recovery.img"
|
||||||
|
contains("init_boot") -> "init_boot.img"
|
||||||
|
else -> "boot.img"
|
||||||
|
}
|
||||||
|
}
|
||||||
outStream.putNextEntry(newTarEntry(name, newBoot.length()))
|
outStream.putNextEntry(newTarEntry(name, newBoot.length()))
|
||||||
}
|
}
|
||||||
newBoot.newInputStream().cleanPump(outStream)
|
newBoot.newInputStream().copyAndClose(outStream)
|
||||||
newBoot.delete()
|
newBoot.delete()
|
||||||
|
|
||||||
console.add("")
|
console.add("")
|
||||||
@@ -313,7 +473,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
console.add("****************************")
|
console.add("****************************")
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
console.add("! Failed to output to $outFile")
|
console.add("! Failed to output to $outFile")
|
||||||
outFile!!.delete()
|
outFile.delete()
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -326,22 +486,6 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun patchBoot(): Boolean {
|
private fun patchBoot(): Boolean {
|
||||||
var isSigned = false
|
|
||||||
if (!srcBoot.isCharacter) {
|
|
||||||
try {
|
|
||||||
srcBoot.newInputStream().use {
|
|
||||||
if (SignBoot.verifySignature(it, null)) {
|
|
||||||
isSigned = true
|
|
||||||
console.add("- Boot image is signed with AVB 1.0")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
console.add("! Unable to check signature")
|
|
||||||
Timber.e(e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val newBoot = installDir.getChildFile("new-boot.img")
|
val newBoot = installDir.getChildFile("new-boot.img")
|
||||||
if (!useRootDir) {
|
if (!useRootDir) {
|
||||||
// Create output files before hand
|
// Create output files before hand
|
||||||
@@ -353,33 +497,15 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
"cd $installDir",
|
"cd $installDir",
|
||||||
"KEEPFORCEENCRYPT=${Config.keepEnc} " +
|
"KEEPFORCEENCRYPT=${Config.keepEnc} " +
|
||||||
"KEEPVERITY=${Config.keepVerity} " +
|
"KEEPVERITY=${Config.keepVerity} " +
|
||||||
"PATCHVBMETAFLAG=${Config.patchVbmeta} " +
|
"PATCHVBMETAFLAG=${Info.patchBootVbmeta} " +
|
||||||
"RECOVERYMODE=${Config.recovery} " +
|
"RECOVERYMODE=${Config.recovery} " +
|
||||||
|
"LEGACYSAR=${Info.legacySAR} " +
|
||||||
"sh boot_patch.sh $srcBoot")
|
"sh boot_patch.sh $srcBoot")
|
||||||
|
val isSuccess = cmds.sh().isSuccess
|
||||||
|
|
||||||
if (!cmds.sh().isSuccess)
|
shell.newJob().add("./magiskboot cleanup", "cd /").exec()
|
||||||
return false
|
|
||||||
|
|
||||||
val job = shell.newJob().add("./magiskboot cleanup", "cd /")
|
return isSuccess
|
||||||
|
|
||||||
if (isSigned) {
|
|
||||||
console.add("- Signing boot image with verity keys")
|
|
||||||
val signed = File.createTempFile("signed", ".img", context.cacheDir)
|
|
||||||
try {
|
|
||||||
val src = newBoot.newInputStream().buffered()
|
|
||||||
val out = signed.outputStream().buffered()
|
|
||||||
withStreams(src, out) { _, _ ->
|
|
||||||
SignBoot.doSignature(null, null, src, out, "/boot")
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
console.add("! Unable to sign image")
|
|
||||||
Timber.e(e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
job.add("cat $signed > $newBoot", "rm -f $signed")
|
|
||||||
}
|
|
||||||
job.exec()
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun flashBoot() = "direct_install $installDir $srcBoot".sh().isSuccess
|
private fun flashBoot() = "direct_install $installDir $srcBoot".sh().isSuccess
|
||||||
@@ -401,6 +527,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Array<String>.eq() = shell.newJob().add(*this).to(console, logs).enqueue()
|
||||||
private fun String.sh() = shell.newJob().add(this).to(console, logs).exec()
|
private fun String.sh() = shell.newJob().add(this).to(console, logs).exec()
|
||||||
private fun Array<String>.sh() = shell.newJob().add(*this).to(console, logs).exec()
|
private fun Array<String>.sh() = shell.newJob().add(*this).to(console, logs).exec()
|
||||||
private fun String.fsh() = ShellUtils.fastCmd(shell, this)
|
private fun String.fsh() = ShellUtils.fastCmd(shell, this)
|
||||||
@@ -501,7 +628,7 @@ abstract class MagiskInstaller(
|
|||||||
override suspend fun exec(): Boolean {
|
override suspend fun exec(): Boolean {
|
||||||
val success = super.exec()
|
val success = super.exec()
|
||||||
callback()
|
callback()
|
||||||
Utils.toast(
|
context.toast(
|
||||||
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
|
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
|
||||||
Toast.LENGTH_LONG
|
Toast.LENGTH_LONG
|
||||||
)
|
)
|
||||||
|
@@ -29,7 +29,7 @@ class AXML(b: ByteArray) {
|
|||||||
* Followed by an array of uint32_t with size = number of strings
|
* Followed by an array of uint32_t with size = number of strings
|
||||||
* Each entry points to an offset into the string data
|
* Each entry points to an offset into the string data
|
||||||
*/
|
*/
|
||||||
fun findAndPatch(vararg patterns: Pair<String, String>): Boolean {
|
fun patchStrings(patchFn: (Array<String>) -> Unit): Boolean {
|
||||||
val buffer = ByteBuffer.wrap(bytes).order(LITTLE_ENDIAN)
|
val buffer = ByteBuffer.wrap(bytes).order(LITTLE_ENDIAN)
|
||||||
|
|
||||||
fun findStringPool(): Int {
|
fun findStringPool(): Int {
|
||||||
@@ -42,7 +42,6 @@ class AXML(b: ByteArray) {
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
var patch = false
|
|
||||||
val start = findStringPool()
|
val start = findStringPool()
|
||||||
if (start < 0)
|
if (start < 0)
|
||||||
return false
|
return false
|
||||||
@@ -57,34 +56,26 @@ class AXML(b: ByteArray) {
|
|||||||
val dataOff = start + intBuf.get()
|
val dataOff = start + intBuf.get()
|
||||||
intBuf.get()
|
intBuf.get()
|
||||||
|
|
||||||
val strings = ArrayList<String>(count)
|
val strList = ArrayList<String>(count)
|
||||||
// Read and patch all strings
|
// Collect all strings in the pool
|
||||||
loop@ for (i in 0 until count) {
|
for (i in 0 until count) {
|
||||||
val off = dataOff + intBuf.get()
|
val off = dataOff + intBuf.get()
|
||||||
val len = buffer.getShort(off)
|
val len = buffer.getShort(off)
|
||||||
val str = String(bytes, off + 2, len * 2, UTF_16LE)
|
strList.add(String(bytes, off + 2, len * 2, UTF_16LE))
|
||||||
for ((from, to) in patterns) {
|
|
||||||
if (str.contains(from)) {
|
|
||||||
strings.add(str.replace(from, to))
|
|
||||||
patch = true
|
|
||||||
continue@loop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
strings.add(str)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!patch)
|
val strArr = strList.toTypedArray()
|
||||||
return false
|
patchFn(strArr)
|
||||||
|
|
||||||
// Write everything before string data, will patch values later
|
// Write everything before string data, will patch values later
|
||||||
val baos = RawByteStream()
|
val baos = RawByteStream()
|
||||||
baos.write(bytes, 0, dataOff)
|
baos.write(bytes, 0, dataOff)
|
||||||
|
|
||||||
// Write string data
|
// Write string data
|
||||||
val strList = IntArray(count)
|
val offList = IntArray(count)
|
||||||
for (i in 0 until count) {
|
for (i in 0 until count) {
|
||||||
strList[i] = baos.size() - dataOff
|
offList[i] = baos.size() - dataOff
|
||||||
val str = strings[i]
|
val str = strArr[i]
|
||||||
baos.write(str.length.toShortBytes())
|
baos.write(str.length.toShortBytes())
|
||||||
baos.write(str.toByteArray(UTF_16LE))
|
baos.write(str.toByteArray(UTF_16LE))
|
||||||
// Null terminate
|
// Null terminate
|
||||||
@@ -103,7 +94,7 @@ class AXML(b: ByteArray) {
|
|||||||
// Patch index table
|
// Patch index table
|
||||||
newBuffer.position(start + STRING_INDICES_OFF)
|
newBuffer.position(start + STRING_INDICES_OFF)
|
||||||
val newIntBuf = newBuffer.asIntBuffer()
|
val newIntBuf = newBuffer.asIntBuffer()
|
||||||
strList.forEach { newIntBuf.put(it) }
|
offList.forEach { newIntBuf.put(it) }
|
||||||
|
|
||||||
// Write the rest of the chunks
|
// Write the rest of the chunks
|
||||||
val nextOff = start + size
|
val nextOff = start + size
|
||||||
|
@@ -1,59 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.utils
|
|
||||||
|
|
||||||
import androidx.biometric.BiometricManager
|
|
||||||
import androidx.biometric.BiometricPrompt
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import com.topjohnwu.magisk.R
|
|
||||||
import com.topjohnwu.magisk.core.Config
|
|
||||||
import com.topjohnwu.magisk.core.di.AppContext
|
|
||||||
|
|
||||||
object BiometricHelper {
|
|
||||||
|
|
||||||
private val mgr by lazy { BiometricManager.from(AppContext) }
|
|
||||||
|
|
||||||
val isSupported get() = when (mgr.canAuthenticate()) {
|
|
||||||
BiometricManager.BIOMETRIC_SUCCESS -> true
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
val isEnabled: Boolean get() {
|
|
||||||
val enabled = Config.suBiometric
|
|
||||||
if (enabled && !isSupported) {
|
|
||||||
Config.suBiometric = false
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
fun authenticate(
|
|
||||||
activity: FragmentActivity,
|
|
||||||
onError: () -> Unit = {},
|
|
||||||
onSuccess: () -> Unit): BiometricPrompt {
|
|
||||||
val prompt = BiometricPrompt(activity,
|
|
||||||
ContextCompat.getMainExecutor(activity),
|
|
||||||
object : BiometricPrompt.AuthenticationCallback() {
|
|
||||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
|
||||||
onError()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAuthenticationFailed() {
|
|
||||||
onError()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
|
||||||
onSuccess()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
val info = BiometricPrompt.PromptInfo.Builder()
|
|
||||||
.setConfirmationRequired(true)
|
|
||||||
.setDeviceCredentialAllowed(false)
|
|
||||||
.setTitle(activity.getString(R.string.authenticate))
|
|
||||||
.setNegativeButtonText(activity.getString(android.R.string.cancel))
|
|
||||||
.build()
|
|
||||||
prompt.authenticate(info)
|
|
||||||
return prompt
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,6 +1,10 @@
|
|||||||
package com.topjohnwu.magisk.core.utils
|
package com.topjohnwu.magisk.core.utils
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Runnable
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.util.concurrent.AbstractExecutorService
|
import java.util.concurrent.AbstractExecutorService
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
@@ -14,7 +14,9 @@ import java.security.KeyPairGenerator
|
|||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
import java.security.PrivateKey
|
import java.security.PrivateKey
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import java.util.*
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.Random
|
||||||
import java.util.zip.GZIPInputStream
|
import java.util.zip.GZIPInputStream
|
||||||
import java.util.zip.GZIPOutputStream
|
import java.util.zip.GZIPOutputStream
|
||||||
|
|
||||||
|
@@ -87,7 +87,7 @@ object MediaStoreUtils {
|
|||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun getFile(displayName: String, skipQuery: Boolean = false): UriFile {
|
fun getFile(displayName: String, skipQuery: Boolean = false): UriFile {
|
||||||
if (Build.VERSION.SDK_INT < 30) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||||
// Fallback to file based I/O pre Android 11
|
// Fallback to file based I/O pre Android 11
|
||||||
val parent = File(Environment.getExternalStorageDirectory(), relativePath)
|
val parent = File(Environment.getExternalStorageDirectory(), relativePath)
|
||||||
parent.mkdirs()
|
parent.mkdirs()
|
||||||
@@ -102,6 +102,8 @@ object MediaStoreUtils {
|
|||||||
|
|
||||||
fun Uri.outputStream() = cr.openOutputStream(this, "rwt") ?: throw FileNotFoundException()
|
fun Uri.outputStream() = cr.openOutputStream(this, "rwt") ?: throw FileNotFoundException()
|
||||||
|
|
||||||
|
fun Uri.fileDescriptor(mode: String) = cr.openFileDescriptor(this, mode) ?: throw FileNotFoundException()
|
||||||
|
|
||||||
val Uri.displayName: String get() {
|
val Uri.displayName: String get() {
|
||||||
if (scheme == "file") {
|
if (scheme == "file") {
|
||||||
// Simple uri wrapper over file, directly get file name
|
// Simple uri wrapper over file, directly get file name
|
||||||
|
@@ -0,0 +1,80 @@
|
|||||||
|
package com.topjohnwu.magisk.core.utils
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.Network
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.net.NetworkRequest
|
||||||
|
import android.os.PowerManager
|
||||||
|
import androidx.collection.ArraySet
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
import com.topjohnwu.magisk.core.ktx.registerRuntimeReceiver
|
||||||
|
|
||||||
|
class NetworkObserver(context: Context): DefaultLifecycleObserver {
|
||||||
|
private val manager = context.getSystemService<ConnectivityManager>()!!
|
||||||
|
|
||||||
|
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||||
|
private val activeList = ArraySet<Network>()
|
||||||
|
|
||||||
|
override fun onAvailable(network: Network) {
|
||||||
|
activeList.add(network)
|
||||||
|
postValue(true)
|
||||||
|
}
|
||||||
|
override fun onLost(network: Network) {
|
||||||
|
activeList.remove(network)
|
||||||
|
postValue(!activeList.isEmpty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val receiver = object : BroadcastReceiver() {
|
||||||
|
private fun Context.isIdleMode(): Boolean {
|
||||||
|
val pwm = getSystemService<PowerManager>() ?: return true
|
||||||
|
val isIgnoringOptimizations = pwm.isIgnoringBatteryOptimizations(packageName)
|
||||||
|
return pwm.isDeviceIdleMode && !isIgnoringOptimizations
|
||||||
|
}
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if (context.isIdleMode()) {
|
||||||
|
postValue(false)
|
||||||
|
} else {
|
||||||
|
postCurrentState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
val request = NetworkRequest.Builder()
|
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||||
|
.build()
|
||||||
|
manager.registerNetworkCallback(request, networkCallback)
|
||||||
|
val filter = IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)
|
||||||
|
context.applicationContext.registerRuntimeReceiver(receiver, filter)
|
||||||
|
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart(owner: LifecycleOwner) {
|
||||||
|
postCurrentState()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun postCurrentState() {
|
||||||
|
postValue(manager.getNetworkCapabilities(manager.activeNetwork)
|
||||||
|
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) ?: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun postValue(b: Boolean) {
|
||||||
|
Info.remote = Info.EMPTY_REMOTE
|
||||||
|
Info.isConnected.postValue(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun init(context: Context): NetworkObserver {
|
||||||
|
return NetworkObserver(context).apply { postCurrentState() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,15 @@
|
|||||||
|
package com.topjohnwu.magisk.core.utils;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.lifecycle.LifecycleDispatcher;
|
||||||
|
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||||
|
|
||||||
|
// Use Java to bypass Kotlin internal visibility modifier
|
||||||
|
public class ProcessLifecycle {
|
||||||
|
public static void init(@NonNull Context context) {
|
||||||
|
LifecycleDispatcher.init(context);
|
||||||
|
ProcessLifecycleOwner.init$lifecycle_process_release(context);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,17 @@
|
|||||||
|
package com.topjohnwu.magisk.core.utils
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.KeyguardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
|
|
||||||
|
class RequestAuthentication: ActivityResultContract<Unit, Boolean>() {
|
||||||
|
|
||||||
|
override fun createIntent(context: Context, input: Unit) =
|
||||||
|
context.getSystemService(KeyguardManager::class.java)
|
||||||
|
.createConfirmDeviceCredentialIntent(null, null)
|
||||||
|
|
||||||
|
override fun parseResult(resultCode: Int, intent: Intent?) =
|
||||||
|
resultCode == Activity.RESULT_OK
|
||||||
|
}
|
@@ -25,7 +25,7 @@ class RequestInstall : ActivityResultContract<Unit, Boolean>() {
|
|||||||
context: Context,
|
context: Context,
|
||||||
input: Unit
|
input: Unit
|
||||||
): SynchronousResult<Boolean>? {
|
): SynchronousResult<Boolean>? {
|
||||||
if (Build.VERSION.SDK_INT < 26)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
|
||||||
return SynchronousResult(true)
|
return SynchronousResult(true)
|
||||||
if (context.packageManager.canRequestPackageInstalls())
|
if (context.packageManager.canRequestPackageInstalls())
|
||||||
return SynchronousResult(true)
|
return SynchronousResult(true)
|
||||||
|
@@ -111,15 +111,23 @@ class RootUtils(stub: Any?) : RootService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun await() {
|
fun await() {
|
||||||
// We cannot await on the main thread
|
if (!Info.isRooted)
|
||||||
if (Info.isRooted && !ShellUtils.onMainThread())
|
return
|
||||||
|
if (!ShellUtils.onMainThread()) {
|
||||||
acquireSharedInterruptibly(1)
|
acquireSharedInterruptibly(1)
|
||||||
|
} else if (state != 0) {
|
||||||
|
throw IllegalStateException("Cannot await on the main thread")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
var bindTask: Shell.Task? = null
|
var bindTask: Shell.Task? = null
|
||||||
var fs = FileSystemManager.getLocal()
|
var fs: FileSystemManager = FileSystemManager.getLocal()
|
||||||
|
get() {
|
||||||
|
Connection.await()
|
||||||
|
return field
|
||||||
|
}
|
||||||
private set
|
private set
|
||||||
var obj: IRootUtils? = null
|
var obj: IRootUtils? = null
|
||||||
get() {
|
get() {
|
||||||
|
@@ -7,10 +7,10 @@ import com.topjohnwu.magisk.core.Config
|
|||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
import com.topjohnwu.magisk.ktx.cachedFile
|
import com.topjohnwu.magisk.core.ktx.cachedFile
|
||||||
import com.topjohnwu.magisk.ktx.deviceProtectedContext
|
import com.topjohnwu.magisk.core.ktx.deviceProtectedContext
|
||||||
import com.topjohnwu.magisk.ktx.rawResource
|
import com.topjohnwu.magisk.core.ktx.rawResource
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.ShellUtils
|
import com.topjohnwu.superuser.ShellUtils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -41,7 +41,7 @@ class ShellInit : Shell.Initializer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (shell.isRoot) {
|
if (shell.isRoot) {
|
||||||
add("export MAGISKTMP=\$(magisk --path)/.magisk")
|
add("export MAGISKTMP=\$(magisk --path)")
|
||||||
// Test if we can properly execute stuff in /data
|
// Test if we can properly execute stuff in /data
|
||||||
Info.noDataExec = !shell.newJob().add("$localBB sh -c \"$localBB true\"").exec().isSuccess
|
Info.noDataExec = !shell.newJob().add("$localBB sh -c \"$localBB true\"").exec().isSuccess
|
||||||
}
|
}
|
||||||
@@ -49,12 +49,12 @@ class ShellInit : Shell.Initializer() {
|
|||||||
if (Info.noDataExec) {
|
if (Info.noDataExec) {
|
||||||
// Copy it out of /data to workaround Samsung bullshit
|
// Copy it out of /data to workaround Samsung bullshit
|
||||||
add(
|
add(
|
||||||
"if [ -x \$MAGISKTMP/busybox/busybox ]; then",
|
"if [ -x \$MAGISKTMP/.magisk/busybox/busybox ]; then",
|
||||||
" cp -af $localBB \$MAGISKTMP/busybox/busybox",
|
" cp -af $localBB \$MAGISKTMP/.magisk/busybox/busybox",
|
||||||
" exec \$MAGISKTMP/busybox/busybox sh",
|
" exec \$MAGISKTMP/.magisk/busybox/busybox sh",
|
||||||
"else",
|
"else",
|
||||||
" cp -af $localBB /dev/.busybox",
|
" cp -af $localBB /dev/busybox",
|
||||||
" exec /dev/.busybox sh",
|
" exec /dev/busybox sh",
|
||||||
"fi"
|
"fi"
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -73,18 +73,17 @@ class ShellInit : Shell.Initializer() {
|
|||||||
fun getVar(name: String) = fastCmd("echo \$$name")
|
fun getVar(name: String) = fastCmd("echo \$$name")
|
||||||
fun getBool(name: String) = getVar(name).toBoolean()
|
fun getBool(name: String) = getVar(name).toBoolean()
|
||||||
|
|
||||||
Const.MAGISKTMP = getVar("MAGISKTMP")
|
Info.isSAR = getBool("SYSTEM_AS_ROOT")
|
||||||
Info.isSAR = getBool("SYSTEM_ROOT")
|
|
||||||
Info.ramdisk = getBool("RAMDISKEXIST")
|
Info.ramdisk = getBool("RAMDISKEXIST")
|
||||||
Info.vbmeta = getBool("VBMETAEXIST")
|
|
||||||
Info.isAB = getBool("ISAB")
|
Info.isAB = getBool("ISAB")
|
||||||
Info.crypto = getVar("CRYPTOTYPE")
|
Info.crypto = getVar("CRYPTOTYPE")
|
||||||
|
Info.patchBootVbmeta = getBool("PATCHVBMETAFLAG")
|
||||||
|
Info.legacySAR = getBool("LEGACYSAR")
|
||||||
|
|
||||||
// Default presets
|
// Default presets
|
||||||
Config.recovery = getBool("RECOVERYMODE")
|
Config.recovery = getBool("RECOVERYMODE")
|
||||||
Config.keepVerity = getBool("KEEPVERITY")
|
Config.keepVerity = getBool("KEEPVERITY")
|
||||||
Config.keepEnc = getBool("KEEPFORCEENCRYPT")
|
Config.keepEnc = getBool("KEEPFORCEENCRYPT")
|
||||||
Config.patchVbmeta = getBool("PATCHVBMETAFLAG")
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@@ -1,49 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.utils.net
|
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.net.Network
|
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import android.net.NetworkRequest
|
|
||||||
import androidx.collection.ArraySet
|
|
||||||
|
|
||||||
@TargetApi(21)
|
|
||||||
open class LollipopNetworkObserver(
|
|
||||||
context: Context,
|
|
||||||
callback: ConnectionCallback
|
|
||||||
): NetworkObserver(context, callback) {
|
|
||||||
|
|
||||||
private val networkCallback = NetCallback()
|
|
||||||
|
|
||||||
init {
|
|
||||||
val request = NetworkRequest.Builder()
|
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
|
||||||
.build()
|
|
||||||
manager.registerNetworkCallback(request, networkCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
override fun getCurrentState() {
|
|
||||||
callback(manager.activeNetworkInfo?.isConnected ?: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun stopObserving() {
|
|
||||||
manager.unregisterNetworkCallback(networkCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class NetCallback : ConnectivityManager.NetworkCallback() {
|
|
||||||
|
|
||||||
private val activeList = ArraySet<Network>()
|
|
||||||
|
|
||||||
override fun onAvailable(network: Network) {
|
|
||||||
activeList.add(network)
|
|
||||||
callback(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLost(network: Network) {
|
|
||||||
activeList.remove(network)
|
|
||||||
callback(!activeList.isEmpty())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,53 +0,0 @@
|
|||||||
@file:Suppress("DEPRECATION")
|
|
||||||
|
|
||||||
package com.topjohnwu.magisk.core.utils.net
|
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import android.os.PowerManager
|
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
|
|
||||||
@TargetApi(23)
|
|
||||||
class MarshmallowNetworkObserver(
|
|
||||||
context: Context,
|
|
||||||
callback: ConnectionCallback
|
|
||||||
): LollipopNetworkObserver(context, callback) {
|
|
||||||
|
|
||||||
private val receiver = IdleBroadcastReceiver()
|
|
||||||
|
|
||||||
init {
|
|
||||||
val filter = IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)
|
|
||||||
app.registerReceiver(receiver, filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun stopObserving() {
|
|
||||||
super.stopObserving()
|
|
||||||
app.unregisterReceiver(receiver)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCurrentState() {
|
|
||||||
callback(manager.getNetworkCapabilities(manager.activeNetwork)
|
|
||||||
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class IdleBroadcastReceiver: BroadcastReceiver() {
|
|
||||||
|
|
||||||
private fun Context.isIdleMode(): Boolean {
|
|
||||||
val pwm = getSystemService<PowerManager>() ?: return true
|
|
||||||
val isIgnoringOptimizations = pwm.isIgnoringBatteryOptimizations(packageName)
|
|
||||||
return pwm.isDeviceIdleMode && !isIgnoringOptimizations
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
if (context.isIdleMode()) {
|
|
||||||
callback(false)
|
|
||||||
} else {
|
|
||||||
getCurrentState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,29 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.utils.net
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
|
|
||||||
typealias ConnectionCallback = (Boolean) -> Unit
|
|
||||||
|
|
||||||
abstract class NetworkObserver(
|
|
||||||
context: Context,
|
|
||||||
protected val callback: ConnectionCallback
|
|
||||||
) {
|
|
||||||
|
|
||||||
protected val app: Context = context.applicationContext
|
|
||||||
protected val manager = context.getSystemService<ConnectivityManager>()!!
|
|
||||||
|
|
||||||
protected abstract fun stopObserving()
|
|
||||||
protected abstract fun getCurrentState()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun observe(context: Context, callback: ConnectionCallback): NetworkObserver {
|
|
||||||
val observer: NetworkObserver = if (Build.VERSION.SDK_INT >= 23)
|
|
||||||
MarshmallowNetworkObserver(context, callback)
|
|
||||||
else LollipopNetworkObserver(context, callback)
|
|
||||||
return observer.apply { getCurrentState() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,37 +1,53 @@
|
|||||||
package com.topjohnwu.magisk.databinding
|
package com.topjohnwu.magisk.databinding
|
||||||
|
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.databinding.ListChangeRegistry
|
import androidx.databinding.ListChangeRegistry
|
||||||
import androidx.databinding.ObservableList
|
import androidx.databinding.ObservableList
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListUpdateCallback
|
import androidx.recyclerview.widget.ListUpdateCallback
|
||||||
import java.util.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.util.AbstractList
|
||||||
|
|
||||||
/**
|
// Only expose the immutable List types
|
||||||
* @param callback The callback that controls the behavior of the DiffObservableList.
|
interface DiffList<T : DiffItem<*>> : List<T> {
|
||||||
* @param detectMoves True if DiffUtil should try to detect moved items, false otherwise.
|
fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult
|
||||||
*/
|
|
||||||
open class DiffObservableList<T>(
|
|
||||||
private val callback: Callback<T>,
|
|
||||||
private val detectMoves: Boolean = true
|
|
||||||
) : AbstractList<T>(), ObservableList<T> {
|
|
||||||
|
|
||||||
protected var list: MutableList<T> = ArrayList()
|
@MainThread
|
||||||
|
fun update(newItems: List<T>, diffResult: DiffUtil.DiffResult)
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
suspend fun update(newItems: List<T>)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilterList<T : DiffItem<*>> : List<T> {
|
||||||
|
fun filter(filter: (T) -> Boolean)
|
||||||
|
|
||||||
|
@MainThread
|
||||||
|
fun set(newItems: List<T>)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T : DiffItem<*>> diffList(): DiffList<T> = DiffObservableList()
|
||||||
|
|
||||||
|
fun <T : DiffItem<*>> filterList(scope: CoroutineScope): FilterList<T> =
|
||||||
|
FilterableDiffObservableList(scope)
|
||||||
|
|
||||||
|
private open class DiffObservableList<T : DiffItem<*>>
|
||||||
|
: AbstractList<T>(), ObservableList<T>, DiffList<T>, ListUpdateCallback {
|
||||||
|
|
||||||
|
protected var list: List<T> = emptyList()
|
||||||
private val listeners = ListChangeRegistry()
|
private val listeners = ListChangeRegistry()
|
||||||
protected val listCallback = ObservableListUpdateCallback()
|
|
||||||
|
|
||||||
override val size: Int get() = list.size
|
override val size: Int get() = list.size
|
||||||
|
|
||||||
/**
|
override fun get(index: Int) = list[index]
|
||||||
* Calculates the list of update operations that can convert this list into the given one.
|
|
||||||
*
|
override fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult {
|
||||||
* @param newItems The items that this list will be set to.
|
return doCalculateDiff(list, newItems)
|
||||||
* @return A DiffResult that contains the information about the edit sequence to covert this
|
|
||||||
* list into the given one.
|
|
||||||
*/
|
|
||||||
fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult {
|
|
||||||
val frozenList = ArrayList(list)
|
|
||||||
return doCalculateDiff(frozenList, newItems)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun doCalculateDiff(oldItems: List<T>, newItems: List<T>): DiffUtil.DiffResult {
|
protected fun doCalculateDiff(oldItems: List<T>, newItems: List<T>): DiffUtil.DiffResult {
|
||||||
@@ -40,47 +56,34 @@ open class DiffObservableList<T>(
|
|||||||
|
|
||||||
override fun getNewListSize() = newItems.size
|
override fun getNewListSize() = newItems.size
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
val oldItem = oldItems[oldItemPosition]
|
val oldItem = oldItems[oldItemPosition]
|
||||||
val newItem = newItems[newItemPosition]
|
val newItem = newItems[newItemPosition]
|
||||||
return callback.areItemsTheSame(oldItem, newItem)
|
return (oldItem as DiffItem<Any>).itemSameAs(newItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
val oldItem = oldItems[oldItemPosition]
|
val oldItem = oldItems[oldItemPosition]
|
||||||
val newItem = newItems[newItemPosition]
|
val newItem = newItems[newItemPosition]
|
||||||
return callback.areContentsTheSame(oldItem, newItem)
|
return (oldItem as DiffItem<Any>).contentSameAs(newItem)
|
||||||
}
|
}
|
||||||
}, detectMoves)
|
}, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the contents of this list to the given one using the DiffResults to dispatch change
|
|
||||||
* notifications.
|
|
||||||
*
|
|
||||||
* @param newItems The items to set this list to.
|
|
||||||
* @param diffResult The diff results to dispatch change notifications.
|
|
||||||
*/
|
|
||||||
@MainThread
|
@MainThread
|
||||||
fun update(newItems: List<T>, diffResult: DiffUtil.DiffResult) {
|
override fun update(newItems: List<T>, diffResult: DiffUtil.DiffResult) {
|
||||||
list = newItems.toMutableList()
|
list = ArrayList(newItems)
|
||||||
diffResult.dispatchUpdatesTo(listCallback)
|
diffResult.dispatchUpdatesTo(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@WorkerThread
|
||||||
* Sets this list to the given items. This is a convenience method for calling [ ][.calculateDiff] followed by [.update].
|
override suspend fun update(newItems: List<T>) {
|
||||||
*
|
val diffResult = calculateDiff(newItems)
|
||||||
*
|
withContext(Dispatchers.Main) {
|
||||||
* **Warning!** If the lists are large this operation may be too slow for the main thread. In
|
update(newItems, diffResult)
|
||||||
* that case, you should call [.calculateDiff] on a background thread and then
|
}
|
||||||
* [.update] on the main thread.
|
|
||||||
*
|
|
||||||
* @param newItems The items to set this list to.
|
|
||||||
*/
|
|
||||||
@MainThread
|
|
||||||
fun update(newItems: List<T>) {
|
|
||||||
val diffResult = doCalculateDiff(list, newItems)
|
|
||||||
update(newItems, diffResult)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addOnListChangedCallback(listener: ObservableList.OnListChangedCallback<out ObservableList<T>>) {
|
override fun addOnListChangedCallback(listener: ObservableList.OnListChangedCallback<out ObservableList<T>>) {
|
||||||
@@ -91,113 +94,63 @@ open class DiffObservableList<T>(
|
|||||||
listeners.remove(listener)
|
listeners.remove(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun get(index: Int) = list[index]
|
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||||
|
listeners.notifyChanged(this, position, count)
|
||||||
override fun add(index: Int, element: T) {
|
|
||||||
list.add(index, element)
|
|
||||||
notifyAdd(index, 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addAll(elements: Collection<T>) = addAll(size, elements)
|
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||||
|
listeners.notifyMoved(this, fromPosition, toPosition, 1)
|
||||||
override fun addAll(index: Int, elements: Collection<T>): Boolean {
|
|
||||||
val added = list.addAll(index, elements)
|
|
||||||
if (added) {
|
|
||||||
notifyAdd(index, elements.size)
|
|
||||||
}
|
|
||||||
return added
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clear() {
|
override fun onInserted(position: Int, count: Int) {
|
||||||
val oldSize = size
|
modCount += 1
|
||||||
list.clear()
|
listeners.notifyInserted(this, position, count)
|
||||||
if (oldSize != 0) {
|
|
||||||
notifyRemove(0, oldSize)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun remove(element: T): Boolean {
|
override fun onRemoved(position: Int, count: Int) {
|
||||||
val index = indexOf(element)
|
modCount += 1
|
||||||
return if (index >= 0) {
|
listeners.notifyRemoved(this, position, count)
|
||||||
removeAt(index)
|
}
|
||||||
true
|
}
|
||||||
} else {
|
|
||||||
false
|
private class FilterableDiffObservableList<T : DiffItem<*>>(
|
||||||
}
|
private val scope: CoroutineScope
|
||||||
}
|
) : DiffObservableList<T>(), FilterList<T> {
|
||||||
|
|
||||||
override fun removeAt(index: Int): T {
|
private var sublist: List<T> = emptyList()
|
||||||
val element = list.removeAt(index)
|
private var job: Job? = null
|
||||||
notifyRemove(index, 1)
|
private var lastFilter: ((T) -> Boolean)? = null
|
||||||
return element
|
|
||||||
}
|
// ---
|
||||||
|
|
||||||
override fun set(index: Int, element: T): T {
|
override fun filter(filter: (T) -> Boolean) {
|
||||||
val old = list.set(index, element)
|
lastFilter = filter
|
||||||
listeners.notifyChanged(this, index, 1)
|
job?.cancel()
|
||||||
return old
|
job = scope.launch(Dispatchers.Default) {
|
||||||
}
|
val oldList = sublist
|
||||||
|
val newList = list.filter(filter)
|
||||||
private fun notifyAdd(start: Int, count: Int) {
|
val diff = doCalculateDiff(oldList, newList)
|
||||||
listeners.notifyInserted(this, start, count)
|
withContext(Dispatchers.Main) {
|
||||||
}
|
sublist = newList
|
||||||
|
diff.dispatchUpdatesTo(this@FilterableDiffObservableList)
|
||||||
private fun notifyRemove(start: Int, count: Int) {
|
}
|
||||||
listeners.notifyRemoved(this, start, count)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ---
|
||||||
* A Callback class used by DiffUtil while calculating the diff between two lists.
|
|
||||||
*/
|
override fun get(index: Int): T {
|
||||||
interface Callback<T> {
|
return sublist[index]
|
||||||
/**
|
}
|
||||||
* Called by the DiffUtil to decide whether two object represent the same Item.
|
|
||||||
*
|
override val size: Int
|
||||||
*
|
get() = sublist.size
|
||||||
* For example, if your items have unique ids, this method should check their id equality.
|
|
||||||
*
|
@MainThread
|
||||||
* @param oldItem The old item.
|
override fun set(newItems: List<T>) {
|
||||||
* @param newItem The new item.
|
onRemoved(0, sublist.size)
|
||||||
* @return True if the two items represent the same object or false if they are different.
|
list = newItems
|
||||||
*/
|
sublist = emptyList()
|
||||||
fun areItemsTheSame(oldItem: T, newItem: T): Boolean
|
lastFilter?.let { filter(it) }
|
||||||
|
|
||||||
/**
|
|
||||||
* Called by the DiffUtil when it wants to check whether two items have the same data.
|
|
||||||
* DiffUtil uses this information to detect if the contents of an item has changed.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* DiffUtil uses this method to check equality instead of [Object.equals] so
|
|
||||||
* that you can change its behavior depending on your UI.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* This method is called only if [.areItemsTheSame] returns `true` for
|
|
||||||
* these items.
|
|
||||||
*
|
|
||||||
* @param oldItem The old item.
|
|
||||||
* @param newItem The new item which replaces the old item.
|
|
||||||
* @return True if the contents of the items are the same or false if they are different.
|
|
||||||
*/
|
|
||||||
fun areContentsTheSame(oldItem: T, newItem: T): Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class ObservableListUpdateCallback : ListUpdateCallback {
|
|
||||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
|
||||||
listeners.notifyChanged(this@DiffObservableList, position, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
|
||||||
listeners.notifyMoved(this@DiffObservableList, fromPosition, toPosition, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInserted(position: Int, count: Int) {
|
|
||||||
modCount += 1
|
|
||||||
listeners.notifyInserted(this@DiffObservableList, position, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRemoved(position: Int, count: Int) {
|
|
||||||
modCount += 1
|
|
||||||
listeners.notifyRemoved(this@DiffObservableList, position, count)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,85 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.databinding
|
|
||||||
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.HandlerThread
|
|
||||||
import android.os.Looper
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class FilterableDiffObservableList<T>(
|
|
||||||
callback: Callback<T>
|
|
||||||
) : DiffObservableList<T>(callback) {
|
|
||||||
|
|
||||||
var filter: ((T) -> Boolean)? = null
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
queueUpdate()
|
|
||||||
}
|
|
||||||
@Volatile
|
|
||||||
private var sublist: MutableList<T> = super.list
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
private val ui by lazy { Handler(Looper.getMainLooper()) }
|
|
||||||
private val handler = Handler(HandlerThread("List${hashCode()}").apply { start() }.looper)
|
|
||||||
private val updater = Runnable {
|
|
||||||
val filter = filter ?: { true }
|
|
||||||
val newList = super.list.filter(filter)
|
|
||||||
val diff = synchronized(this) { doCalculateDiff(sublist, newList) }
|
|
||||||
ui.post {
|
|
||||||
sublist = Collections.synchronizedList(newList)
|
|
||||||
diff.dispatchUpdatesTo(listCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun queueUpdate() {
|
|
||||||
handler.removeCallbacks(updater)
|
|
||||||
handler.post(updater)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasFilter() = filter != null
|
|
||||||
|
|
||||||
fun filter(switch: (T) -> Boolean) {
|
|
||||||
filter = switch
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reset() {
|
|
||||||
filter = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
override fun get(index: Int): T {
|
|
||||||
return sublist.get(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun add(element: T): Boolean {
|
|
||||||
return sublist.add(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun add(index: Int, element: T) {
|
|
||||||
sublist.add(index, element)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addAll(elements: Collection<T>): Boolean {
|
|
||||||
return sublist.addAll(elements)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addAll(index: Int, elements: Collection<T>): Boolean {
|
|
||||||
return sublist.addAll(index, elements)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun remove(element: T): Boolean {
|
|
||||||
return sublist.remove(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun removeAt(index: Int): T {
|
|
||||||
return sublist.removeAt(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun set(index: Int, element: T): T {
|
|
||||||
return sublist.set(index, element)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val size: Int
|
|
||||||
get() = sublist.size
|
|
||||||
}
|
|
@@ -1,7 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.databinding
|
|
||||||
|
|
||||||
fun <T : AnyDiffRvItem> diffListOf() =
|
|
||||||
DiffObservableList(DiffRvItem.callback<T>())
|
|
||||||
|
|
||||||
fun <T : AnyDiffRvItem> filterableListOf() =
|
|
||||||
FilterableDiffObservableList(DiffRvItem.callback<T>())
|
|
@@ -46,11 +46,11 @@ class MergeObservableList<T> : AbstractList<T>(), ObservableList<T> {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun insertList(list: ObservableList<out T>): MergeObservableList<T> {
|
fun insertList(list: List<T>): MergeObservableList<T> {
|
||||||
val idx = size
|
val idx = size
|
||||||
lists.add(list)
|
lists.add(list)
|
||||||
++modCount
|
++modCount
|
||||||
(list as ObservableList<T>).addOnListChangedCallback(callback)
|
(list as? ObservableList<T>)?.addOnListChangedCallback(callback)
|
||||||
if (list.isNotEmpty())
|
if (list.isNotEmpty())
|
||||||
listeners.notifyInserted(this, idx, list.size)
|
listeners.notifyInserted(this, idx, list.size)
|
||||||
return this
|
return this
|
||||||
@@ -72,11 +72,11 @@ class MergeObservableList<T> : AbstractList<T>(), ObservableList<T> {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeList(listToRemove: ObservableList<out T>): Boolean {
|
fun removeList(listToRemove: List<T>): Boolean {
|
||||||
var idx = 0
|
var idx = 0
|
||||||
for ((i, list) in lists.withIndex()) {
|
for ((i, list) in lists.withIndex()) {
|
||||||
if (listToRemove === list) {
|
if (listToRemove === list) {
|
||||||
(list as ObservableList<T>).removeOnListChangedCallback(callback)
|
(list as? ObservableList<T>)?.removeOnListChangedCallback(callback)
|
||||||
lists.removeAt(i)
|
lists.removeAt(i)
|
||||||
++modCount
|
++modCount
|
||||||
listeners.notifyRemoved(this, idx, list.size)
|
listeners.notifyRemoved(this, idx, list.size)
|
||||||
@@ -90,8 +90,8 @@ class MergeObservableList<T> : AbstractList<T>(), ObservableList<T> {
|
|||||||
override fun clear() {
|
override fun clear() {
|
||||||
val sz = size
|
val sz = size
|
||||||
for (list in lists) {
|
for (list in lists) {
|
||||||
if (list is ObservableList<*>) {
|
if (list is ObservableList) {
|
||||||
(list as ObservableList<T>).removeOnListChangedCallback(callback)
|
list.removeOnListChangedCallback(callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
++modCount
|
++modCount
|
||||||
|
@@ -8,60 +8,28 @@ abstract class RvItem {
|
|||||||
abstract val layoutRes: Int
|
abstract val layoutRes: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RvContainer<E> {
|
|
||||||
val item: E
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ViewAwareRvItem {
|
|
||||||
fun onBind(binding: ViewDataBinding, recyclerView: RecyclerView)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ComparableRv<T> : Comparable<T> {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
fun comparableEqual(o: Any?) =
|
|
||||||
o != null && o::class == this::class && compareTo(o as T) == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class DiffRvItem<T> : RvItem() {
|
|
||||||
|
|
||||||
// Defer to contentSameAs by default
|
|
||||||
open fun itemSameAs(other: T) = true
|
|
||||||
|
|
||||||
open fun contentSameAs(other: T) =
|
|
||||||
when (this) {
|
|
||||||
is RvContainer<*> -> item == (other as RvContainer<*>).item
|
|
||||||
is ComparableRv<*> -> comparableEqual(other)
|
|
||||||
else -> this == other
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val callback = object : DiffObservableList.Callback<DiffRvItem<Any>> {
|
|
||||||
override fun areItemsTheSame(
|
|
||||||
oldItem: DiffRvItem<Any>,
|
|
||||||
newItem: DiffRvItem<Any>
|
|
||||||
): Boolean {
|
|
||||||
return oldItem::class == newItem::class && oldItem.itemSameAs(newItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(
|
|
||||||
oldItem: DiffRvItem<Any>,
|
|
||||||
newItem: DiffRvItem<Any>
|
|
||||||
): Boolean {
|
|
||||||
return oldItem.contentSameAs(newItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
fun <T : AnyDiffRvItem> callback() = callback as DiffObservableList.Callback<T>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
typealias AnyDiffRvItem = DiffRvItem<*>
|
|
||||||
|
|
||||||
abstract class ObservableDiffRvItem<T> : DiffRvItem<T>(), ObservableHost {
|
|
||||||
override var callbacks: PropertyChangeRegistry? = null
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class ObservableRvItem : RvItem(), ObservableHost {
|
abstract class ObservableRvItem : RvItem(), ObservableHost {
|
||||||
override var callbacks: PropertyChangeRegistry? = null
|
override var callbacks: PropertyChangeRegistry? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ItemWrapper<E> {
|
||||||
|
val item: E
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ViewAwareItem {
|
||||||
|
fun onBind(binding: ViewDataBinding, recyclerView: RecyclerView)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DiffItem<T : Any> {
|
||||||
|
|
||||||
|
fun itemSameAs(other: T): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
return when (this) {
|
||||||
|
is ItemWrapper<*> -> item == (other as ItemWrapper<*>).item
|
||||||
|
is Comparable<*> -> compareValues(this, other as Comparable<*>) == 0
|
||||||
|
else -> this == other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun contentSameAs(other: T) = true
|
||||||
|
}
|
||||||
|
@@ -15,8 +15,8 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
|
|
||||||
class RvItemAdapter<T: RvItem>(
|
class RvItemAdapter<T: RvItem>(
|
||||||
private val items: List<T>,
|
val items: List<T>,
|
||||||
private val extraBindings: SparseArray<*>?
|
val extraBindings: SparseArray<*>?
|
||||||
) : RecyclerView.Adapter<RvItemAdapter.ViewHolder>() {
|
) : RecyclerView.Adapter<RvItemAdapter.ViewHolder>() {
|
||||||
|
|
||||||
private var lifecycleOwner: LifecycleOwner? = null
|
private var lifecycleOwner: LifecycleOwner? = null
|
||||||
@@ -53,7 +53,7 @@ class RvItemAdapter<T: RvItem>(
|
|||||||
holder.binding.lifecycleOwner = lifecycleOwner
|
holder.binding.lifecycleOwner = lifecycleOwner
|
||||||
holder.binding.executePendingBindings()
|
holder.binding.executePendingBindings()
|
||||||
recyclerView?.let {
|
recyclerView?.let {
|
||||||
if (item is ViewAwareRvItem)
|
if (item is ViewAwareItem)
|
||||||
item.onBind(holder.binding, it)
|
item.onBind(holder.binding, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,6 +113,9 @@ inline fun bindExtra(body: (SparseArray<Any?>) -> Unit) = SparseArray<Any?>().al
|
|||||||
@BindingAdapter("items", "extraBindings", requireAll = false)
|
@BindingAdapter("items", "extraBindings", requireAll = false)
|
||||||
fun <T: RvItem> RecyclerView.setAdapter(items: List<T>?, extraBindings: SparseArray<*>?) {
|
fun <T: RvItem> RecyclerView.setAdapter(items: List<T>?, extraBindings: SparseArray<*>?) {
|
||||||
if (items != null) {
|
if (items != null) {
|
||||||
adapter = RvItemAdapter(items, extraBindings)
|
val rva = (adapter as? RvItemAdapter<*>)
|
||||||
|
if (rva == null || rva.items !== items || rva.extraBindings !== extraBindings) {
|
||||||
|
adapter = RvItemAdapter(items, extraBindings)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,14 @@
|
|||||||
package com.topjohnwu.magisk.events.dialog
|
package com.topjohnwu.magisk.dialog
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.UIActivity
|
import com.topjohnwu.magisk.arch.UIActivity
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
|
import com.topjohnwu.magisk.events.DialogBuilder
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
|
|
||||||
class DarkThemeDialog : DialogEvent() {
|
class DarkThemeDialog : DialogBuilder {
|
||||||
|
|
||||||
override fun build(dialog: MagiskDialog) {
|
override fun build(dialog: MagiskDialog) {
|
||||||
val activity = dialog.ownerActivity!!
|
val activity = dialog.ownerActivity!!
|
@@ -1,4 +1,4 @@
|
|||||||
package com.topjohnwu.magisk.events.dialog
|
package com.topjohnwu.magisk.dialog
|
||||||
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
@@ -6,11 +6,12 @@ import com.topjohnwu.magisk.R
|
|||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||||
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
||||||
|
import com.topjohnwu.magisk.events.DialogBuilder
|
||||||
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class EnvFixDialog(private val vm: HomeViewModel) : DialogEvent() {
|
class EnvFixDialog(private val vm: HomeViewModel, private val code: Int) : DialogBuilder {
|
||||||
|
|
||||||
override fun build(dialog: MagiskDialog) {
|
override fun build(dialog: MagiskDialog) {
|
||||||
dialog.apply {
|
dialog.apply {
|
||||||
@@ -38,8 +39,10 @@ class EnvFixDialog(private val vm: HomeViewModel) : DialogEvent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Info.env.versionCode != BuildConfig.VERSION_CODE ||
|
if (code == 2 || // No rules block, module policy not loaded
|
||||||
|
Info.env.versionCode != BuildConfig.VERSION_CODE ||
|
||||||
Info.env.versionString != BuildConfig.VERSION_NAME) {
|
Info.env.versionString != BuildConfig.VERSION_NAME) {
|
||||||
|
dialog.setMessage(R.string.env_full_fix_msg)
|
||||||
dialog.setButton(MagiskDialog.ButtonType.POSITIVE) {
|
dialog.setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||||
text = android.R.string.ok
|
text = android.R.string.ok
|
||||||
onClick {
|
onClick {
|
@@ -0,0 +1,33 @@
|
|||||||
|
package com.topjohnwu.magisk.dialog
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.topjohnwu.magisk.MainDirections
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.core.Const
|
||||||
|
import com.topjohnwu.magisk.events.DialogBuilder
|
||||||
|
import com.topjohnwu.magisk.ui.module.ModuleViewModel
|
||||||
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
|
|
||||||
|
class LocalModuleInstallDialog(
|
||||||
|
private val viewModel: ModuleViewModel,
|
||||||
|
private val uri: Uri,
|
||||||
|
private val displayName: String
|
||||||
|
) : DialogBuilder {
|
||||||
|
override fun build(dialog: MagiskDialog) {
|
||||||
|
dialog.apply {
|
||||||
|
setTitle(R.string.confirm_install_title)
|
||||||
|
setMessage(context.getString(R.string.confirm_install, displayName))
|
||||||
|
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||||
|
text = android.R.string.ok
|
||||||
|
onClick {
|
||||||
|
viewModel.apply {
|
||||||
|
MainDirections.actionFlashFragment(Const.Value.FLASH_ZIP, uri).navigate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||||
|
text = android.R.string.cancel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
package com.topjohnwu.magisk.events.dialog
|
package com.topjohnwu.magisk.dialog
|
||||||
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
@@ -29,7 +29,7 @@ class ManagerInstallDialog : MarkDownDialog() {
|
|||||||
setCancelable(true)
|
setCancelable(true)
|
||||||
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||||
text = R.string.install
|
text = R.string.install
|
||||||
onClick { DownloadService.start(context, Subject.App()) }
|
onClick { DownloadService.start(activity, Subject.App()) }
|
||||||
}
|
}
|
||||||
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||||
text = android.R.string.cancel
|
text = android.R.string.cancel
|
@@ -1,12 +1,12 @@
|
|||||||
package com.topjohnwu.magisk.events.dialog
|
package com.topjohnwu.magisk.dialog
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.events.DialogBuilder
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -14,7 +14,7 @@ import kotlinx.coroutines.withContext
|
|||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
abstract class MarkDownDialog : DialogEvent() {
|
abstract class MarkDownDialog : DialogBuilder {
|
||||||
|
|
||||||
abstract suspend fun getMarkdownText(): String
|
abstract suspend fun getMarkdownText(): String
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ abstract class MarkDownDialog : DialogEvent() {
|
|||||||
val view = LayoutInflater.from(context).inflate(R.layout.markdown_window_md2, null)
|
val view = LayoutInflater.from(context).inflate(R.layout.markdown_window_md2, null)
|
||||||
setView(view)
|
setView(view)
|
||||||
val tv = view.findViewById<TextView>(R.id.md_txt)
|
val tv = view.findViewById<TextView>(R.id.md_txt)
|
||||||
(ownerActivity as BaseActivity).lifecycleScope.launch {
|
activity.lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
val text = withContext(Dispatchers.IO) { getMarkdownText() }
|
val text = withContext(Dispatchers.IO) { getMarkdownText() }
|
||||||
ServiceLocator.markwon.setMarkdown(tv, text)
|
ServiceLocator.markwon.setMarkdown(tv, text)
|
@@ -1,4 +1,4 @@
|
|||||||
package com.topjohnwu.magisk.events.dialog
|
package com.topjohnwu.magisk.dialog
|
||||||
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
@@ -8,7 +8,7 @@ import com.topjohnwu.magisk.core.download.Subject
|
|||||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
|
|
||||||
class ModuleInstallDialog(private val item: OnlineModule) : MarkDownDialog() {
|
class OnlineModuleInstallDialog(private val item: OnlineModule) : MarkDownDialog() {
|
||||||
|
|
||||||
private val svc get() = ServiceLocator.networkService
|
private val svc get() = ServiceLocator.networkService
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ class ModuleInstallDialog(private val item: OnlineModule) : MarkDownDialog() {
|
|||||||
fun download(install: Boolean) {
|
fun download(install: Boolean) {
|
||||||
val action = if (install) Action.Flash else Action.Download
|
val action = if (install) Action.Flash else Action.Download
|
||||||
val subject = Subject.Module(item, action)
|
val subject = Subject.Module(item, action)
|
||||||
DownloadService.start(context, subject)
|
DownloadService.start(activity, subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
val title = context.getString(R.string.repo_install_title,
|
val title = context.getString(R.string.repo_install_title,
|
@@ -1,9 +1,10 @@
|
|||||||
package com.topjohnwu.magisk.events.dialog
|
package com.topjohnwu.magisk.dialog
|
||||||
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.events.DialogBuilder
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
|
|
||||||
class SecondSlotWarningDialog : DialogEvent() {
|
class SecondSlotWarningDialog : DialogBuilder {
|
||||||
|
|
||||||
override fun build(dialog: MagiskDialog) {
|
override fun build(dialog: MagiskDialog) {
|
||||||
dialog.apply {
|
dialog.apply {
|
@@ -0,0 +1,25 @@
|
|||||||
|
package com.topjohnwu.magisk.dialog
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.events.DialogBuilder
|
||||||
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
|
|
||||||
|
class SuperuserRevokeDialog(
|
||||||
|
private val appName: String,
|
||||||
|
private val onSuccess: () -> Unit
|
||||||
|
) : DialogBuilder {
|
||||||
|
|
||||||
|
override fun build(dialog: MagiskDialog) {
|
||||||
|
dialog.apply {
|
||||||
|
setTitle(R.string.su_revoke_title)
|
||||||
|
setMessage(R.string.su_revoke_msg, appName)
|
||||||
|
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||||
|
text = android.R.string.ok
|
||||||
|
onClick { onSuccess() }
|
||||||
|
}
|
||||||
|
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||||
|
text = android.R.string.cancel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,16 +1,17 @@
|
|||||||
package com.topjohnwu.magisk.events.dialog
|
package com.topjohnwu.magisk.dialog
|
||||||
|
|
||||||
import android.app.ProgressDialog
|
import android.app.ProgressDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.NavigationActivity
|
import com.topjohnwu.magisk.arch.NavigationActivity
|
||||||
|
import com.topjohnwu.magisk.core.ktx.toast
|
||||||
|
import com.topjohnwu.magisk.events.DialogBuilder
|
||||||
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
|
|
||||||
class UninstallDialog : DialogEvent() {
|
class UninstallDialog : DialogBuilder {
|
||||||
|
|
||||||
override fun build(dialog: MagiskDialog) {
|
override fun build(dialog: MagiskDialog) {
|
||||||
dialog.apply {
|
dialog.apply {
|
||||||
@@ -37,9 +38,9 @@ class UninstallDialog : DialogEvent() {
|
|||||||
Shell.cmd("restore_imgs").submit { result ->
|
Shell.cmd("restore_imgs").submit { result ->
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
Utils.toast(R.string.restore_done, Toast.LENGTH_SHORT)
|
context.toast(R.string.restore_done, Toast.LENGTH_SHORT)
|
||||||
} else {
|
} else {
|
||||||
Utils.toast(R.string.restore_fail, Toast.LENGTH_LONG)
|
context.toast(R.string.restore_fail, Toast.LENGTH_LONG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -5,10 +5,15 @@ import android.view.View
|
|||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.topjohnwu.magisk.arch.*
|
import com.topjohnwu.magisk.arch.ActivityExecutor
|
||||||
|
import com.topjohnwu.magisk.arch.ContextExecutor
|
||||||
|
import com.topjohnwu.magisk.arch.NavigationActivity
|
||||||
|
import com.topjohnwu.magisk.arch.UIActivity
|
||||||
|
import com.topjohnwu.magisk.arch.ViewEvent
|
||||||
import com.topjohnwu.magisk.core.base.ContentResultCallback
|
import com.topjohnwu.magisk.core.base.ContentResultCallback
|
||||||
import com.topjohnwu.magisk.utils.TextHolder
|
import com.topjohnwu.magisk.utils.TextHolder
|
||||||
import com.topjohnwu.magisk.utils.asText
|
import com.topjohnwu.magisk.utils.asText
|
||||||
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
import com.topjohnwu.magisk.view.Shortcuts
|
import com.topjohnwu.magisk.view.Shortcuts
|
||||||
|
|
||||||
class PermissionEvent(
|
class PermissionEvent(
|
||||||
@@ -46,6 +51,16 @@ class RecreateEvent : ViewEvent(), ActivityExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AuthEvent(
|
||||||
|
private val callback: () -> Unit
|
||||||
|
) : ViewEvent(), ActivityExecutor {
|
||||||
|
|
||||||
|
override fun invoke(activity: UIActivity<*>) {
|
||||||
|
activity.authenticateCallback = { if (it) callback() }
|
||||||
|
activity.requestAuthenticate.launch(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class GetContentEvent(
|
class GetContentEvent(
|
||||||
private val type: String,
|
private val type: String,
|
||||||
private val callback: ContentResultCallback
|
private val callback: ContentResultCallback
|
||||||
@@ -95,3 +110,15 @@ class SnackbarEvent(
|
|||||||
activity.showSnackbar(msg.getText(activity.resources), length, builder)
|
activity.showSnackbar(msg.getText(activity.resources), length, builder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DialogEvent(
|
||||||
|
private val builder: DialogBuilder
|
||||||
|
) : ViewEvent(), ActivityExecutor {
|
||||||
|
override fun invoke(activity: UIActivity<*>) {
|
||||||
|
MagiskDialog(activity).apply(builder::build).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DialogBuilder {
|
||||||
|
fun build(dialog: MagiskDialog)
|
||||||
|
}
|
||||||
|
@@ -1,38 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.events.dialog
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.arch.ActivityExecutor
|
|
||||||
import com.topjohnwu.magisk.arch.UIActivity
|
|
||||||
import com.topjohnwu.magisk.arch.ViewEvent
|
|
||||||
import com.topjohnwu.magisk.core.utils.BiometricHelper
|
|
||||||
|
|
||||||
class BiometricEvent(
|
|
||||||
builder: Builder.() -> Unit
|
|
||||||
) : ViewEvent(), ActivityExecutor {
|
|
||||||
|
|
||||||
private var listenerOnFailure: GenericDialogListener = {}
|
|
||||||
private var listenerOnSuccess: GenericDialogListener = {}
|
|
||||||
|
|
||||||
init {
|
|
||||||
builder(Builder())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun invoke(activity: UIActivity<*>) {
|
|
||||||
BiometricHelper.authenticate(
|
|
||||||
activity,
|
|
||||||
onError = listenerOnFailure,
|
|
||||||
onSuccess = listenerOnSuccess
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class Builder internal constructor() {
|
|
||||||
|
|
||||||
fun onFailure(listener: GenericDialogListener) {
|
|
||||||
listenerOnFailure = listener
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSuccess(listener: GenericDialogListener) {
|
|
||||||
listenerOnSuccess = listener
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,20 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.events.dialog
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.arch.ActivityExecutor
|
|
||||||
import com.topjohnwu.magisk.arch.UIActivity
|
|
||||||
import com.topjohnwu.magisk.arch.ViewEvent
|
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
|
||||||
|
|
||||||
abstract class DialogEvent : ViewEvent(), ActivityExecutor {
|
|
||||||
|
|
||||||
override fun invoke(activity: UIActivity<*>) {
|
|
||||||
MagiskDialog(activity)
|
|
||||||
.apply { setOwnerActivity(activity) }
|
|
||||||
.apply(this::build).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract fun build(dialog: MagiskDialog)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
typealias GenericDialogListener = () -> Unit
|
|
@@ -1,35 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.events.dialog
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.R
|
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
|
||||||
|
|
||||||
class SuperuserRevokeDialog(
|
|
||||||
builder: Builder.() -> Unit
|
|
||||||
) : DialogEvent() {
|
|
||||||
|
|
||||||
private val callbacks = Builder().apply(builder)
|
|
||||||
|
|
||||||
override fun build(dialog: MagiskDialog) {
|
|
||||||
dialog.apply {
|
|
||||||
setTitle(R.string.su_revoke_title)
|
|
||||||
setMessage(R.string.su_revoke_msg, callbacks.appName)
|
|
||||||
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
|
||||||
text = android.R.string.ok
|
|
||||||
onClick { callbacks.listenerOnSuccess() }
|
|
||||||
}
|
|
||||||
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
|
||||||
text = android.R.string.cancel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class Builder internal constructor() {
|
|
||||||
var appName: String = ""
|
|
||||||
|
|
||||||
internal var listenerOnSuccess: GenericDialogListener = {}
|
|
||||||
|
|
||||||
fun onSuccess(listener: GenericDialogListener) {
|
|
||||||
listenerOnSuccess = listener
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,267 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.ktx
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.ContextWrapper
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.ApplicationInfo
|
|
||||||
import android.content.pm.PackageInfo
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.drawable.AdaptiveIconDrawable
|
|
||||||
import android.graphics.drawable.BitmapDrawable
|
|
||||||
import android.graphics.drawable.LayerDrawable
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build.VERSION.SDK_INT
|
|
||||||
import android.os.Process
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.inputmethod.InputMethodManager
|
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
|
||||||
import androidx.transition.AutoTransition
|
|
||||||
import androidx.transition.TransitionManager
|
|
||||||
import com.topjohnwu.magisk.R
|
|
||||||
import com.topjohnwu.magisk.core.Const
|
|
||||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
|
||||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import java.io.File
|
|
||||||
import kotlin.Array
|
|
||||||
import kotlin.String
|
|
||||||
import java.lang.reflect.Array as JArray
|
|
||||||
|
|
||||||
fun Context.rawResource(id: Int) = resources.openRawResource(id)
|
|
||||||
|
|
||||||
fun Context.getBitmap(id: Int): Bitmap {
|
|
||||||
var drawable = AppCompatResources.getDrawable(this, id)!!
|
|
||||||
if (drawable is BitmapDrawable)
|
|
||||||
return drawable.bitmap
|
|
||||||
if (SDK_INT >= 26 && drawable is AdaptiveIconDrawable) {
|
|
||||||
drawable = LayerDrawable(arrayOf(drawable.background, drawable.foreground))
|
|
||||||
}
|
|
||||||
val bitmap = Bitmap.createBitmap(
|
|
||||||
drawable.intrinsicWidth, drawable.intrinsicHeight,
|
|
||||||
Bitmap.Config.ARGB_8888
|
|
||||||
)
|
|
||||||
val canvas = Canvas(bitmap)
|
|
||||||
drawable.setBounds(0, 0, canvas.width, canvas.height)
|
|
||||||
drawable.draw(canvas)
|
|
||||||
return bitmap
|
|
||||||
}
|
|
||||||
|
|
||||||
val Context.deviceProtectedContext: Context get() =
|
|
||||||
if (SDK_INT >= 24) {
|
|
||||||
createDeviceProtectedStorageContext()
|
|
||||||
} else { this }
|
|
||||||
|
|
||||||
fun Intent.startActivityWithRoot() {
|
|
||||||
val args = mutableListOf("am", "start", "--user", Const.USER_ID.toString())
|
|
||||||
val cmd = toCommand(args).joinToString(" ")
|
|
||||||
Shell.cmd(cmd).submit()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Intent.toCommand(args: MutableList<String> = mutableListOf()): MutableList<String> {
|
|
||||||
action?.also {
|
|
||||||
args.add("-a")
|
|
||||||
args.add(it)
|
|
||||||
}
|
|
||||||
component?.also {
|
|
||||||
args.add("-n")
|
|
||||||
args.add(it.flattenToString())
|
|
||||||
}
|
|
||||||
data?.also {
|
|
||||||
args.add("-d")
|
|
||||||
args.add(it.toString())
|
|
||||||
}
|
|
||||||
categories?.also {
|
|
||||||
for (cat in it) {
|
|
||||||
args.add("-c")
|
|
||||||
args.add(cat)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type?.also {
|
|
||||||
args.add("-t")
|
|
||||||
args.add(it)
|
|
||||||
}
|
|
||||||
extras?.also {
|
|
||||||
loop@ for (key in it.keySet()) {
|
|
||||||
val v = it[key] ?: continue
|
|
||||||
var value: Any = v
|
|
||||||
val arg: String
|
|
||||||
when {
|
|
||||||
v is String -> arg = "--es"
|
|
||||||
v is Boolean -> arg = "--ez"
|
|
||||||
v is Int -> arg = "--ei"
|
|
||||||
v is Long -> arg = "--el"
|
|
||||||
v is Float -> arg = "--ef"
|
|
||||||
v is Uri -> arg = "--eu"
|
|
||||||
v is ComponentName -> {
|
|
||||||
arg = "--ecn"
|
|
||||||
value = v.flattenToString()
|
|
||||||
}
|
|
||||||
v is List<*> -> {
|
|
||||||
if (v.isEmpty())
|
|
||||||
continue@loop
|
|
||||||
|
|
||||||
arg = if (v[0] is Int)
|
|
||||||
"--eial"
|
|
||||||
else if (v[0] is Long)
|
|
||||||
"--elal"
|
|
||||||
else if (v[0] is Float)
|
|
||||||
"--efal"
|
|
||||||
else if (v[0] is String)
|
|
||||||
"--esal"
|
|
||||||
else
|
|
||||||
continue@loop /* Unsupported */
|
|
||||||
|
|
||||||
val sb = StringBuilder()
|
|
||||||
for (o in v) {
|
|
||||||
sb.append(o.toString().replace(",", "\\,"))
|
|
||||||
sb.append(',')
|
|
||||||
}
|
|
||||||
// Remove trailing comma
|
|
||||||
sb.deleteCharAt(sb.length - 1)
|
|
||||||
value = sb
|
|
||||||
}
|
|
||||||
v.javaClass.isArray -> {
|
|
||||||
arg = if (v is IntArray)
|
|
||||||
"--eia"
|
|
||||||
else if (v is LongArray)
|
|
||||||
"--ela"
|
|
||||||
else if (v is FloatArray)
|
|
||||||
"--efa"
|
|
||||||
else if (v is Array<*> && v.isArrayOf<String>())
|
|
||||||
"--esa"
|
|
||||||
else
|
|
||||||
continue@loop /* Unsupported */
|
|
||||||
|
|
||||||
val sb = StringBuilder()
|
|
||||||
val len = JArray.getLength(v)
|
|
||||||
for (i in 0 until len) {
|
|
||||||
sb.append(JArray.get(v, i)!!.toString().replace(",", "\\,"))
|
|
||||||
sb.append(',')
|
|
||||||
}
|
|
||||||
// Remove trailing comma
|
|
||||||
sb.deleteCharAt(sb.length - 1)
|
|
||||||
value = sb
|
|
||||||
}
|
|
||||||
else -> continue@loop
|
|
||||||
} /* Unsupported */
|
|
||||||
|
|
||||||
args.add(arg)
|
|
||||||
args.add(key)
|
|
||||||
args.add(value.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
args.add("-f")
|
|
||||||
args.add(flags.toString())
|
|
||||||
return args
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.cachedFile(name: String) = File(cacheDir, name)
|
|
||||||
|
|
||||||
fun ApplicationInfo.getLabel(pm: PackageManager): String {
|
|
||||||
runCatching {
|
|
||||||
if (labelRes > 0) {
|
|
||||||
val res = pm.getResourcesForApplication(this)
|
|
||||||
val config = Configuration()
|
|
||||||
config.setLocale(currentLocale)
|
|
||||||
res.updateConfiguration(config, res.displayMetrics)
|
|
||||||
return res.getString(labelRes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return loadLabel(pm).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.unwrap(): Context {
|
|
||||||
var context = this
|
|
||||||
while (true) {
|
|
||||||
if (context is ContextWrapper)
|
|
||||||
context = context.baseContext
|
|
||||||
else
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.hasPermissions(vararg permissions: String) = permissions.all {
|
|
||||||
ContextCompat.checkSelfPermission(this, it) == PERMISSION_GRANTED
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Activity.hideKeyboard() {
|
|
||||||
val view = currentFocus ?: return
|
|
||||||
getSystemService<InputMethodManager>()
|
|
||||||
?.hideSoftInputFromWindow(view.windowToken, 0)
|
|
||||||
view.clearFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ViewGroup.startAnimations() {
|
|
||||||
val transition = AutoTransition()
|
|
||||||
.setInterpolator(FastOutSlowInInterpolator())
|
|
||||||
.setDuration(400)
|
|
||||||
.excludeTarget(R.id.main_toolbar, true)
|
|
||||||
TransitionManager.beginDelayedTransition(
|
|
||||||
this,
|
|
||||||
transition
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val View.activity: Activity get() {
|
|
||||||
var context = context
|
|
||||||
while(true) {
|
|
||||||
if (context !is ContextWrapper)
|
|
||||||
error("View is not attached to activity")
|
|
||||||
if (context is Activity)
|
|
||||||
return context
|
|
||||||
context = context.baseContext
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("PrivateApi")
|
|
||||||
fun getProperty(key: String, def: String): String {
|
|
||||||
runCatching {
|
|
||||||
val clazz = Class.forName("android.os.SystemProperties")
|
|
||||||
val get = clazz.getMethod("get", String::class.java, String::class.java)
|
|
||||||
return get.invoke(clazz, key, def) as String
|
|
||||||
}
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
|
||||||
@Throws(PackageManager.NameNotFoundException::class)
|
|
||||||
fun PackageManager.getPackageInfo(uid: Int, pid: Int): PackageInfo? {
|
|
||||||
val flag = PackageManager.MATCH_UNINSTALLED_PACKAGES
|
|
||||||
val pkgs = getPackagesForUid(uid) ?: throw PackageManager.NameNotFoundException()
|
|
||||||
if (pkgs.size > 1) {
|
|
||||||
if (pid <= 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
// Try to find package name from PID
|
|
||||||
val proc = RootUtils.obj?.getAppProcess(pid)
|
|
||||||
if (proc == null) {
|
|
||||||
if (uid == Process.SHELL_UID) {
|
|
||||||
// It is possible that some apps installed are sharing UID with shell.
|
|
||||||
// We will not be able to find a package from the active process list,
|
|
||||||
// because the client is forked from ADB shell, not any app process.
|
|
||||||
return getPackageInfo("com.android.shell", flag)
|
|
||||||
}
|
|
||||||
} else if (uid == proc.uid) {
|
|
||||||
return getPackageInfo(proc.pkgList[0], flag)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (pkgs.size == 1) {
|
|
||||||
return getPackageInfo(pkgs[0], flag)
|
|
||||||
}
|
|
||||||
throw PackageManager.NameNotFoundException()
|
|
||||||
}
|
|
@@ -680,7 +680,7 @@ public abstract class ApkSignerV2 {
|
|||||||
return "SHA-512";
|
return "SHA-512";
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"Unknown content digest algorthm: " + digestAlgorithm);
|
"Unknown content digest algorithm: " + digestAlgorithm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -692,7 +692,7 @@ public abstract class ApkSignerV2 {
|
|||||||
return 512 / 8;
|
return 512 / 8;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"Unknown content digest algorthm: " + digestAlgorithm);
|
"Unknown content digest algorithm: " + digestAlgorithm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,115 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.signing;
|
|
||||||
|
|
||||||
import org.bouncycastle.asn1.ASN1InputStream;
|
|
||||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
|
||||||
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
|
|
||||||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
|
|
||||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
|
|
||||||
import org.bouncycastle.asn1.x9.X9ObjectIdentifiers;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.security.GeneralSecurityException;
|
|
||||||
import java.security.Key;
|
|
||||||
import java.security.KeyFactory;
|
|
||||||
import java.security.PrivateKey;
|
|
||||||
import java.security.PublicKey;
|
|
||||||
import java.security.cert.CertificateFactory;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.security.spec.ECPrivateKeySpec;
|
|
||||||
import java.security.spec.ECPublicKeySpec;
|
|
||||||
import java.security.spec.InvalidKeySpecException;
|
|
||||||
import java.security.spec.PKCS8EncodedKeySpec;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class CryptoUtils {
|
|
||||||
|
|
||||||
static final Map<String, String> ID_TO_ALG;
|
|
||||||
static final Map<String, String> ALG_TO_ID;
|
|
||||||
|
|
||||||
static {
|
|
||||||
ID_TO_ALG = new HashMap<>();
|
|
||||||
ALG_TO_ID = new HashMap<>();
|
|
||||||
ID_TO_ALG.put(X9ObjectIdentifiers.ecdsa_with_SHA256.getId(), "SHA256withECDSA");
|
|
||||||
ID_TO_ALG.put(X9ObjectIdentifiers.ecdsa_with_SHA384.getId(), "SHA384withECDSA");
|
|
||||||
ID_TO_ALG.put(X9ObjectIdentifiers.ecdsa_with_SHA512.getId(), "SHA512withECDSA");
|
|
||||||
ID_TO_ALG.put(PKCSObjectIdentifiers.sha1WithRSAEncryption.getId(), "SHA1withRSA");
|
|
||||||
ID_TO_ALG.put(PKCSObjectIdentifiers.sha256WithRSAEncryption.getId(), "SHA256withRSA");
|
|
||||||
ID_TO_ALG.put(PKCSObjectIdentifiers.sha512WithRSAEncryption.getId(), "SHA512withRSA");
|
|
||||||
ALG_TO_ID.put("SHA256withECDSA", X9ObjectIdentifiers.ecdsa_with_SHA256.getId());
|
|
||||||
ALG_TO_ID.put("SHA384withECDSA", X9ObjectIdentifiers.ecdsa_with_SHA384.getId());
|
|
||||||
ALG_TO_ID.put("SHA512withECDSA", X9ObjectIdentifiers.ecdsa_with_SHA512.getId());
|
|
||||||
ALG_TO_ID.put("SHA1withRSA", PKCSObjectIdentifiers.sha1WithRSAEncryption.getId());
|
|
||||||
ALG_TO_ID.put("SHA256withRSA", PKCSObjectIdentifiers.sha256WithRSAEncryption.getId());
|
|
||||||
ALG_TO_ID.put("SHA512withRSA", PKCSObjectIdentifiers.sha512WithRSAEncryption.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
static String getSignatureAlgorithm(Key key) throws Exception {
|
|
||||||
if ("EC".equals(key.getAlgorithm())) {
|
|
||||||
int curveSize;
|
|
||||||
KeyFactory factory = KeyFactory.getInstance("EC");
|
|
||||||
if (key instanceof PublicKey) {
|
|
||||||
ECPublicKeySpec spec = factory.getKeySpec(key, ECPublicKeySpec.class);
|
|
||||||
curveSize = spec.getParams().getCurve().getField().getFieldSize();
|
|
||||||
} else if (key instanceof PrivateKey) {
|
|
||||||
ECPrivateKeySpec spec = factory.getKeySpec(key, ECPrivateKeySpec.class);
|
|
||||||
curveSize = spec.getParams().getCurve().getField().getFieldSize();
|
|
||||||
} else {
|
|
||||||
throw new InvalidKeySpecException();
|
|
||||||
}
|
|
||||||
if (curveSize <= 256) {
|
|
||||||
return "SHA256withECDSA";
|
|
||||||
} else if (curveSize <= 384) {
|
|
||||||
return "SHA384withECDSA";
|
|
||||||
} else {
|
|
||||||
return "SHA512withECDSA";
|
|
||||||
}
|
|
||||||
} else if ("RSA".equals(key.getAlgorithm())) {
|
|
||||||
return "SHA256withRSA";
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("Unsupported key type " + key.getAlgorithm());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static AlgorithmIdentifier getSignatureAlgorithmIdentifier(Key key) throws Exception {
|
|
||||||
String id = ALG_TO_ID.get(getSignatureAlgorithm(key));
|
|
||||||
if (id == null) {
|
|
||||||
throw new IllegalArgumentException("Unsupported key type " + key.getAlgorithm());
|
|
||||||
}
|
|
||||||
return new AlgorithmIdentifier(new ASN1ObjectIdentifier(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static X509Certificate readCertificate(InputStream input)
|
|
||||||
throws IOException, GeneralSecurityException {
|
|
||||||
try {
|
|
||||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
|
||||||
return (X509Certificate) cf.generateCertificate(input);
|
|
||||||
} finally {
|
|
||||||
input.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Read a PKCS#8 format private key. */
|
|
||||||
public static PrivateKey readPrivateKey(InputStream input)
|
|
||||||
throws IOException, GeneralSecurityException {
|
|
||||||
try {
|
|
||||||
ByteArrayStream buf = new ByteArrayStream();
|
|
||||||
buf.readFrom(input);
|
|
||||||
byte[] bytes = buf.toByteArray();
|
|
||||||
/* Check to see if this is in an EncryptedPrivateKeyInfo structure. */
|
|
||||||
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
|
|
||||||
/*
|
|
||||||
* Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm
|
|
||||||
* OID and use that to construct a KeyFactory.
|
|
||||||
*/
|
|
||||||
ASN1InputStream bIn = new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded()));
|
|
||||||
PrivateKeyInfo pki = PrivateKeyInfo.getInstance(bIn.readObject());
|
|
||||||
String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId();
|
|
||||||
return KeyFactory.getInstance(algOid).generatePrivate(spec);
|
|
||||||
} finally {
|
|
||||||
input.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,382 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.signing;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.bouncycastle.asn1.ASN1Encodable;
|
|
||||||
import org.bouncycastle.asn1.ASN1EncodableVector;
|
|
||||||
import org.bouncycastle.asn1.ASN1InputStream;
|
|
||||||
import org.bouncycastle.asn1.ASN1Integer;
|
|
||||||
import org.bouncycastle.asn1.ASN1Object;
|
|
||||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
|
||||||
import org.bouncycastle.asn1.ASN1Primitive;
|
|
||||||
import org.bouncycastle.asn1.ASN1Sequence;
|
|
||||||
import org.bouncycastle.asn1.DEROctetString;
|
|
||||||
import org.bouncycastle.asn1.DERPrintableString;
|
|
||||||
import org.bouncycastle.asn1.DERSequence;
|
|
||||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FilterInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.ByteOrder;
|
|
||||||
import java.security.PrivateKey;
|
|
||||||
import java.security.PublicKey;
|
|
||||||
import java.security.Signature;
|
|
||||||
import java.security.cert.CertificateEncodingException;
|
|
||||||
import java.security.cert.CertificateFactory;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
public class SignBoot {
|
|
||||||
|
|
||||||
private static final int BOOT_IMAGE_HEADER_V1_RECOVERY_DTBO_SIZE_OFFSET = 1632;
|
|
||||||
private static final int BOOT_IMAGE_HEADER_V2_DTB_SIZE_OFFSET = 1648;
|
|
||||||
|
|
||||||
// Arbitrary maximum header version value; when greater assume the field is dt/extra size
|
|
||||||
private static final int BOOT_IMAGE_HEADER_VERSION_MAXIMUM = 8;
|
|
||||||
|
|
||||||
// Maximum header size byte value to read (currently the bootimg minimum page size)
|
|
||||||
private static final int BOOT_IMAGE_HEADER_SIZE_MAXIMUM = 2048;
|
|
||||||
|
|
||||||
private static class PushBackRWStream extends FilterInputStream {
|
|
||||||
private OutputStream out;
|
|
||||||
private int pos = 0;
|
|
||||||
private byte[] backBuf;
|
|
||||||
|
|
||||||
PushBackRWStream(InputStream in, OutputStream o) {
|
|
||||||
super(in);
|
|
||||||
out = o;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read() throws IOException {
|
|
||||||
int b;
|
|
||||||
if (backBuf != null && backBuf.length > pos) {
|
|
||||||
b = backBuf[pos++];
|
|
||||||
} else {
|
|
||||||
b = super.read();
|
|
||||||
out.write(b);
|
|
||||||
}
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read(byte[] bytes, int off, int len) throws IOException {
|
|
||||||
int read = 0;
|
|
||||||
if (backBuf != null && backBuf.length > pos) {
|
|
||||||
read = Math.min(len, backBuf.length - pos);
|
|
||||||
System.arraycopy(backBuf, pos, bytes, off, read);
|
|
||||||
pos += read;
|
|
||||||
off += read;
|
|
||||||
len -= read;
|
|
||||||
}
|
|
||||||
if (len > 0) {
|
|
||||||
int ar = super.read(bytes, off, len);
|
|
||||||
read += ar;
|
|
||||||
out.write(bytes, off, ar);
|
|
||||||
}
|
|
||||||
return read;
|
|
||||||
}
|
|
||||||
|
|
||||||
void unread(byte[] buf) {
|
|
||||||
backBuf = buf;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int fullRead(InputStream in, byte[] b) throws IOException {
|
|
||||||
return fullRead(in, b, 0, b.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int fullRead(InputStream in, byte[] b, int off, int len) throws IOException {
|
|
||||||
int n = 0;
|
|
||||||
while (n < len) {
|
|
||||||
int count = in.read(b, off + n, len - n);
|
|
||||||
if (count <= 0)
|
|
||||||
break;
|
|
||||||
n += count;
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean doSignature(
|
|
||||||
@Nullable X509Certificate cert, @Nullable PrivateKey key,
|
|
||||||
@NonNull InputStream imgIn, @NonNull OutputStream imgOut, @NonNull String target
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
PushBackRWStream in = new PushBackRWStream(imgIn, imgOut);
|
|
||||||
byte[] hdr = new byte[BOOT_IMAGE_HEADER_SIZE_MAXIMUM];
|
|
||||||
// First read the header
|
|
||||||
fullRead(in, hdr);
|
|
||||||
int signableSize = getSignableImageSize(hdr);
|
|
||||||
// Unread header
|
|
||||||
in.unread(hdr);
|
|
||||||
BootSignature bootsig = new BootSignature(target, signableSize);
|
|
||||||
if (cert == null) {
|
|
||||||
cert = CryptoUtils.readCertificate(
|
|
||||||
new ByteArrayInputStream(KeyData.verityCert()));
|
|
||||||
}
|
|
||||||
bootsig.setCertificate(cert);
|
|
||||||
if (key == null) {
|
|
||||||
key = CryptoUtils.readPrivateKey(
|
|
||||||
new ByteArrayInputStream(KeyData.verityKey()));
|
|
||||||
}
|
|
||||||
byte[] sig = bootsig.sign(key, in, signableSize);
|
|
||||||
bootsig.setSignature(sig, CryptoUtils.getSignatureAlgorithmIdentifier(key));
|
|
||||||
byte[] encoded_bootsig = bootsig.getEncoded();
|
|
||||||
imgOut.write(encoded_bootsig);
|
|
||||||
imgOut.flush();
|
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean verifySignature(InputStream imgIn, X509Certificate cert) {
|
|
||||||
try {
|
|
||||||
// Read the header for size
|
|
||||||
byte[] hdr = new byte[BOOT_IMAGE_HEADER_SIZE_MAXIMUM];
|
|
||||||
if (fullRead(imgIn, hdr) != hdr.length) {
|
|
||||||
System.err.println("Unable to read image header");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
int signableSize = getSignableImageSize(hdr);
|
|
||||||
|
|
||||||
// Read the rest of the image
|
|
||||||
byte[] rawImg = Arrays.copyOf(hdr, signableSize);
|
|
||||||
int remain = signableSize - hdr.length;
|
|
||||||
if (fullRead(imgIn, rawImg, hdr.length, remain) != remain) {
|
|
||||||
System.err.println("Unable to read image");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read footer, which contains the signature
|
|
||||||
byte[] signature = new byte[4096];
|
|
||||||
if (imgIn.read(signature) == -1 || Arrays.equals(signature, new byte [signature.length])) {
|
|
||||||
System.err.println("Invalid image: not signed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
BootSignature bootsig = new BootSignature(signature);
|
|
||||||
if (cert != null) {
|
|
||||||
bootsig.setCertificate(cert);
|
|
||||||
}
|
|
||||||
if (bootsig.verify(rawImg, signableSize)) {
|
|
||||||
System.err.println("Signature is VALID");
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
System.err.println("Signature is INVALID");
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getSignableImageSize(byte[] data) throws Exception {
|
|
||||||
if (!Arrays.equals(Arrays.copyOfRange(data, 0, 8),
|
|
||||||
"ANDROID!".getBytes("US-ASCII"))) {
|
|
||||||
throw new IllegalArgumentException("Invalid image header: missing magic");
|
|
||||||
}
|
|
||||||
ByteBuffer image = ByteBuffer.wrap(data);
|
|
||||||
image.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
image.getLong(); // magic
|
|
||||||
int kernelSize = image.getInt();
|
|
||||||
image.getInt(); // kernel_addr
|
|
||||||
int ramdskSize = image.getInt();
|
|
||||||
image.getInt(); // ramdisk_addr
|
|
||||||
int secondSize = image.getInt();
|
|
||||||
image.getLong(); // second_addr + tags_addr
|
|
||||||
int pageSize = image.getInt();
|
|
||||||
if (pageSize >= 0x02000000) {
|
|
||||||
throw new IllegalArgumentException("Invalid image header: PXA header detected");
|
|
||||||
}
|
|
||||||
int length = pageSize // include the page aligned image header
|
|
||||||
+ ((kernelSize + pageSize - 1) / pageSize) * pageSize
|
|
||||||
+ ((ramdskSize + pageSize - 1) / pageSize) * pageSize
|
|
||||||
+ ((secondSize + pageSize - 1) / pageSize) * pageSize;
|
|
||||||
int headerVersion = image.getInt(); // boot image header version or dt/extra size
|
|
||||||
if (headerVersion > 0 && headerVersion < BOOT_IMAGE_HEADER_VERSION_MAXIMUM) {
|
|
||||||
image.position(BOOT_IMAGE_HEADER_V1_RECOVERY_DTBO_SIZE_OFFSET);
|
|
||||||
int recoveryDtboLength = image.getInt();
|
|
||||||
length += ((recoveryDtboLength + pageSize - 1) / pageSize) * pageSize;
|
|
||||||
image.getLong(); // recovery_dtbo address
|
|
||||||
int headerSize = image.getInt();
|
|
||||||
if (headerVersion == 2) {
|
|
||||||
image.position(BOOT_IMAGE_HEADER_V2_DTB_SIZE_OFFSET);
|
|
||||||
int dtbLength = image.getInt();
|
|
||||||
length += ((dtbLength + pageSize - 1) / pageSize) * pageSize;
|
|
||||||
image.getLong(); // dtb address
|
|
||||||
}
|
|
||||||
if (image.position() != headerSize) {
|
|
||||||
throw new IllegalArgumentException("Invalid image header: invalid header length");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// headerVersion is 0 or actually dt/extra size in this case
|
|
||||||
length += ((headerVersion + pageSize - 1) / pageSize) * pageSize;
|
|
||||||
}
|
|
||||||
length = ((length + pageSize - 1) / pageSize) * pageSize;
|
|
||||||
if (length <= 0) {
|
|
||||||
throw new IllegalArgumentException("Invalid image header: invalid length");
|
|
||||||
}
|
|
||||||
return length;
|
|
||||||
}
|
|
||||||
|
|
||||||
static class BootSignature extends ASN1Object {
|
|
||||||
private ASN1Integer formatVersion;
|
|
||||||
private ASN1Encodable certificate;
|
|
||||||
private AlgorithmIdentifier algId;
|
|
||||||
private DERPrintableString target;
|
|
||||||
private ASN1Integer length;
|
|
||||||
private DEROctetString signature;
|
|
||||||
private PublicKey publicKey;
|
|
||||||
private static final int FORMAT_VERSION = 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the object for signing an image file
|
|
||||||
* @param target Target name, included in the signed data
|
|
||||||
* @param length Length of the image, included in the signed data
|
|
||||||
*/
|
|
||||||
public BootSignature(String target, int length) {
|
|
||||||
this.formatVersion = new ASN1Integer(FORMAT_VERSION);
|
|
||||||
this.target = new DERPrintableString(target);
|
|
||||||
this.length = new ASN1Integer(length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the object for verifying a signed image file
|
|
||||||
* @param signature Signature footer
|
|
||||||
*/
|
|
||||||
public BootSignature(byte[] signature) throws Exception {
|
|
||||||
ASN1InputStream stream = new ASN1InputStream(signature);
|
|
||||||
ASN1Sequence sequence = (ASN1Sequence) stream.readObject();
|
|
||||||
formatVersion = (ASN1Integer) sequence.getObjectAt(0);
|
|
||||||
if (formatVersion.getValue().intValue() != FORMAT_VERSION) {
|
|
||||||
throw new IllegalArgumentException("Unsupported format version");
|
|
||||||
}
|
|
||||||
certificate = sequence.getObjectAt(1);
|
|
||||||
byte[] encoded = ((ASN1Object) certificate).getEncoded();
|
|
||||||
ByteArrayInputStream bis = new ByteArrayInputStream(encoded);
|
|
||||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
|
||||||
X509Certificate c = (X509Certificate) cf.generateCertificate(bis);
|
|
||||||
publicKey = c.getPublicKey();
|
|
||||||
ASN1Sequence algId = (ASN1Sequence) sequence.getObjectAt(2);
|
|
||||||
this.algId = new AlgorithmIdentifier((ASN1ObjectIdentifier) algId.getObjectAt(0));
|
|
||||||
ASN1Sequence attrs = (ASN1Sequence) sequence.getObjectAt(3);
|
|
||||||
target = (DERPrintableString) attrs.getObjectAt(0);
|
|
||||||
length = (ASN1Integer) attrs.getObjectAt(1);
|
|
||||||
this.signature = (DEROctetString) sequence.getObjectAt(4);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ASN1Object getAuthenticatedAttributes() {
|
|
||||||
ASN1EncodableVector attrs = new ASN1EncodableVector();
|
|
||||||
attrs.add(target);
|
|
||||||
attrs.add(length);
|
|
||||||
return new DERSequence(attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getEncodedAuthenticatedAttributes() throws IOException {
|
|
||||||
return getAuthenticatedAttributes().getEncoded();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSignature(byte[] sig, AlgorithmIdentifier algId) {
|
|
||||||
this.algId = algId;
|
|
||||||
signature = new DEROctetString(sig);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCertificate(X509Certificate cert)
|
|
||||||
throws CertificateEncodingException, IOException {
|
|
||||||
ASN1InputStream s = new ASN1InputStream(cert.getEncoded());
|
|
||||||
certificate = s.readObject();
|
|
||||||
publicKey = cert.getPublicKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] sign(PrivateKey key, InputStream is, int len) throws Exception {
|
|
||||||
Signature signer = Signature.getInstance(CryptoUtils.getSignatureAlgorithm(key));
|
|
||||||
signer.initSign(key);
|
|
||||||
int read;
|
|
||||||
byte buffer[] = new byte[4096];
|
|
||||||
while ((read = is.read(buffer, 0, Math.min(len, buffer.length))) > 0) {
|
|
||||||
signer.update(buffer, 0, read);
|
|
||||||
len -= read;
|
|
||||||
}
|
|
||||||
signer.update(getEncodedAuthenticatedAttributes());
|
|
||||||
return signer.sign();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean verify(byte[] image, int length) throws Exception {
|
|
||||||
if (this.length.getValue().intValue() != length) {
|
|
||||||
throw new IllegalArgumentException("Invalid image length");
|
|
||||||
}
|
|
||||||
String algName = CryptoUtils.ID_TO_ALG.get(algId.getAlgorithm().getId());
|
|
||||||
if (algName == null) {
|
|
||||||
throw new IllegalArgumentException("Unsupported algorithm " + algId.getAlgorithm());
|
|
||||||
}
|
|
||||||
Signature verifier = Signature.getInstance(algName);
|
|
||||||
verifier.initVerify(publicKey);
|
|
||||||
verifier.update(image, 0, length);
|
|
||||||
verifier.update(getEncodedAuthenticatedAttributes());
|
|
||||||
return verifier.verify(signature.getOctets());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ASN1Primitive toASN1Primitive() {
|
|
||||||
ASN1EncodableVector v = new ASN1EncodableVector();
|
|
||||||
v.add(formatVersion);
|
|
||||||
v.add(certificate);
|
|
||||||
v.add(algId);
|
|
||||||
v.add(getAuthenticatedAttributes());
|
|
||||||
v.add(signature);
|
|
||||||
return new DERSequence(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
|
||||||
if (args.length > 0 && "-verify".equals(args[0])) {
|
|
||||||
X509Certificate cert = null;
|
|
||||||
if (args.length >= 2) {
|
|
||||||
// args[1] is the path to a public key certificate
|
|
||||||
cert = CryptoUtils.readCertificate(new FileInputStream(args[1]));
|
|
||||||
}
|
|
||||||
boolean signed = SignBoot.verifySignature(System.in, cert);
|
|
||||||
System.exit(signed ? 0 : 1);
|
|
||||||
} else if (args.length > 0 && "-sign".equals(args[0])) {
|
|
||||||
X509Certificate cert = null;
|
|
||||||
PrivateKey key = null;
|
|
||||||
String name = "/boot";
|
|
||||||
|
|
||||||
if (args.length >= 3) {
|
|
||||||
cert = CryptoUtils.readCertificate(new FileInputStream(args[1]));
|
|
||||||
key = CryptoUtils.readPrivateKey(new FileInputStream(args[2]));
|
|
||||||
}
|
|
||||||
if (args.length == 2) {
|
|
||||||
name = args[1];
|
|
||||||
} else if (args.length >= 4) {
|
|
||||||
name = args[3];
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean result = SignBoot.doSignature(cert, key, System.in, System.out, name);
|
|
||||||
System.exit(result ? 0 : 1);
|
|
||||||
} else {
|
|
||||||
System.err.println(
|
|
||||||
"BootSigner <actions> [args]\n" +
|
|
||||||
"Input from stdin, output to stdout\n" +
|
|
||||||
"\n" +
|
|
||||||
"Actions:\n" +
|
|
||||||
" -verify [x509.pem]\n" +
|
|
||||||
" verify image. cert is optional.\n" +
|
|
||||||
" -sign [x509.pem] [pk8] [name]\n" +
|
|
||||||
" sign image. name and the cert/key pair are optional.\n" +
|
|
||||||
" name should be either /boot (default) or /recovery.\n"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,5 +1,7 @@
|
|||||||
package com.topjohnwu.magisk.ui
|
package com.topjohnwu.magisk.ui
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -10,22 +12,25 @@ import androidx.core.content.pm.ShortcutManagerCompat
|
|||||||
import androidx.core.view.forEach
|
import androidx.core.view.forEach
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
import com.topjohnwu.magisk.MainDirections
|
import com.topjohnwu.magisk.MainDirections
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||||
|
import com.topjohnwu.magisk.arch.startAnimations
|
||||||
import com.topjohnwu.magisk.arch.viewModel
|
import com.topjohnwu.magisk.arch.viewModel
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
import com.topjohnwu.magisk.core.model.module.LocalModule
|
import com.topjohnwu.magisk.core.model.module.LocalModule
|
||||||
|
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||||
import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding
|
import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding
|
||||||
import com.topjohnwu.magisk.ktx.startAnimations
|
|
||||||
import com.topjohnwu.magisk.ui.home.HomeFragmentDirections
|
import com.topjohnwu.magisk.ui.home.HomeFragmentDirections
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
import com.topjohnwu.magisk.view.Shortcuts
|
import com.topjohnwu.magisk.view.Shortcuts
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class MainViewModel : BaseViewModel()
|
class MainViewModel : BaseViewModel()
|
||||||
@@ -52,10 +57,19 @@ class MainActivity : SplashActivity<ActivityMainMd2Binding>() {
|
|||||||
|
|
||||||
private var isRootFragment = true
|
private var isRootFragment = true
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
override fun showMainUI(savedInstanceState: Bundle?) {
|
override fun showMainUI(savedInstanceState: Bundle?) {
|
||||||
setContentView()
|
setContentView()
|
||||||
showUnsupportedMessage()
|
showUnsupportedMessage()
|
||||||
askForHomeShortcut()
|
askForHomeShortcut()
|
||||||
|
checkStubComponent()
|
||||||
|
|
||||||
|
// Ask permission to post notifications for background update check
|
||||||
|
if (Config.checkUpdate) {
|
||||||
|
withPermission(Manifest.permission.POST_NOTIFICATIONS) {
|
||||||
|
Config.checkUpdate = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||||
|
|
||||||
@@ -88,7 +102,7 @@ class MainActivity : SplashActivity<ActivityMainMd2Binding>() {
|
|||||||
// https://issuetracker.google.com/issues/124538620
|
// https://issuetracker.google.com/issues/124538620
|
||||||
}
|
}
|
||||||
binding.mainNavigation.menu.apply {
|
binding.mainNavigation.menu.apply {
|
||||||
findItem(R.id.superuserFragment)?.isEnabled = Utils.showSuperUser()
|
findItem(R.id.superuserFragment)?.isEnabled = Info.showSuperUser
|
||||||
findItem(R.id.modulesFragment)?.isEnabled = Info.env.isActive && LocalModule.loaded()
|
findItem(R.id.modulesFragment)?.isEnabled = Info.env.isActive && LocalModule.loaded()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,4 +231,22 @@ class MainActivity : SplashActivity<ActivityMainMd2Binding>() {
|
|||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
private fun checkStubComponent() {
|
||||||
|
if (intent.component?.className?.contains(HideAPK.PLACEHOLDER) == true) {
|
||||||
|
// The stub APK was not properly patched, re-apply our changes
|
||||||
|
withPermission(Manifest.permission.REQUEST_INSTALL_PACKAGES) { granted ->
|
||||||
|
if (granted) {
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val apk = File(applicationInfo.sourceDir)
|
||||||
|
HideAPK.upgrade(this@MainActivity, apk)?.let {
|
||||||
|
startActivity(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -6,6 +6,7 @@ import android.os.Bundle
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.databinding.ViewDataBinding
|
import androidx.databinding.ViewDataBinding
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
@@ -16,12 +17,11 @@ import com.topjohnwu.magisk.core.Const
|
|||||||
import com.topjohnwu.magisk.core.JobService
|
import com.topjohnwu.magisk.core.JobService
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
|
import com.topjohnwu.magisk.core.ktx.toast
|
||||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||||
import com.topjohnwu.magisk.ui.theme.Theme
|
import com.topjohnwu.magisk.ui.theme.Theme
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
|
||||||
import com.topjohnwu.magisk.view.Shortcuts
|
import com.topjohnwu.magisk.view.Shortcuts
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -30,13 +30,15 @@ import kotlinx.coroutines.launch
|
|||||||
abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Binding>() {
|
abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Binding>() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var skipSplash = false
|
private var splashShown = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var needShowMainUI = false
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
setTheme(Theme.selected.themeRes)
|
setTheme(Theme.selected.themeRes)
|
||||||
|
|
||||||
if (isRunningAsStub && !skipSplash) {
|
if (isRunningAsStub && !splashShown) {
|
||||||
// Manually apply splash theme for stub
|
// Manually apply splash theme for stub
|
||||||
theme.applyStyle(R.style.StubSplashTheme, true)
|
theme.applyStyle(R.style.StubSplashTheme, true)
|
||||||
}
|
}
|
||||||
@@ -45,11 +47,11 @@ abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Bi
|
|||||||
|
|
||||||
if (!isRunningAsStub) {
|
if (!isRunningAsStub) {
|
||||||
val splashScreen = installSplashScreen()
|
val splashScreen = installSplashScreen()
|
||||||
splashScreen.setKeepOnScreenCondition { !skipSplash }
|
splashScreen.setKeepOnScreenCondition { !splashShown }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (skipSplash) {
|
if (splashShown) {
|
||||||
showMainUI(savedInstanceState)
|
doShowMainUI(savedInstanceState)
|
||||||
} else {
|
} else {
|
||||||
Shell.getShell(Shell.EXECUTOR) {
|
Shell.getShell(Shell.EXECUTOR) {
|
||||||
if (isRunningAsStub && !it.isRoot) {
|
if (isRunningAsStub && !it.isRoot) {
|
||||||
@@ -61,6 +63,11 @@ abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Bi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun doShowMainUI(savedInstanceState: Bundle?) {
|
||||||
|
needShowMainUI = false
|
||||||
|
showMainUI(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
abstract fun showMainUI(savedInstanceState: Bundle?)
|
abstract fun showMainUI(savedInstanceState: Bundle?)
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
@@ -73,7 +80,7 @@ abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Bi
|
|||||||
onClick {
|
onClick {
|
||||||
withPermission(REQUEST_INSTALL_PACKAGES) {
|
withPermission(REQUEST_INSTALL_PACKAGES) {
|
||||||
if (!it) {
|
if (!it) {
|
||||||
Utils.toast(R.string.install_unknown_denied, Toast.LENGTH_SHORT)
|
toast(R.string.install_unknown_denied, Toast.LENGTH_SHORT)
|
||||||
showInvalidStateMessage()
|
showInvalidStateMessage()
|
||||||
} else {
|
} else {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
@@ -88,6 +95,13 @@ abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Bi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
if (needShowMainUI) {
|
||||||
|
doShowMainUI(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun preLoad(savedState: Bundle?) {
|
private fun preLoad(savedState: Bundle?) {
|
||||||
val prevPkg = intent.getStringExtra(Const.Key.PREV_PKG)?.let {
|
val prevPkg = intent.getStringExtra(Const.Key.PREV_PKG)?.let {
|
||||||
// Make sure the calling package matches (prevent DoS)
|
// Make sure the calling package matches (prevent DoS)
|
||||||
@@ -107,7 +121,6 @@ abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Bi
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Notifications.setup(this)
|
|
||||||
JobService.schedule(this)
|
JobService.schedule(this)
|
||||||
Shortcuts.setupDynamic(this)
|
Shortcuts.setupDynamic(this)
|
||||||
|
|
||||||
@@ -118,12 +131,16 @@ abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Bi
|
|||||||
RootUtils.Connection.await()
|
RootUtils.Connection.await()
|
||||||
|
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
skipSplash = true
|
splashShown = true
|
||||||
if (isRunningAsStub) {
|
if (isRunningAsStub) {
|
||||||
// Re-launch main activity without splash theme
|
// Re-launch main activity without splash theme
|
||||||
relaunch()
|
relaunch()
|
||||||
} else {
|
} else {
|
||||||
showMainUI(savedState)
|
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
|
||||||
|
doShowMainUI(savedState)
|
||||||
|
} else {
|
||||||
|
needShowMainUI = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,11 +153,10 @@ abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Bi
|
|||||||
Shell.cmd("(pm uninstall $APPLICATION_ID)& >/dev/null 2>&1").exec()
|
Shell.cmd("(pm uninstall $APPLICATION_ID)& >/dev/null 2>&1").exec()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!Const.Version.atLeast_25_0() && Config.suManager.isNotEmpty())
|
if (Config.suManager.isNotEmpty())
|
||||||
Config.suManager = ""
|
Config.suManager = ""
|
||||||
pkg ?: return
|
pkg ?: return
|
||||||
Shell.cmd("(pm uninstall $pkg)& >/dev/null 2>&1").exec()
|
Shell.cmd("(pm uninstall $pkg)& >/dev/null 2>&1").exec()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -7,10 +7,11 @@ import android.content.pm.PackageManager
|
|||||||
import android.content.pm.PackageManager.*
|
import android.content.pm.PackageManager.*
|
||||||
import android.content.pm.ServiceInfo
|
import android.content.pm.ServiceInfo
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.os.Build
|
||||||
import android.os.Build.VERSION.SDK_INT
|
import android.os.Build.VERSION.SDK_INT
|
||||||
import androidx.core.os.ProcessCompat
|
import androidx.core.os.ProcessCompat
|
||||||
|
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||||
import com.topjohnwu.magisk.ktx.getLabel
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class CmdlineListItem(line: String) {
|
class CmdlineListItem(line: String) {
|
||||||
@@ -67,7 +68,8 @@ class AppProcessInfo(
|
|||||||
val proc = info.processName ?: info.packageName
|
val proc = info.processName ?: info.packageName
|
||||||
createProcess("${proc}_zygote")
|
createProcess("${proc}_zygote")
|
||||||
} else {
|
} else {
|
||||||
val proc = if (SDK_INT >= 29) "${it.getProcName()}:${it.name}" else it.getProcName()
|
val proc = if (SDK_INT >= Build.VERSION_CODES.Q)
|
||||||
|
"${it.getProcName()}:${it.name}" else it.getProcName()
|
||||||
createProcess(proc, ISOLATED_MAGIC)
|
createProcess(proc, ISOLATED_MAGIC)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@@ -1,33 +1,32 @@
|
|||||||
package com.topjohnwu.magisk.ui.deny
|
package com.topjohnwu.magisk.ui.deny
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.BaseFragment
|
import com.topjohnwu.magisk.arch.BaseFragment
|
||||||
import com.topjohnwu.magisk.arch.viewModel
|
import com.topjohnwu.magisk.arch.viewModel
|
||||||
|
import com.topjohnwu.magisk.core.ktx.hideKeyboard
|
||||||
import com.topjohnwu.magisk.databinding.FragmentDenyMd2Binding
|
import com.topjohnwu.magisk.databinding.FragmentDenyMd2Binding
|
||||||
import com.topjohnwu.magisk.ktx.hideKeyboard
|
|
||||||
import rikka.recyclerview.addEdgeSpacing
|
import rikka.recyclerview.addEdgeSpacing
|
||||||
import rikka.recyclerview.addItemSpacing
|
import rikka.recyclerview.addItemSpacing
|
||||||
import rikka.recyclerview.fixEdgeEffect
|
import rikka.recyclerview.fixEdgeEffect
|
||||||
|
|
||||||
class DenyListFragment : BaseFragment<FragmentDenyMd2Binding>() {
|
class DenyListFragment : BaseFragment<FragmentDenyMd2Binding>(), MenuProvider {
|
||||||
|
|
||||||
override val layoutRes = R.layout.fragment_deny_md2
|
override val layoutRes = R.layout.fragment_deny_md2
|
||||||
override val viewModel by viewModel<DenyListViewModel>()
|
override val viewModel by viewModel<DenyListViewModel>()
|
||||||
|
|
||||||
private lateinit var searchView: SearchView
|
private lateinit var searchView: SearchView
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onStart() {
|
||||||
super.onAttach(context)
|
super.onStart()
|
||||||
activity?.setTitle(R.string.denylist)
|
activity?.setTitle(R.string.denylist)
|
||||||
setHasOptionsMenu(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
@@ -56,7 +55,7 @@ class DenyListFragment : BaseFragment<FragmentDenyMd2Binding>() {
|
|||||||
return super.onBackPressed()
|
return super.onBackPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
inflater.inflate(R.menu.menu_deny_md2, menu)
|
inflater.inflate(R.menu.menu_deny_md2, menu)
|
||||||
searchView = menu.findItem(R.id.action_search).actionView as SearchView
|
searchView = menu.findItem(R.id.action_search).actionView as SearchView
|
||||||
searchView.queryHint = searchView.context.getString(R.string.hide_filter_hint)
|
searchView.queryHint = searchView.context.getString(R.string.hide_filter_hint)
|
||||||
@@ -73,7 +72,7 @@ class DenyListFragment : BaseFragment<FragmentDenyMd2Binding>() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onMenuItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_show_system -> {
|
R.id.action_show_system -> {
|
||||||
val check = !item.isChecked
|
val check = !item.isChecked
|
||||||
@@ -91,7 +90,7 @@ class DenyListFragment : BaseFragment<FragmentDenyMd2Binding>() {
|
|||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
override fun onPrepareMenu(menu: Menu) {
|
||||||
val showSystem = menu.findItem(R.id.action_show_system)
|
val showSystem = menu.findItem(R.id.action_show_system)
|
||||||
val showOS = menu.findItem(R.id.action_show_OS)
|
val showOS = menu.findItem(R.id.action_show_OS)
|
||||||
showOS.isEnabled = showSystem.isChecked
|
showOS.isEnabled = showSystem.isChecked
|
||||||
|
@@ -5,17 +5,17 @@ import android.view.ViewGroup
|
|||||||
import androidx.databinding.Bindable
|
import androidx.databinding.Bindable
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.databinding.ComparableRv
|
import com.topjohnwu.magisk.arch.startAnimations
|
||||||
import com.topjohnwu.magisk.databinding.ObservableDiffRvItem
|
import com.topjohnwu.magisk.databinding.DiffItem
|
||||||
|
import com.topjohnwu.magisk.databinding.ObservableRvItem
|
||||||
import com.topjohnwu.magisk.databinding.addOnPropertyChangedCallback
|
import com.topjohnwu.magisk.databinding.addOnPropertyChangedCallback
|
||||||
import com.topjohnwu.magisk.databinding.set
|
import com.topjohnwu.magisk.databinding.set
|
||||||
import com.topjohnwu.magisk.ktx.startAnimations
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class DenyListRvItem(
|
class DenyListRvItem(
|
||||||
val info: AppProcessInfo
|
val info: AppProcessInfo
|
||||||
) : ObservableDiffRvItem<DenyListRvItem>(), ComparableRv<DenyListRvItem> {
|
) : ObservableRvItem(), DiffItem<DenyListRvItem>, Comparable<DenyListRvItem> {
|
||||||
|
|
||||||
override val layoutRes get() = R.layout.item_hide_md2
|
override val layoutRes get() = R.layout.item_hide_md2
|
||||||
|
|
||||||
@@ -44,9 +44,18 @@ class DenyListRvItem(
|
|||||||
processes
|
processes
|
||||||
.filterNot { it.isEnabled }
|
.filterNot { it.isEnabled }
|
||||||
.filter { isExpanded || it.defaultSelection }
|
.filter { isExpanded || it.defaultSelection }
|
||||||
|
.forEach { it.toggle() }
|
||||||
} else {
|
} else {
|
||||||
processes.filter { it.isEnabled }
|
Shell.cmd("magisk --denylist rm ${info.packageName}").submit()
|
||||||
}.forEach { it.toggle() }
|
processes.filter { it.isEnabled }.forEach {
|
||||||
|
if (it.process.isIsolated) {
|
||||||
|
it.toggle()
|
||||||
|
} else {
|
||||||
|
it.isEnabled = !it.isEnabled
|
||||||
|
notifyPropertyChanged(BR.enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -91,7 +100,7 @@ class DenyListRvItem(
|
|||||||
|
|
||||||
class ProcessRvItem(
|
class ProcessRvItem(
|
||||||
val process: ProcessInfo
|
val process: ProcessInfo
|
||||||
) : ObservableDiffRvItem<ProcessRvItem>() {
|
) : ObservableRvItem(), DiffItem<ProcessRvItem> {
|
||||||
|
|
||||||
override val layoutRes get() = R.layout.item_hide_process_md2
|
override val layoutRes get() = R.layout.item_hide_process_md2
|
||||||
|
|
||||||
@@ -113,10 +122,9 @@ class ProcessRvItem(
|
|||||||
val defaultSelection get() =
|
val defaultSelection get() =
|
||||||
process.isIsolated || process.isAppZygote || process.name == process.packageName
|
process.isIsolated || process.isAppZygote || process.name == process.packageName
|
||||||
|
|
||||||
override fun contentSameAs(other: ProcessRvItem) =
|
|
||||||
process.isEnabled == other.process.isEnabled
|
|
||||||
|
|
||||||
override fun itemSameAs(other: ProcessRvItem) =
|
override fun itemSameAs(other: ProcessRvItem) =
|
||||||
process.name == other.process.name && process.packageName == other.process.packageName
|
process.name == other.process.name && process.packageName == other.process.packageName
|
||||||
|
|
||||||
|
override fun contentSameAs(other: ProcessRvItem) =
|
||||||
|
process.isEnabled == other.process.isEnabled
|
||||||
}
|
}
|
||||||
|
@@ -3,13 +3,14 @@ package com.topjohnwu.magisk.ui.deny
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
|
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||||
import androidx.databinding.Bindable
|
import androidx.databinding.Bindable
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
||||||
import com.topjohnwu.magisk.core.di.AppContext
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
|
import com.topjohnwu.magisk.core.ktx.concurrentMap
|
||||||
import com.topjohnwu.magisk.databinding.bindExtra
|
import com.topjohnwu.magisk.databinding.bindExtra
|
||||||
import com.topjohnwu.magisk.databinding.filterableListOf
|
import com.topjohnwu.magisk.databinding.filterList
|
||||||
import com.topjohnwu.magisk.databinding.set
|
import com.topjohnwu.magisk.databinding.set
|
||||||
import com.topjohnwu.magisk.ktx.concurrentMap
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.asFlow
|
||||||
@@ -22,22 +23,22 @@ class DenyListViewModel : AsyncLoadViewModel() {
|
|||||||
var isShowSystem = false
|
var isShowSystem = false
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
query()
|
doQuery(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
var isShowOS = false
|
var isShowOS = false
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
query()
|
doQuery(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
var query = ""
|
var query = ""
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
query()
|
doQuery(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
val items = filterableListOf<DenyListRvItem>()
|
val items = filterList<DenyListRvItem>(viewModelScope)
|
||||||
val extraBindings = bindExtra {
|
val extraBindings = bindExtra {
|
||||||
it.put(BR.viewModel, this)
|
it.put(BR.viewModel, this)
|
||||||
}
|
}
|
||||||
@@ -49,7 +50,7 @@ class DenyListViewModel : AsyncLoadViewModel() {
|
|||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
override suspend fun doLoadWork() {
|
override suspend fun doLoadWork() {
|
||||||
loading = true
|
loading = true
|
||||||
val (apps, diff) = withContext(Dispatchers.Default) {
|
val apps = withContext(Dispatchers.Default) {
|
||||||
val pm = AppContext.packageManager
|
val pm = AppContext.packageManager
|
||||||
val denyList = Shell.cmd("magisk --denylist ls").exec().out
|
val denyList = Shell.cmd("magisk --denylist ls").exec().out
|
||||||
.map { CmdlineListItem(it) }
|
.map { CmdlineListItem(it) }
|
||||||
@@ -62,22 +63,22 @@ class DenyListViewModel : AsyncLoadViewModel() {
|
|||||||
.toCollection(ArrayList(size))
|
.toCollection(ArrayList(size))
|
||||||
}
|
}
|
||||||
apps.sort()
|
apps.sort()
|
||||||
apps to items.calculateDiff(apps)
|
apps
|
||||||
}
|
}
|
||||||
items.update(apps, diff)
|
items.set(apps)
|
||||||
query()
|
doQuery(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun query() {
|
private fun doQuery(s: String) {
|
||||||
items.filter {
|
items.filter {
|
||||||
fun filterSystem() = isShowSystem || !it.info.isSystemApp()
|
fun filterSystem() = isShowSystem || !it.info.isSystemApp()
|
||||||
|
|
||||||
fun filterOS() = (isShowSystem && isShowOS) || it.info.isApp()
|
fun filterOS() = (isShowSystem && isShowOS) || it.info.isApp()
|
||||||
|
|
||||||
fun filterQuery(): Boolean {
|
fun filterQuery(): Boolean {
|
||||||
fun inName() = it.info.label.contains(query, true)
|
fun inName() = it.info.label.contains(s, true)
|
||||||
fun inPackage() = it.info.packageName.contains(query, true)
|
fun inPackage() = it.info.packageName.contains(s, true)
|
||||||
fun inProcesses() = it.processes.any { p -> p.process.name.contains(query, true) }
|
fun inProcesses() = it.processes.any { p -> p.process.name.contains(s, true) }
|
||||||
return inName() || inPackage() || inProcesses()
|
return inName() || inPackage() || inProcesses()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -6,14 +6,15 @@ import androidx.core.view.updateLayoutParams
|
|||||||
import androidx.databinding.ViewDataBinding
|
import androidx.databinding.ViewDataBinding
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.databinding.DiffRvItem
|
import com.topjohnwu.magisk.databinding.DiffItem
|
||||||
import com.topjohnwu.magisk.databinding.RvContainer
|
import com.topjohnwu.magisk.databinding.ItemWrapper
|
||||||
import com.topjohnwu.magisk.databinding.ViewAwareRvItem
|
import com.topjohnwu.magisk.databinding.RvItem
|
||||||
|
import com.topjohnwu.magisk.databinding.ViewAwareItem
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class ConsoleItem(
|
class ConsoleItem(
|
||||||
override val item: String
|
override val item: String
|
||||||
) : DiffRvItem<ConsoleItem>(), ViewAwareRvItem, RvContainer<String> {
|
) : RvItem(), ViewAwareItem, DiffItem<ConsoleItem>, ItemWrapper<String> {
|
||||||
override val layoutRes = R.layout.item_console_md2
|
override val layoutRes = R.layout.item_console_md2
|
||||||
|
|
||||||
private var parentWidth = -1
|
private var parentWidth = -1
|
||||||
|
@@ -6,6 +6,7 @@ import android.content.pm.ActivityInfo
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.*
|
import android.view.*
|
||||||
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.navigation.NavDeepLinkBuilder
|
import androidx.navigation.NavDeepLinkBuilder
|
||||||
import com.topjohnwu.magisk.MainDirections
|
import com.topjohnwu.magisk.MainDirections
|
||||||
@@ -17,7 +18,7 @@ import com.topjohnwu.magisk.core.cmp
|
|||||||
import com.topjohnwu.magisk.databinding.FragmentFlashMd2Binding
|
import com.topjohnwu.magisk.databinding.FragmentFlashMd2Binding
|
||||||
import com.topjohnwu.magisk.ui.MainActivity
|
import com.topjohnwu.magisk.ui.MainActivity
|
||||||
|
|
||||||
class FlashFragment : BaseFragment<FragmentFlashMd2Binding>() {
|
class FlashFragment : BaseFragment<FragmentFlashMd2Binding>(), MenuProvider {
|
||||||
|
|
||||||
override val layoutRes = R.layout.fragment_flash_md2
|
override val layoutRes = R.layout.fragment_flash_md2
|
||||||
override val viewModel by viewModel<FlashViewModel>()
|
override val viewModel by viewModel<FlashViewModel>()
|
||||||
@@ -34,7 +35,6 @@ class FlashFragment : BaseFragment<FragmentFlashMd2Binding>() {
|
|||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
setHasOptionsMenu(true)
|
|
||||||
activity?.setTitle(R.string.flash_screen_title)
|
activity?.setTitle(R.string.flash_screen_title)
|
||||||
|
|
||||||
viewModel.state.observe(this) {
|
viewModel.state.observe(this) {
|
||||||
@@ -54,11 +54,11 @@ class FlashFragment : BaseFragment<FragmentFlashMd2Binding>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
inflater.inflate(R.menu.menu_flash, menu)
|
inflater.inflate(R.menu.menu_flash, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onMenuItemSelected(item: MenuItem): Boolean {
|
||||||
return viewModel.onMenuItemClicked(item)
|
return viewModel.onMenuItemClicked(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,26 +2,26 @@ package com.topjohnwu.magisk.ui.flash
|
|||||||
|
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.databinding.Bindable
|
import androidx.databinding.Bindable
|
||||||
|
import androidx.databinding.ObservableArrayList
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.Transformations
|
import androidx.lifecycle.map
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
import com.topjohnwu.magisk.core.ktx.reboot
|
||||||
|
import com.topjohnwu.magisk.core.ktx.synchronized
|
||||||
|
import com.topjohnwu.magisk.core.ktx.timeFormatStandard
|
||||||
|
import com.topjohnwu.magisk.core.ktx.toTime
|
||||||
import com.topjohnwu.magisk.core.tasks.FlashZip
|
import com.topjohnwu.magisk.core.tasks.FlashZip
|
||||||
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||||
import com.topjohnwu.magisk.databinding.diffListOf
|
|
||||||
import com.topjohnwu.magisk.databinding.set
|
import com.topjohnwu.magisk.databinding.set
|
||||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||||
import com.topjohnwu.magisk.ktx.reboot
|
|
||||||
import com.topjohnwu.magisk.ktx.synchronized
|
|
||||||
import com.topjohnwu.magisk.ktx.timeFormatStandard
|
|
||||||
import com.topjohnwu.magisk.ktx.toTime
|
|
||||||
import com.topjohnwu.superuser.CallbackList
|
import com.topjohnwu.superuser.CallbackList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -34,13 +34,13 @@ class FlashViewModel : BaseViewModel() {
|
|||||||
|
|
||||||
private val _state = MutableLiveData(State.FLASHING)
|
private val _state = MutableLiveData(State.FLASHING)
|
||||||
val state: LiveData<State> get() = _state
|
val state: LiveData<State> get() = _state
|
||||||
val flashing = Transformations.map(state) { it == State.FLASHING }
|
val flashing = state.map { it == State.FLASHING }
|
||||||
|
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
var showReboot = Info.isRooted
|
var showReboot = Info.isRooted
|
||||||
set(value) = set(value, field, { field = it }, BR.showReboot)
|
set(value) = set(value, field, { field = it }, BR.showReboot)
|
||||||
|
|
||||||
val items = diffListOf<ConsoleItem>()
|
val items = ObservableArrayList<ConsoleItem>()
|
||||||
lateinit var args: FlashFragmentArgs
|
lateinit var args: FlashFragmentArgs
|
||||||
|
|
||||||
private val logItems = mutableListOf<String>().synchronized()
|
private val logItems = mutableListOf<String>().synchronized()
|
||||||
|
@@ -24,6 +24,10 @@ private interface RikkaImpl : Dev {
|
|||||||
override val name get() = "RikkaW"
|
override val name get() = "RikkaW"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private interface CanyieImpl : Dev {
|
||||||
|
override val name get() = "canyie"
|
||||||
|
}
|
||||||
|
|
||||||
sealed class DeveloperItem : Dev {
|
sealed class DeveloperItem : Dev {
|
||||||
|
|
||||||
abstract val items: List<IconLink>
|
abstract val items: List<IconLink>
|
||||||
@@ -61,6 +65,14 @@ sealed class DeveloperItem : Dev {
|
|||||||
object : IconLink.Github.User(), RikkaImpl {}
|
object : IconLink.Github.User(), RikkaImpl {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object Canyie : DeveloperItem(), CanyieImpl {
|
||||||
|
override val items =
|
||||||
|
listOf<IconLink>(
|
||||||
|
object : IconLink.Twitter() { override val name = "canyie2977" },
|
||||||
|
object : IconLink.Github.User(), CanyieImpl {}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class IconLink : RvItem() {
|
sealed class IconLink : RvItem() {
|
||||||
|
@@ -1,26 +1,30 @@
|
|||||||
package com.topjohnwu.magisk.ui.home
|
package com.topjohnwu.magisk.ui.home
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.*
|
import android.view.LayoutInflater
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.MenuProvider
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.BaseFragment
|
import com.topjohnwu.magisk.arch.BaseFragment
|
||||||
import com.topjohnwu.magisk.arch.viewModel
|
import com.topjohnwu.magisk.arch.viewModel
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.download.DownloadService
|
import com.topjohnwu.magisk.core.download.DownloadService
|
||||||
import com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding
|
import com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding
|
||||||
import com.topjohnwu.magisk.events.RebootEvent
|
|
||||||
|
|
||||||
class HomeFragment : BaseFragment<FragmentHomeMd2Binding>() {
|
class HomeFragment : BaseFragment<FragmentHomeMd2Binding>(), MenuProvider {
|
||||||
|
|
||||||
override val layoutRes = R.layout.fragment_home_md2
|
override val layoutRes = R.layout.fragment_home_md2
|
||||||
override val viewModel by viewModel<HomeViewModel>()
|
override val viewModel by viewModel<HomeViewModel>()
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
activity?.title = resources.getString(R.string.section_home)
|
activity?.setTitle(R.string.section_home)
|
||||||
setHasOptionsMenu(true)
|
|
||||||
DownloadService.observeProgress(this, viewModel::onProgressUpdate)
|
DownloadService.observeProgress(this, viewModel::onProgressUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,17 +58,17 @@ class HomeFragment : BaseFragment<FragmentHomeMd2Binding>() {
|
|||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
inflater.inflate(R.menu.menu_home_md2, menu)
|
inflater.inflate(R.menu.menu_home_md2, menu)
|
||||||
if (!Info.isRooted)
|
if (!Info.isRooted)
|
||||||
menu.removeItem(R.id.action_reboot)
|
menu.removeItem(R.id.action_reboot)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onMenuItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_settings ->
|
R.id.action_settings ->
|
||||||
HomeFragmentDirections.actionHomeFragmentToSettingsFragment().navigate()
|
HomeFragmentDirections.actionHomeFragmentToSettingsFragment().navigate()
|
||||||
R.id.action_reboot -> activity?.let { RebootEvent.inflateMenu(it).show() }
|
R.id.action_reboot -> activity?.let { RebootMenu.inflate(it).show() }
|
||||||
else -> return super.onOptionsItemSelected(item)
|
else -> return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
@@ -1,25 +1,32 @@
|
|||||||
package com.topjohnwu.magisk.ui.home
|
package com.topjohnwu.magisk.ui.home
|
||||||
|
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.databinding.Bindable
|
import androidx.databinding.Bindable
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.*
|
import com.topjohnwu.magisk.arch.ActivityExecutor
|
||||||
|
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
||||||
|
import com.topjohnwu.magisk.arch.ContextExecutor
|
||||||
|
import com.topjohnwu.magisk.arch.UIActivity
|
||||||
|
import com.topjohnwu.magisk.arch.ViewEvent
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.download.Subject
|
import com.topjohnwu.magisk.core.download.Subject
|
||||||
import com.topjohnwu.magisk.core.download.Subject.App
|
import com.topjohnwu.magisk.core.download.Subject.App
|
||||||
|
import com.topjohnwu.magisk.core.ktx.await
|
||||||
|
import com.topjohnwu.magisk.core.ktx.toast
|
||||||
import com.topjohnwu.magisk.core.repository.NetworkService
|
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||||
import com.topjohnwu.magisk.databinding.bindExtra
|
import com.topjohnwu.magisk.databinding.bindExtra
|
||||||
import com.topjohnwu.magisk.databinding.set
|
import com.topjohnwu.magisk.databinding.set
|
||||||
|
import com.topjohnwu.magisk.dialog.EnvFixDialog
|
||||||
|
import com.topjohnwu.magisk.dialog.ManagerInstallDialog
|
||||||
|
import com.topjohnwu.magisk.dialog.UninstallDialog
|
||||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||||
import com.topjohnwu.magisk.events.dialog.EnvFixDialog
|
|
||||||
import com.topjohnwu.magisk.events.dialog.ManagerInstallDialog
|
|
||||||
import com.topjohnwu.magisk.events.dialog.UninstallDialog
|
|
||||||
import com.topjohnwu.magisk.ktx.await
|
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import com.topjohnwu.magisk.utils.asText
|
import com.topjohnwu.magisk.utils.asText
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -43,6 +50,7 @@ class HomeViewModel(
|
|||||||
|
|
||||||
val magiskState
|
val magiskState
|
||||||
get() = when {
|
get() = when {
|
||||||
|
Info.isRooted && Info.env.isUnsupported -> State.OUTDATED
|
||||||
!Info.env.isActive -> State.INVALID
|
!Info.env.isActive -> State.INVALID
|
||||||
Info.env.versionCode < BuildConfig.VERSION_CODE -> State.OUTDATED
|
Info.env.versionCode < BuildConfig.VERSION_CODE -> State.OUTDATED
|
||||||
else -> State.UP_TO_DATE
|
else -> State.UP_TO_DATE
|
||||||
@@ -66,7 +74,6 @@ class HomeViewModel(
|
|||||||
|
|
||||||
val managerInstalledVersion
|
val managerInstalledVersion
|
||||||
get() = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})" +
|
get() = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})" +
|
||||||
Info.stub?.let { " (${it.version})" }.orEmpty() +
|
|
||||||
if (BuildConfig.DEBUG) " (D)" else ""
|
if (BuildConfig.DEBUG) " (D)" else ""
|
||||||
|
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
@@ -91,7 +98,7 @@ class HomeViewModel(
|
|||||||
|
|
||||||
val isDebug = Config.updateChannel == Config.Value.DEBUG_CHANNEL
|
val isDebug = Config.updateChannel == Config.Value.DEBUG_CHANNEL
|
||||||
managerRemoteVersion =
|
managerRemoteVersion =
|
||||||
("${magisk.version} (${magisk.versionCode}) (${stub.versionCode})" +
|
("${magisk.version} (${magisk.versionCode})" +
|
||||||
if (isDebug) " (D)" else "").asText()
|
if (isDebug) " (D)" else "").asText()
|
||||||
} ?: run {
|
} ?: run {
|
||||||
appState = State.INVALID
|
appState = State.INVALID
|
||||||
@@ -108,17 +115,25 @@ class HomeViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onLinkPressed(link: String) = object : ViewEvent(), ContextExecutor {
|
fun onLinkPressed(link: String) = object : ViewEvent(), ContextExecutor {
|
||||||
override fun invoke(context: Context) = Utils.openLink(context, link.toUri())
|
override fun invoke(context: Context) {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, link.toUri())
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
try {
|
||||||
|
context.startActivity(intent)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
context.toast(R.string.open_link_failed_toast, Toast.LENGTH_SHORT)
|
||||||
|
}
|
||||||
|
}
|
||||||
}.publish()
|
}.publish()
|
||||||
|
|
||||||
fun onDeletePressed() = UninstallDialog().publish()
|
fun onDeletePressed() = UninstallDialog().show()
|
||||||
|
|
||||||
fun onManagerPressed() = when (appState) {
|
fun onManagerPressed() = when (appState) {
|
||||||
State.LOADING -> SnackbarEvent(R.string.loading).publish()
|
State.LOADING -> SnackbarEvent(R.string.loading).publish()
|
||||||
State.INVALID -> SnackbarEvent(R.string.no_connection).publish()
|
State.INVALID -> SnackbarEvent(R.string.no_connection).publish()
|
||||||
else -> withExternalRW {
|
else -> withExternalRW {
|
||||||
withInstallPermission {
|
withInstallPermission {
|
||||||
ManagerInstallDialog().publish()
|
ManagerInstallDialog().show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,8 +150,9 @@ class HomeViewModel(
|
|||||||
private suspend fun ensureEnv() {
|
private suspend fun ensureEnv() {
|
||||||
if (magiskState == State.INVALID || checkedEnv) return
|
if (magiskState == State.INVALID || checkedEnv) return
|
||||||
val cmd = "env_check ${Info.env.versionString} ${Info.env.versionCode}"
|
val cmd = "env_check ${Info.env.versionString} ${Info.env.versionCode}"
|
||||||
if (!Shell.cmd(cmd).await().isSuccess) {
|
val code = Shell.cmd(cmd).await().code
|
||||||
EnvFixDialog(this).publish()
|
if (code != 0) {
|
||||||
|
EnvFixDialog(this, code).show()
|
||||||
}
|
}
|
||||||
checkedEnv = true
|
checkedEnv = true
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user