mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-12-13 08:00:57 -08:00
Compare commits
1676 Commits
manager-v8
...
v26.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
6066b5cf86 | ||
|
|
5cdf95a4d0 | ||
|
|
910a36fdc1 | ||
|
|
8331206acb | ||
|
|
8423dc8d63 | ||
|
|
6077c989a7 | ||
|
|
c97d1044fa | ||
|
|
f42c089b26 | ||
|
|
1f8c063dc6 | ||
|
|
4874520d65 | ||
|
|
5e53639969 | ||
|
|
83ab0ca6cd | ||
|
|
70fd03d5fc | ||
|
|
2e52875b50 | ||
|
|
fd9b990ad7 | ||
|
|
69978a9442 | ||
|
|
d155da52ce | ||
|
|
9c5b131913 | ||
|
|
9d740cec1a | ||
|
|
c2978eb9c3 | ||
|
|
38abad1e44 | ||
|
|
b4863eb51b | ||
|
|
3817167ba1 | ||
|
|
d1a35dd2ba | ||
|
|
26116ac414 | ||
|
|
0b26882fce | ||
|
|
a2495fb5fb | ||
|
|
0beb3bf16a | ||
|
|
b68658e974 | ||
|
|
3ae7344747 | ||
|
|
4eb71830b3 | ||
|
|
9183a0a6ea | ||
|
|
bb64ba0ef6 | ||
|
|
d89a568897 | ||
|
|
9fd1f41e8b | ||
|
|
c1ab348673 | ||
|
|
00247c7901 | ||
|
|
3c75f474c6 | ||
|
|
db1f5b0397 | ||
|
|
db277c3e55 | ||
|
|
b9c93c66f6 | ||
|
|
a250e2b56c | ||
|
|
cd96454886 | ||
|
|
741b679306 | ||
|
|
90013e486d | ||
|
|
4e2ecdb920 | ||
|
|
6e5df1f06b | ||
|
|
9469e79e3c | ||
|
|
db78c20161 | ||
|
|
1699da1754 | ||
|
|
754e690274 | ||
|
|
6f74ed6ceb | ||
|
|
71205bc530 | ||
|
|
10e236abdf | ||
|
|
2248af00f3 | ||
|
|
7e61716277 | ||
|
|
50edb8d072 | ||
|
|
515f81944c | ||
|
|
46d4708386 | ||
|
|
aabc36f86b | ||
|
|
e0d5d90267 | ||
|
|
482a5b991b | ||
|
|
20124fe410 | ||
|
|
f8dcec116a | ||
|
|
343a339aae | ||
|
|
42606efe56 | ||
|
|
cae58c8790 | ||
|
|
3a39dd4049 | ||
|
|
89ff3c6572 | ||
|
|
7bf9c74216 | ||
|
|
e2f3753551 | ||
|
|
cacf873645 | ||
|
|
11e1e7ee36 | ||
|
|
87801b6f23 | ||
|
|
7ce4789e17 | ||
|
|
9dc6d9afce | ||
|
|
d6a5354bff | ||
|
|
07af37475b | ||
|
|
1b9c273b10 | ||
|
|
262c52db56 | ||
|
|
eb777296d4 | ||
|
|
fc70a384d3 | ||
|
|
34b2f525a3 | ||
|
|
569e9ad937 | ||
|
|
c495b3d183 | ||
|
|
8b16bfbb54 | ||
|
|
b2f1fd9966 | ||
|
|
317153c53a | ||
|
|
fa60daf9b5 | ||
|
|
aadb2d825c | ||
|
|
0e7fe537e3 | ||
|
|
409de3ac44 | ||
|
|
759055eaa5 | ||
|
|
9016e6727d | ||
|
|
a3381da7ed | ||
|
|
351e094440 | ||
|
|
2106751ea4 | ||
|
|
7ae3cd1c43 | ||
|
|
edfd4dcddf | ||
|
|
fb89cf1367 | ||
|
|
b7b345cf8a | ||
|
|
0be487e47e | ||
|
|
5471147422 | ||
|
|
6305159c5e | ||
|
|
2ed092c9db | ||
|
|
5c6a7ffa6f | ||
|
|
9ab7550970 | ||
|
|
47e7a0a434 | ||
|
|
4cc5e9f986 | ||
|
|
6a2ae89846 | ||
|
|
3c93539e02 | ||
|
|
05e5ac2ad2 | ||
|
|
10b1782732 | ||
|
|
e029994ef8 | ||
|
|
9679874874 | ||
|
|
8186f253e8 | ||
|
|
d4fe8632ec | ||
|
|
d7776f6597 | ||
|
|
3219d945f5 | ||
|
|
8a73a16029 | ||
|
|
ce90f9b60d | ||
|
|
bdf54d562f | ||
|
|
e744cc8ea6 | ||
|
|
babcf36495 | ||
|
|
e4094c0caa | ||
|
|
2e51fe20a1 | ||
|
|
c29636c452 | ||
|
|
22017a5543 | ||
|
|
50e2f33d1c | ||
|
|
5e6eb8dd01 | ||
|
|
18acb97dfe | ||
|
|
bf2f823b8c | ||
|
|
d0c4226997 | ||
|
|
4ea8bd0229 | ||
|
|
ee0d58a9b8 | ||
|
|
bf04fa134b | ||
|
|
297662cafb | ||
|
|
f464a9b269 | ||
|
|
d19fcd5e21 | ||
|
|
c0981174a8 | ||
|
|
0b5f973b31 | ||
|
|
4159b3871c | ||
|
|
580c993c0b | ||
|
|
0cc29350a0 | ||
|
|
490a784993 | ||
|
|
9c774f96db | ||
|
|
99afe7ac07 | ||
|
|
b3f05fd925 | ||
|
|
683cfee88b | ||
|
|
3bcaf0ed5b | ||
|
|
edb76503d3 | ||
|
|
484038638f | ||
|
|
8dfb30fefe | ||
|
|
2a252d13b8 | ||
|
|
afa364cfc3 | ||
|
|
dfa36fb25d | ||
|
|
c8492b0c58 | ||
|
|
083ef803fe | ||
|
|
351f0269ae | ||
|
|
a29ae15ff7 | ||
|
|
34dded3b25 | ||
|
|
975b1a5e36 | ||
|
|
e11508f84d | ||
|
|
0772f6dcaf | ||
|
|
d3fe3a711a | ||
|
|
756d8356ca | ||
|
|
42003b4006 | ||
|
|
dc65a2b884 | ||
|
|
071ae79fa8 | ||
|
|
c11ccbae2d | ||
|
|
6ef86d8d20 | ||
|
|
985249c3d0 | ||
|
|
622e09862a | ||
|
|
7505599ea0 | ||
|
|
575c417403 | ||
|
|
9f7a3db8be | ||
|
|
029422679c | ||
|
|
05d6d2b51b | ||
|
|
4cff0384f7 | ||
|
|
68db366696 | ||
|
|
358538717c | ||
|
|
24603b3cef | ||
|
|
4eb9240806 | ||
|
|
0469f0b5ae | ||
|
|
0b8577d02b | ||
|
|
97135879a1 | ||
|
|
fef41f68c0 | ||
|
|
0ac19e3a4e | ||
|
|
2793d209a4 | ||
|
|
71e9c044e6 | ||
|
|
42e5f5150a | ||
|
|
90545057e9 | ||
|
|
cffd024e9e | ||
|
|
8c858592c4 | ||
|
|
4f1a1879e5 | ||
|
|
e88eed9a8d | ||
|
|
9581ae8245 | ||
|
|
4202b7a9dc | ||
|
|
b4c398542a | ||
|
|
081148b2d7 | ||
|
|
a32c4561ed | ||
|
|
cc79a96fa3 | ||
|
|
ff340ce3d8 | ||
|
|
134508193d | ||
|
|
c2b74aa83e | ||
|
|
3358eab991 | ||
|
|
a609e0aad4 | ||
|
|
f97866a961 | ||
|
|
e1987c42c4 | ||
|
|
18566715e1 | ||
|
|
79f0f3230c | ||
|
|
63a89d9f04 | ||
|
|
f639f39e79 | ||
|
|
b4099fc5f9 | ||
|
|
ff2513e276 | ||
|
|
f24d52436b | ||
|
|
9de6e8846b | ||
|
|
01a1213463 | ||
|
|
f0fbd9214a | ||
|
|
c4f37c550f | ||
|
|
448384af06 | ||
|
|
3f840f53a0 | ||
|
|
d8718d8ac8 | ||
|
|
2fb46a11dc | ||
|
|
9a11412719 | ||
|
|
98874be171 | ||
|
|
704f91545e | ||
|
|
efb3239cbd | ||
|
|
7e7ddeb9e2 | ||
|
|
9e8218089b | ||
|
|
3f660a3963 | ||
|
|
daeb6711b0 | ||
|
|
4e1aec28a0 | ||
|
|
5512917ec1 | ||
|
|
cd1edc5d56 | ||
|
|
4f52587586 | ||
|
|
d7ee4ef5f5 | ||
|
|
31f88e0f05 | ||
|
|
9f1740cc4f | ||
|
|
f2c15c7701 | ||
|
|
e67d0678f9 | ||
|
|
b1faa5eed4 | ||
|
|
7f1f0b9048 | ||
|
|
183e5f2ecc | ||
|
|
14efe4939a | ||
|
|
3dc7d77ea9 | ||
|
|
0f07bbb3e5 | ||
|
|
dd5a3416bf | ||
|
|
2fb49ad780 | ||
|
|
92f0e53fee | ||
|
|
876132694d | ||
|
|
1257ba41c6 | ||
|
|
2cc71ac7ed | ||
|
|
753808a4ce | ||
|
|
32cd694ad5 | ||
|
|
f008420891 | ||
|
|
fa8900be65 | ||
|
|
69c2f407d6 | ||
|
|
ffcd093db1 | ||
|
|
8dbf93750f | ||
|
|
e266a81167 | ||
|
|
e841aab9e7 | ||
|
|
49f259065d | ||
|
|
b10379e700 | ||
|
|
810d27a618 | ||
|
|
9b60c005c7 | ||
|
|
cc6ca0bda2 | ||
|
|
4512232637 | ||
|
|
2c092ffdef | ||
|
|
66406227d6 | ||
|
|
a11d25bb44 | ||
|
|
2e58d902b7 | ||
|
|
237794b05c | ||
|
|
563a587882 | ||
|
|
24505cd111 | ||
|
|
0c681cdab4 | ||
|
|
13ef3058c6 | ||
|
|
50b159b43d | ||
|
|
8c6c328730 | ||
|
|
c9812ddf08 | ||
|
|
2ef0449c2c | ||
|
|
5edc750c47 | ||
|
|
2f0e396d7f | ||
|
|
000a163beb | ||
|
|
80dd37ee31 | ||
|
|
e0b5645064 | ||
|
|
e51aacb0b7 | ||
|
|
2d6af94aa0 | ||
|
|
7cfce9ff7a | ||
|
|
7f088d6241 | ||
|
|
d11038f3de | ||
|
|
6df42a4be7 | ||
|
|
7fd111b91f | ||
|
|
dd7dc2ec5a | ||
|
|
86c586d882 | ||
|
|
66ac6f72fc | ||
|
|
f21f448099 | ||
|
|
548d70f30c | ||
|
|
39e714c6d8 | ||
|
|
9968af0785 | ||
|
|
be7586137c | ||
|
|
7999b66c3c | ||
|
|
c82a46c1ee | ||
|
|
666ab1941f | ||
|
|
71e37345b4 | ||
|
|
e7c82f20e3 | ||
|
|
afa771a980 | ||
|
|
0d1de98cca | ||
|
|
02bf7dca01 | ||
|
|
8cc76b1d86 | ||
|
|
77a275cbcd | ||
|
|
3956cbe2d2 | ||
|
|
945de8d9a0 | ||
|
|
6dabd3bb2d | ||
|
|
4c80808997 | ||
|
|
5a39f7cdde | ||
|
|
5d400fbe90 | ||
|
|
e36596470c | ||
|
|
668e549208 | ||
|
|
256ff31d11 | ||
|
|
2414d5d7f5 | ||
|
|
b7fc15d399 | ||
|
|
c09b4dabc4 | ||
|
|
a4aa4a91a3 | ||
|
|
8f0ea5925a | ||
|
|
936ad1aa20 | ||
|
|
d021bca6ef | ||
|
|
55ed6109c1 | ||
|
|
f6d765bf81 | ||
|
|
88e8f2bf83 | ||
|
|
c849759682 | ||
|
|
605eae21bc | ||
|
|
93eb277a88 | ||
|
|
8edf556c9e | ||
|
|
7fcb63230f | ||
|
|
12093a3dad | ||
|
|
ebb0ec6c42 | ||
|
|
188546515c | ||
|
|
c8990b0f68 | ||
|
|
7dced4b9d9 | ||
|
|
3145e67feb | ||
|
|
e9348d9b6a | ||
|
|
1a1b346c05 | ||
|
|
920d059837 | ||
|
|
bef5c3bd1b | ||
|
|
97037f7d03 | ||
|
|
a7392ed3d7 | ||
|
|
3eb1a7e384 | ||
|
|
1ecdc78c2f | ||
|
|
d279dba37e | ||
|
|
a4f97fa151 | ||
|
|
ff7ac582f0 | ||
|
|
d2c2456fbe | ||
|
|
e9f562a8b7 | ||
|
|
084e0a73dc | ||
|
|
10f991b8d0 | ||
|
|
79620c97d1 | ||
|
|
ffec9a4ddd | ||
|
|
9b18960bbd | ||
|
|
a009fdbdc3 | ||
|
|
c1fc3f373c | ||
|
|
f4cf5dc0cd | ||
|
|
355341f0ab | ||
|
|
7f65f7d3ca | ||
|
|
9fa096c6f4 | ||
|
|
70415a396a | ||
|
|
c921964938 | ||
|
|
3bf47a6838 | ||
|
|
d3d28f0623 | ||
|
|
f880b57544 | ||
|
|
32b7a26fa6 | ||
|
|
32fc34f922 | ||
|
|
b82a393692 | ||
|
|
3c7e792167 | ||
|
|
0ad66875ab | ||
|
|
1191ac2671 | ||
|
|
928b3425e3 | ||
|
|
0726a00e3b | ||
|
|
5a88984d34 | ||
|
|
18de60f68c | ||
|
|
1893359142 | ||
|
|
f5e5ab2436 | ||
|
|
ff5ea1a70d | ||
|
|
54ee63a409 | ||
|
|
f095606b50 | ||
|
|
e8f31c78d7 | ||
|
|
b34c477d5e | ||
|
|
28611304f7 | ||
|
|
76af9e6e1f | ||
|
|
7b3b965ed7 | ||
|
|
567b905ef1 | ||
|
|
a94268329c | ||
|
|
a11a18686a | ||
|
|
c58e3a99ee | ||
|
|
b166663e89 | ||
|
|
ac13ac14f6 | ||
|
|
06531f6d06 | ||
|
|
f6274d94f6 | ||
|
|
2b303a7e23 | ||
|
|
2bb074a5ad | ||
|
|
3b2db56243 | ||
|
|
45483fde74 | ||
|
|
d742cfa48f | ||
|
|
95353ce9eb | ||
|
|
ab2cc72814 | ||
|
|
5c54a2c008 | ||
|
|
2fe3082518 | ||
|
|
5a889d28c8 | ||
|
|
45e7c1c030 | ||
|
|
c6dcff0ae7 | ||
|
|
b791dc5e1a | ||
|
|
46db281006 | ||
|
|
636479b15b | ||
|
|
dcbb4eabb5 | ||
|
|
068cedaa84 | ||
|
|
02dd962601 | ||
|
|
256d715648 | ||
|
|
cbe97cdfde | ||
|
|
407dfc7547 | ||
|
|
a8e4e077ec | ||
|
|
3d06ba1878 | ||
|
|
8a23d1da58 | ||
|
|
d3eb61e0e4 | ||
|
|
7cdf2d244d | ||
|
|
c59a41a607 | ||
|
|
e0410b6f10 | ||
|
|
8eac6c0b48 | ||
|
|
bf8b74e996 | ||
|
|
691e41e22e | ||
|
|
15e91d42ee | ||
|
|
5e8e94fd0f | ||
|
|
5313a46aa2 | ||
|
|
761a8dde65 | ||
|
|
a73acfb9c2 | ||
|
|
fbe17dde03 | ||
|
|
a01a3404fe | ||
|
|
454e5dfc5d | ||
|
|
47545b45b8 | ||
|
|
7c9908d953 | ||
|
|
5f4cd50cc4 | ||
|
|
b0fba6ce5b | ||
|
|
1f5992f2c2 | ||
|
|
abfd3c3e5d | ||
|
|
97da7f9691 | ||
|
|
2752083d29 | ||
|
|
c826318da4 | ||
|
|
6582a4abd9 | ||
|
|
a699dab5b3 | ||
|
|
21c8ad5b9e | ||
|
|
195d885887 | ||
|
|
519bd2f30f | ||
|
|
20ef724fad | ||
|
|
f443cbaa2b | ||
|
|
dbf45da8ab | ||
|
|
6b67902d53 | ||
|
|
0ad0ef485c | ||
|
|
7dfe3e53d5 | ||
|
|
5be3bd1e64 | ||
|
|
bc0c1980db | ||
|
|
2997258fd0 | ||
|
|
11600fc116 | ||
|
|
a8640f52ef | ||
|
|
0f4e44c38f | ||
|
|
053f4d481d | ||
|
|
f466c27da9 | ||
|
|
bfe6bc3095 | ||
|
|
ff8f3e766e | ||
|
|
6635ea3e29 | ||
|
|
591788c0df | ||
|
|
571b8986a4 | ||
|
|
bb7a74e4b4 | ||
|
|
76ddfeb93a | ||
|
|
c38b826abf | ||
|
|
21d7db0959 | ||
|
|
d7b51d2807 | ||
|
|
a7af8b5722 | ||
|
|
9c93fe6003 | ||
|
|
21505a7470 | ||
|
|
ba6e6cc15a | ||
|
|
fd7bf2bc3a | ||
|
|
b2cd24ed1b | ||
|
|
66cf2c984a | ||
|
|
de1b2b19b0 | ||
|
|
e31583485d | ||
|
|
490e51c1d7 | ||
|
|
1df2a04713 | ||
|
|
42804d5314 | ||
|
|
558710bbdd | ||
|
|
f4926cb822 | ||
|
|
1e77e0862a | ||
|
|
8c696cb8ca | ||
|
|
62ef8ade8f | ||
|
|
3d88dd3123 | ||
|
|
880b348ce6 | ||
|
|
31fe3a1cd8 | ||
|
|
19182ffddf | ||
|
|
afcc60066e | ||
|
|
d3ade06421 | ||
|
|
f1a3ef9590 | ||
|
|
d1d73f11a5 | ||
|
|
05697372f8 | ||
|
|
0c1f68816e | ||
|
|
92546e8a74 | ||
|
|
a4faa3f392 | ||
|
|
df191cd2b5 | ||
|
|
baa19f0ccf | ||
|
|
5a49bd3ac9 | ||
|
|
b37d7e0500 | ||
|
|
f4ed6274a4 | ||
|
|
56eb1a1cf9 | ||
|
|
a7c156a9e3 | ||
|
|
d81ca77231 | ||
|
|
bf013f6ebb | ||
|
|
dd8116e285 | ||
|
|
b5d80a88d1 | ||
|
|
7f4f95cf83 | ||
|
|
87c2f6ad14 | ||
|
|
ad47dba064 | ||
|
|
41b701846f | ||
|
|
5c42830328 | ||
|
|
69617309f8 | ||
|
|
48e2d6a8da | ||
|
|
b4120cddfb | ||
|
|
54e3f1998a | ||
|
|
edcf9f1b0c | ||
|
|
de3747d65e | ||
|
|
b76a3614da | ||
|
|
94cc64c51b | ||
|
|
0f71edee96 | ||
|
|
e097c097fe | ||
|
|
1443a5b175 | ||
|
|
2d82ad93dd | ||
|
|
384c257a74 | ||
|
|
49dfa2c3a0 | ||
|
|
7bd3e768db | ||
|
|
65224ed22b | ||
|
|
0a28dfe1e2 | ||
|
|
1c8ebfacb0 | ||
|
|
5d6d241791 | ||
|
|
4f116d15b9 | ||
|
|
228570640e | ||
|
|
65a79610aa | ||
|
|
24984ea4f2 | ||
|
|
048b2af0fc | ||
|
|
449989ddd9 | ||
|
|
01ebe5724a | ||
|
|
95fb230b8c | ||
|
|
632971af15 | ||
|
|
5787aa1078 | ||
|
|
d8b9265484 | ||
|
|
9ea3169ca9 | ||
|
|
aebf2672cd | ||
|
|
68ac409bfd | ||
|
|
fef44bd24f | ||
|
|
e4a7617dde | ||
|
|
4dfb193d10 | ||
|
|
c248d94995 | ||
|
|
d4ac458d17 | ||
|
|
93e443c4ad | ||
|
|
4b3988cef9 | ||
|
|
4eb5ee17b4 | ||
|
|
e1b63d7dec | ||
|
|
4b5651bd6f | ||
|
|
50515d9128 | ||
|
|
28b5faab0c | ||
|
|
82a01c22d3 | ||
|
|
be9b0c2e8f | ||
|
|
b6affe06a5 | ||
|
|
1e05f8c646 | ||
|
|
7e9d4512b6 | ||
|
|
5fa127c415 | ||
|
|
ac26681fe7 | ||
|
|
3c62636133 | ||
|
|
ca874fa12c | ||
|
|
c3508bbb99 | ||
|
|
6935033db5 | ||
|
|
421277d730 | ||
|
|
56988944b5 | ||
|
|
528601d25a | ||
|
|
ddd153c00d | ||
|
|
b8c1588284 | ||
|
|
4dac9e40bd | ||
|
|
def1811d48 | ||
|
|
c53e507713 | ||
|
|
e0ea777249 | ||
|
|
4c1962f3c7 | ||
|
|
258e89c964 | ||
|
|
3d3bfb42e5 | ||
|
|
6dbd8baa7e | ||
|
|
e660fabc57 | ||
|
|
2115bcd8b0 | ||
|
|
1bdd6e1a9d | ||
|
|
98deec232b | ||
|
|
022c217cfe | ||
|
|
81f57949ed | ||
|
|
fca5eb083f | ||
|
|
a3695cc66b | ||
|
|
6723d20616 | ||
|
|
627ec91687 | ||
|
|
9126cf0c73 | ||
|
|
16322ab30c | ||
|
|
5682917356 | ||
|
|
c91ccc8b4e | ||
|
|
63f670fc36 | ||
|
|
e20b07fa24 | ||
|
|
472656517f | ||
|
|
d232cba02d | ||
|
|
e49d29a914 | ||
|
|
3aa1a68cdc | ||
|
|
f94452083f | ||
|
|
ce1ee5cb9d | ||
|
|
48df6b8485 | ||
|
|
ae23ae2d37 | ||
|
|
e34e04af04 | ||
|
|
ff3f377911 | ||
|
|
18065826b9 | ||
|
|
84e19ceef0 | ||
|
|
59161efd08 | ||
|
|
6663fd3526 | ||
|
|
2c44e1bb93 | ||
|
|
e3f6399473 | ||
|
|
89c2c21774 | ||
|
|
2954eb4bdc | ||
|
|
e08de91666 | ||
|
|
a170acb9d7 | ||
|
|
6a086bb222 | ||
|
|
b2f152e641 | ||
|
|
6c5b261804 | ||
|
|
8bd0c44e83 | ||
|
|
34c36984e9 | ||
|
|
8bd6aca0dd | ||
|
|
983b74be77 | ||
|
|
a3eafdd2c6 | ||
|
|
ea75a09f95 | ||
|
|
4c747c4148 | ||
|
|
49abfcafed | ||
|
|
50710c72ad | ||
|
|
2e299b3814 | ||
|
|
43d11d877d | ||
|
|
d7e7df3bd9 | ||
|
|
8d8ba11221 | ||
|
|
2536a18c00 | ||
|
|
11728b2b15 | ||
|
|
627501b9ba | ||
|
|
3599384b38 | ||
|
|
4b307cad2c | ||
|
|
7496d51580 | ||
|
|
4194ac894c | ||
|
|
ffb5d9ea9c | ||
|
|
770b28ca30 | ||
|
|
62e464f706 | ||
|
|
8d0dc37ec0 | ||
|
|
fe41df87bb | ||
|
|
8276a0775d | ||
|
|
abfb3bb3bb | ||
|
|
e184eb4a23 | ||
|
|
d0fc372ecd | ||
|
|
6f54c57647 | ||
|
|
e8ae103d5f | ||
|
|
b0198dab6c | ||
|
|
b75ec09998 | ||
|
|
c8ac6c07b0 | ||
|
|
27814e3015 | ||
|
|
f59309a445 | ||
|
|
b0292d7319 | ||
|
|
7f18616cc0 | ||
|
|
2fef98a5af | ||
|
|
36765caedc | ||
|
|
f7aed10ea2 | ||
|
|
410bbb8285 | ||
|
|
f56ea52932 | ||
|
|
cb4361b7b7 | ||
|
|
ecd332c573 | ||
|
|
a0fe78a728 | ||
|
|
49cc9c529e | ||
|
|
7635b2c33f | ||
|
|
50c26d33ab | ||
|
|
f642fb3b99 | ||
|
|
e68dd866a3 | ||
|
|
73d36fdff0 | ||
|
|
5561cd3c77 | ||
|
|
32a9acb913 | ||
|
|
f7f23c6e77 | ||
|
|
3d4edbd9dc | ||
|
|
bdf385f374 | ||
|
|
9f78c3e64b | ||
|
|
f370052815 | ||
|
|
9df4b10067 | ||
|
|
d20517483e | ||
|
|
713ce4719b | ||
|
|
f3d39e7515 | ||
|
|
61783ffc82 | ||
|
|
05c4ad01d5 | ||
|
|
12647dcf30 | ||
|
|
da38f59e62 | ||
|
|
cf4ef54dc5 | ||
|
|
12e9873514 | ||
|
|
f7c0e407ca | ||
|
|
82c7662cdf | ||
|
|
4f0bced53e | ||
|
|
f1b6c9f4aa | ||
|
|
0ab31ab0df | ||
|
|
46e8f0779f | ||
|
|
3fb72a4d20 | ||
|
|
db20f65d7c | ||
|
|
63cfe7b47b | ||
|
|
db590091b3 | ||
|
|
7b25e74418 | ||
|
|
82f303e1c6 | ||
|
|
c038683b54 | ||
|
|
3a37ed6b60 | ||
|
|
706a492218 | ||
|
|
c0be5383de | ||
|
|
3b8ce85092 | ||
|
|
b6298f8602 | ||
|
|
abfec57972 | ||
|
|
470fc97d1f | ||
|
|
8d59caf635 | ||
|
|
acf25aa4d3 | ||
|
|
16de4674ec | ||
|
|
65b0ea792e | ||
|
|
fc6b02f607 | ||
|
|
136d8c39d9 | ||
|
|
24a8b41182 | ||
|
|
810cf4dee8 | ||
|
|
9bf835e810 | ||
|
|
eca37bce38 | ||
|
|
3ee6a2baf2 | ||
|
|
69fa7f238d | ||
|
|
de2306bd12 | ||
|
|
714feeb9a7 | ||
|
|
ca99808fd2 | ||
|
|
f8f8c28fec | ||
|
|
f497867ba5 | ||
|
|
383192784d | ||
|
|
605189bc6e | ||
|
|
c0a2e3674c | ||
|
|
76f0602684 | ||
|
|
477ff12cde | ||
|
|
9c09ad3b62 | ||
|
|
a967afc629 | ||
|
|
dcc1fd3ee4 | ||
|
|
933f020b3c | ||
|
|
f5c02be5bf | ||
|
|
68fbdd474c | ||
|
|
2cbc048352 | ||
|
|
e990ffd4a0 | ||
|
|
743c7c9326 | ||
|
|
067248da75 | ||
|
|
f5c982355a | ||
|
|
f98c68a280 | ||
|
|
773bf0c6bc | ||
|
|
080ab6032c | ||
|
|
350144df29 | ||
|
|
9ac0f11d9a | ||
|
|
8079d456ab | ||
|
|
acf166cf9d | ||
|
|
439d497a13 | ||
|
|
0580932610 | ||
|
|
85399f609c | ||
|
|
4bcfee397b | ||
|
|
34bcb1dd26 | ||
|
|
117d1ed080 | ||
|
|
f324252681 | ||
|
|
0dad06cdfe | ||
|
|
9396288ca2 | ||
|
|
f89f08833e | ||
|
|
79e8962854 | ||
|
|
34e5a7cd24 | ||
|
|
7343c195b7 | ||
|
|
0af041b54e | ||
|
|
92a8a3e91f | ||
|
|
f41575d8b0 | ||
|
|
d93c4a5103 | ||
|
|
6fe9b69aad | ||
|
|
5d162f81c4 | ||
|
|
4771c2810b | ||
|
|
0cd99712fa | ||
|
|
b591af7803 | ||
|
|
171d68ca72 | ||
|
|
bade4f2c6a | ||
|
|
5754782a4e | ||
|
|
decdd54c19 | ||
|
|
ffe47300a1 | ||
|
|
6f9c3c4ff3 | ||
|
|
9b3efffba9 | ||
|
|
003fea52b1 | ||
|
|
2b17c77195 | ||
|
|
c252a50fd7 | ||
|
|
cf8f042a20 | ||
|
|
844bc2d808 | ||
|
|
27f7fa7153 | ||
|
|
b325aa4555 | ||
|
|
c2c3bf0ba4 | ||
|
|
0d977b54f7 | ||
|
|
20860da4b4 | ||
|
|
3ea10b7cf9 | ||
|
|
1ec33863bc | ||
|
|
a260e99090 | ||
|
|
25efdd3d6f | ||
|
|
00a1e18959 | ||
|
|
c59f8adc4a | ||
|
|
1eb83ad812 | ||
|
|
7717f0a6b0 | ||
|
|
5e1fba3603 | ||
|
|
66cc9bc545 | ||
|
|
12aa5838d9 | ||
|
|
4f73534837 | ||
|
|
c4d145835c | ||
|
|
f822ca5b23 | ||
|
|
8aaa45c62a | ||
|
|
2f4f257070 | ||
|
|
97c1e181c5 | ||
|
|
ea80cddd57 | ||
|
|
09a294c219 | ||
|
|
408399eae0 | ||
|
|
391852a102 | ||
|
|
5b37de8fe5 | ||
|
|
7df23ceb74 | ||
|
|
6099f3b015 | ||
|
|
a5cc31783c | ||
|
|
6b34ec3ab9 | ||
|
|
5c333dec33 | ||
|
|
775d095b3c | ||
|
|
7679b5d516 | ||
|
|
7702094053 | ||
|
|
3798d50457 | ||
|
|
95e1e57407 | ||
|
|
93ba4cca68 | ||
|
|
fe4981da21 | ||
|
|
e4f94c4c52 | ||
|
|
708fe514f8 | ||
|
|
11c882380f | ||
|
|
fb93af665d | ||
|
|
0db405f2cc | ||
|
|
fb8000b58b | ||
|
|
1b9d8e068a | ||
|
|
038f73a5f7 | ||
|
|
649b49ff45 | ||
|
|
1418bc454d | ||
|
|
29cc372bfa | ||
|
|
69b00d3782 | ||
|
|
a328e2bf3c | ||
|
|
4c1ea0e421 | ||
|
|
7e01f9c95e | ||
|
|
8b28baabd7 | ||
|
|
f49966d86e | ||
|
|
f4ac7c8e7c | ||
|
|
2b65e1ffc2 | ||
|
|
c81a3fa286 | ||
|
|
44f005077d | ||
|
|
013b6e68ec | ||
|
|
95c964673d | ||
|
|
94ec11db58 | ||
|
|
c4e8dda37c | ||
|
|
e136fb3a4f | ||
|
|
01b985eded | ||
|
|
1f0a35f073 | ||
|
|
2b9b019093 | ||
|
|
10186a9e3d | ||
|
|
89d8fea7d2 | ||
|
|
f623b98858 | ||
|
|
632cee1613 | ||
|
|
c0f2164bc5 | ||
|
|
f6e4a27fdd | ||
|
|
257ceb99f7 | ||
|
|
706d53065b | ||
|
|
0f95a7babe | ||
|
|
7cb2806878 | ||
|
|
9c0e18975c | ||
|
|
3da318b48e | ||
|
|
dfe1f2c108 | ||
|
|
f42a87b51a | ||
|
|
ab25857176 | ||
|
|
7da36079c1 | ||
|
|
2bef967af1 | ||
|
|
7e4194418a | ||
|
|
aa02057895 | ||
|
|
fb8dc07599 | ||
|
|
66e30a7723 | ||
|
|
0298ab99c4 | ||
|
|
d11358671e | ||
|
|
8b5cb4c7b0 | ||
|
|
aad52ae743 | ||
|
|
8ddab84745 | ||
|
|
6865652125 | ||
|
|
ed4d0867e8 | ||
|
|
1c71e02454 | ||
|
|
f332e87cab | ||
|
|
023dbc6cb5 | ||
|
|
4dd3f55407 | ||
|
|
7b9a71c9af | ||
|
|
901d22cdfa | ||
|
|
93e1266ee7 | ||
|
|
0a4e7eea41 | ||
|
|
e3801d6965 | ||
|
|
336f1687c1 | ||
|
|
d4e2f2df6e | ||
|
|
f152b4c26e | ||
|
|
bd935b0553 | ||
|
|
a9b3b7a359 | ||
|
|
7a007b342a | ||
|
|
0783f3d5b6 | ||
|
|
afe3c2bc1b | ||
|
|
82f8948fd4 | ||
|
|
b9cdc755d1 | ||
|
|
a6f81c66e5 | ||
|
|
1ff45ac5f5 | ||
|
|
48bde7375f | ||
|
|
0601fa3b3d | ||
|
|
d0d3c8dbfd | ||
|
|
8057de1973 | ||
|
|
43c1105d62 | ||
|
|
6adf516b30 | ||
|
|
bf80b08b5f | ||
|
|
3e0b1df46d | ||
|
|
84811c80b6 | ||
|
|
45e0df9c57 | ||
|
|
bc51ce7c7b | ||
|
|
b693d13b93 | ||
|
|
39982d57ef | ||
|
|
15e27e54fb | ||
|
|
851404205b | ||
|
|
117ae71025 | ||
|
|
027ec70262 | ||
|
|
55fdee4d65 | ||
|
|
0d42f937dd | ||
|
|
ac8372dd26 | ||
|
|
5e56a6bbee | ||
|
|
3c6c409df0 | ||
|
|
d05408c89f | ||
|
|
ee0ec3fbfa | ||
|
|
122a73e086 | ||
|
|
29a9b18c4c | ||
|
|
1561272109 | ||
|
|
3e61ab0d25 | ||
|
|
a49dc6ccb7 | ||
|
|
60f3d62f00 | ||
|
|
e613855a4f | ||
|
|
22662d7e03 | ||
|
|
6e7e5be1a2 | ||
|
|
8b2ab778c9 | ||
|
|
35f3766ecf | ||
|
|
995304dabb | ||
|
|
803982a271 | ||
|
|
9164bf22c2 | ||
|
|
911a576893 | ||
|
|
79ee85c0f9 | ||
|
|
483dbcdc40 | ||
|
|
a1096b5bf0 | ||
|
|
5ac0e64edb | ||
|
|
60b2624607 | ||
|
|
d2e2847b03 | ||
|
|
b9669f54f7 | ||
|
|
8c7bd77d33 | ||
|
|
ba1ce16b8b | ||
|
|
68090943f4 | ||
|
|
a4fb1297b0 | ||
|
|
860a05abf2 | ||
|
|
8bb2f356c0 | ||
|
|
4950020635 | ||
|
|
0a6140c6eb | ||
|
|
bba2ac8817 | ||
|
|
331b1f542f | ||
|
|
ccb55205e6 | ||
|
|
9cc91b30b3 | ||
|
|
e836caf31e | ||
|
|
beaa1e5be2 | ||
|
|
ea545bae26 | ||
|
|
1c9ec2df45 | ||
|
|
b76c80e2ce | ||
|
|
236990f4a3 | ||
|
|
1ed32df20d | ||
|
|
8476eb9f4b | ||
|
|
735af7843b | ||
|
|
ded73e958b | ||
|
|
6dcb84d4f4 | ||
|
|
501bc9f438 | ||
|
|
f88e812b63 | ||
|
|
be6386c410 | ||
|
|
2af4fd17c4 | ||
|
|
f870418bd0 | ||
|
|
00659e4795 | ||
|
|
cdda10207e | ||
|
|
701700279f | ||
|
|
a9d804724a | ||
|
|
e033a9ab47 | ||
|
|
059e5fb8aa | ||
|
|
a78f255928 | ||
|
|
1d10e69288 | ||
|
|
63590d379c | ||
|
|
5f63e88984 | ||
|
|
75584e2b19 | ||
|
|
1426ee2ebd | ||
|
|
b6643b7bfc | ||
|
|
721dfdf553 | ||
|
|
2963747d14 | ||
|
|
e7350d5041 | ||
|
|
f37e8f4ca8 | ||
|
|
594c2accc0 | ||
|
|
7acfac6a91 | ||
|
|
0646f48e14 | ||
|
|
37565fd067 | ||
|
|
26b2e7dc5d | ||
|
|
c3313623e4 | ||
|
|
2089223690 | ||
|
|
52e1b84d41 | ||
|
|
8794141b7f | ||
|
|
f6126dd20e | ||
|
|
18acfda99b | ||
|
|
bec5edca84 | ||
|
|
6fb20b3ee5 | ||
|
|
eaf4d8064b | ||
|
|
2a5f5b1bba | ||
|
|
c538a77937 | ||
|
|
aa9e7b1ed1 | ||
|
|
a3066eddab | ||
|
|
d1729fa787 | ||
|
|
93961dde2c | ||
|
|
1024e68eb6 | ||
|
|
6ae2c9387d | ||
|
|
fba83e2330 | ||
|
|
f1295cb7d6 | ||
|
|
dc61dfbde6 | ||
|
|
21466426da | ||
|
|
3f0136362b | ||
|
|
e92d77bbec | ||
|
|
07bd36c94b | ||
|
|
b1dbbdef12 | ||
|
|
3e479726ec | ||
|
|
4cc41eccb3 | ||
|
|
8f08ae59ac | ||
|
|
e8d4e492d6 | ||
|
|
b8090a8e18 | ||
|
|
c609a01e55 | ||
|
|
c97fb385cd | ||
|
|
da6c57750e | ||
|
|
6951d926f7 | ||
|
|
6623195bd5 | ||
|
|
ec31bb9a82 | ||
|
|
8618cc383a | ||
|
|
4b01e3a3c7 | ||
|
|
7f748c23c1 | ||
|
|
963d248cc7 | ||
|
|
657056e636 | ||
|
|
9d5efea66e | ||
|
|
658d74e026 | ||
|
|
5113f6d375 | ||
|
|
96405c26d0 | ||
|
|
4ea5f34bf3 | ||
|
|
dbd13a2019 | ||
|
|
06773235da | ||
|
|
e57556a8af | ||
|
|
b54b78c29d | ||
|
|
317336f771 | ||
|
|
b4e52f6135 | ||
|
|
f2ca042915 | ||
|
|
1060dd2906 | ||
|
|
2e0f7a82fa | ||
|
|
5798536559 | ||
|
|
ab9a83c82f | ||
|
|
c87fdbea0f | ||
|
|
ec8fffe61c | ||
|
|
61d52991f1 | ||
|
|
9100186dce | ||
|
|
d2bc2cfcf8 | ||
|
|
5a71998b4e | ||
|
|
42278f12ff | ||
|
|
f5593e051c | ||
|
|
a27e30cf54 | ||
|
|
79140c7636 | ||
|
|
1f4c595cd3 | ||
|
|
b5b62e03af | ||
|
|
67e2a4720e | ||
|
|
f5c2d72429 | ||
|
|
2f5331ab48 | ||
|
|
7f8257152f | ||
|
|
0cd80f2556 | ||
|
|
1717387876 | ||
|
|
109363ebf6 | ||
|
|
716c4fa386 | ||
|
|
9a09b4eb20 | ||
|
|
95a5b57265 | ||
|
|
13fbf397d1 | ||
|
|
20be99ec8a | ||
|
|
04c53c3578 | ||
|
|
51bc27a869 | ||
|
|
71b083794c | ||
|
|
b100d0c503 | ||
|
|
76061296c9 | ||
|
|
bb303d2da1 | ||
|
|
c91c070343 | ||
|
|
aec06a6f61 | ||
|
|
e8ba671fc2 | ||
|
|
1860e5d133 | ||
|
|
f2cb3c38fe | ||
|
|
9a28dd4f6e | ||
|
|
d2acd59ea8 | ||
|
|
79dfdb29e7 | ||
|
|
eb21c8b42e | ||
|
|
541bb53553 |
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,21 +1,22 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us improve
|
about: Create a report to help us improve
|
||||||
title: ''
|
title: ""
|
||||||
labels: ''
|
labels: ""
|
||||||
assignees: ''
|
assignees: ""
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
## READ BEFORE OPENING ISSUES
|
## READ BEFORE OPENING ISSUES
|
||||||
|
|
||||||
All bug reports require you to **USE CANARY BUILDS**. Please include the version name and version code in the bug report.
|
All bug reports require you to **USE DEBUG BUILDS**. Please include the version name and version code in the bug report.
|
||||||
|
|
||||||
If you experience a bootloop, attach a `dmesg` (kernel logs) when the device refuse to boot. This may very likely require a custom kernel on some devices as `last_kmsg` or `pstore ramoops` are usually not enabled by default. In addition, please also upload the result of `cat /proc/mounts` when your device is working correctly **WITHOUT ROOT**.
|
If you experience a bootloop, attach a `dmesg` (kernel logs) when the device refuse to boot. This may very likely require a custom kernel on some devices as `last_kmsg` or `pstore ramoops` are usually not enabled by default. In addition, please also upload the result of `cat /proc/mounts` when your device is working correctly **WITHOUT MAGISK**.
|
||||||
|
|
||||||
If you experience issues during installation, in recovery, upload the recovery logs, or in Magisk Manager, upload the install logs. Please also upload the `boot.img` or `recovery.img` that you are using for patching.
|
If you experience issues during installation, in recovery, upload the recovery logs, or in Magisk, upload the install logs. Please also upload the `boot.img` or `recovery.img` that you are using for patching.
|
||||||
|
|
||||||
If you experience a crash of Magisk Manager, dump the full `logcat` **when the crash happens**. **DO NOT** upload `magisk.log`.
|
If you experience a crash of Magisk app, dump the full `logcat` **when the crash happens**.
|
||||||
|
|
||||||
If you experience other issues related to Magisk, upload `magisk.log`, and preferably also include a boot `logcat` (start dumping `logcat` when the device boots up)
|
If you experience other issues related to Magisk, upload `magisk.log`, and preferably also include a boot `logcat` (start dumping `logcat` when the device boots up)
|
||||||
|
|
||||||
@@ -26,3 +27,10 @@ If you experience other issues related to Magisk, upload `magisk.log`, and prefe
|
|||||||
**DO NOT** report issues if you have any modules installed.
|
**DO NOT** report issues if you have any modules installed.
|
||||||
|
|
||||||
Without following the rules above, your issue will be closed without explanation.
|
Without following the rules above, your issue will be closed without explanation.
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
Device:
|
||||||
|
Android version:
|
||||||
|
Magisk version name:
|
||||||
|
Magisk version code:
|
||||||
|
|||||||
6
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
6
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: XDA Community Support
|
||||||
|
url: https://forum.xda-developers.com/f/magisk.5903/
|
||||||
|
about: Please ask and answer questions here.
|
||||||
|
|
||||||
134
.github/workflows/build.yml
vendored
134
.github/workflows/build.yml
vendored
@@ -2,16 +2,17 @@ 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"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [master]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -21,74 +22,113 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-latest, windows-latest, macOS-latest ]
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out
|
- name: Check out
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: 'recursive'
|
submodules: "recursive"
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: '11'
|
distribution: "temurin"
|
||||||
|
java-version: "17"
|
||||||
|
|
||||||
- name: Set up Python 3
|
- name: Set up Python 3
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.x'
|
python-version: "3.x"
|
||||||
|
|
||||||
- name: Set up GitHub env (Windows)
|
- name: Set up sccache
|
||||||
if: runner.os == 'Windows'
|
uses: hendrikmuhs/ccache-action@v1.2
|
||||||
run: |
|
with:
|
||||||
$oldAndroidPath = $env:ANDROID_SDK_ROOT
|
variant: sccache
|
||||||
$sdk_root = "C:\Android"
|
key: ${{ runner.os }}-${{ github.sha }}
|
||||||
New-Item -Path $sdk_root -ItemType SymbolicLink -Value $oldAndroidPath
|
restore-keys: ${{ runner.os }}
|
||||||
$ndk_ver = Select-String -Path "gradle.properties" -Pattern "^magisk.fullNdkVersion=" | % { $_ -replace ".*=" }
|
max-size: 10000M
|
||||||
echo "ANDROID_SDK_ROOT=$sdk_root" >> $env:GITHUB_ENV
|
|
||||||
echo "ANDROID_HOME=$sdk_root" >> $env:GITHUB_ENV
|
|
||||||
echo "MAGISK_NDK_VERSION=$ndk_ver" >> $env:GITHUB_ENV
|
|
||||||
echo "GRADLE_OPTS=-Dorg.gradle.daemon=false" >> $env:GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Set up GitHub env (Unix)
|
- name: Cache Gradle dependencies
|
||||||
if: runner.os != 'Windows'
|
uses: actions/cache@v3
|
||||||
run: |
|
|
||||||
ndk_ver=$(sed -n 's/^magisk.fullNdkVersion=//p' gradle.properties)
|
|
||||||
echo ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT >> $GITHUB_ENV
|
|
||||||
echo MAGISK_NDK_VERSION=$ndk_ver >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Cache Gradle
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
~/.gradle/wrapper
|
~/.gradle/wrapper
|
||||||
|
!~/.gradle/caches/build-cache-*
|
||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
|
||||||
restore-keys: ${{ runner.os }}-gradle-
|
restore-keys: ${{ runner.os }}-gradle-
|
||||||
|
|
||||||
- name: Cache NDK
|
- name: Cache build cache
|
||||||
id: ndk-cache
|
uses: actions/cache@v3
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
with:
|
||||||
path: ${{ env.ANDROID_SDK_ROOT }}/ndk/magisk
|
path: |
|
||||||
key: ${{ runner.os }}-ndk-${{ env.MAGISK_NDK_VERSION }}
|
~/.gradle/caches/build-cache-*
|
||||||
|
key: ${{ runner.os }}-build-cache-${{ github.sha }}
|
||||||
|
restore-keys: ${{ runner.os }}-build-cache-
|
||||||
|
|
||||||
- name: Set up NDK
|
- name: Set up NDK
|
||||||
if: steps.ndk-cache.outputs.cache-hit != 'true'
|
run: python build.py -v ndk
|
||||||
run: python build.py ndk
|
|
||||||
|
|
||||||
- name: Build release
|
- name: Build release
|
||||||
run: python build.py -vr all
|
run: |
|
||||||
|
python build.py -vr all
|
||||||
|
|
||||||
- name: Build debug
|
- name: Build debug
|
||||||
run: python build.py -v all
|
run: |
|
||||||
|
python build.py -v all
|
||||||
|
|
||||||
|
- name: Stop gradle daemon
|
||||||
|
run: ./gradlew --stop
|
||||||
|
|
||||||
# Only upload artifacts built on Linux
|
# Only upload artifacts built on Linux
|
||||||
- name: Upload build artifact
|
- name: Upload build artifact
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: ${{ github.sha }}
|
name: ${{ github.sha }}
|
||||||
path: out
|
path: out
|
||||||
|
|
||||||
|
- name: Upload mapping and native debug symbols
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ github.sha }}-symbols
|
||||||
|
path: app/build/outputs
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Test on ${{ matrix.api }}
|
||||||
|
runs-on: macos-latest
|
||||||
|
needs: build
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
api: [23, 26, 28, 29, 34]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
distribution: "temurin"
|
||||||
|
java-version: "17"
|
||||||
|
|
||||||
|
- name: Set up Python 3
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.x"
|
||||||
|
|
||||||
|
- name: Download build artifacts
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ github.sha }}
|
||||||
|
path: out
|
||||||
|
|
||||||
|
- name: AVD test
|
||||||
|
run: |
|
||||||
|
brew install coreutils
|
||||||
|
scripts/avd_test.sh ${{ matrix.api }}
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,7 @@ out
|
|||||||
*.apk
|
*.apk
|
||||||
/config.prop
|
/config.prop
|
||||||
/update.sh
|
/update.sh
|
||||||
|
/dict.txt
|
||||||
|
|
||||||
# Built binaries
|
# Built binaries
|
||||||
native/out
|
native/out
|
||||||
|
|||||||
45
.gitmodules
vendored
45
.gitmodules
vendored
@@ -1,33 +1,48 @@
|
|||||||
[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"]
|
[submodule "dtc"]
|
||||||
path = native/jni/external/dtc
|
path = native/src/external/dtc
|
||||||
url = https://github.com/dgibson/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"]
|
[submodule "nanopb"]
|
||||||
path = native/jni/external/nanopb
|
path = native/src/external/nanopb
|
||||||
url = https://github.com/nanopb/nanopb.git
|
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 "xhook"]
|
[submodule "libcxx"]
|
||||||
path = native/jni/external/xhook
|
path = native/src/external/libcxx
|
||||||
url = https://github.com/iqiyi/xHook.git
|
url = https://github.com/topjohnwu/libcxx.git
|
||||||
|
[submodule "zlib"]
|
||||||
|
path = native/src/external/zlib
|
||||||
|
url = https://android.googlesource.com/platform/external/zlib
|
||||||
|
[submodule "parallel-hashmap"]
|
||||||
|
path = native/src/external/parallel-hashmap
|
||||||
|
url = https://github.com/greg7mdp/parallel-hashmap.git
|
||||||
|
[submodule "zopfli"]
|
||||||
|
path = native/src/external/zopfli
|
||||||
|
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
|
||||||
|
|||||||
63
README.MD
63
README.MD
@@ -1,72 +1,45 @@
|
|||||||

|

|
||||||
|
|
||||||

|
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/count/count.json)
|
||||||

|
|
||||||
|
#### This is not an officially supported Google product
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
Magisk is a suite of open source tools for customizing Android, supporting devices higher than Android 4.2. It covers fundamental parts of Android customization: root, boot scripts, SELinux patches, AVB2.0 / dm-verity / forceencrypt removals etc.
|
Magisk is a suite of open source software for customizing Android, supporting devices higher than Android 6.0.<br>
|
||||||
|
Some highlight features:
|
||||||
|
|
||||||
Here are some feature highlights:
|
- **MagiskSU**: Provide root access for applications
|
||||||
|
|
||||||
- **MagiskSU**: Provide root access to your device
|
|
||||||
- **Magisk Modules**: Modify read-only partitions by installing modules
|
- **Magisk Modules**: Modify read-only partitions by installing modules
|
||||||
- **MagiskHide**: Hide Magisk from root detections / system integrity checks
|
- **MagiskBoot**: The most complete tool for unpacking and repacking Android boot images
|
||||||
|
- **Zygisk**: Run code in every Android applications' processes
|
||||||
|
|
||||||
## Downloads
|
## Downloads
|
||||||
|
|
||||||
[](https://github.com/topjohnwu/Magisk/releases/download/manager-v8.0.4/MagiskManager-v8.0.4.apk)
|
[Github](https://github.com/topjohnwu/Magisk/) is the only source where you can get official Magisk information and downloads.
|
||||||
[](https://raw.githubusercontent.com/topjohnwu/magisk_files/canary/app-debug.apk)
|
|
||||||
<br>
|
[](https://github.com/topjohnwu/Magisk/releases/tag/v26.1)
|
||||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v20.4)
|
[](https://github.com/topjohnwu/Magisk/releases/tag/v26.2)
|
||||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v21.2)
|
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-release.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)
|
||||||
- [Frequently Asked Questions](https://topjohnwu.github.io/Magisk/faq.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))
|
|
||||||
|
|
||||||
## Android Version Support
|
|
||||||
|
|
||||||
- Android 4.2+: MagiskSU and Magisk Modules Only
|
|
||||||
- Android 4.4+: All core features available
|
|
||||||
- Android 6.0+: Guaranteed MagiskHide support
|
|
||||||
- Android 7.0+: Full MagiskHide protection
|
|
||||||
- Android 9.0+: Magisk Manager stealth mode
|
|
||||||
|
|
||||||
## Bug Reports
|
## Bug Reports
|
||||||
|
|
||||||
Canary Channels are cutting edge builds for those adventurous. To access canary builds, install the Canary Magisk Manager, switch to the Canary Channel in settings and upgrade.
|
**Only bug reports from Debug builds will be accepted.**
|
||||||
|
|
||||||
**Only bug reports from Canary builds will be accepted.**
|
|
||||||
|
|
||||||
For installation issues, upload both boot image and install logs.<br>
|
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 Manager 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/jdk/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 in Android Studio. Both app (Kotlin/Java) and native (C++/C) source code can be properly developed using the IDE, but *always* use `build.py` for building.
|
|
||||||
- Optionally, set custom configs with `config.prop`. A sample `config.prop.sample` is provided.
|
|
||||||
- To sign APKs and zips with your own private keys, set signing configs in `config.prop`. For more info, check [Google's Documentation](https://developer.android.com/studio/publish/app-signing.html#generate-key).
|
|
||||||
|
|
||||||
## Translation Contributions
|
## Translation Contributions
|
||||||
|
|
||||||
Default string resources for Magisk Manager and its stub APK are located here:
|
Default string resources for the Magisk app and its stub APK are located here:
|
||||||
|
|
||||||
- `app/src/main/res/values/strings.xml`
|
- `app/src/main/res/values/strings.xml`
|
||||||
- `stub/src/main/res/values/strings.xml`
|
- `stub/src/main/res/values/strings.xml`
|
||||||
|
|||||||
6
app/.gitignore
vendored
6
app/.gitignore
vendored
@@ -3,9 +3,9 @@
|
|||||||
/local.properties
|
/local.properties
|
||||||
.idea/
|
.idea/
|
||||||
/build
|
/build
|
||||||
app/release
|
|
||||||
*.hprof
|
*.hprof
|
||||||
.externalNativeBuild/
|
.externalNativeBuild/
|
||||||
public.certificate.x509.pem
|
|
||||||
private.key.pk8
|
|
||||||
*.apk
|
*.apk
|
||||||
|
src/*/assets
|
||||||
|
src/*/jniLibs
|
||||||
|
src/*/resources
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import java.io.PrintStream
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
kotlin("android")
|
kotlin("android")
|
||||||
@@ -15,169 +13,109 @@ kapt {
|
|||||||
javacOptions {
|
javacOptions {
|
||||||
option("-Xmaxerrs", 1000)
|
option("-Xmaxerrs", 1000)
|
||||||
}
|
}
|
||||||
|
arguments {
|
||||||
|
arg("room.incremental", "true")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
namespace = "com.topjohnwu.magisk"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.topjohnwu.magisk"
|
applicationId = "com.topjohnwu.magisk"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
multiDexEnabled = true
|
versionName = Config.version
|
||||||
versionName = Config.appVersion
|
versionCode = Config.versionCode
|
||||||
versionCode = Config.appVersionCode
|
ndk {
|
||||||
|
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||||
javaCompileOptions.annotationProcessorOptions.arguments(
|
debugSymbolLevel = "FULL"
|
||||||
mapOf("room.incremental" to "true")
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
getByName("release") {
|
release {
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
isShrinkResources = true
|
isShrinkResources = true
|
||||||
proguardFiles(
|
proguardFiles("proguard-rules.pro")
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
||||||
"proguard-rules.pro"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
dataBinding = true
|
dataBinding = true
|
||||||
|
aidl = true
|
||||||
}
|
}
|
||||||
|
|
||||||
dependenciesInfo {
|
packaging {
|
||||||
includeInApk = false
|
resources {
|
||||||
includeInBundle = false
|
excludes += "/META-INF/*"
|
||||||
}
|
excludes += "/META-INF/versions/**"
|
||||||
|
excludes += "/org/bouncycastle/**"
|
||||||
packagingOptions {
|
excludes += "/kotlin/**"
|
||||||
exclude("/META-INF/**")
|
excludes += "/kotlinx/**"
|
||||||
exclude("/org/bouncycastle/**")
|
excludes += "/okhttp3/**"
|
||||||
exclude("/kotlin/**")
|
excludes += "/*.txt"
|
||||||
exclude("/kotlinx/**")
|
excludes += "/*.bin"
|
||||||
exclude("/okhttp3/**")
|
excludes += "/*.json"
|
||||||
exclude("/*.txt")
|
}
|
||||||
exclude("/*.bin")
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "1.8"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks["preBuild"]?.dependsOn(tasks.register("copyUtils", Copy::class) {
|
setupApp()
|
||||||
from(rootProject.file("scripts/util_functions.sh"))
|
|
||||||
into("src/main/res/raw")
|
|
||||||
})
|
|
||||||
|
|
||||||
android.applicationVariants.all {
|
configurations.all {
|
||||||
val keysDir = rootProject.file("tools/keys")
|
exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk7")
|
||||||
val outSrcDir = File(buildDir, "generated/source/keydata/$name")
|
exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8")
|
||||||
val outSrc = File(outSrcDir, "com/topjohnwu/signing/KeyData.java")
|
|
||||||
|
|
||||||
fun PrintStream.newField(name: String, file: File) {
|
|
||||||
println("public static byte[] $name() {")
|
|
||||||
print("byte[] buf = {")
|
|
||||||
val bytes = file.readBytes()
|
|
||||||
print(bytes.joinToString(",") { "(byte)(${it.toInt() and 0xff})" })
|
|
||||||
println("};")
|
|
||||||
println("return buf;")
|
|
||||||
println("}")
|
|
||||||
}
|
|
||||||
|
|
||||||
val genSrcTask = tasks.register("generate${name.capitalize()}KeyData") {
|
|
||||||
inputs.dir(keysDir)
|
|
||||||
outputs.file(outSrc)
|
|
||||||
doLast {
|
|
||||||
outSrc.parentFile.mkdirs()
|
|
||||||
PrintStream(outSrc).use {
|
|
||||||
it.println("package com.topjohnwu.signing;")
|
|
||||||
it.println("public final class KeyData {")
|
|
||||||
|
|
||||||
it.newField("testCert", File(keysDir, "testkey.x509.pem"))
|
|
||||||
it.newField("testKey", File(keysDir, "testkey.pk8"))
|
|
||||||
it.newField("verityCert", File(keysDir, "verity.x509.pem"))
|
|
||||||
it.newField("verityKey", File(keysDir, "verity.pk8"))
|
|
||||||
|
|
||||||
it.println("}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
registerJavaGeneratingTask(genSrcTask.get(), outSrcDir)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
|
|
||||||
implementation(kotlin("stdlib"))
|
|
||||||
implementation(project(":app:shared"))
|
implementation(project(":app:shared"))
|
||||||
|
|
||||||
implementation("com.github.topjohnwu:jtar:1.0.0")
|
implementation("com.github.topjohnwu:jtar:1.0.0")
|
||||||
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:4.7.1")
|
implementation("com.jakewharton.timber:timber:5.0.1")
|
||||||
|
implementation("org.bouncycastle:bcpkix-jdk18on:1.76")
|
||||||
|
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.3.0")
|
||||||
|
implementation("dev.rikka.rikkax.insets:insets:1.3.0")
|
||||||
|
implementation("dev.rikka.rikkax.recyclerview:recyclerview-ktx:1.3.2")
|
||||||
|
implementation("io.noties.markwon:core:4.6.2")
|
||||||
|
|
||||||
val vBC = "1.68"
|
val vLibsu = "5.2.1"
|
||||||
implementation("org.bouncycastle:bcprov-jdk15on:${vBC}")
|
|
||||||
implementation("org.bouncycastle:bcpkix-jdk15on:${vBC}")
|
|
||||||
|
|
||||||
val vBAdapt = "4.0.0"
|
|
||||||
val bindingAdapter = "me.tatarka.bindingcollectionadapter2:bindingcollectionadapter"
|
|
||||||
implementation("${bindingAdapter}:${vBAdapt}")
|
|
||||||
implementation("${bindingAdapter}-recyclerview:${vBAdapt}")
|
|
||||||
|
|
||||||
val vMarkwon = "4.6.0"
|
|
||||||
implementation("io.noties.markwon:core:${vMarkwon}")
|
|
||||||
implementation("io.noties.markwon:html:${vMarkwon}")
|
|
||||||
implementation("io.noties.markwon:image:${vMarkwon}")
|
|
||||||
implementation("com.caverock:androidsvg:1.4")
|
|
||||||
|
|
||||||
val vLibsu = "3.0.2"
|
|
||||||
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
|
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
|
||||||
implementation("com.github.topjohnwu.libsu:io:${vLibsu}")
|
implementation("com.github.topjohnwu.libsu:service:${vLibsu}")
|
||||||
|
implementation("com.github.topjohnwu.libsu:nio:${vLibsu}")
|
||||||
val vKoin = "2.1.6"
|
|
||||||
implementation("org.koin:koin-core:${vKoin}")
|
|
||||||
implementation("org.koin:koin-android:${vKoin}")
|
|
||||||
implementation("org.koin:koin-androidx-viewmodel:${vKoin}")
|
|
||||||
|
|
||||||
val vRetrofit = "2.9.0"
|
val vRetrofit = "2.9.0"
|
||||||
implementation("com.squareup.retrofit2:retrofit:${vRetrofit}")
|
implementation("com.squareup.retrofit2:retrofit:${vRetrofit}")
|
||||||
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 = "3.12.12"
|
val vOkHttp = "4.11.0"
|
||||||
implementation("com.squareup.okhttp3:okhttp") {
|
implementation("com.squareup.okhttp3:okhttp:${vOkHttp}")
|
||||||
version {
|
|
||||||
strictly(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.11.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.3.0-alpha04"
|
val vRoom = "2.6.0-beta01"
|
||||||
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: String by rootProject.extra
|
val vNav = "2.7.1"
|
||||||
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.0.1")
|
implementation("androidx.biometric:biometric:1.1.0")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.0.4")
|
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
implementation("androidx.browser:browser:1.3.0")
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||||
implementation("androidx.preference:preference:1.1.1")
|
implementation("androidx.recyclerview:recyclerview:1.3.1")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.1.0")
|
implementation("androidx.fragment:fragment-ktx:1.6.1")
|
||||||
implementation("androidx.fragment:fragment-ktx:1.2.5")
|
implementation("androidx.transition:transition:1.4.1")
|
||||||
implementation("androidx.work:work-runtime-ktx:2.4.0")
|
implementation("androidx.core:core-ktx:1.10.1")
|
||||||
implementation("androidx.transition:transition:1.3.1")
|
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||||
implementation("androidx.multidex:multidex:2.0.1")
|
implementation("com.google.android.material:material:1.9.0")
|
||||||
implementation("androidx.core:core-ktx:1.3.2")
|
|
||||||
implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.0.0")
|
|
||||||
implementation("com.google.android.material:material:1.2.1")
|
|
||||||
}
|
}
|
||||||
|
|||||||
82
app/proguard-rules.pro
vendored
82
app/proguard-rules.pro
vendored
@@ -1,51 +1,63 @@
|
|||||||
# Add project specific ProGuard rules here.
|
# Parcelable
|
||||||
# By default, the flags in this file are appended to flags specified
|
-keepclassmembers class * implements android.os.Parcelable {
|
||||||
# in /Users/topjohnwu/Library/Android/sdk/tools/proguard/proguard-android.txt
|
public static final ** CREATOR;
|
||||||
# You can edit the include path and order by changing the proguardFiles
|
}
|
||||||
# directive in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# Add any project specific keep options here:
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Kotlin
|
# Kotlin
|
||||||
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
||||||
public static void checkExpressionValueIsNotNull(...);
|
public static void check*(...);
|
||||||
public static void checkNotNullExpressionValue(...);
|
public static void throw*(...);
|
||||||
public static void checkReturnedValueIsNotNull(...);
|
}
|
||||||
public static void checkFieldIsNotNull(...);
|
-assumenosideeffects class java.util.Objects {
|
||||||
public static void checkParameterIsNotNull(...);
|
public static ** requireNonNull(...);
|
||||||
|
}
|
||||||
|
-assumenosideeffects public class kotlin.coroutines.jvm.internal.DebugMetadataKt {
|
||||||
|
private static ** getDebugMetadataAnnotation(...) return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Stubs
|
# Stub
|
||||||
-keep class a.* { *; }
|
-keep class com.topjohnwu.magisk.core.App { <init>(java.lang.Object); }
|
||||||
|
-keepclassmembers class androidx.appcompat.app.AppCompatDelegateImpl {
|
||||||
|
boolean mActivityHandlesConfigFlagsChecked;
|
||||||
|
int mActivityHandlesConfigFlags;
|
||||||
|
}
|
||||||
|
|
||||||
# Snet
|
# main
|
||||||
-keepclassmembers class com.topjohnwu.magisk.ui.safetynet.SafetyNetHelper { *; }
|
-keep,allowoptimization public class com.topjohnwu.magisk.signing.SignBoot {
|
||||||
-keep,allowobfuscation interface com.topjohnwu.magisk.ui.safetynet.SafetyNetHelper$Callback
|
public static void main(java.lang.String[]);
|
||||||
-keepclassmembers class * implements com.topjohnwu.magisk.ui.safetynet.SafetyNetHelper$Callback {
|
|
||||||
void onResponse(org.json.JSONObject);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Strip Timber verbose and debug logging
|
# Strip Timber verbose and debug logging
|
||||||
-assumenosideeffects class timber.log.Timber.Tree {
|
-assumenosideeffects class timber.log.Timber$Tree {
|
||||||
public void v(**);
|
public void v(**);
|
||||||
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
|
-repackageclasses 'a'
|
||||||
-allowaccessmodification
|
-allowaccessmodification
|
||||||
|
|
||||||
# QOL
|
-obfuscationdictionary ../dict.txt
|
||||||
-dontnote **
|
-classobfuscationdictionary ../dict.txt
|
||||||
-dontwarn com.caverock.androidsvg.**
|
-packageobfuscationdictionary ../dict.txt
|
||||||
-dontwarn ru.noties.markwon.**
|
|
||||||
|
-dontwarn org.bouncycastle.jsse.BCSSLParameters
|
||||||
|
-dontwarn org.bouncycastle.jsse.BCSSLSocket
|
||||||
|
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
|
||||||
|
-dontwarn org.commonmark.ext.gfm.strikethrough.Strikethrough
|
||||||
|
-dontwarn org.conscrypt.Conscrypt*
|
||||||
|
-dontwarn org.conscrypt.ConscryptHostnameVerifier
|
||||||
|
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
|
||||||
|
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
|
||||||
|
-dontwarn org.openjsse.net.ssl.OpenJSSE
|
||||||
|
|||||||
@@ -2,13 +2,8 @@ plugins {
|
|||||||
id("com.android.library")
|
id("com.android.library")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
setupCommon()
|
||||||
defaultConfig {
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
|
||||||
consumerProguardFiles("proguard-rules.pro")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
android {
|
||||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
|
namespace = "com.topjohnwu.shared"
|
||||||
}
|
}
|
||||||
|
|||||||
25
app/shared/proguard-rules.pro
vendored
25
app/shared/proguard-rules.pro
vendored
@@ -1,25 +0,0 @@
|
|||||||
# Add project specific ProGuard rules here.
|
|
||||||
# You can control the set of applied configuration files using the
|
|
||||||
# proguardFiles setting in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
|
||||||
# debugging stack traces.
|
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
|
||||||
# hide the original source file name.
|
|
||||||
#-renamesourcefileattribute SourceFile
|
|
||||||
|
|
||||||
-keepclassmembers class * extends javax.net.ssl.SSLSocketFactory {
|
|
||||||
** delegate;
|
|
||||||
}
|
|
||||||
8
app/shared/src/debug/AndroidManifest.xml
Normal file
8
app/shared/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
|
tools:ignore="UnusedAttribute" />
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -1,17 +1,31 @@
|
|||||||
<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">
|
||||||
|
|
||||||
|
<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_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.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
|
||||||
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="29"
|
||||||
|
tools:ignore="ScopedStorage" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="Magisk Manager"
|
android:allowBackup="false"
|
||||||
android:installLocation="internalOnly"
|
android:label="Magisk"
|
||||||
android:usesCleartextTraffic="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||||
tools:ignore="UnusedAttribute">
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
package com.topjohnwu.magisk;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.AssetManager;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static android.os.Build.VERSION.SDK_INT;
|
|
||||||
|
|
||||||
public class DynAPK {
|
|
||||||
|
|
||||||
// Indices of the object array
|
|
||||||
private static final int STUB_VERSION_ENTRY = 0;
|
|
||||||
private static final int CLASS_COMPONENT_MAP = 1;
|
|
||||||
|
|
||||||
private static File dynDir;
|
|
||||||
private static Method addAssetPath;
|
|
||||||
|
|
||||||
private static File getDynDir(Context c) {
|
|
||||||
if (dynDir == null) {
|
|
||||||
if (SDK_INT >= 24) {
|
|
||||||
// Use protected context to allow directBootAware
|
|
||||||
c = c.createDeviceProtectedStorageContext();
|
|
||||||
}
|
|
||||||
dynDir = new File(c.getFilesDir().getParent(), "dyn");
|
|
||||||
dynDir.mkdir();
|
|
||||||
}
|
|
||||||
return dynDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static File current(Context c) {
|
|
||||||
return new File(getDynDir(c), "current.apk");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static File update(Context c) {
|
|
||||||
return new File(getDynDir(c), "update.apk");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Data load(Object o) {
|
|
||||||
Object[] arr = (Object[]) o;
|
|
||||||
Data data = new Data();
|
|
||||||
data.version = (int) arr[STUB_VERSION_ENTRY];
|
|
||||||
data.classToComponent = (Map<String, String>) arr[CLASS_COMPONENT_MAP];
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Object pack(Data data) {
|
|
||||||
Object[] arr = new Object[2];
|
|
||||||
arr[STUB_VERSION_ENTRY] = data.version;
|
|
||||||
arr[CLASS_COMPONENT_MAP] = data.classToComponent;
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void addAssetPath(AssetManager asset, String path) {
|
|
||||||
try {
|
|
||||||
if (addAssetPath == null)
|
|
||||||
addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
|
|
||||||
addAssetPath.invoke(asset, path);
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Data {
|
|
||||||
public int version;
|
|
||||||
public Map<String, String> classToComponent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,341 +0,0 @@
|
|||||||
package com.topjohnwu.magisk;
|
|
||||||
|
|
||||||
import android.content.ContentProvider;
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.pm.ProviderInfo;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.MatrixCursor;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Environment;
|
|
||||||
import android.os.ParcelFileDescriptor;
|
|
||||||
import android.provider.OpenableColumns;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.webkit.MimeTypeMap;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Modified from androidx.core.content.FileProvider
|
|
||||||
*/
|
|
||||||
public class FileProvider extends ContentProvider {
|
|
||||||
private static final String[] COLUMNS = {
|
|
||||||
OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE };
|
|
||||||
|
|
||||||
private static final File DEVICE_ROOT = new File("/");
|
|
||||||
|
|
||||||
private static HashMap<String, PathStrategy> sCache = new HashMap<>();
|
|
||||||
|
|
||||||
private PathStrategy mStrategy;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreate() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void attachInfo(Context context, ProviderInfo info) {
|
|
||||||
super.attachInfo(context, info);
|
|
||||||
|
|
||||||
|
|
||||||
if (info.exported) {
|
|
||||||
throw new SecurityException("Provider must not be exported");
|
|
||||||
}
|
|
||||||
if (!info.grantUriPermissions) {
|
|
||||||
throw new SecurityException("Provider must grant uri permissions");
|
|
||||||
}
|
|
||||||
|
|
||||||
mStrategy = getPathStrategy(context, info.authority);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static Uri getUriForFile(Context context, String authority,
|
|
||||||
File file) {
|
|
||||||
final PathStrategy strategy = getPathStrategy(context, authority);
|
|
||||||
return strategy.getUriForFile(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Cursor query(Uri uri, String[] projection, String selection,
|
|
||||||
String[] selectionArgs,
|
|
||||||
String sortOrder) {
|
|
||||||
|
|
||||||
final File file = mStrategy.getFileForUri(uri);
|
|
||||||
|
|
||||||
if (projection == null) {
|
|
||||||
projection = COLUMNS;
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] cols = new String[projection.length];
|
|
||||||
Object[] values = new Object[projection.length];
|
|
||||||
int i = 0;
|
|
||||||
for (String col : projection) {
|
|
||||||
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
|
|
||||||
cols[i] = OpenableColumns.DISPLAY_NAME;
|
|
||||||
values[i++] = file.getName();
|
|
||||||
} else if (OpenableColumns.SIZE.equals(col)) {
|
|
||||||
cols[i] = OpenableColumns.SIZE;
|
|
||||||
values[i++] = file.length();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cols = copyOf(cols, i);
|
|
||||||
values = copyOf(values, i);
|
|
||||||
|
|
||||||
final MatrixCursor cursor = new MatrixCursor(cols, 1);
|
|
||||||
cursor.addRow(values);
|
|
||||||
return cursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getType(Uri uri) {
|
|
||||||
|
|
||||||
final File file = mStrategy.getFileForUri(uri);
|
|
||||||
|
|
||||||
final int lastDot = file.getName().lastIndexOf('.');
|
|
||||||
if (lastDot >= 0) {
|
|
||||||
final String extension = file.getName().substring(lastDot + 1);
|
|
||||||
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
|
||||||
if (mime != null) {
|
|
||||||
return mime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "application/octet-stream";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Uri insert(Uri uri, ContentValues values) {
|
|
||||||
throw new UnsupportedOperationException("No external inserts");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int update(Uri uri, ContentValues values, String selection,
|
|
||||||
String[] selectionArgs) {
|
|
||||||
throw new UnsupportedOperationException("No external updates");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int delete(Uri uri, String selection,
|
|
||||||
String[] selectionArgs) {
|
|
||||||
|
|
||||||
final File file = mStrategy.getFileForUri(uri);
|
|
||||||
return file.delete() ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ParcelFileDescriptor openFile(Uri uri, String mode)
|
|
||||||
throws FileNotFoundException {
|
|
||||||
|
|
||||||
final File file = mStrategy.getFileForUri(uri);
|
|
||||||
final int fileMode = modeToMode(mode);
|
|
||||||
return ParcelFileDescriptor.open(file, fileMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static PathStrategy getPathStrategy(Context context, String authority) {
|
|
||||||
PathStrategy strat;
|
|
||||||
synchronized (sCache) {
|
|
||||||
strat = sCache.get(authority);
|
|
||||||
if (strat == null) {
|
|
||||||
strat = createPathStrategy(context, authority);
|
|
||||||
sCache.put(authority, strat);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strat;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static PathStrategy createPathStrategy(Context context, String authority) {
|
|
||||||
final SimplePathStrategy strat = new SimplePathStrategy(authority);
|
|
||||||
|
|
||||||
strat.addRoot("root_files", buildPath(DEVICE_ROOT, "."));
|
|
||||||
strat.addRoot("internal_files", buildPath(context.getFilesDir(), "."));
|
|
||||||
strat.addRoot("cache_files", buildPath(context.getCacheDir(), "."));
|
|
||||||
strat.addRoot("external_files", buildPath(Environment.getExternalStorageDirectory(), "."));
|
|
||||||
{
|
|
||||||
File[] externalFilesDirs = getExternalFilesDirs(context, null);
|
|
||||||
if (externalFilesDirs.length > 0) {
|
|
||||||
strat.addRoot("external_file_files", buildPath(externalFilesDirs[0], "."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
{
|
|
||||||
File[] externalCacheDirs = getExternalCacheDirs(context);
|
|
||||||
if (externalCacheDirs.length > 0) {
|
|
||||||
strat.addRoot("external_cache_files", buildPath(externalCacheDirs[0], "."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
File[] externalMediaDirs = context.getExternalMediaDirs();
|
|
||||||
if (externalMediaDirs.length > 0) {
|
|
||||||
strat.addRoot("external_media_files", buildPath(externalMediaDirs[0], "."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return strat;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PathStrategy {
|
|
||||||
|
|
||||||
Uri getUriForFile(File file);
|
|
||||||
|
|
||||||
File getFileForUri(Uri uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
static class SimplePathStrategy implements PathStrategy {
|
|
||||||
private final String mAuthority;
|
|
||||||
private final HashMap<String, File> mRoots = new HashMap<>();
|
|
||||||
|
|
||||||
SimplePathStrategy(String authority) {
|
|
||||||
mAuthority = authority;
|
|
||||||
}
|
|
||||||
|
|
||||||
void addRoot(String name, File root) {
|
|
||||||
if (TextUtils.isEmpty(name)) {
|
|
||||||
throw new IllegalArgumentException("Name must not be empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
root = root.getCanonicalFile();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Failed to resolve canonical path for " + root, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
mRoots.put(name, root);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Uri getUriForFile(File file) {
|
|
||||||
String path;
|
|
||||||
try {
|
|
||||||
path = file.getCanonicalPath();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Map.Entry<String, File> mostSpecific = null;
|
|
||||||
for (Map.Entry<String, File> root : mRoots.entrySet()) {
|
|
||||||
final String rootPath = root.getValue().getPath();
|
|
||||||
if (path.startsWith(rootPath) && (mostSpecific == null
|
|
||||||
|| rootPath.length() > mostSpecific.getValue().getPath().length())) {
|
|
||||||
mostSpecific = root;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mostSpecific == null) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Failed to find configured root that contains " + path);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
final String rootPath = mostSpecific.getValue().getPath();
|
|
||||||
if (rootPath.endsWith("/")) {
|
|
||||||
path = path.substring(rootPath.length());
|
|
||||||
} else {
|
|
||||||
path = path.substring(rootPath.length() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
|
|
||||||
return new Uri.Builder().scheme("content")
|
|
||||||
.authority(mAuthority).encodedPath(path).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public File getFileForUri(Uri uri) {
|
|
||||||
String path = uri.getEncodedPath();
|
|
||||||
|
|
||||||
final int splitIndex = path.indexOf('/', 1);
|
|
||||||
final String tag = Uri.decode(path.substring(1, splitIndex));
|
|
||||||
path = Uri.decode(path.substring(splitIndex + 1));
|
|
||||||
|
|
||||||
final File root = mRoots.get(tag);
|
|
||||||
if (root == null) {
|
|
||||||
throw new IllegalArgumentException("Unable to find configured root for " + uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
File file = new File(root, path);
|
|
||||||
try {
|
|
||||||
file = file.getCanonicalFile();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!file.getPath().startsWith(root.getPath())) {
|
|
||||||
throw new SecurityException("Resolved path jumped beyond configured root");
|
|
||||||
}
|
|
||||||
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static int modeToMode(String mode) {
|
|
||||||
int modeBits;
|
|
||||||
if ("r".equals(mode)) {
|
|
||||||
modeBits = ParcelFileDescriptor.MODE_READ_ONLY;
|
|
||||||
} else if ("w".equals(mode) || "wt".equals(mode)) {
|
|
||||||
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
|
|
||||||
| ParcelFileDescriptor.MODE_CREATE
|
|
||||||
| ParcelFileDescriptor.MODE_TRUNCATE;
|
|
||||||
} else if ("wa".equals(mode)) {
|
|
||||||
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
|
|
||||||
| ParcelFileDescriptor.MODE_CREATE
|
|
||||||
| ParcelFileDescriptor.MODE_APPEND;
|
|
||||||
} else if ("rw".equals(mode)) {
|
|
||||||
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
|
|
||||||
| ParcelFileDescriptor.MODE_CREATE;
|
|
||||||
} else if ("rwt".equals(mode)) {
|
|
||||||
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
|
|
||||||
| ParcelFileDescriptor.MODE_CREATE
|
|
||||||
| ParcelFileDescriptor.MODE_TRUNCATE;
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("Invalid mode: " + mode);
|
|
||||||
}
|
|
||||||
return modeBits;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static File buildPath(File base, String... segments) {
|
|
||||||
File cur = base;
|
|
||||||
for (String segment : segments) {
|
|
||||||
if (segment != null) {
|
|
||||||
cur = new File(cur, segment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cur;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String[] copyOf(String[] original, int newLength) {
|
|
||||||
final String[] result = new String[newLength];
|
|
||||||
System.arraycopy(original, 0, result, 0, newLength);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Object[] copyOf(Object[] original, int newLength) {
|
|
||||||
final Object[] result = new Object[newLength];
|
|
||||||
System.arraycopy(original, 0, result, 0, newLength);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static File[] getExternalFilesDirs(Context context, String type) {
|
|
||||||
if (Build.VERSION.SDK_INT >= 19) {
|
|
||||||
return context.getExternalFilesDirs(type);
|
|
||||||
} else {
|
|
||||||
return new File[] { context.getExternalFilesDir(type) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static File[] getExternalCacheDirs(Context context) {
|
|
||||||
if (Build.VERSION.SDK_INT >= 19) {
|
|
||||||
return context.getExternalCacheDirs();
|
|
||||||
} else {
|
|
||||||
return new File[] { context.getExternalCacheDir() };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.topjohnwu.magisk;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
public class ProviderInstaller {
|
||||||
|
|
||||||
|
public static boolean install(Context context) {
|
||||||
|
try {
|
||||||
|
// Try installing new SSL provider from Google Play Service
|
||||||
|
Context gms = context.createPackageContext("com.google.android.gms",
|
||||||
|
Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
|
||||||
|
gms.getClassLoader()
|
||||||
|
.loadClass("com.google.android.gms.common.security.ProviderInstallerImpl")
|
||||||
|
.getMethod("insertProvider", Context.class)
|
||||||
|
.invoke(null, gms);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
120
app/shared/src/main/java/com/topjohnwu/magisk/StubApk.java
Normal file
120
app/shared/src/main/java/com/topjohnwu/magisk/StubApk.java
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package com.topjohnwu.magisk;
|
||||||
|
|
||||||
|
import static android.os.Build.VERSION.SDK_INT;
|
||||||
|
import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
|
import android.content.res.AssetManager;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.content.res.loader.ResourcesLoader;
|
||||||
|
import android.content.res.loader.ResourcesProvider;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class StubApk {
|
||||||
|
private static File dynDir;
|
||||||
|
private static Method addAssetPath;
|
||||||
|
|
||||||
|
private static File getDynDir(ApplicationInfo info) {
|
||||||
|
if (dynDir == null) {
|
||||||
|
final String dataDir;
|
||||||
|
if (SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
// Use device protected path to allow directBootAware
|
||||||
|
dataDir = info.deviceProtectedDataDir;
|
||||||
|
} else {
|
||||||
|
dataDir = info.dataDir;
|
||||||
|
}
|
||||||
|
dynDir = new File(dataDir, "dyn");
|
||||||
|
dynDir.mkdirs();
|
||||||
|
}
|
||||||
|
return dynDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File current(Context c) {
|
||||||
|
return new File(getDynDir(c.getApplicationInfo()), "current.apk");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File current(ApplicationInfo info) {
|
||||||
|
return new File(getDynDir(info), "current.apk");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File update(Context c) {
|
||||||
|
return new File(getDynDir(c.getApplicationInfo()), "update.apk");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File update(ApplicationInfo info) {
|
||||||
|
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) {
|
||||||
|
if (SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
try {
|
||||||
|
res.addLoaders(getResourcesLoader(new File(path)));
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
} else {
|
||||||
|
AssetManager asset = res.getAssets();
|
||||||
|
try {
|
||||||
|
if (addAssetPath == null)
|
||||||
|
addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
|
||||||
|
addAssetPath.invoke(asset, path);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void restartProcess(Activity activity) {
|
||||||
|
Intent intent = activity.getPackageManager()
|
||||||
|
.getLaunchIntentForPackage(activity.getPackageName());
|
||||||
|
activity.finishAffinity();
|
||||||
|
activity.startActivity(intent);
|
||||||
|
Runtime.getRuntime().exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Data {
|
||||||
|
// Indices of the object array
|
||||||
|
private static final int STUB_VERSION = 0;
|
||||||
|
private static final int CLASS_COMPONENT_MAP = 1;
|
||||||
|
private static final int ROOT_SERVICE = 2;
|
||||||
|
private static final int ARR_SIZE = 3;
|
||||||
|
|
||||||
|
private final Object[] arr;
|
||||||
|
|
||||||
|
public Data() { arr = new Object[ARR_SIZE]; }
|
||||||
|
public Data(Object o) { arr = (Object[]) o; }
|
||||||
|
public Object getObject() { return arr; }
|
||||||
|
|
||||||
|
public int getVersion() { return (int) arr[STUB_VERSION]; }
|
||||||
|
public void setVersion(int version) { arr[STUB_VERSION] = version; }
|
||||||
|
public Map<String, String> getClassToComponent() {
|
||||||
|
// noinspection unchecked
|
||||||
|
return (Map<String, String>) arr[CLASS_COMPONENT_MAP];
|
||||||
|
}
|
||||||
|
public void setClassToComponent(Map<String, String> map) {
|
||||||
|
arr[CLASS_COMPONENT_MAP] = map;
|
||||||
|
}
|
||||||
|
public Class<?> getRootService() { return (Class<?>) arr[ROOT_SERVICE]; }
|
||||||
|
public void setRootService(Class<?> service) { arr[ROOT_SERVICE] = service; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.net;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.Socket;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javax.net.ssl.HttpsURLConnection;
|
|
||||||
import javax.net.ssl.SSLSocket;
|
|
||||||
import javax.net.ssl.SSLSocketFactory;
|
|
||||||
|
|
||||||
public class NoSSLv3SocketFactory extends SSLSocketFactory {
|
|
||||||
|
|
||||||
private final static SSLSocketFactory delegate = HttpsURLConnection.getDefaultSSLSocketFactory();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String[] getDefaultCipherSuites() {
|
|
||||||
return delegate.getDefaultCipherSuites();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String[] getSupportedCipherSuites() {
|
|
||||||
return delegate.getSupportedCipherSuites();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Socket createSafeSocket(Socket socket) {
|
|
||||||
if (socket instanceof SSLSocket)
|
|
||||||
return new SSLSocketWrapper((SSLSocket) socket) {
|
|
||||||
@Override
|
|
||||||
public void setEnabledProtocols(String[] protocols) {
|
|
||||||
List<String> proto = new ArrayList<>(Arrays.asList(getSupportedProtocols()));
|
|
||||||
proto.remove("SSLv3");
|
|
||||||
super.setEnabledProtocols(proto.toArray(new String[0]));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return socket;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
|
|
||||||
return createSafeSocket(delegate.createSocket(s, host, port, autoClose));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Socket createSocket() throws IOException {
|
|
||||||
return createSafeSocket(delegate.createSocket());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Socket createSocket(String host, int port) throws IOException {
|
|
||||||
return createSafeSocket(delegate.createSocket(host, port));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
|
|
||||||
return createSafeSocket(delegate.createSocket(host, port, localHost, localPort));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Socket createSocket(InetAddress host, int port) throws IOException {
|
|
||||||
return createSafeSocket(delegate.createSocket(host, port));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
|
|
||||||
return createSafeSocket(delegate.createSocket(address, port, localAddress, localPort));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,333 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.net;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.SocketAddress;
|
|
||||||
import java.net.SocketException;
|
|
||||||
import java.nio.channels.SocketChannel;
|
|
||||||
|
|
||||||
import javax.net.ssl.HandshakeCompletedListener;
|
|
||||||
import javax.net.ssl.SSLParameters;
|
|
||||||
import javax.net.ssl.SSLSession;
|
|
||||||
import javax.net.ssl.SSLSocket;
|
|
||||||
|
|
||||||
class SSLSocketWrapper extends SSLSocket {
|
|
||||||
|
|
||||||
private SSLSocket mBase;
|
|
||||||
|
|
||||||
SSLSocketWrapper(SSLSocket socket) {
|
|
||||||
mBase = socket;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String[] getSupportedCipherSuites() {
|
|
||||||
return mBase.getSupportedCipherSuites();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String[] getEnabledCipherSuites() {
|
|
||||||
return mBase.getEnabledCipherSuites();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setEnabledCipherSuites(String[] suites) {
|
|
||||||
mBase.setEnabledCipherSuites(suites);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String[] getSupportedProtocols() {
|
|
||||||
return mBase.getSupportedProtocols();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String[] getEnabledProtocols() {
|
|
||||||
return mBase.getEnabledProtocols();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setEnabledProtocols(String[] protocols) {
|
|
||||||
mBase.setEnabledProtocols(protocols);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SSLSession getSession() {
|
|
||||||
return mBase.getSession();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SSLSession getHandshakeSession() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addHandshakeCompletedListener(HandshakeCompletedListener listener) {
|
|
||||||
mBase.addHandshakeCompletedListener(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void removeHandshakeCompletedListener(HandshakeCompletedListener listener) {
|
|
||||||
mBase.removeHandshakeCompletedListener(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void startHandshake() throws IOException {
|
|
||||||
mBase.startHandshake();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setUseClientMode(boolean mode) {
|
|
||||||
mBase.setUseClientMode(mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean getUseClientMode() {
|
|
||||||
return mBase.getUseClientMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setNeedClientAuth(boolean need) {
|
|
||||||
mBase.setNeedClientAuth(need);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean getNeedClientAuth() {
|
|
||||||
return mBase.getNeedClientAuth();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setWantClientAuth(boolean want) {
|
|
||||||
mBase.setWantClientAuth(want);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean getWantClientAuth() {
|
|
||||||
return mBase.getWantClientAuth();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setEnableSessionCreation(boolean flag) {
|
|
||||||
mBase.setEnableSessionCreation(flag);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean getEnableSessionCreation() {
|
|
||||||
return mBase.getEnableSessionCreation();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SSLParameters getSSLParameters() {
|
|
||||||
return mBase.getSSLParameters();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setSSLParameters(SSLParameters params) {
|
|
||||||
mBase.setSSLParameters(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return mBase.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void connect(SocketAddress endpoint) throws IOException {
|
|
||||||
mBase.connect(endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void connect(SocketAddress endpoint, int timeout) throws IOException {
|
|
||||||
mBase.connect(endpoint, timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void bind(SocketAddress bindpoint) throws IOException {
|
|
||||||
mBase.bind(bindpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InetAddress getInetAddress() {
|
|
||||||
return mBase.getInetAddress();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InetAddress getLocalAddress() {
|
|
||||||
return mBase.getLocalAddress();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getPort() {
|
|
||||||
return mBase.getPort();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getLocalPort() {
|
|
||||||
return mBase.getLocalPort();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SocketAddress getRemoteSocketAddress() {
|
|
||||||
return mBase.getRemoteSocketAddress();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SocketAddress getLocalSocketAddress() {
|
|
||||||
return mBase.getLocalSocketAddress();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SocketChannel getChannel() {
|
|
||||||
return mBase.getChannel();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InputStream getInputStream() throws IOException {
|
|
||||||
return mBase.getInputStream();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public OutputStream getOutputStream() throws IOException {
|
|
||||||
return mBase.getOutputStream();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setTcpNoDelay(boolean on) throws SocketException {
|
|
||||||
mBase.setTcpNoDelay(on);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean getTcpNoDelay() throws SocketException {
|
|
||||||
return mBase.getTcpNoDelay();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setSoLinger(boolean on, int linger) throws SocketException {
|
|
||||||
mBase.setSoLinger(on, linger);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getSoLinger() throws SocketException {
|
|
||||||
return mBase.getSoLinger();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendUrgentData(int data) throws IOException {
|
|
||||||
mBase.sendUrgentData(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setOOBInline(boolean on) throws SocketException {
|
|
||||||
mBase.setOOBInline(on);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean getOOBInline() throws SocketException {
|
|
||||||
return mBase.getOOBInline();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setSoTimeout(int timeout) throws SocketException {
|
|
||||||
mBase.setSoTimeout(timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getSoTimeout() throws SocketException {
|
|
||||||
return mBase.getSoTimeout();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setSendBufferSize(int size) throws SocketException {
|
|
||||||
mBase.setSendBufferSize(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getSendBufferSize() throws SocketException {
|
|
||||||
return mBase.getSendBufferSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setReceiveBufferSize(int size) throws SocketException {
|
|
||||||
mBase.setReceiveBufferSize(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getReceiveBufferSize() throws SocketException {
|
|
||||||
return mBase.getReceiveBufferSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setKeepAlive(boolean on) throws SocketException {
|
|
||||||
mBase.setKeepAlive(on);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean getKeepAlive() throws SocketException {
|
|
||||||
return mBase.getKeepAlive();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setTrafficClass(int tc) throws SocketException {
|
|
||||||
mBase.setTrafficClass(tc);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getTrafficClass() throws SocketException {
|
|
||||||
return mBase.getTrafficClass();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setReuseAddress(boolean on) throws SocketException {
|
|
||||||
mBase.setReuseAddress(on);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean getReuseAddress() throws SocketException {
|
|
||||||
return mBase.getReuseAddress();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() throws IOException {
|
|
||||||
mBase.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void shutdownInput() throws IOException {
|
|
||||||
mBase.shutdownInput();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void shutdownOutput() throws IOException {
|
|
||||||
mBase.shutdownOutput();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isConnected() {
|
|
||||||
return mBase.isConnected();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isBound() {
|
|
||||||
return mBase.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isClosed() {
|
|
||||||
return mBase.isClosed();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isInputShutdown() {
|
|
||||||
return mBase.isInputShutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isOutputShutdown() {
|
|
||||||
return mBase.isOutputShutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setPerformancePreferences(int connectionTime, int latency, int bandwidth) {
|
|
||||||
mBase.setPerformancePreferences(connectionTime, latency, bandwidth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +1,179 @@
|
|||||||
package com.topjohnwu.magisk.utils;
|
package com.topjohnwu.magisk.utils;
|
||||||
|
|
||||||
|
import static android.content.pm.PackageInstaller.EXTRA_SESSION_ID;
|
||||||
|
import static android.content.pm.PackageInstaller.EXTRA_STATUS;
|
||||||
|
import static android.content.pm.PackageInstaller.STATUS_FAILURE_INVALID;
|
||||||
|
import static android.content.pm.PackageInstaller.STATUS_PENDING_USER_ACTION;
|
||||||
|
import static android.content.pm.PackageInstaller.STATUS_SUCCESS;
|
||||||
|
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.content.pm.PackageInstaller.SessionParams;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
import com.topjohnwu.magisk.FileProvider;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FilterOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class APKInstall {
|
public final class APKInstall {
|
||||||
public static void install(Context c, File apk) {
|
|
||||||
c.startActivity(installIntent(c, apk));
|
public static void transfer(InputStream in, OutputStream out) throws IOException {
|
||||||
|
int size = 8192;
|
||||||
|
var buffer = new byte[size];
|
||||||
|
int read;
|
||||||
|
while ((read = in.read(buffer, 0, size)) >= 0) {
|
||||||
|
out.write(buffer, 0, read);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Intent installIntent(Context c, File apk) {
|
public static void registerReceiver(
|
||||||
Intent install = new Intent(Intent.ACTION_INSTALL_PACKAGE);
|
Context context, BroadcastReceiver receiver, IntentFilter filter) {
|
||||||
install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
// noinspection InlinedApi
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
context.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED);
|
||||||
install.setData(FileProvider.getUriForFile(c, c.getPackageName() + ".provider", apk));
|
|
||||||
} else {
|
} else {
|
||||||
apk.setReadable(true, false);
|
context.registerReceiver(receiver, filter);
|
||||||
install.setData(Uri.fromFile(apk));
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Session startSession(Context context) {
|
||||||
|
return startSession(context, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Session startSession(Context context, String pkg,
|
||||||
|
Runnable onFailure, Runnable onSuccess) {
|
||||||
|
var receiver = new InstallReceiver(pkg, onSuccess, onFailure);
|
||||||
|
context = context.getApplicationContext();
|
||||||
|
if (pkg != null) {
|
||||||
|
// If pkg is not null, look for package added event
|
||||||
|
var filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
|
||||||
|
filter.addDataScheme("package");
|
||||||
|
registerReceiver(context, receiver, filter);
|
||||||
|
}
|
||||||
|
registerReceiver(context, receiver, new IntentFilter(receiver.sessionId));
|
||||||
|
return receiver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Session {
|
||||||
|
// @WorkerThread
|
||||||
|
OutputStream openStream(Context context) throws IOException;
|
||||||
|
// @WorkerThread
|
||||||
|
void install(Context context, File apk) throws IOException;
|
||||||
|
// @WorkerThread @Nullable
|
||||||
|
Intent waitIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class InstallReceiver extends BroadcastReceiver implements Session {
|
||||||
|
private final String packageName;
|
||||||
|
private final Runnable onSuccess;
|
||||||
|
private final Runnable onFailure;
|
||||||
|
private final CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
private Intent userAction = null;
|
||||||
|
|
||||||
|
final String sessionId = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
private InstallReceiver(String packageName, Runnable onSuccess, Runnable onFailure) {
|
||||||
|
this.packageName = packageName;
|
||||||
|
this.onSuccess = onSuccess;
|
||||||
|
this.onFailure = onFailure;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) {
|
||||||
|
Uri data = intent.getData();
|
||||||
|
if (data == null)
|
||||||
|
return;
|
||||||
|
String pkg = data.getSchemeSpecificPart();
|
||||||
|
if (pkg.equals(packageName)) {
|
||||||
|
onSuccess(context);
|
||||||
|
}
|
||||||
|
} else if (sessionId.equals(intent.getAction())) {
|
||||||
|
int status = intent.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID);
|
||||||
|
switch (status) {
|
||||||
|
case STATUS_PENDING_USER_ACTION ->
|
||||||
|
userAction = intent.getParcelableExtra(Intent.EXTRA_INTENT);
|
||||||
|
case STATUS_SUCCESS -> {
|
||||||
|
if (packageName == null) {
|
||||||
|
onSuccess(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
int id = intent.getIntExtra(EXTRA_SESSION_ID, 0);
|
||||||
|
var installer = context.getPackageManager().getPackageInstaller();
|
||||||
|
try {
|
||||||
|
installer.abandonSession(id);
|
||||||
|
} catch (SecurityException ignored) {
|
||||||
|
}
|
||||||
|
if (onFailure != null) {
|
||||||
|
onFailure.run();
|
||||||
|
}
|
||||||
|
context.getApplicationContext().unregisterReceiver(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onSuccess(Context context) {
|
||||||
|
if (onSuccess != null)
|
||||||
|
onSuccess.run();
|
||||||
|
context.getApplicationContext().unregisterReceiver(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Intent waitIntent() {
|
||||||
|
try {
|
||||||
|
// noinspection ResultOfMethodCallIgnored
|
||||||
|
latch.await(5, TimeUnit.SECONDS);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
return userAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OutputStream openStream(Context context) throws IOException {
|
||||||
|
// noinspection InlinedApi
|
||||||
|
var flag = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE;
|
||||||
|
var intent = new Intent(sessionId).setPackage(context.getPackageName());
|
||||||
|
var pending = PendingIntent.getBroadcast(context, 0, intent, flag);
|
||||||
|
|
||||||
|
var installer = context.getPackageManager().getPackageInstaller();
|
||||||
|
var params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
params.setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED);
|
||||||
|
}
|
||||||
|
var session = installer.openSession(installer.createSession(params));
|
||||||
|
var out = session.openWrite(sessionId, 0, -1);
|
||||||
|
return new FilterOutputStream(out) {
|
||||||
|
@Override
|
||||||
|
public void write(byte[] b, int off, int len) throws IOException {
|
||||||
|
out.write(b, off, len);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
super.close();
|
||||||
|
session.commit(pending.getIntentSender());
|
||||||
|
session.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void install(Context context, File apk) throws IOException {
|
||||||
|
try (var src = new FileInputStream(apk);
|
||||||
|
var out = openStream(context)) {
|
||||||
|
transfer(src, out);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return install;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,35 @@
|
|||||||
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;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
|
|
||||||
import dalvik.system.DexClassLoader;
|
import dalvik.system.BaseDexClassLoader;
|
||||||
|
|
||||||
public class DynamicClassLoader extends DexClassLoader {
|
public class DynamicClassLoader extends BaseDexClassLoader {
|
||||||
|
|
||||||
private ClassLoader base = Object.class.getClassLoader();
|
public DynamicClassLoader(File apk) {
|
||||||
|
this(apk, getSystemClassLoader());
|
||||||
|
}
|
||||||
|
|
||||||
public DynamicClassLoader(File apk, ClassLoader parent) {
|
public DynamicClassLoader(File apk, ClassLoader parent) {
|
||||||
super(apk.getPath(), apk.getParent(), null, parent);
|
// Set optimizedDirectory to null for RootService to bypass DexFile's security checks
|
||||||
|
super(apk.getPath(), Process.myUid() == 0 ? null : apk.getParentFile(), null, parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||||
// First check if already loaded
|
// First check if already loaded
|
||||||
Class cls = findLoadedClass(name);
|
Class<?> cls = findLoadedClass(name);
|
||||||
if (cls != null)
|
if (cls != null)
|
||||||
return cls;
|
return cls;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Then check boot classpath
|
// Then check boot classpath
|
||||||
return base.loadClass(name);
|
return getSystemClassLoader().loadClass(name);
|
||||||
} catch (ClassNotFoundException ignored) {
|
} catch (ClassNotFoundException ignored) {
|
||||||
try {
|
try {
|
||||||
// Next try current dex
|
// Next try current dex
|
||||||
@@ -42,7 +47,7 @@ public class DynamicClassLoader extends DexClassLoader {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public URL getResource(String name) {
|
public URL getResource(String name) {
|
||||||
URL resource = base.getResource(name);
|
URL resource = getSystemClassLoader().getResource(name);
|
||||||
if (resource != null)
|
if (resource != null)
|
||||||
return resource;
|
return resource;
|
||||||
resource = findResource(name);
|
resource = findResource(name);
|
||||||
@@ -54,7 +59,7 @@ public class DynamicClassLoader extends DexClassLoader {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Enumeration<URL> getResources(String name) throws IOException {
|
public Enumeration<URL> getResources(String name) throws IOException {
|
||||||
return new CompoundEnumeration<>(base.getResources(name),
|
return new CompoundEnumeration<>(getSystemClassLoader().getResources(name),
|
||||||
findResources(name), getParent().getResources(name));
|
findResources(name), getParent().getResources(name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<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.magisk">
|
|
||||||
|
<permission
|
||||||
|
android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
|
||||||
|
android:protectionLevel="signature"
|
||||||
|
tools:node="remove" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
|
||||||
android:maxSdkVersion="29" />
|
tools:node="remove" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".core.App"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:name="a.e"
|
android:multiArch="true"
|
||||||
android:allowBackup="false"
|
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning"
|
||||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
|
tools:remove="android:appComponentFactory">
|
||||||
|
|
||||||
<!-- Splash -->
|
|
||||||
<activity
|
<activity
|
||||||
android:name="a.c"
|
android:name=".ui.MainActivity"
|
||||||
|
android:exported="true"
|
||||||
android:theme="@style/SplashTheme">
|
android:theme="@style/SplashTheme">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
@@ -29,29 +32,25 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<!-- Main -->
|
|
||||||
<activity android:name="a.b" />
|
|
||||||
|
|
||||||
<!-- Superuser -->
|
|
||||||
<activity
|
<activity
|
||||||
android:name="a.m"
|
android:name=".ui.surequest.SuRequestActivity"
|
||||||
android:directBootAware="true"
|
android:directBootAware="true"
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
android:taskAffinity=""
|
||||||
tools:ignore="AppLinkUrlError">
|
tools:ignore="AppLinkUrlError">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<!-- Receiver -->
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name="a.h"
|
android:name=".core.Receiver"
|
||||||
android:directBootAware="true">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.REBOOT" />
|
|
||||||
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
||||||
|
<action android:name="android.intent.action.UID_REMOVED" />
|
||||||
|
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.PACKAGE_REPLACED" />
|
<action android:name="android.intent.action.PACKAGE_REPLACED" />
|
||||||
@@ -61,37 +60,33 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<!-- DownloadService -->
|
<service
|
||||||
<service android:name="a.j" />
|
android:name=".core.download.DownloadService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".core.JobService"
|
||||||
|
android:exported="false"
|
||||||
|
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||||
|
|
||||||
<!-- FileProvider -->
|
|
||||||
<provider
|
<provider
|
||||||
android:name="a.p"
|
android:name=".core.Provider"
|
||||||
android:authorities="${applicationId}.provider"
|
android:authorities="${applicationId}.provider"
|
||||||
android:directBootAware="true"
|
android:directBootAware="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:grantUriPermissions="true">
|
android:grantUriPermissions="true" />
|
||||||
</provider>
|
|
||||||
|
|
||||||
<!-- Hardcode GMS version -->
|
|
||||||
<meta-data
|
|
||||||
android:name="com.google.android.gms.version"
|
|
||||||
android:value="12451000" />
|
|
||||||
|
|
||||||
<!-- Initialize WorkManager on-demand -->
|
|
||||||
<provider
|
|
||||||
android:name="androidx.work.impl.WorkManagerInitializer"
|
|
||||||
android:authorities="${applicationId}.workmanager-init"
|
|
||||||
tools:node="remove" />
|
|
||||||
|
|
||||||
<!-- We don't invalidate Room -->
|
<!-- We don't invalidate Room -->
|
||||||
<service
|
<service
|
||||||
android:name="androidx.room.MultiInstanceInvalidationService"
|
android:name="androidx.room.MultiInstanceInvalidationService"
|
||||||
tools:node="remove"/>
|
tools:node="remove" />
|
||||||
|
|
||||||
<!-- We don't use Device Credentials -->
|
<!-- We don't need emoji compat -->
|
||||||
<activity
|
<provider
|
||||||
android:name="androidx.biometric.DeviceCredentialHandlerActivity"
|
android:name="androidx.startup.InitializationProvider"
|
||||||
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
|
android:exported="false"
|
||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// IRootUtils.aidl
|
||||||
|
package com.topjohnwu.magisk.core.utils;
|
||||||
|
|
||||||
|
// Declare any non-default types here with import statements
|
||||||
|
|
||||||
|
interface IRootUtils {
|
||||||
|
android.app.ActivityManager.RunningAppProcessInfo getAppProcess(int pid);
|
||||||
|
IBinder getFileSystem();
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
@file:JvmName("a")
|
|
||||||
package a
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.core.App
|
|
||||||
import com.topjohnwu.magisk.core.Provider
|
|
||||||
import com.topjohnwu.magisk.core.Receiver
|
|
||||||
import com.topjohnwu.magisk.core.SplashActivity
|
|
||||||
import com.topjohnwu.magisk.core.download.DownloadService
|
|
||||||
import com.topjohnwu.magisk.ui.MainActivity
|
|
||||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
|
||||||
import com.topjohnwu.signing.SignBoot
|
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
SignBoot.main(args)
|
|
||||||
}
|
|
||||||
|
|
||||||
class b : MainActivity()
|
|
||||||
|
|
||||||
class c : SplashActivity()
|
|
||||||
|
|
||||||
class e : App {
|
|
||||||
constructor() : super()
|
|
||||||
constructor(o: Any) : super(o)
|
|
||||||
}
|
|
||||||
|
|
||||||
class h : Receiver()
|
|
||||||
|
|
||||||
class j : DownloadService()
|
|
||||||
|
|
||||||
class m : SuRequestActivity()
|
|
||||||
|
|
||||||
class p : Provider()
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.topjohnwu.magisk.arch
|
||||||
|
|
||||||
|
import androidx.annotation.MainThread
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
abstract class AsyncLoadViewModel : BaseViewModel() {
|
||||||
|
|
||||||
|
private var loadingJob: Job? = null
|
||||||
|
|
||||||
|
@MainThread
|
||||||
|
fun startLoading() {
|
||||||
|
if (loadingJob?.isActive == true) {
|
||||||
|
// Prevent multiple jobs from running at the same time
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loadingJob = viewModelScope.launch { doLoadWork() }
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract suspend fun doLoadWork()
|
||||||
|
}
|
||||||
@@ -5,28 +5,28 @@ 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.graphics.Insets
|
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 BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
|
abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHolder {
|
||||||
Fragment(), BaseUIComponent<VM> {
|
|
||||||
|
|
||||||
val activity get() = requireActivity() as BaseUIActivity<*, *>
|
val activity get() = getActivity() as? NavigationActivity<*>
|
||||||
protected lateinit var binding: Binding
|
protected lateinit var binding: Binding
|
||||||
protected abstract val layoutRes: Int
|
protected abstract val layoutRes: Int
|
||||||
|
|
||||||
override val viewRoot: View get() = binding.root
|
private val navigation get() = activity?.navigation
|
||||||
private val navigation get() = activity.navigation
|
open val snackbarView: View? get() = null
|
||||||
|
open val snackbarAnchorView: View? get() = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
startObserveEvents()
|
startObserveLiveData()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
@@ -36,14 +36,27 @@ abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
|
|||||||
): View? {
|
): View? {
|
||||||
binding = DataBindingUtil.inflate<Binding>(inflater, layoutRes, container, false).also {
|
binding = DataBindingUtil.inflate<Binding>(inflater, layoutRes, container, false).also {
|
||||||
it.setVariable(BR.viewModel, viewModel)
|
it.setVariable(BR.viewModel, viewModel)
|
||||||
it.lifecycleOwner = this
|
it.lifecycleOwner = viewLifecycleOwner
|
||||||
}
|
}
|
||||||
|
if (this is MenuProvider) {
|
||||||
|
activity?.addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.STARTED)
|
||||||
|
}
|
||||||
|
savedInstanceState?.let { viewModel.onRestoreState(it) }
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
viewModel.onSaveState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
activity?.supportActionBar?.subtitle = null
|
||||||
|
}
|
||||||
|
|
||||||
override fun onEventDispatched(event: ViewEvent) = when(event) {
|
override fun onEventDispatched(event: ViewEvent) = when(event) {
|
||||||
is ContextExecutor -> event(requireContext())
|
is ContextExecutor -> event(requireContext())
|
||||||
is ActivityExecutor -> event(activity)
|
is ActivityExecutor -> activity?.let { event(it) } ?: Unit
|
||||||
is FragmentExecutor -> event(this)
|
is FragmentExecutor -> event(this)
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
@@ -59,7 +72,7 @@ abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
|
|||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
binding.addOnRebindCallback(object : OnRebindCallback<Binding>() {
|
binding.addOnRebindCallback(object : OnRebindCallback<Binding>() {
|
||||||
override fun onPreBind(binding: Binding): Boolean {
|
override fun onPreBind(binding: Binding): Boolean {
|
||||||
this@BaseUIFragment.onPreBind(binding)
|
this@BaseFragment.onPreBind(binding)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -67,7 +80,10 @@ abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
viewModel.requestRefresh()
|
viewModel.let {
|
||||||
|
if (it is AsyncLoadViewModel)
|
||||||
|
it.startLoading()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun onPreBind(binding: Binding) {
|
protected open fun onPreBind(binding: Binding) {
|
||||||
@@ -75,13 +91,6 @@ abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun NavDirections.navigate() {
|
fun NavDirections.navigate() {
|
||||||
navigation?.navigate(this)
|
navigation?.currentDestination?.getAction(actionId)?.let { navigation!!.navigate(this) }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ReselectionTarget {
|
|
||||||
|
|
||||||
fun onReselected()
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
|
||||||
|
|
||||||
import android.content.res.Resources
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.view.View
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
|
||||||
import androidx.core.content.res.use
|
|
||||||
import androidx.databinding.DataBindingUtil
|
|
||||||
import androidx.databinding.ViewDataBinding
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.NavDirections
|
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
|
||||||
import com.topjohnwu.magisk.BR
|
|
||||||
import com.topjohnwu.magisk.core.Config
|
|
||||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
|
||||||
import com.topjohnwu.magisk.ui.inflater.LayoutInflaterFactory
|
|
||||||
import com.topjohnwu.magisk.ui.theme.Theme
|
|
||||||
|
|
||||||
abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
|
|
||||||
BaseActivity(), BaseUIComponent<VM> {
|
|
||||||
|
|
||||||
protected lateinit var binding: Binding
|
|
||||||
protected abstract val layoutRes: Int
|
|
||||||
protected open val themeRes: Int = Theme.selected.themeRes
|
|
||||||
|
|
||||||
private val navHostFragment by lazy {
|
|
||||||
supportFragmentManager.findFragmentById(navHost) as? NavHostFragment
|
|
||||||
}
|
|
||||||
private val topFragment get() = navHostFragment?.childFragmentManager?.fragments?.getOrNull(0)
|
|
||||||
protected val currentFragment get() = topFragment as? BaseUIFragment<*, *>
|
|
||||||
|
|
||||||
override val viewRoot: View get() = binding.root
|
|
||||||
open val navigation: NavController? get() = navHostFragment?.navController
|
|
||||||
|
|
||||||
open val navHost: Int = 0
|
|
||||||
open val snackbarView get() = binding.root
|
|
||||||
|
|
||||||
init {
|
|
||||||
val theme = Config.darkTheme
|
|
||||||
AppCompatDelegate.setDefaultNightMode(theme)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
layoutInflater.factory2 = LayoutInflaterFactory(delegate)
|
|
||||||
|
|
||||||
setTheme(themeRes)
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
startObserveEvents()
|
|
||||||
|
|
||||||
// We need to set the window background explicitly since for whatever reason it's not
|
|
||||||
// propagated upstream
|
|
||||||
obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
|
|
||||||
.use { it.getDrawable(0) }
|
|
||||||
.also { window.setBackgroundDrawable(it) }
|
|
||||||
|
|
||||||
directionsDispatcher.observe(this) {
|
|
||||||
it?.navigate()
|
|
||||||
// we don't want the directions to be re-dispatched, so we preemptively set them to null
|
|
||||||
if (it != null) {
|
|
||||||
directionsDispatcher.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
window?.decorView?.let {
|
|
||||||
it.systemUiVisibility = (it.systemUiVisibility
|
|
||||||
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
|
||||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
|
||||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
window?.decorView?.post {
|
|
||||||
// If navigation bar is short enough (gesture navigation enabled), make it transparent
|
|
||||||
if (window.decorView.rootWindowInsets?.systemWindowInsetBottom ?: 0 < Resources.getSystem().displayMetrics.density * 40) {
|
|
||||||
window.navigationBarColor = Color.TRANSPARENT
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
window.navigationBarDividerColor = Color.TRANSPARENT
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
window.isNavigationBarContrastEnforced = false
|
|
||||||
window.isStatusBarContrastEnforced = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setContentView() {
|
|
||||||
binding = DataBindingUtil.setContentView<Binding>(this, layoutRes).also {
|
|
||||||
it.setVariable(BR.viewModel, viewModel)
|
|
||||||
it.lifecycleOwner = this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setAccessibilityDelegate(delegate: View.AccessibilityDelegate?) {
|
|
||||||
viewRoot.rootView.accessibilityDelegate = delegate
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
viewModel.requestRefresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
|
||||||
return currentFragment?.onKeyEvent(event) == true || super.dispatchKeyEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onEventDispatched(event: ViewEvent) = when (event) {
|
|
||||||
is ContextExecutor -> event(this)
|
|
||||||
is ActivityExecutor -> event(this)
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed() {
|
|
||||||
if (navigation == null || currentFragment?.onBackPressed()?.not() == true) {
|
|
||||||
super.onBackPressed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun NavDirections.navigate() {
|
|
||||||
navigation?.navigate(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private val directionsDispatcher = MutableLiveData<NavDirections?>()
|
|
||||||
|
|
||||||
fun postDirections(navDirections: NavDirections) =
|
|
||||||
directionsDispatcher.postValue(navDirections)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
|
|
||||||
interface BaseUIComponent<VM : BaseViewModel> : LifecycleOwner {
|
|
||||||
|
|
||||||
val viewRoot: View
|
|
||||||
val viewModel: VM
|
|
||||||
|
|
||||||
fun startObserveEvents() {
|
|
||||||
viewModel.viewEvents.observe(this) {
|
|
||||||
onEventDispatched(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called for all [ViewEvent]s published by associated viewModel.
|
|
||||||
*/
|
|
||||||
fun onEventDispatched(event: ViewEvent) {}
|
|
||||||
}
|
|
||||||
@@ -1,93 +1,39 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
package com.topjohnwu.magisk.arch
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest.permission.*
|
||||||
import android.os.Build
|
import android.annotation.SuppressLint
|
||||||
import androidx.annotation.CallSuper
|
import android.os.Bundle
|
||||||
import androidx.core.graphics.Insets
|
|
||||||
import androidx.databinding.Bindable
|
|
||||||
import androidx.databinding.Observable
|
|
||||||
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.BR
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.databinding.ObservableHost
|
||||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
import com.topjohnwu.magisk.events.BackPressEvent
|
||||||
import com.topjohnwu.magisk.events.*
|
import com.topjohnwu.magisk.events.DialogBuilder
|
||||||
import com.topjohnwu.magisk.utils.ObservableHost
|
import com.topjohnwu.magisk.events.DialogEvent
|
||||||
import com.topjohnwu.magisk.utils.set
|
import com.topjohnwu.magisk.events.NavigationEvent
|
||||||
import kotlinx.coroutines.Job
|
import com.topjohnwu.magisk.events.PermissionEvent
|
||||||
import org.koin.core.KoinComponent
|
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||||
|
|
||||||
abstract class BaseViewModel(
|
abstract class BaseViewModel : ViewModel(), ObservableHost {
|
||||||
initialState: State = State.LOADING
|
|
||||||
) : ViewModel(), ObservableHost, KoinComponent {
|
|
||||||
|
|
||||||
override var callbacks: PropertyChangeRegistry? = null
|
override var callbacks: PropertyChangeRegistry? = null
|
||||||
|
|
||||||
enum class State {
|
private val _viewEvents = MutableLiveData<ViewEvent>()
|
||||||
LOADED, LOADING, LOADING_FAILED
|
|
||||||
}
|
|
||||||
|
|
||||||
@get:Bindable
|
|
||||||
val loading get() = state == State.LOADING
|
|
||||||
@get:Bindable
|
|
||||||
val loaded get() = state == State.LOADED
|
|
||||||
@get:Bindable
|
|
||||||
val loadFailed get() = state == State.LOADING_FAILED
|
|
||||||
|
|
||||||
val isConnected get() = Info.isConnected
|
|
||||||
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
|
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
|
||||||
|
|
||||||
@get:Bindable
|
open fun onSaveState(state: Bundle) {}
|
||||||
var insets = Insets.NONE
|
open fun onRestoreState(state: Bundle) {}
|
||||||
set(value) = set(value, field, { field = it }, BR.insets)
|
open fun onNetworkChanged(network: Boolean) {}
|
||||||
|
|
||||||
var state= initialState
|
|
||||||
set(value) = set(value, field, { field = it }, BR.loading, BR.loaded, BR.loadFailed)
|
|
||||||
|
|
||||||
private val _viewEvents = MutableLiveData<ViewEvent>()
|
|
||||||
private var runningJob: Job? = null
|
|
||||||
private val refreshCallback = object : Observable.OnPropertyChangedCallback() {
|
|
||||||
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
|
||||||
requestRefresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
isConnected.addOnPropertyChangedCallback(refreshCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** This should probably never be called manually, it's called manually via delegate. */
|
|
||||||
@Synchronized
|
|
||||||
fun requestRefresh() {
|
|
||||||
if (runningJob?.isActive == true) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
runningJob = refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun refresh(): Job? = null
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
override fun onCleared() {
|
|
||||||
isConnected.removeOnPropertyChangedCallback(refreshCallback)
|
|
||||||
super.onCleared()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun withView(action: BaseActivity.() -> Unit) {
|
|
||||||
ViewActionEvent(action).publish()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
||||||
PermissionEvent(permission, callback).publish()
|
PermissionEvent(permission, callback).publish()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun withExternalRW(callback: () -> Unit) {
|
inline fun withExternalRW(crossinline callback: () -> Unit) {
|
||||||
withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) {
|
withPermission(WRITE_EXTERNAL_STORAGE) {
|
||||||
if (!it) {
|
if (!it) {
|
||||||
SnackbarEvent(R.string.external_rw_permission_denied).publish()
|
SnackbarEvent(R.string.external_rw_permission_denied).publish()
|
||||||
} else {
|
} else {
|
||||||
@@ -96,19 +42,40 @@ abstract class BaseViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
inline fun withInstallPermission(crossinline callback: () -> Unit) {
|
||||||
|
withPermission(REQUEST_INSTALL_PACKAGES) {
|
||||||
|
if (!it) {
|
||||||
|
SnackbarEvent(R.string.install_unknown_denied).publish()
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.publish() {
|
fun NavDirections.navigate(pop: Boolean = false) {
|
||||||
_viewEvents.postValue(NavigationEvent(this))
|
_viewEvents.postValue(NavigationEvent(this, pop))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
|
||||||
|
|
||||||
import androidx.databinding.ViewDataBinding
|
|
||||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
|
||||||
import com.topjohnwu.magisk.databinding.RvItem
|
|
||||||
import com.topjohnwu.magisk.utils.DiffObservableList
|
|
||||||
import com.topjohnwu.magisk.utils.FilterableDiffObservableList
|
|
||||||
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
|
|
||||||
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
|
||||||
import me.tatarka.bindingcollectionadapter2.OnItemBind
|
|
||||||
|
|
||||||
fun <T : ComparableRvItem<*>> diffListOf(
|
|
||||||
vararg newItems: T
|
|
||||||
) = diffListOf(newItems.toList())
|
|
||||||
|
|
||||||
fun <T : ComparableRvItem<*>> diffListOf(
|
|
||||||
newItems: List<T>
|
|
||||||
) = DiffObservableList(object : DiffObservableList.Callback<T> {
|
|
||||||
override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.genericItemSameAs(newItem)
|
|
||||||
override fun areContentsTheSame(oldItem: T, newItem: T) = oldItem.genericContentSameAs(newItem)
|
|
||||||
}).also { it.update(newItems) }
|
|
||||||
|
|
||||||
fun <T : ComparableRvItem<*>> filterableListOf(
|
|
||||||
vararg newItems: T
|
|
||||||
) = FilterableDiffObservableList(object : DiffObservableList.Callback<T> {
|
|
||||||
override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.genericItemSameAs(newItem)
|
|
||||||
override fun areContentsTheSame(oldItem: T, newItem: T) = oldItem.genericContentSameAs(newItem)
|
|
||||||
}).also { it.update(newItems.toList()) }
|
|
||||||
|
|
||||||
fun <T : RvItem> adapterOf() = object : BindingRecyclerViewAdapter<T>() {
|
|
||||||
override fun onBindBinding(
|
|
||||||
binding: ViewDataBinding,
|
|
||||||
variableId: Int,
|
|
||||||
layoutRes: Int,
|
|
||||||
position: Int,
|
|
||||||
item: T
|
|
||||||
) {
|
|
||||||
super.onBindBinding(binding, variableId, layoutRes, position, item)
|
|
||||||
item.onBindingBound(binding)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <T : RvItem> itemBindingOf(
|
|
||||||
crossinline body: (ItemBinding<*>) -> Unit = {}
|
|
||||||
) = OnItemBind<T> { itemBinding, _, item ->
|
|
||||||
item.bind(itemBinding)
|
|
||||||
body(itemBinding)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.topjohnwu.magisk.arch
|
||||||
|
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import androidx.databinding.ViewDataBinding
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavDirections
|
||||||
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
|
|
||||||
|
abstract class NavigationActivity<Binding : ViewDataBinding> : UIActivity<Binding>() {
|
||||||
|
|
||||||
|
abstract val navHostId: Int
|
||||||
|
|
||||||
|
private val navHostFragment by lazy {
|
||||||
|
supportFragmentManager.findFragmentById(navHostId) as NavHostFragment
|
||||||
|
}
|
||||||
|
|
||||||
|
protected val currentFragment get() =
|
||||||
|
navHostFragment.childFragmentManager.fragments.getOrNull(0) as? BaseFragment<*>
|
||||||
|
|
||||||
|
val navigation: NavController get() = navHostFragment.navController
|
||||||
|
|
||||||
|
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||||
|
return if (binded && currentFragment?.onKeyEvent(event) == true) true else super.dispatchKeyEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
if (binded) {
|
||||||
|
if (currentFragment?.onBackPressed() == false) {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavDirections.navigate() {
|
||||||
|
navigation.navigate(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
|
||||||
|
|
||||||
import android.os.Handler
|
|
||||||
import androidx.core.os.postDelayed
|
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
|
||||||
|
|
||||||
interface Queryable {
|
|
||||||
|
|
||||||
val queryDelay: Long
|
|
||||||
val queryHandler: Handler get() = UiThreadHandler.handler
|
|
||||||
|
|
||||||
fun submitQuery() {
|
|
||||||
queryHandler.postDelayed(queryDelay) { query() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun query()
|
|
||||||
}
|
|
||||||
115
app/src/main/java/com/topjohnwu/magisk/arch/UIActivity.kt
Normal file
115
app/src/main/java/com/topjohnwu/magisk/arch/UIActivity.kt
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package com.topjohnwu.magisk.arch
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.core.content.res.use
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.databinding.DataBindingUtil
|
||||||
|
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.topjohnwu.magisk.BR
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.core.Config
|
||||||
|
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||||
|
import rikka.insets.WindowInsetsHelper
|
||||||
|
import rikka.layoutinflater.view.LayoutInflaterFactory
|
||||||
|
|
||||||
|
abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModelHolder {
|
||||||
|
|
||||||
|
protected lateinit var binding: Binding
|
||||||
|
protected abstract val layoutRes: Int
|
||||||
|
|
||||||
|
protected val binded get() = ::binding.isInitialized
|
||||||
|
|
||||||
|
open val snackbarView get() = binding.root
|
||||||
|
open val snackbarAnchorView: View? get() = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
AppCompatDelegate.setDefaultNightMode(Config.darkTheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
layoutInflater.factory2 = LayoutInflaterFactory(delegate)
|
||||||
|
.addOnViewCreatedListener(WindowInsetsHelper.LISTENER)
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
startObserveLiveData()
|
||||||
|
|
||||||
|
// We need to set the window background explicitly since for whatever reason it's not
|
||||||
|
// propagated upstream
|
||||||
|
obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
|
||||||
|
.use { it.getDrawable(0) }
|
||||||
|
.also { window.setBackgroundDrawable(it) }
|
||||||
|
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
window?.decorView?.post {
|
||||||
|
// If navigation bar is short enough (gesture navigation enabled), make it transparent
|
||||||
|
if ((window.decorView.rootWindowInsets?.systemWindowInsetBottom
|
||||||
|
?: 0) < Resources.getSystem().displayMetrics.density * 40) {
|
||||||
|
window.navigationBarColor = Color.TRANSPARENT
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
window.navigationBarDividerColor = Color.TRANSPARENT
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
window.isNavigationBarContrastEnforced = false
|
||||||
|
window.isStatusBarContrastEnforced = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setContentView() {
|
||||||
|
binding = DataBindingUtil.setContentView<Binding>(this, layoutRes).also {
|
||||||
|
it.setVariable(BR.viewModel, viewModel)
|
||||||
|
it.lifecycleOwner = this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAccessibilityDelegate(delegate: View.AccessibilityDelegate?) {
|
||||||
|
binding.root.rootView.accessibilityDelegate = delegate
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showSnackbar(
|
||||||
|
message: CharSequence,
|
||||||
|
length: Int = Snackbar.LENGTH_SHORT,
|
||||||
|
builder: Snackbar.() -> Unit = {}
|
||||||
|
) = Snackbar.make(snackbarView, message, length)
|
||||||
|
.setAnchorView(snackbarAnchorView).apply(builder).show()
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
viewModel.let {
|
||||||
|
if (it is AsyncLoadViewModel)
|
||||||
|
it.startLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEventDispatched(event: ViewEvent) = when (event) {
|
||||||
|
is ContextExecutor -> event(this)
|
||||||
|
is ActivityExecutor -> event(this)
|
||||||
|
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,18 +8,14 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ActivityExecutor {
|
interface ActivityExecutor {
|
||||||
operator fun invoke(activity: BaseUIActivity<*, *>)
|
operator fun invoke(activity: UIActivity<*>)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FragmentExecutor {
|
interface FragmentExecutor {
|
||||||
operator fun invoke(fragment: BaseUIFragment<*, *>)
|
operator fun invoke(fragment: BaseFragment<*>)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.topjohnwu.magisk.arch
|
||||||
|
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.ViewModelStoreOwner
|
||||||
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
||||||
|
import com.topjohnwu.magisk.ui.install.InstallViewModel
|
||||||
|
import com.topjohnwu.magisk.ui.log.LogViewModel
|
||||||
|
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
|
||||||
|
import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel
|
||||||
|
|
||||||
|
interface ViewModelHolder : LifecycleOwner, ViewModelStoreOwner {
|
||||||
|
|
||||||
|
val viewModel: BaseViewModel
|
||||||
|
|
||||||
|
fun startObserveLiveData() {
|
||||||
|
viewModel.viewEvents.observe(this, this::onEventDispatched)
|
||||||
|
Info.isConnected.observe(this, viewModel::onNetworkChanged)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called for all [ViewEvent]s published by associated viewModel.
|
||||||
|
*/
|
||||||
|
fun onEventDispatched(event: ViewEvent) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
object VMFactory : ViewModelProvider.Factory {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return when (modelClass) {
|
||||||
|
HomeViewModel::class.java -> HomeViewModel(ServiceLocator.networkService)
|
||||||
|
LogViewModel::class.java -> LogViewModel(ServiceLocator.logRepo)
|
||||||
|
SuperuserViewModel::class.java -> SuperuserViewModel(ServiceLocator.policyDB)
|
||||||
|
InstallViewModel::class.java ->
|
||||||
|
InstallViewModel(ServiceLocator.networkService, ServiceLocator.markwon)
|
||||||
|
SuRequestViewModel::class.java ->
|
||||||
|
SuRequestViewModel(ServiceLocator.policyDB, ServiceLocator.timeoutPrefs)
|
||||||
|
else -> modelClass.newInstance()
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified VM : ViewModel> ViewModelHolder.viewModel() =
|
||||||
|
lazy(LazyThreadSafetyMode.NONE) {
|
||||||
|
ViewModelProvider(this, VMFactory)[VM::class.java]
|
||||||
|
}
|
||||||
@@ -5,36 +5,36 @@ 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.appcompat.app.AppCompatDelegate
|
import com.topjohnwu.magisk.StubApk
|
||||||
import androidx.multidex.MultiDex
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
import androidx.work.WorkManager
|
import com.topjohnwu.magisk.core.utils.DispatcherExecutor
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.core.utils.ProcessLifecycle
|
||||||
import com.topjohnwu.magisk.DynAPK
|
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||||
import com.topjohnwu.magisk.core.utils.IODispatcherExecutor
|
import com.topjohnwu.magisk.core.utils.ShellInit
|
||||||
import com.topjohnwu.magisk.core.utils.RootInit
|
import com.topjohnwu.magisk.core.utils.refreshLocale
|
||||||
import com.topjohnwu.magisk.core.utils.updateConfig
|
import com.topjohnwu.magisk.core.utils.setConfig
|
||||||
import com.topjohnwu.magisk.di.koinModules
|
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
||||||
import com.topjohnwu.magisk.ktx.unwrap
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import org.koin.android.ext.koin.androidContext
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
import org.koin.core.context.startKoin
|
import com.topjohnwu.superuser.ipc.RootService
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
open class App() : Application() {
|
open class App() : Application() {
|
||||||
|
|
||||||
constructor(o: Any) : this() {
|
constructor(o: Any) : this() {
|
||||||
Info.stub = DynAPK.load(o)
|
val data = StubApk.Data(o)
|
||||||
|
// Add the root service name mapping
|
||||||
|
data.classToComponent[RootUtils::class.java.name] = data.rootService.name
|
||||||
|
// Send back the actual root service class
|
||||||
|
data.rootService = RootUtils::class.java
|
||||||
|
Info.stub = data
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
|
||||||
Shell.setDefaultBuilder(Shell.Builder.create()
|
|
||||||
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
|
||||||
.setInitializers(RootInit::class.java)
|
|
||||||
.setTimeout(2))
|
|
||||||
Shell.EXECUTOR = IODispatcherExecutor()
|
|
||||||
|
|
||||||
// Always log full stack trace with Timber
|
// Always log full stack trace with Timber
|
||||||
Timber.plant(Timber.DebugTree())
|
Timber.plant(Timber.DebugTree())
|
||||||
Thread.setDefaultUncaughtExceptionHandler { _, e ->
|
Thread.setDefaultUncaughtExceptionHandler { _, e ->
|
||||||
@@ -43,59 +43,71 @@ open class App() : Application() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
override fun attachBaseContext(context: Context) {
|
||||||
// Basic setup
|
// Get the actual ContextImpl
|
||||||
if (BuildConfig.DEBUG)
|
|
||||||
MultiDex.install(base)
|
|
||||||
|
|
||||||
// Some context magic
|
|
||||||
val app: Application
|
val app: Application
|
||||||
val impl: Context
|
val base: Context
|
||||||
if (base is Application) {
|
if (context is Application) {
|
||||||
app = base
|
app = context
|
||||||
impl = base.baseContext
|
base = context.baseContext
|
||||||
|
AppApkPath = StubApk.current(base).path
|
||||||
} else {
|
} else {
|
||||||
app = this
|
app = this
|
||||||
impl = base
|
base = context
|
||||||
|
AppApkPath = base.packageResourcePath
|
||||||
}
|
}
|
||||||
val wrapped = impl.wrap()
|
super.attachBaseContext(base)
|
||||||
super.attachBaseContext(wrapped)
|
ServiceLocator.context = base
|
||||||
|
app.registerActivityLifecycleCallbacks(ActivityTracker)
|
||||||
|
|
||||||
// Normal startup
|
Shell.setDefaultBuilder(Shell.Builder.create()
|
||||||
startKoin {
|
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
||||||
androidContext(wrapped)
|
.setInitializers(ShellInit::class.java)
|
||||||
modules(koinModules)
|
.setContext(base)
|
||||||
}
|
.setTimeout(2))
|
||||||
ResMgr.init(impl)
|
Shell.EXECUTOR = DispatcherExecutor(Dispatchers.IO)
|
||||||
app.registerActivityLifecycleCallbacks(ForegroundTracker)
|
RootUtils.bindTask = RootService.bindOrTask(
|
||||||
WorkManager.initialize(impl.wrapJob(), androidx.work.Configuration.Builder().build())
|
intent<RootUtils>(),
|
||||||
|
UiThreadHandler.executor,
|
||||||
|
RootUtils.Connection
|
||||||
|
)
|
||||||
|
// Pre-heat the shell ASAP
|
||||||
|
Shell.getShell(null) {}
|
||||||
|
|
||||||
|
refreshLocale()
|
||||||
|
resources.patch()
|
||||||
|
Notifications.setup()
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is required as some platforms expect ContextImpl
|
override fun onCreate() {
|
||||||
override fun getBaseContext(): Context {
|
super.onCreate()
|
||||||
return super.getBaseContext().unwrap()
|
ProcessLifecycle.init(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
resources.updateConfig(newConfig)
|
if (resources.configuration.diff(newConfig) != 0) {
|
||||||
|
resources.setConfig(newConfig)
|
||||||
|
}
|
||||||
if (!isRunningAsStub)
|
if (!isRunningAsStub)
|
||||||
super.onConfigurationChanged(newConfig)
|
super.onConfigurationChanged(newConfig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object ForegroundTracker : Application.ActivityLifecycleCallbacks {
|
object ActivityTracker : Application.ActivityLifecycleCallbacks {
|
||||||
|
|
||||||
|
val foreground: Activity? get() = ref.get()
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
var foreground: Activity? = null
|
private var ref = WeakReference<Activity>(null)
|
||||||
|
|
||||||
val hasForeground get() = foreground != null
|
|
||||||
|
|
||||||
override fun onActivityResumed(activity: Activity) {
|
override fun onActivityResumed(activity: Activity) {
|
||||||
foreground = activity
|
if (activity is SuRequestActivity) return
|
||||||
|
ref = WeakReference(activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityPaused(activity: Activity) {
|
override fun onActivityPaused(activity: Activity) {
|
||||||
foreground = null
|
if (activity is SuRequestActivity) return
|
||||||
|
ref.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
||||||
|
|||||||
@@ -1,38 +1,33 @@
|
|||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
|
||||||
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.magiskdb.SettingsDao
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
import com.topjohnwu.magisk.core.magiskdb.StringDao
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
import com.topjohnwu.magisk.core.utils.BiometricHelper
|
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||||
|
import com.topjohnwu.magisk.core.repository.BoolDBPropertyNoWrite
|
||||||
|
import com.topjohnwu.magisk.core.repository.DBConfig
|
||||||
|
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.data.preference.PreferenceModel
|
|
||||||
import com.topjohnwu.magisk.data.repository.DBConfig
|
|
||||||
import com.topjohnwu.magisk.di.Protected
|
|
||||||
import com.topjohnwu.magisk.ktx.inject
|
|
||||||
import com.topjohnwu.magisk.ui.theme.Theme
|
import com.topjohnwu.magisk.ui.theme.Theme
|
||||||
import org.xmlpull.v1.XmlPullParser
|
import kotlinx.coroutines.GlobalScope
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
object Config : PreferenceModel, DBConfig {
|
object Config : PreferenceConfig, DBConfig {
|
||||||
|
|
||||||
override val stringDao: StringDao by inject()
|
override val stringDB get() = ServiceLocator.stringDB
|
||||||
override val settingsDao: SettingsDao by inject()
|
override val settingsDB get() = ServiceLocator.settingsDB
|
||||||
override val context: Context by inject(Protected)
|
override val context get() = ServiceLocator.deContext
|
||||||
|
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 {
|
||||||
@@ -41,6 +36,8 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
const val SU_MULTIUSER_MODE = "multiuser_mode"
|
const val SU_MULTIUSER_MODE = "multiuser_mode"
|
||||||
const val SU_MNT_NS = "mnt_ns"
|
const val SU_MNT_NS = "mnt_ns"
|
||||||
const val SU_BIOMETRIC = "su_biometric"
|
const val SU_BIOMETRIC = "su_biometric"
|
||||||
|
const val ZYGISK = "zygisk"
|
||||||
|
const val DENYLIST = "denylist"
|
||||||
const val SU_MANAGER = "requester"
|
const val SU_MANAGER = "requester"
|
||||||
const val KEYSTORE = "keystore"
|
const val KEYSTORE = "keystore"
|
||||||
|
|
||||||
@@ -63,9 +60,6 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
const val BOOT_ID = "boot_id"
|
const val BOOT_ID = "boot_id"
|
||||||
const val ASKED_HOME = "asked_home"
|
const val ASKED_HOME = "asked_home"
|
||||||
const val DOH = "doh"
|
const val DOH = "doh"
|
||||||
|
|
||||||
// system state
|
|
||||||
const val MAGISKHIDE = "magiskhide"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object Value {
|
object Value {
|
||||||
@@ -75,6 +69,7 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
const val BETA_CHANNEL = 1
|
const val BETA_CHANNEL = 1
|
||||||
const val CUSTOM_CHANNEL = 2
|
const val CUSTOM_CHANNEL = 2
|
||||||
const val CANARY_CHANNEL = 3
|
const val CANARY_CHANNEL = 3
|
||||||
|
const val DEBUG_CHANNEL = 4
|
||||||
|
|
||||||
// root access mode
|
// root access mode
|
||||||
const val ROOT_ACCESS_DISABLED = 0
|
const val ROOT_ACCESS_DISABLED = 0
|
||||||
@@ -111,13 +106,15 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
|
|
||||||
private val defaultChannel =
|
private val defaultChannel =
|
||||||
if (BuildConfig.DEBUG)
|
if (BuildConfig.DEBUG)
|
||||||
|
Value.DEBUG_CHANNEL
|
||||||
|
else if (Const.APP_IS_CANARY)
|
||||||
Value.CANARY_CHANNEL
|
Value.CANARY_CHANNEL
|
||||||
else
|
else
|
||||||
Value.DEFAULT_CHANNEL
|
Value.DEFAULT_CHANNEL
|
||||||
|
|
||||||
@JvmStatic var keepVerity = false
|
@JvmField var keepVerity = false
|
||||||
@JvmStatic var keepEnc = false
|
@JvmField var keepEnc = false
|
||||||
@JvmStatic var recovery = false
|
@JvmField var recovery = false
|
||||||
|
|
||||||
var bootId by preference(Key.BOOT_ID, "")
|
var bootId by preference(Key.BOOT_ID, "")
|
||||||
var askedHome by preference(Key.ASKED_HOME, false)
|
var askedHome by preference(Key.ASKED_HOME, false)
|
||||||
@@ -135,9 +132,16 @@ object Config : PreferenceModel, 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 magiskHide by preference(Key.MAGISKHIDE, true)
|
|
||||||
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
|
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
|
||||||
|
|
||||||
var customChannelUrl by preference(Key.CUSTOM_CHANNEL, "")
|
var customChannelUrl by preference(Key.CUSTOM_CHANNEL, "")
|
||||||
@@ -153,6 +157,8 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
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)
|
var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
|
||||||
|
var zygisk by dbSettings(Key.ZYGISK, false)
|
||||||
|
var denyList by BoolDBPropertyNoWrite(Key.DENYLIST, false)
|
||||||
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
||||||
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
|
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
|
||||||
|
|
||||||
@@ -161,9 +167,8 @@ object Config : PreferenceModel, 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.PREFS_URI(pkg))?.use {
|
context.contentResolver.openInputStream(Provider.preferencesUri(pkg))?.writeTo(prefsFile)
|
||||||
prefs.edit { parsePrefs(it) }
|
return
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
@@ -172,63 +177,10 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
suBiometric = true
|
suBiometric = true
|
||||||
remove(SU_FINGERPRINT)
|
remove(SU_FINGERPRINT)
|
||||||
prefs.getString(Key.UPDATE_CHANNEL, null).also {
|
prefs.getString(Key.UPDATE_CHANNEL, null).also {
|
||||||
if (it == null)
|
if (it == null ||
|
||||||
|
it.toInt() > Value.DEBUG_CHANNEL ||
|
||||||
|
it.toInt() < Value.DEFAULT_CHANNEL) {
|
||||||
putString(Key.UPDATE_CHANNEL, defaultChannel.toString())
|
putString(Key.UPDATE_CHANNEL, defaultChannel.toString())
|
||||||
else if (it.toInt() > Value.CANARY_CHANNEL)
|
|
||||||
putString(Key.UPDATE_CHANNEL, Value.CANARY_CHANNEL.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write database configs
|
|
||||||
putString(Key.ROOT_ACCESS, rootMode.toString())
|
|
||||||
putString(Key.SU_MNT_NS, suMntNamespaceMode.toString())
|
|
||||||
putString(Key.SU_MULTIUSER_MODE, suMultiuserMode.toString())
|
|
||||||
putBoolean(Key.SU_BIOMETRIC, BiometricHelper.isEnabled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,54 @@
|
|||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
object Const {
|
object Const {
|
||||||
|
|
||||||
// Paths
|
val CPU_ABI: String get() = Build.SUPPORTED_ABIS[0]
|
||||||
lateinit var MAGISKTMP: String
|
|
||||||
val MAGISK_PATH get() = "$MAGISKTMP/modules"
|
|
||||||
const val TMP_FOLDER_PATH = "/dev/tmp"
|
|
||||||
const val MAGISK_LOG = "/cache/magisk.log"
|
|
||||||
|
|
||||||
// Versions
|
// Null if 32-bit only or 64-bit only
|
||||||
const val SNET_EXT_VER = 15
|
val CPU_ABI_32 =
|
||||||
const val SNET_REVISION = "18ab78817087c337ae0edd1ecac38aec49217880"
|
if (Build.SUPPORTED_64_BIT_ABIS.isEmpty()) null
|
||||||
const val BOOTCTL_REVISION = "18ab78817087c337ae0edd1ecac38aec49217880"
|
else Build.SUPPORTED_32_BIT_ABIS.firstOrNull()
|
||||||
|
|
||||||
|
// Paths
|
||||||
|
const val MAGISK_PATH = "/data/adb/modules"
|
||||||
|
const val TMPDIR = "/dev/tmp"
|
||||||
|
const val MAGISK_LOG = "/cache/magisk.log"
|
||||||
|
|
||||||
// Misc
|
// Misc
|
||||||
val USER_ID = Process.myUid() / 100000
|
val USER_ID = Process.myUid() / 100000
|
||||||
|
val APP_IS_CANARY get() = Version.isCanary(BuildConfig.VERSION_CODE)
|
||||||
|
|
||||||
object Version {
|
object Version {
|
||||||
const val MIN_VERSION = "v19.0"
|
const val MIN_VERSION = "v22.0"
|
||||||
const val MIN_VERCODE = 19000
|
const val MIN_VERCODE = 22000
|
||||||
|
|
||||||
fun atLeast_20_2() = Info.env.magiskVersionCode >= 20200 || isCanary()
|
fun atLeast_24_0() = Info.env.versionCode >= 24000 || isCanary()
|
||||||
fun atLeast_20_4() = Info.env.magiskVersionCode >= 20400 || isCanary()
|
fun atLeast_25_0() = Info.env.versionCode >= 25000 || isCanary()
|
||||||
fun atLeast_21_0() = Info.env.magiskVersionCode >= 21000 || isCanary()
|
fun isCanary() = isCanary(Info.env.versionCode)
|
||||||
fun atLeast_21_2() = Info.env.magiskVersionCode >= 21200 || isCanary()
|
|
||||||
fun isCanary() = Info.env.magiskVersionCode % 100 != 0
|
fun isCanary(ver: Int) = ver > 0 && ver % 100 != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
object ID {
|
object ID {
|
||||||
const val FETCH_ZIP = 2
|
const val JOB_SERVICE_ID = 7
|
||||||
const val SELECT_FILE = 3
|
|
||||||
const val MAX_ACTIVITY_RESULT = 10
|
|
||||||
|
|
||||||
// notifications
|
|
||||||
const val MAGISK_UPDATE_NOTIFICATION_ID = 4
|
|
||||||
const val APK_UPDATE_NOTIFICATION_ID = 5
|
|
||||||
const val UPDATE_NOTIFICATION_CHANNEL = "update"
|
|
||||||
const val PROGRESS_NOTIFICATION_CHANNEL = "progress"
|
|
||||||
const val CHECK_MAGISK_UPDATE_WORKER_ID = "magisk_update"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object Url {
|
object Url {
|
||||||
const val PATREON_URL = "https://www.patreon.com/topjohnwu"
|
const val PATREON_URL = "https://www.patreon.com/topjohnwu"
|
||||||
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
|
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
|
||||||
|
|
||||||
|
val CHANGELOG_URL = if (APP_IS_CANARY) Info.remote.magisk.note
|
||||||
|
else "https://topjohnwu.github.io/Magisk/releases/${BuildConfig.VERSION_CODE}.md"
|
||||||
|
|
||||||
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com/"
|
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com/"
|
||||||
const val GITHUB_API_URL = "https://api.github.com/"
|
const val GITHUB_API_URL = "https://api.github.com/"
|
||||||
const val GITHUB_PAGE_URL = "https://topjohnwu.github.io/magisk_files/"
|
const val GITHUB_PAGE_URL = "https://topjohnwu.github.io/magisk-files/"
|
||||||
const val JS_DELIVR_URL = "https://cdn.jsdelivr.net/gh/"
|
const val JS_DELIVR_URL = "https://cdn.jsdelivr.net/gh/"
|
||||||
const val OFFICIAL_REPO = "https://magisk-modules-repo.github.io/submission/modules.json"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object Key {
|
object Key {
|
||||||
@@ -70,7 +68,6 @@ object Const {
|
|||||||
object Nav {
|
object Nav {
|
||||||
const val HOME = "home"
|
const val HOME = "home"
|
||||||
const val SETTINGS = "settings"
|
const val SETTINGS = "settings"
|
||||||
const val HIDE = "hide"
|
|
||||||
const val MODULES = "modules"
|
const val MODULES = "modules"
|
||||||
const val SUPERUSER = "superuser"
|
const val SUPERUSER = "superuser"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,6 @@
|
|||||||
|
|
||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.job.JobInfo
|
|
||||||
import android.app.job.JobScheduler
|
|
||||||
import android.app.job.JobWorkItem
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
@@ -14,148 +9,53 @@ import android.content.Intent
|
|||||||
import android.content.res.AssetManager
|
import android.content.res.AssetManager
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import androidx.annotation.RequiresApi
|
import android.util.DisplayMetrics
|
||||||
import com.topjohnwu.magisk.DynAPK
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.download.DownloadService
|
import com.topjohnwu.magisk.StubApk
|
||||||
import com.topjohnwu.magisk.core.utils.refreshLocale
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
import com.topjohnwu.magisk.core.utils.updateConfig
|
import com.topjohnwu.magisk.core.ktx.unwrap
|
||||||
import com.topjohnwu.magisk.ktx.forceGetDeclaredField
|
import com.topjohnwu.magisk.core.utils.syncLocale
|
||||||
import com.topjohnwu.magisk.ui.MainActivity
|
|
||||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
|
||||||
|
|
||||||
fun AssetManager.addAssetPath(path: String) {
|
lateinit var AppApkPath: String
|
||||||
DynAPK.addAssetPath(this, path)
|
|
||||||
|
fun Resources.addAssetPath(path: String) = StubApk.addAssetPath(this, path)
|
||||||
|
|
||||||
|
fun Resources.patch(): Resources {
|
||||||
|
if (isRunningAsStub)
|
||||||
|
addAssetPath(AppApkPath)
|
||||||
|
syncLocale()
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.wrap(global: Boolean = true): Context =
|
fun Context.patch(): Context {
|
||||||
if (global) GlobalResContext(this) else ResContext(this)
|
unwrap().resources.patch()
|
||||||
|
return this
|
||||||
fun Context.wrapJob(): Context = object : GlobalResContext(this) {
|
|
||||||
|
|
||||||
override fun getApplicationContext(): Context {
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
override fun getSystemService(name: String): Any? {
|
|
||||||
return if (!isRunningAsStub) super.getSystemService(name) else
|
|
||||||
when (name) {
|
|
||||||
Context.JOB_SCHEDULER_SERVICE ->
|
|
||||||
JobSchedulerWrapper(super.getSystemService(name) as JobScheduler)
|
|
||||||
else -> super.getSystemService(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Class<*>.cmp(pkg: String): ComponentName {
|
// Wrapping is only necessary for ContextThemeWrapper to support configuration overrides
|
||||||
val name = ClassMap[this].name
|
fun Context.wrap(): Context {
|
||||||
return ComponentName(pkg, Info.stub?.classToComponent?.get(name) ?: name)
|
patch()
|
||||||
}
|
return object : ContextWrapper(this) {
|
||||||
|
override fun createConfigurationContext(config: Configuration): Context {
|
||||||
inline fun <reified T> Activity.redirect() = Intent(intent)
|
return super.createConfigurationContext(config).wrap()
|
||||||
.setComponent(T::class.java.cmp(packageName))
|
|
||||||
.setFlags(0)
|
|
||||||
|
|
||||||
inline fun <reified T> Context.intent() = Intent().setComponent(T::class.java.cmp(packageName))
|
|
||||||
|
|
||||||
private open class GlobalResContext(base: Context) : ContextWrapper(base) {
|
|
||||||
open val mRes: Resources get() = ResMgr.resource
|
|
||||||
|
|
||||||
override fun getResources(): Resources {
|
|
||||||
return mRes
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getClassLoader(): ClassLoader {
|
|
||||||
return javaClass.classLoader!!
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createConfigurationContext(config: Configuration): Context {
|
|
||||||
return ResContext(super.createConfigurationContext(config))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ResContext(base: Context) : GlobalResContext(base) {
|
|
||||||
override val mRes by lazy { base.resources.patch() }
|
|
||||||
|
|
||||||
private fun Resources.patch(): Resources {
|
|
||||||
updateConfig()
|
|
||||||
if (isRunningAsStub)
|
|
||||||
assets.addAssetPath(ResMgr.apk)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object ResMgr {
|
|
||||||
|
|
||||||
lateinit var resource: Resources
|
|
||||||
lateinit var apk: String
|
|
||||||
|
|
||||||
fun init(context: Context) {
|
|
||||||
resource = context.resources
|
|
||||||
refreshLocale()
|
|
||||||
if (isRunningAsStub) {
|
|
||||||
apk = DynAPK.current(context).path
|
|
||||||
resource.assets.addAssetPath(apk)
|
|
||||||
} else {
|
|
||||||
apk = context.packageResourcePath
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(28)
|
fun createNewResources(): Resources {
|
||||||
private class JobSchedulerWrapper(private val base: JobScheduler) : JobScheduler() {
|
val asset = AssetManager::class.java.newInstance()
|
||||||
|
val config = Configuration(AppContext.resources.configuration)
|
||||||
override fun schedule(job: JobInfo): Int {
|
val metrics = DisplayMetrics()
|
||||||
return base.schedule(job.patch())
|
metrics.setTo(AppContext.resources.displayMetrics)
|
||||||
}
|
val res = Resources(asset, metrics, config)
|
||||||
|
res.addAssetPath(AppApkPath)
|
||||||
override fun enqueue(job: JobInfo, work: JobWorkItem): Int {
|
return res
|
||||||
return base.enqueue(job.patch(), work)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancel(jobId: Int) {
|
|
||||||
base.cancel(jobId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancelAll() {
|
|
||||||
base.cancelAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getAllPendingJobs(): List<JobInfo> {
|
|
||||||
return base.allPendingJobs
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPendingJob(jobId: Int): JobInfo? {
|
|
||||||
return base.getPendingJob(jobId)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun JobInfo.patch(): JobInfo {
|
|
||||||
// We need to swap out the service of JobInfo
|
|
||||||
val name = service.className
|
|
||||||
val component = ComponentName(
|
|
||||||
service.packageName,
|
|
||||||
Info.stubChk.classToComponent[name] ?: name
|
|
||||||
)
|
|
||||||
|
|
||||||
javaClass.forceGetDeclaredField("service")?.set(this, component)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private object ClassMap {
|
fun Class<*>.cmp(pkg: String) =
|
||||||
|
ComponentName(pkg, Info.stub?.classToComponent?.get(name) ?: name)
|
||||||
|
|
||||||
private val map = mapOf(
|
inline fun <reified T> Context.intent() = Intent().setComponent(T::class.java.cmp(packageName))
|
||||||
App::class.java to a.e::class.java,
|
|
||||||
MainActivity::class.java to a.b::class.java,
|
|
||||||
SplashActivity::class.java to a.c::class.java,
|
|
||||||
Receiver::class.java to a.h::class.java,
|
|
||||||
DownloadService::class.java to a.j::class.java,
|
|
||||||
SuRequestActivity::class.java to a.m::class.java
|
|
||||||
)
|
|
||||||
|
|
||||||
operator fun get(c: Class<*>) = map.getOrElse(c) { c }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep a reference to these resources to prevent it from
|
// Keep a reference to these resources to prevent it from
|
||||||
// being removed when running "remove unused resources"
|
// being removed when running "remove unused resources"
|
||||||
@@ -164,10 +64,11 @@ val shouldKeepResources = listOf(
|
|||||||
R.string.release_notes,
|
R.string.release_notes,
|
||||||
R.string.invalid_update_channel,
|
R.string.invalid_update_channel,
|
||||||
R.string.update_available,
|
R.string.update_available,
|
||||||
R.string.safetynet_api_error,
|
|
||||||
R.raw.changelog,
|
|
||||||
R.drawable.ic_device,
|
R.drawable.ic_device,
|
||||||
R.drawable.ic_hide_select_md2,
|
|
||||||
R.drawable.ic_more,
|
R.drawable.ic_more,
|
||||||
R.drawable.ic_magisk_delete
|
R.drawable.ic_magisk_delete,
|
||||||
|
R.drawable.ic_refresh_data_md2,
|
||||||
|
R.drawable.ic_order_date,
|
||||||
|
R.drawable.ic_order_name,
|
||||||
|
R.array.allow_timeout,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,87 +1,80 @@
|
|||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import androidx.databinding.ObservableBoolean
|
import androidx.lifecycle.LiveData
|
||||||
import com.topjohnwu.magisk.DynAPK
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import com.topjohnwu.magisk.StubApk
|
||||||
|
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.utils.net.NetworkObserver
|
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||||
import com.topjohnwu.magisk.ktx.get
|
import com.topjohnwu.magisk.core.utils.NetworkObserver
|
||||||
import com.topjohnwu.magisk.utils.CachedValue
|
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import com.topjohnwu.superuser.ShellUtils.fastCmd
|
import com.topjohnwu.superuser.ShellUtils.fastCmd
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
val isRunningAsStub get() = Info.stub != null
|
val isRunningAsStub get() = Info.stub != null
|
||||||
|
|
||||||
object Info {
|
object Info {
|
||||||
|
|
||||||
val envRef = CachedValue { loadState() }
|
var stub: StubApk.Data? = null
|
||||||
|
|
||||||
@JvmStatic val env by envRef
|
val EMPTY_REMOTE = UpdateInfo()
|
||||||
|
var remote = EMPTY_REMOTE
|
||||||
var stub: DynAPK.Data? = null
|
suspend fun getRemote(svc: NetworkService): UpdateInfo? {
|
||||||
val stubChk: DynAPK.Data
|
return if (remote === EMPTY_REMOTE) {
|
||||||
get() = stub as DynAPK.Data
|
svc.fetchUpdate()?.apply { remote = this }
|
||||||
|
} else remote
|
||||||
var remote = UpdateInfo()
|
}
|
||||||
|
|
||||||
// Device state
|
// Device state
|
||||||
var crypto = ""
|
@JvmStatic val env by lazy { loadState() }
|
||||||
@JvmStatic var isSAR = false
|
@JvmField var isSAR = false
|
||||||
@JvmStatic var isAB = false
|
var legacySAR = false
|
||||||
|
var isAB = false
|
||||||
|
@JvmField val isZygiskEnabled = System.getenv("ZYGISK_ENABLED") == "1"
|
||||||
@JvmStatic val isFDE get() = crypto == "block"
|
@JvmStatic val isFDE get() = crypto == "block"
|
||||||
@JvmStatic var ramdisk = false
|
@JvmField var ramdisk = false
|
||||||
@JvmStatic var hasGMS = true
|
var patchBootVbmeta = false
|
||||||
@JvmStatic var isPixel = false
|
var crypto = ""
|
||||||
@JvmStatic val cryptoText get() = crypto.capitalize(Locale.US)
|
var noDataExec = false
|
||||||
|
var isRooted = false
|
||||||
|
|
||||||
val isConnected by lazy {
|
@JvmField var hasGMS = true
|
||||||
ObservableBoolean(false).also { field ->
|
@JvmField val isEmulator =
|
||||||
NetworkObserver.observe(get()) {
|
getProperty("ro.kernel.qemu", "0") == "1" ||
|
||||||
UiThreadHandler.run { field.set(it) }
|
getProperty("ro.boot.qemu", "0") == "1"
|
||||||
|
|
||||||
|
val isConnected: LiveData<Boolean> by lazy {
|
||||||
|
MutableLiveData(false).also { field ->
|
||||||
|
NetworkObserver.observe(AppContext) {
|
||||||
|
remote = EMPTY_REMOTE
|
||||||
|
field.postValue(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val isNewReboot by lazy {
|
val showSuperUser: Boolean get() {
|
||||||
try {
|
return env.isActive && (Const.USER_ID == 0
|
||||||
FileInputStream("/proc/sys/kernel/random/boot_id").bufferedReader().use {
|
|| Config.suMultiuserMode == Config.Value.MULTIUSER_MODE_USER)
|
||||||
val id = it.readLine()
|
|
||||||
if (id != Config.bootId) {
|
|
||||||
Config.bootId = id
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadState() = Env(
|
private fun loadState(): Env {
|
||||||
fastCmd("magisk -v").split(":".toRegex())[0],
|
val v = fastCmd("magisk -v").split(":".toRegex())
|
||||||
runCatching { fastCmd("magisk -V").toInt() }.getOrDefault(-1),
|
return Env(
|
||||||
Shell.su("magiskhide --status").exec().isSuccess
|
v[0], v.size >= 3 && v[2] == "D",
|
||||||
)
|
runCatching { fastCmd("magisk -V").toInt() }.getOrDefault(-1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
class Env(
|
class Env(
|
||||||
val magiskVersionString: String = "",
|
val versionString: String = "",
|
||||||
code: Int = -1,
|
val isDebug: Boolean = false,
|
||||||
hide: Boolean = false
|
code: Int = -1
|
||||||
) {
|
) {
|
||||||
val magiskHide get() = Config.magiskHide
|
val versionCode = when {
|
||||||
val magiskVersionCode = when (code) {
|
code < Const.Version.MIN_VERCODE -> -1
|
||||||
in Int.MIN_VALUE..Const.Version.MIN_VERCODE -> -1
|
isRooted -> code
|
||||||
else -> if (Shell.rootAccess()) code else -1
|
else -> -1
|
||||||
}
|
}
|
||||||
val isUnsupported = code > 0 && code < Const.Version.MIN_VERCODE
|
val isUnsupported = code > 0 && code < Const.Version.MIN_VERCODE
|
||||||
val isActive = magiskVersionCode >= 0
|
val isActive = versionCode > 0
|
||||||
|
|
||||||
init {
|
|
||||||
Config.magiskHide = hide
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
61
app/src/main/java/com/topjohnwu/magisk/core/JobService.kt
Normal file
61
app/src/main/java/com/topjohnwu/magisk/core/JobService.kt
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
|
import android.app.job.JobInfo
|
||||||
|
import android.app.job.JobParameters
|
||||||
|
import android.app.job.JobScheduler
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
|
import com.topjohnwu.magisk.core.base.BaseJobService
|
||||||
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class JobService : BaseJobService() {
|
||||||
|
|
||||||
|
private val job = Job()
|
||||||
|
private val svc get() = ServiceLocator.networkService
|
||||||
|
|
||||||
|
override fun onStartJob(params: JobParameters): Boolean {
|
||||||
|
val coroutineScope = CoroutineScope(Dispatchers.IO + job)
|
||||||
|
coroutineScope.launch {
|
||||||
|
doWork()
|
||||||
|
jobFinished(params, false)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun doWork() {
|
||||||
|
svc.fetchUpdate()?.let {
|
||||||
|
Info.remote = it
|
||||||
|
if (Info.env.isActive && BuildConfig.VERSION_CODE < it.magisk.versionCode)
|
||||||
|
Notifications.updateAvailable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopJob(params: JobParameters): Boolean {
|
||||||
|
job.cancel()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun schedule(context: Context) {
|
||||||
|
val scheduler = context.getSystemService<JobScheduler>() ?: return
|
||||||
|
if (Config.checkUpdate) {
|
||||||
|
val cmp = JobService::class.java.cmp(context.packageName)
|
||||||
|
val info = JobInfo.Builder(Const.ID.JOB_SERVICE_ID, cmp)
|
||||||
|
.setPeriodic(TimeUnit.HOURS.toMillis(12))
|
||||||
|
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
|
||||||
|
.setRequiresDeviceIdle(true)
|
||||||
|
.build()
|
||||||
|
scheduler.schedule(info)
|
||||||
|
} else {
|
||||||
|
scheduler.cancel(Const.ID.JOB_SERVICE_ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,39 +1,28 @@
|
|||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.ProviderInfo
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
||||||
import com.topjohnwu.magisk.FileProvider
|
import com.topjohnwu.magisk.core.base.BaseProvider
|
||||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
open class Provider : FileProvider() {
|
class Provider : BaseProvider() {
|
||||||
|
|
||||||
override fun attachInfo(context: Context, info: ProviderInfo?) {
|
|
||||||
super.attachInfo(context.wrap(), info)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
|
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
|
||||||
SuCallbackHandler(context!!, method, extras)
|
SuCallbackHandler.run(context!!, method, extras)
|
||||||
return Bundle.EMPTY
|
return Bundle.EMPTY
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
"/apk_file" -> ParcelFileDescriptor.open(File(context!!.packageCodePath), MODE_READ_ONLY)
|
"/prefs_file" -> ParcelFileDescriptor.open(Config.getPrefsFile(), MODE_READ_ONLY)
|
||||||
"/prefs_file" -> ParcelFileDescriptor.open(Config.prefsFile, MODE_READ_ONLY)
|
|
||||||
else -> super.openFile(uri, mode)
|
else -> super.openFile(uri, mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun APK_URI(pkg: String) =
|
fun preferencesUri(pkg: String): Uri =
|
||||||
Uri.Builder().scheme("content").authority("$pkg.provider").path("apk_file").build()
|
|
||||||
|
|
||||||
fun PREFS_URI(pkg: String) =
|
|
||||||
Uri.Builder().scheme("content").authority("$pkg.provider").path("prefs_file").build()
|
Uri.Builder().scheme("content").authority("$pkg.provider").path("prefs_file").build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,59 @@
|
|||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import android.content.ContextWrapper
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.topjohnwu.magisk.core.base.BaseReceiver
|
import com.topjohnwu.magisk.core.base.BaseReceiver
|
||||||
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
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.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.inject
|
|
||||||
|
|
||||||
open class Receiver : BaseReceiver() {
|
open class Receiver : BaseReceiver() {
|
||||||
|
|
||||||
private val policyDB: PolicyDao by inject()
|
private val policyDB get() = ServiceLocator.policyDB
|
||||||
|
|
||||||
private fun getPkg(intent: Intent): String {
|
@SuppressLint("InlinedApi")
|
||||||
return intent.data?.encodedSchemeSpecificPart.orEmpty()
|
private fun getPkg(intent: Intent): String? {
|
||||||
|
val pkg = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME)
|
||||||
|
return pkg ?: intent.data?.schemeSpecificPart
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReceive(context: ContextWrapper, intent: Intent?) {
|
private fun getUid(intent: Intent): Int? {
|
||||||
intent ?: return
|
val uid = intent.getIntExtra(Intent.EXTRA_UID, -1)
|
||||||
|
return if (uid == -1) null else uid
|
||||||
|
}
|
||||||
|
|
||||||
fun rmPolicy(pkg: String) = GlobalScope.launch {
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
policyDB.delete(pkg)
|
intent ?: return
|
||||||
|
super.onReceive(context, intent)
|
||||||
|
|
||||||
|
fun rmPolicy(uid: Int) = GlobalScope.launch {
|
||||||
|
policyDB.delete(uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
when (intent.action ?: return) {
|
when (intent.action ?: return) {
|
||||||
Intent.ACTION_REBOOT -> {
|
|
||||||
SuCallbackHandler(context, intent.getStringExtra("action"), intent.extras)
|
|
||||||
}
|
|
||||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
Intent.ACTION_PACKAGE_REPLACED -> {
|
||||||
// This will only work pre-O
|
// This will only work pre-O
|
||||||
if (Config.suReAuth)
|
if (Config.suReAuth)
|
||||||
rmPolicy(getPkg(intent))
|
getUid(intent)?.let { rmPolicy(it) }
|
||||||
|
}
|
||||||
|
Intent.ACTION_UID_REMOVED -> {
|
||||||
|
getUid(intent)?.let { rmPolicy(it) }
|
||||||
}
|
}
|
||||||
Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
|
Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
|
||||||
val pkg = getPkg(intent)
|
getPkg(intent)?.let { Shell.cmd("magisk --denylist rm $it").submit() }
|
||||||
rmPolicy(pkg)
|
|
||||||
Shell.su("magiskhide --rm $pkg").submit()
|
|
||||||
}
|
}
|
||||||
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setupDynamic(context)
|
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setupDynamic(context)
|
||||||
|
Intent.ACTION_MY_PACKAGE_REPLACED -> {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val installer = context.packageManager.getInstallerPackageName(context.packageName)
|
||||||
|
if (installer == context.packageName) {
|
||||||
|
Notifications.updateDone()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
|
||||||
import com.topjohnwu.magisk.R
|
|
||||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
|
||||||
import com.topjohnwu.magisk.ktx.get
|
|
||||||
import com.topjohnwu.magisk.ui.MainActivity
|
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
|
||||||
import com.topjohnwu.magisk.view.Shortcuts
|
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
open class SplashActivity : Activity() {
|
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
|
||||||
super.attachBaseContext(base.wrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
setTheme(R.style.SplashTheme)
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
|
||||||
initAndStart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleRepackage(pkg: String?) {
|
|
||||||
if (packageName != APPLICATION_ID) {
|
|
||||||
runCatching {
|
|
||||||
// Hidden, remove com.topjohnwu.magisk if exist as it could be malware
|
|
||||||
packageManager.getApplicationInfo(APPLICATION_ID, 0)
|
|
||||||
Shell.su("(pm uninstall $APPLICATION_ID)& >/dev/null 2>&1").exec()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (Config.suManager.isNotEmpty())
|
|
||||||
Config.suManager = ""
|
|
||||||
pkg ?: return
|
|
||||||
Shell.su("(pm uninstall $pkg)& >/dev/null 2>&1").exec()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initAndStart() {
|
|
||||||
// Pre-initialize root shell
|
|
||||||
Shell.getShell()
|
|
||||||
|
|
||||||
val prevPkg = intent.getStringExtra(Const.Key.PREV_PKG)
|
|
||||||
|
|
||||||
Config.load(prevPkg)
|
|
||||||
handleRepackage(prevPkg)
|
|
||||||
Notifications.setup(this)
|
|
||||||
UpdateCheckService.schedule(this)
|
|
||||||
Shortcuts.setupDynamic(this)
|
|
||||||
|
|
||||||
// Pre-fetch network services
|
|
||||||
get<NetworkService>()
|
|
||||||
|
|
||||||
DONE = true
|
|
||||||
|
|
||||||
redirect<MainActivity>().also { startActivity(it) }
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
var DONE = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.work.*
|
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
|
||||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.koin.core.KoinComponent
|
|
||||||
import org.koin.core.inject
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class UpdateCheckService(context: Context, workerParams: WorkerParameters)
|
|
||||||
: CoroutineWorker(context, workerParams), KoinComponent {
|
|
||||||
|
|
||||||
private val svc: NetworkService by inject()
|
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
|
||||||
// Make sure shell initializer was ran
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
Shell.getShell()
|
|
||||||
}
|
|
||||||
return svc.fetchUpdate()?.let {
|
|
||||||
if (BuildConfig.VERSION_CODE < it.app.versionCode)
|
|
||||||
Notifications.managerUpdate(applicationContext)
|
|
||||||
else if (Info.env.isActive && Info.env.magiskVersionCode < it.magisk.versionCode)
|
|
||||||
Notifications.magiskUpdate(applicationContext)
|
|
||||||
Result.success()
|
|
||||||
} ?: Result.failure()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun schedule(context: Context) {
|
|
||||||
if (Config.checkUpdate) {
|
|
||||||
val constraints = Constraints.Builder()
|
|
||||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
|
||||||
.setRequiresDeviceIdle(true)
|
|
||||||
.build()
|
|
||||||
val request = PeriodicWorkRequestBuilder<UpdateCheckService>(12, TimeUnit.HOURS)
|
|
||||||
.setConstraints(constraints)
|
|
||||||
.build()
|
|
||||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
|
||||||
Const.ID.CHECK_MAGISK_UPDATE_WORKER_ID,
|
|
||||||
ExistingPeriodicWorkPolicy.REPLACE, request)
|
|
||||||
} else {
|
|
||||||
WorkManager.getInstance(context)
|
|
||||||
.cancelUniqueWork(Const.ID.CHECK_MAGISK_UPDATE_WORKER_ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,103 +1,116 @@
|
|||||||
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.WRITE_EXTERNAL_STORAGE
|
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
|
import android.app.Activity
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.net.Uri
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.CallSuper
|
import androidx.activity.result.ActivityResultCallback
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts.GetContent
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.collection.SparseArrayCompat
|
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
import com.topjohnwu.magisk.core.ktx.reflectField
|
||||||
|
import com.topjohnwu.magisk.core.ktx.toast
|
||||||
|
import com.topjohnwu.magisk.core.utils.RequestInstall
|
||||||
import com.topjohnwu.magisk.core.wrap
|
import com.topjohnwu.magisk.core.wrap
|
||||||
import com.topjohnwu.magisk.ktx.set
|
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
typealias ActivityResultCallback = BaseActivity.(Int, Intent?) -> Unit
|
interface ContentResultCallback: ActivityResultCallback<Uri>, Parcelable {
|
||||||
|
fun onActivityLaunch() {}
|
||||||
|
// Make the result type explicitly non-null
|
||||||
|
override fun onActivityResult(result: Uri)
|
||||||
|
}
|
||||||
|
|
||||||
abstract class BaseActivity : AppCompatActivity() {
|
abstract class BaseActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private val resultCallbacks by lazy { SparseArrayCompat<ActivityResultCallback>() }
|
private var permissionCallback: ((Boolean) -> Unit)? = null
|
||||||
|
private val requestPermission = registerForActivityResult(RequestPermission()) {
|
||||||
|
permissionCallback?.invoke(it)
|
||||||
|
permissionCallback = null
|
||||||
|
}
|
||||||
|
|
||||||
override fun applyOverrideConfiguration(config: Configuration?) {
|
private var installCallback: ((Boolean) -> Unit)? = null
|
||||||
// Force applying our preferred local
|
private val requestInstall = registerForActivityResult(RequestInstall()) {
|
||||||
config?.setLocale(currentLocale)
|
installCallback?.invoke(it)
|
||||||
super.applyOverrideConfiguration(config)
|
installCallback = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private var contentCallback: ContentResultCallback? = null
|
||||||
|
private val getContent = registerForActivityResult(GetContent()) {
|
||||||
|
if (it != null) contentCallback?.onActivityResult(it)
|
||||||
|
contentCallback = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private val mReferrerField by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
|
Activity::class.java.reflectField("mReferrer")
|
||||||
|
}
|
||||||
|
|
||||||
|
val realCallingPackage: String? get() {
|
||||||
|
callingPackage?.let { return it }
|
||||||
|
mReferrerField.get(this)?.let { return it as String }
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
override fun attachBaseContext(base: Context) {
|
||||||
super.attachBaseContext(base.wrap(false))
|
super.attachBaseContext(base.wrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun withPermission(permission: String, builder: PermissionRequestBuilder.() -> Unit) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
val request = PermissionRequestBuilder().apply(builder).build()
|
if (isRunningAsStub) {
|
||||||
|
// Overwrite private members to avoid nasty "false" stack traces being logged
|
||||||
|
val delegate = delegate
|
||||||
|
val clz = delegate.javaClass
|
||||||
|
clz.reflectField("mActivityHandlesConfigFlagsChecked").set(delegate, true)
|
||||||
|
clz.reflectField("mActivityHandlesConfigFlags").set(delegate, 0)
|
||||||
|
}
|
||||||
|
contentCallback = savedInstanceState?.getParcelable(CONTENT_CALLBACK_KEY)
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
if (permission == WRITE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= 30) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
// We do not need external rw on 30+
|
super.onSaveInstanceState(outState)
|
||||||
request.onSuccess()
|
contentCallback?.let {
|
||||||
|
outState.putParcelable(CONTENT_CALLBACK_KEY, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
|
||||||
|
permission == WRITE_EXTERNAL_STORAGE) {
|
||||||
|
// We do not need external rw on R+
|
||||||
|
callback(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU &&
|
||||||
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
|
permission == POST_NOTIFICATIONS) {
|
||||||
request.onSuccess()
|
// All apps have notification permissions before T
|
||||||
|
callback(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (permission == REQUEST_INSTALL_PACKAGES) {
|
||||||
|
installCallback = callback
|
||||||
|
requestInstall.launch(Unit)
|
||||||
} else {
|
} else {
|
||||||
var requestCode: Int
|
permissionCallback = callback
|
||||||
do {
|
requestPermission.launch(permission)
|
||||||
requestCode = Random.nextInt(Const.ID.MAX_ACTIVITY_RESULT + 1, 1 shl 15)
|
|
||||||
} while (resultCallbacks.containsKey(requestCode))
|
|
||||||
resultCallbacks[requestCode] = { result, _ ->
|
|
||||||
if (result > 0)
|
|
||||||
request.onSuccess()
|
|
||||||
else
|
|
||||||
request.onFailure()
|
|
||||||
}
|
|
||||||
ActivityCompat.requestPermissions(this, arrayOf(permission), requestCode)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun withExternalRW(builder: PermissionRequestBuilder.() -> Unit) {
|
fun getContent(type: String, callback: ContentResultCallback) {
|
||||||
withPermission(WRITE_EXTERNAL_STORAGE, builder = builder)
|
contentCallback = callback
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(
|
|
||||||
requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
|
||||||
var success = true
|
|
||||||
for (res in grantResults) {
|
|
||||||
if (res != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
success = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resultCallbacks[requestCode]?.also {
|
|
||||||
resultCallbacks.remove(requestCode)
|
|
||||||
it(this, if (success) 1 else -1, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
resultCallbacks[requestCode]?.also { callback ->
|
|
||||||
resultCallbacks.remove(requestCode)
|
|
||||||
callback(this, resultCode, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startActivityForResult(intent: Intent, requestCode: Int, callback: ActivityResultCallback) {
|
|
||||||
resultCallbacks[requestCode] = callback
|
|
||||||
try {
|
try {
|
||||||
startActivityForResult(intent, requestCode)
|
getContent.launch(type)
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,4 +119,12 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun relaunch() {
|
||||||
|
startActivity(Intent(intent).setFlags(0))
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val CONTENT_CALLBACK_KEY = "content_callback"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.topjohnwu.magisk.core.base
|
||||||
|
|
||||||
|
import android.app.job.JobService
|
||||||
|
import android.content.Context
|
||||||
|
import com.topjohnwu.magisk.core.patch
|
||||||
|
|
||||||
|
abstract class BaseJobService : JobService() {
|
||||||
|
override fun attachBaseContext(base: Context) {
|
||||||
|
super.attachBaseContext(base.patch())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.topjohnwu.magisk.core.base
|
||||||
|
|
||||||
|
import android.content.ContentProvider
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.ProviderInfo
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.net.Uri
|
||||||
|
import com.topjohnwu.magisk.core.patch
|
||||||
|
|
||||||
|
open class BaseProvider : ContentProvider() {
|
||||||
|
override fun attachInfo(context: Context, info: ProviderInfo) {
|
||||||
|
super.attachInfo(context.patch(), info)
|
||||||
|
}
|
||||||
|
override fun onCreate() = true
|
||||||
|
override fun getType(uri: Uri): String? = null
|
||||||
|
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
|
||||||
|
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?) = 0
|
||||||
|
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?) = 0
|
||||||
|
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? = null
|
||||||
|
}
|
||||||
@@ -2,16 +2,13 @@ package com.topjohnwu.magisk.core.base
|
|||||||
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.topjohnwu.magisk.core.wrap
|
import androidx.annotation.CallSuper
|
||||||
import org.koin.core.KoinComponent
|
import com.topjohnwu.magisk.core.patch
|
||||||
|
|
||||||
abstract class BaseReceiver : BroadcastReceiver(), KoinComponent {
|
abstract class BaseReceiver : BroadcastReceiver() {
|
||||||
|
@CallSuper
|
||||||
final override fun onReceive(context: Context, intent: Intent?) {
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
onReceive(context.wrap() as ContextWrapper, intent)
|
context.patch()
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun onReceive(context: ContextWrapper, intent: Intent?)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ package com.topjohnwu.magisk.core.base
|
|||||||
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.topjohnwu.magisk.core.wrap
|
import android.content.Intent
|
||||||
import org.koin.core.KoinComponent
|
import android.os.IBinder
|
||||||
|
import com.topjohnwu.magisk.core.patch
|
||||||
|
|
||||||
abstract class BaseService : Service(), KoinComponent {
|
open class BaseService : Service() {
|
||||||
override fun attachBaseContext(base: Context) {
|
override fun attachBaseContext(base: Context) {
|
||||||
super.attachBaseContext(base.wrap())
|
super.attachBaseContext(base.patch())
|
||||||
}
|
}
|
||||||
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.base
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Network
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.annotation.MainThread
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.work.Data
|
|
||||||
import androidx.work.ListenableWorker
|
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
abstract class BaseWorkerWrapper {
|
|
||||||
|
|
||||||
private lateinit var worker: ListenableWorker
|
|
||||||
|
|
||||||
val applicationContext: Context
|
|
||||||
get() = worker.applicationContext
|
|
||||||
|
|
||||||
val id: UUID
|
|
||||||
get() = worker.id
|
|
||||||
|
|
||||||
val inputData: Data
|
|
||||||
get() = worker.inputData
|
|
||||||
|
|
||||||
val tags: Set<String>
|
|
||||||
get() = worker.tags
|
|
||||||
|
|
||||||
val triggeredContentUris: List<Uri>
|
|
||||||
@RequiresApi(24)
|
|
||||||
get() = worker.triggeredContentUris
|
|
||||||
|
|
||||||
val triggeredContentAuthorities: List<String>
|
|
||||||
@RequiresApi(24)
|
|
||||||
get() = worker.triggeredContentAuthorities
|
|
||||||
|
|
||||||
val network: Network?
|
|
||||||
@RequiresApi(28)
|
|
||||||
get() = worker.network
|
|
||||||
|
|
||||||
val runAttemptCount: Int
|
|
||||||
get() = worker.runAttemptCount
|
|
||||||
|
|
||||||
val isStopped: Boolean
|
|
||||||
get() = worker.isStopped
|
|
||||||
|
|
||||||
abstract fun doWork(): ListenableWorker.Result
|
|
||||||
|
|
||||||
fun onStopped() {}
|
|
||||||
|
|
||||||
fun attachWorker(w: ListenableWorker) {
|
|
||||||
worker = w
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainThread
|
|
||||||
fun startWork(): ListenableFuture<ListenableWorker.Result> {
|
|
||||||
return worker.startWork()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.base
|
|
||||||
|
|
||||||
typealias SimpleCallback = () -> Unit
|
|
||||||
typealias PermissionRationaleCallback = (List<String>) -> Unit
|
|
||||||
|
|
||||||
class PermissionRequestBuilder {
|
|
||||||
|
|
||||||
private var onSuccessCallback: SimpleCallback = {}
|
|
||||||
private var onFailureCallback: SimpleCallback = {}
|
|
||||||
private var onShowRationaleCallback: PermissionRationaleCallback = {}
|
|
||||||
|
|
||||||
fun onSuccess(callback: SimpleCallback) {
|
|
||||||
onSuccessCallback = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onFailure(callback: SimpleCallback) {
|
|
||||||
onFailureCallback = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onShowRationale(callback: PermissionRationaleCallback) {
|
|
||||||
onShowRationaleCallback = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
fun build(): PermissionRequest {
|
|
||||||
return PermissionRequest(onSuccessCallback, onFailureCallback, onShowRationaleCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class PermissionRequest(
|
|
||||||
private val onSuccessCallback: SimpleCallback,
|
|
||||||
private val onFailureCallback: SimpleCallback,
|
|
||||||
private val onShowRationaleCallback: PermissionRationaleCallback
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun onSuccess() = onSuccessCallback()
|
|
||||||
fun onFailure() = onFailureCallback()
|
|
||||||
fun onShowRationale(permissions: List<String>) = onShowRationaleCallback(permissions)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.topjohnwu.magisk.core.data
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.core.model.BranchInfo
|
||||||
|
import com.topjohnwu.magisk.core.model.ModuleJson
|
||||||
|
import com.topjohnwu.magisk.core.model.UpdateInfo
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import retrofit2.http.*
|
||||||
|
|
||||||
|
private const val BRANCH = "branch"
|
||||||
|
private const val REPO = "repo"
|
||||||
|
private const val FILE = "file"
|
||||||
|
|
||||||
|
interface GithubPageServices {
|
||||||
|
|
||||||
|
@GET
|
||||||
|
suspend fun fetchUpdateJSON(@Url file: String): UpdateInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RawServices {
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Streaming
|
||||||
|
suspend fun fetchFile(@Url url: String): ResponseBody
|
||||||
|
|
||||||
|
@GET
|
||||||
|
suspend fun fetchString(@Url url: String): String
|
||||||
|
|
||||||
|
@GET
|
||||||
|
suspend fun fetchModuleJson(@Url url: String): ModuleJson
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GithubApiServices {
|
||||||
|
|
||||||
|
@GET("repos/{$REPO}/branches/{$BRANCH}")
|
||||||
|
@Headers("Accept: application/vnd.github.v3+json")
|
||||||
|
suspend fun fetchBranch(
|
||||||
|
@Path(REPO, encoded = true) repo: String,
|
||||||
|
@Path(BRANCH) branch: String
|
||||||
|
): BranchInfo
|
||||||
|
}
|
||||||
@@ -1,15 +1,27 @@
|
|||||||
package com.topjohnwu.magisk.data.database
|
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
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.topjohnwu.magisk.core.data.magiskdb
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.core.ktx.await
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
open class MagiskDB {
|
||||||
|
|
||||||
|
suspend fun <R> exec(
|
||||||
|
query: String,
|
||||||
|
mapper: suspend (Map<String, String>) -> R
|
||||||
|
): List<R> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val out = Shell.cmd("magisk --sqlite '$query'").await().out
|
||||||
|
out.map { line ->
|
||||||
|
line.split("\\|".toRegex())
|
||||||
|
.map { it.split("=", limit = 2) }
|
||||||
|
.filter { it.size == 2 }
|
||||||
|
.associate { it[0] to it[1] }
|
||||||
|
.let { mapper(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend inline fun exec(query: String) {
|
||||||
|
exec(query) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Map<String, Any>.toQuery(): String {
|
||||||
|
val keys = this.keys.joinToString(",")
|
||||||
|
val values = this.values.joinToString(",") {
|
||||||
|
when (it) {
|
||||||
|
is Boolean -> if (it) "1" else "0"
|
||||||
|
is Number -> it.toString()
|
||||||
|
else -> "\"$it\""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "($keys) VALUES($values)"
|
||||||
|
}
|
||||||
|
|
||||||
|
object Table {
|
||||||
|
const val POLICY = "policies"
|
||||||
|
const val SETTINGS = "settings"
|
||||||
|
const val STRINGS = "strings"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.topjohnwu.magisk.core.data.magiskdb
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.core.Const
|
||||||
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
|
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class PolicyDao : MagiskDB() {
|
||||||
|
|
||||||
|
suspend fun deleteOutdated() {
|
||||||
|
val nowSeconds = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())
|
||||||
|
val query = "DELETE FROM ${Table.POLICY} WHERE " +
|
||||||
|
"(until > 0 AND until < $nowSeconds) OR until < 0"
|
||||||
|
exec(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun delete(uid: Int) {
|
||||||
|
val query = "DELETE FROM ${Table.POLICY} WHERE uid == $uid"
|
||||||
|
exec(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetch(uid: Int): SuPolicy? {
|
||||||
|
val query = "SELECT * FROM ${Table.POLICY} WHERE uid == $uid LIMIT = 1"
|
||||||
|
return exec(query, ::toPolicy).firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun update(policy: SuPolicy) {
|
||||||
|
val map = policy.toMap()
|
||||||
|
if (!Const.Version.atLeast_25_0()) {
|
||||||
|
// Put in package_name for old database
|
||||||
|
map["package_name"] = AppContext.packageManager.getNameForUid(policy.uid)!!
|
||||||
|
}
|
||||||
|
val query = "REPLACE INTO ${Table.POLICY} ${map.toQuery()}"
|
||||||
|
exec(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchAll(): List<SuPolicy> {
|
||||||
|
val query = "SELECT * FROM ${Table.POLICY} WHERE uid/100000 == ${Const.USER_ID}"
|
||||||
|
return exec(query, ::toPolicy).filterNotNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toPolicy(map: Map<String, String>): SuPolicy? {
|
||||||
|
val uid = map["uid"]?.toInt() ?: return null
|
||||||
|
val policy = SuPolicy(uid)
|
||||||
|
|
||||||
|
map["policy"]?.toInt()?.let { policy.policy = it }
|
||||||
|
map["until"]?.toLong()?.let { policy.until = it }
|
||||||
|
map["logging"]?.toInt()?.let { policy.logging = it != 0 }
|
||||||
|
map["notification"]?.toInt()?.let { policy.notification = it != 0 }
|
||||||
|
return policy
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.topjohnwu.magisk.core.data.magiskdb
|
||||||
|
|
||||||
|
class SettingsDao : MagiskDB() {
|
||||||
|
|
||||||
|
suspend fun delete(key: String) {
|
||||||
|
val query = "DELETE FROM ${Table.SETTINGS} WHERE key == \"$key\""
|
||||||
|
exec(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun put(key: String, value: Int) {
|
||||||
|
val kv = mapOf("key" to key, "value" to value)
|
||||||
|
val query = "REPLACE INTO ${Table.SETTINGS} ${kv.toQuery()}"
|
||||||
|
exec(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetch(key: String, default: Int = -1): Int {
|
||||||
|
val query = "SELECT value FROM ${Table.SETTINGS} WHERE key == \"$key\" LIMIT 1"
|
||||||
|
return exec(query) { it["value"]?.toInt() }.firstOrNull() ?: default
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.topjohnwu.magisk.core.data.magiskdb
|
||||||
|
|
||||||
|
class StringDao : MagiskDB() {
|
||||||
|
|
||||||
|
suspend fun delete(key: String) {
|
||||||
|
val query = "DELETE FROM ${Table.STRINGS} WHERE key == \"$key\""
|
||||||
|
exec(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun put(key: String, value: String) {
|
||||||
|
val kv = mapOf("key" to key, "value" to value)
|
||||||
|
val query = "REPLACE INTO ${Table.STRINGS} ${kv.toQuery()}"
|
||||||
|
exec(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetch(key: String, default: String = ""): String {
|
||||||
|
val query = "SELECT value FROM ${Table.STRINGS} WHERE key == \"$key\" LIMIT 1"
|
||||||
|
return exec(query) { it["value"] }.firstOrNull() ?: default
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,55 +1,36 @@
|
|||||||
package com.topjohnwu.magisk.di
|
package com.topjohnwu.magisk.core.di
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
|
import com.topjohnwu.magisk.ProviderInstaller
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.Const
|
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.data.network.GithubApiServices
|
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||||
import com.topjohnwu.magisk.data.network.GithubPageServices
|
import okhttp3.Cache
|
||||||
import com.topjohnwu.magisk.data.network.JSDelivrServices
|
import okhttp3.ConnectionSpec
|
||||||
import com.topjohnwu.magisk.data.network.RawServices
|
|
||||||
import com.topjohnwu.magisk.ktx.precomputedText
|
|
||||||
import com.topjohnwu.magisk.net.Networking
|
|
||||||
import com.topjohnwu.magisk.net.NoSSLv3SocketFactory
|
|
||||||
import com.topjohnwu.magisk.utils.MarkwonImagePlugin
|
|
||||||
import io.noties.markwon.Markwon
|
|
||||||
import io.noties.markwon.html.HtmlPlugin
|
|
||||||
import okhttp3.Dns
|
import okhttp3.Dns
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.dnsoverhttps.DnsOverHttps
|
import okhttp3.dnsoverhttps.DnsOverHttps
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
import org.koin.dsl.module
|
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||||
import retrofit2.converter.scalars.ScalarsConverterFactory
|
import retrofit2.converter.scalars.ScalarsConverterFactory
|
||||||
|
import java.io.File
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.UnknownHostException
|
import java.net.UnknownHostException
|
||||||
|
|
||||||
val networkingModule = module {
|
|
||||||
single { createOkHttpClient(get()) }
|
|
||||||
single { createRetrofit(get()) }
|
|
||||||
single { createApiService<RawServices>(get(), Const.Url.GITHUB_RAW_URL) }
|
|
||||||
single { createApiService<GithubApiServices>(get(), Const.Url.GITHUB_API_URL) }
|
|
||||||
single { createApiService<GithubPageServices>(get(), Const.Url.GITHUB_PAGE_URL) }
|
|
||||||
single { createApiService<JSDelivrServices>(get(), Const.Url.JS_DELIVR_URL) }
|
|
||||||
single { createMarkwon(get(), get()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DnsResolver(client: OkHttpClient) : Dns {
|
private class DnsResolver(client: OkHttpClient) : Dns {
|
||||||
|
|
||||||
private val doh by lazy {
|
private val doh by lazy {
|
||||||
DnsOverHttps.Builder().client(client)
|
DnsOverHttps.Builder().client(client)
|
||||||
.url(HttpUrl.get("https://cloudflare-dns.com/dns-query"))
|
.url("https://cloudflare-dns.com/dns-query".toHttpUrl())
|
||||||
.bootstrapDnsHosts(listOf(
|
.bootstrapDnsHosts(listOf(
|
||||||
InetAddress.getByName("162.159.36.1"),
|
InetAddress.getByName("162.159.36.1"),
|
||||||
InetAddress.getByName("162.159.46.1"),
|
InetAddress.getByName("162.159.46.1"),
|
||||||
InetAddress.getByName("1.1.1.1"),
|
InetAddress.getByName("1.1.1.1"),
|
||||||
InetAddress.getByName("1.0.0.1"),
|
InetAddress.getByName("1.0.0.1"),
|
||||||
InetAddress.getByName("162.159.132.53"),
|
|
||||||
InetAddress.getByName("2606:4700:4700::1111"),
|
InetAddress.getByName("2606:4700:4700::1111"),
|
||||||
InetAddress.getByName("2606:4700:4700::1001"),
|
InetAddress.getByName("2606:4700:4700::1001"),
|
||||||
InetAddress.getByName("2606:4700:4700::0064"),
|
InetAddress.getByName("2606:4700:4700::0064"),
|
||||||
@@ -69,23 +50,32 @@ private class DnsResolver(client: OkHttpClient) : Dns {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
fun createOkHttpClient(context: Context): OkHttpClient {
|
fun createOkHttpClient(context: Context): OkHttpClient {
|
||||||
val builder = OkHttpClient.Builder()
|
val appCache = Cache(File(context.cacheDir, "okhttp"), 10 * 1024 * 1024)
|
||||||
|
val builder = OkHttpClient.Builder().cache(appCache)
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
builder.addInterceptor(HttpLoggingInterceptor().apply {
|
builder.addInterceptor(HttpLoggingInterceptor().apply {
|
||||||
level = HttpLoggingInterceptor.Level.BASIC
|
level = HttpLoggingInterceptor.Level.BASIC
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
builder.connectionSpecs(listOf(ConnectionSpec.MODERN_TLS))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Networking.init(context)) {
|
|
||||||
Info.hasGMS = false
|
|
||||||
if (Build.VERSION.SDK_INT < 21)
|
|
||||||
builder.sslSocketFactory(NoSSLv3SocketFactory())
|
|
||||||
}
|
|
||||||
builder.dns(DnsResolver(builder.build()))
|
builder.dns(DnsResolver(builder.build()))
|
||||||
|
|
||||||
|
builder.addInterceptor { chain ->
|
||||||
|
val request = chain.request().newBuilder()
|
||||||
|
request.header("User-Agent", "Magisk/${BuildConfig.VERSION_CODE}")
|
||||||
|
request.header("Accept-Language", currentLocale.toLanguageTag())
|
||||||
|
chain.proceed(request.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ProviderInstaller.install(context)) {
|
||||||
|
Info.hasGMS = false
|
||||||
|
}
|
||||||
|
|
||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,14 +97,3 @@ inline fun <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseU
|
|||||||
.build()
|
.build()
|
||||||
.create(T::class.java)
|
.create(T::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createMarkwon(context: Context, okHttpClient: OkHttpClient): Markwon {
|
|
||||||
return Markwon.builder(context)
|
|
||||||
.textSetter { textView, spanned, _, onComplete ->
|
|
||||||
textView.tag = onComplete
|
|
||||||
textView.precomputedText = spanned
|
|
||||||
}
|
|
||||||
.usePlugin(HtmlPlugin.create())
|
|
||||||
.usePlugin(MarkwonImagePlugin(okHttpClient))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.topjohnwu.magisk.core.di
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
|
import androidx.room.Room
|
||||||
|
import com.topjohnwu.magisk.core.Const
|
||||||
|
import com.topjohnwu.magisk.core.data.SuLogDatabase
|
||||||
|
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
||||||
|
import com.topjohnwu.magisk.core.data.magiskdb.SettingsDao
|
||||||
|
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.NetworkService
|
||||||
|
import com.topjohnwu.magisk.core.utils.BiometricHelper
|
||||||
|
import io.noties.markwon.Markwon
|
||||||
|
import io.noties.markwon.utils.NoCopySpannableFactory
|
||||||
|
|
||||||
|
val AppContext: Context inline get() = ServiceLocator.context
|
||||||
|
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
object ServiceLocator {
|
||||||
|
|
||||||
|
lateinit var context: Context
|
||||||
|
val deContext by lazy { context.deviceProtectedContext }
|
||||||
|
val timeoutPrefs by lazy { deContext.getSharedPreferences("su_timeout", 0) }
|
||||||
|
val biometrics by lazy { BiometricHelper(context) }
|
||||||
|
|
||||||
|
// Database
|
||||||
|
val policyDB = PolicyDao()
|
||||||
|
val settingsDB = SettingsDao()
|
||||||
|
val stringDB = StringDao()
|
||||||
|
val sulogDB by lazy { createSuLogDatabase(deContext).suLogDao() }
|
||||||
|
val logRepo by lazy { LogRepository(sulogDB) }
|
||||||
|
|
||||||
|
// Networking
|
||||||
|
val okhttp by lazy { createOkHttpClient(context) }
|
||||||
|
val retrofit by lazy { createRetrofit(okhttp) }
|
||||||
|
val markwon by lazy { createMarkwon(context) }
|
||||||
|
val networkService by lazy {
|
||||||
|
NetworkService(
|
||||||
|
createApiService(retrofit, Const.Url.GITHUB_PAGE_URL),
|
||||||
|
createApiService(retrofit, Const.Url.GITHUB_RAW_URL),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSuLogDatabase(context: Context) =
|
||||||
|
Room.databaseBuilder(context, SuLogDatabase::class.java, "sulogs.db")
|
||||||
|
.addMigrations(SuLogDatabase.MIGRATION_1_2)
|
||||||
|
.fallbackToDestructiveMigration()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private fun createMarkwon(context: Context) =
|
||||||
|
Markwon.builder(context).textSetter { textView, spanned, bufferType, onComplete ->
|
||||||
|
textView.apply {
|
||||||
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
setSpannableFactory(NoCopySpannableFactory.getInstance())
|
||||||
|
setText(spanned, bufferType)
|
||||||
|
onComplete.run()
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.download
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
sealed class Action : Parcelable {
|
|
||||||
|
|
||||||
sealed class Flash : Action() {
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
object Primary : Flash()
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
object Secondary : Flash()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
object Download : Action()
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
object Uninstall : Action()
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
object EnvFix : Action()
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Patch(val fileUri: Uri) : Action()
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.download
|
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.IBinder
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import com.topjohnwu.magisk.R
|
|
||||||
import com.topjohnwu.magisk.core.ForegroundTracker
|
|
||||||
import com.topjohnwu.magisk.core.base.BaseService
|
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.checkSum
|
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
|
||||||
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
|
||||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
|
||||||
import com.topjohnwu.magisk.ktx.withStreams
|
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import okhttp3.ResponseBody
|
|
||||||
import org.koin.android.ext.android.inject
|
|
||||||
import org.koin.core.KoinComponent
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.collections.HashMap
|
|
||||||
import kotlin.random.Random.Default.nextInt
|
|
||||||
|
|
||||||
abstract class BaseDownloader : BaseService(), KoinComponent {
|
|
||||||
|
|
||||||
private val hasNotifications get() = notifications.isNotEmpty()
|
|
||||||
private val notifications = Collections.synchronizedMap(HashMap<Int, Notification.Builder>())
|
|
||||||
private val coroutineScope = CoroutineScope(Dispatchers.IO)
|
|
||||||
|
|
||||||
val service: NetworkService by inject()
|
|
||||||
|
|
||||||
// -- Service overrides
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? = null
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
|
||||||
intent.getParcelableExtra<Subject>(ACTION_KEY)?.let { subject ->
|
|
||||||
update(subject.notifyID())
|
|
||||||
coroutineScope.launch {
|
|
||||||
try {
|
|
||||||
subject.startDownload()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Timber.e(e)
|
|
||||||
notifyFail(subject)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return START_REDELIVER_INTENT
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
|
||||||
super.onTaskRemoved(rootIntent)
|
|
||||||
notifications.forEach { cancel(it.key) }
|
|
||||||
notifications.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
coroutineScope.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Download logic
|
|
||||||
|
|
||||||
private suspend fun Subject.startDownload() {
|
|
||||||
val skip = this is Subject.Magisk && file.checkSum("MD5", magisk.md5)
|
|
||||||
if (!skip) {
|
|
||||||
val stream = service.fetchFile(url).toProgressStream(this)
|
|
||||||
when (this) {
|
|
||||||
is Subject.Module -> // Download and process on-the-fly
|
|
||||||
stream.toModule(file, service.fetchInstaller().byteStream())
|
|
||||||
else -> {
|
|
||||||
withStreams(stream, file.outputStream()) { it, out -> it.copyTo(out) }
|
|
||||||
if (this is Subject.Manager)
|
|
||||||
handleAPK(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val newId = notifyFinish(this)
|
|
||||||
if (ForegroundTracker.hasForeground)
|
|
||||||
onFinish(this, newId)
|
|
||||||
if (!hasNotifications)
|
|
||||||
stopSelf()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ResponseBody.toProgressStream(subject: Subject): InputStream {
|
|
||||||
val max = contentLength()
|
|
||||||
val total = max.toFloat() / 1048576
|
|
||||||
val id = subject.notifyID()
|
|
||||||
|
|
||||||
update(id) { it.setContentTitle(subject.title) }
|
|
||||||
|
|
||||||
return ProgressInputStream(byteStream()) {
|
|
||||||
val progress = it.toFloat() / 1048576
|
|
||||||
update(id) { notification ->
|
|
||||||
if (max > 0) {
|
|
||||||
broadcast(progress / total, subject)
|
|
||||||
notification
|
|
||||||
.setProgress(max.toInt(), it.toInt(), false)
|
|
||||||
.setContentText("%.2f / %.2f MB".format(progress, total))
|
|
||||||
} else {
|
|
||||||
broadcast(-1f, subject)
|
|
||||||
notification.setContentText("%.2f MB / ??".format(progress))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Notification managements
|
|
||||||
|
|
||||||
fun Subject.notifyID() = hashCode()
|
|
||||||
|
|
||||||
private fun notifyFail(subject: Subject) = lastNotify(subject.notifyID()) {
|
|
||||||
broadcast(-2f, subject)
|
|
||||||
it.setContentText(getString(R.string.download_file_error))
|
|
||||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
|
||||||
.setOngoing(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun notifyFinish(subject: Subject) = lastNotify(subject.notifyID()) {
|
|
||||||
broadcast(1f, subject)
|
|
||||||
it.setIntent(subject)
|
|
||||||
.setContentTitle(subject.title)
|
|
||||||
.setContentText(getString(R.string.download_complete))
|
|
||||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
|
||||||
.setProgress(0, 0, false)
|
|
||||||
.setOngoing(false)
|
|
||||||
.setAutoCancel(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun create() = Notifications.progress(this, "")
|
|
||||||
|
|
||||||
fun update(id: Int, editor: (Notification.Builder) -> Unit = {}) {
|
|
||||||
val wasEmpty = !hasNotifications
|
|
||||||
val notification = notifications.getOrPut(id, ::create).also(editor)
|
|
||||||
if (wasEmpty)
|
|
||||||
updateForeground()
|
|
||||||
else
|
|
||||||
notify(id, notification.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun lastNotify(
|
|
||||||
id: Int,
|
|
||||||
editor: (Notification.Builder) -> Notification.Builder? = { null }
|
|
||||||
) : Int {
|
|
||||||
val notification = remove(id)?.run(editor) ?: return -1
|
|
||||||
val newId: Int = nextInt()
|
|
||||||
notify(newId, notification.build())
|
|
||||||
return newId
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun remove(id: Int) = notifications.remove(id)
|
|
||||||
?.also { updateForeground(); cancel(id) }
|
|
||||||
?: { cancel(id); null }()
|
|
||||||
|
|
||||||
private fun notify(id: Int, notification: Notification) {
|
|
||||||
Notifications.mgr.notify(id, notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cancel(id: Int) {
|
|
||||||
Notifications.mgr.cancel(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateForeground() {
|
|
||||||
if (hasNotifications) {
|
|
||||||
val (id, notification) = notifications.entries.first()
|
|
||||||
startForeground(id, notification.build())
|
|
||||||
} else {
|
|
||||||
stopForeground(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Implement custom logic
|
|
||||||
|
|
||||||
protected abstract suspend fun onFinish(subject: Subject, id: Int)
|
|
||||||
|
|
||||||
protected abstract fun Notification.Builder.setIntent(subject: Subject): Notification.Builder
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
companion object : KoinComponent {
|
|
||||||
const val ACTION_KEY = "download_action"
|
|
||||||
|
|
||||||
private val progressBroadcast = MutableLiveData<Pair<Float, Subject>>()
|
|
||||||
|
|
||||||
fun observeProgress(owner: LifecycleOwner, callback: (Float, Subject) -> Unit) {
|
|
||||||
progressBroadcast.value = null
|
|
||||||
progressBroadcast.observe(owner) {
|
|
||||||
val (progress, subject) = it ?: return@observe
|
|
||||||
callback(progress, subject)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun broadcast(progress: Float, subject: Subject) {
|
|
||||||
progressBroadcast.postValue(progress to subject)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,110 +1,220 @@
|
|||||||
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.Notification
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
|
import android.app.PendingIntent.*
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
import com.topjohnwu.magisk.core.download.Action.*
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import com.topjohnwu.magisk.core.download.Action.Flash.Secondary
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.download.Subject.*
|
import com.topjohnwu.magisk.StubApk
|
||||||
|
import com.topjohnwu.magisk.core.ActivityTracker
|
||||||
|
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.tasks.EnvFixTask
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
import com.topjohnwu.magisk.core.ktx.*
|
||||||
|
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||||
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||||
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||||
import com.topjohnwu.magisk.utils.APKInstall
|
import com.topjohnwu.magisk.utils.APKInstall
|
||||||
import kotlin.random.Random.Default.nextInt
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.util.Properties
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipFile
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
@SuppressLint("Registered")
|
class DownloadService : NotificationService() {
|
||||||
open class DownloadService : BaseDownloader() {
|
|
||||||
|
|
||||||
private val context get() = this
|
private val job = Job()
|
||||||
|
|
||||||
override suspend fun onFinish(subject: Subject, id: Int) = when (subject) {
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||||
is Magisk -> subject.onFinish(id)
|
intent.getParcelableExtra<Subject>(SUBJECT_KEY)?.let { download(it) }
|
||||||
is Module -> subject.onFinish(id)
|
return START_NOT_STICKY
|
||||||
is Manager -> subject.onFinish(id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun Magisk.onFinish(id: Int) = when (val action = action) {
|
override fun onDestroy() {
|
||||||
Uninstall -> FlashFragment.uninstall(file, id)
|
job.cancel()
|
||||||
EnvFix -> {
|
}
|
||||||
remove(id)
|
|
||||||
EnvFixTask(file).exec()
|
private fun download(subject: Subject) {
|
||||||
Unit
|
notifyUpdate(subject.notifyId)
|
||||||
|
CoroutineScope(job + Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val stream = service.fetchFile(subject.url).toProgressStream(subject)
|
||||||
|
when (subject) {
|
||||||
|
is Subject.App -> handleApp(stream, subject)
|
||||||
|
is Subject.Module -> handleModule(stream, subject.file)
|
||||||
|
}
|
||||||
|
val activity = ActivityTracker.foreground
|
||||||
|
if (activity != null && subject.autoLaunch) {
|
||||||
|
notifyRemove(subject.notifyId)
|
||||||
|
subject.pendingIntent(activity)?.send()
|
||||||
|
} else {
|
||||||
|
notifyFinish(subject)
|
||||||
|
}
|
||||||
|
subject.postDownload?.invoke()
|
||||||
|
if (!hasNotifications)
|
||||||
|
stopSelf()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
notifyFail(subject)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is Patch -> FlashFragment.patch(file, action.fileUri, id)
|
|
||||||
is Flash -> FlashFragment.flash(file, action is Secondary, id)
|
|
||||||
else -> Unit
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Module.onFinish(id: Int) = when (action) {
|
private fun handleApp(stream: InputStream, subject: Subject.App) {
|
||||||
is Flash -> FlashFragment.install(file, id)
|
fun writeTee(output: OutputStream) {
|
||||||
else -> Unit
|
val uri = MediaStoreUtils.getFile("${subject.title}.apk").uri
|
||||||
|
val external = uri.outputStream()
|
||||||
|
stream.copyAndClose(TeeOutputStream(external, output))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRunningAsStub) {
|
||||||
|
val updateApk = StubApk.update(this)
|
||||||
|
try {
|
||||||
|
// Download full APK to stub update path
|
||||||
|
writeTee(updateApk.outputStream())
|
||||||
|
|
||||||
|
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
|
||||||
|
notifyUpdate(subject.notifyId) {
|
||||||
|
it.setProgress(0, 0, true)
|
||||||
|
.setContentTitle(getString(R.string.hide_app_title))
|
||||||
|
.setContentText("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract stub
|
||||||
|
val apk = subject.file.toFile()
|
||||||
|
zf.getInputStream(zf.getEntry("assets/stub.apk")).writeTo(apk)
|
||||||
|
zf.close()
|
||||||
|
|
||||||
|
// Patch and install
|
||||||
|
subject.intent = HideAPK.upgrade(this, apk)
|
||||||
|
?: throw IOException("HideAPK patch error")
|
||||||
|
apk.delete()
|
||||||
|
} else {
|
||||||
|
ActivityTracker.foreground?.let {
|
||||||
|
// Relaunch the process if we are foreground
|
||||||
|
StubApk.restartProcess(it)
|
||||||
|
} ?: run {
|
||||||
|
// Or else kill the current process after posting notification
|
||||||
|
subject.intent = selfLaunchIntent()
|
||||||
|
subject.postDownload = { Runtime.getRuntime().exit(0) }
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// If any error occurred, do not let stub load the new APK
|
||||||
|
updateApk.delete()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val session = APKInstall.startSession(this)
|
||||||
|
writeTee(session.openStream(this))
|
||||||
|
subject.intent = session.waitIntent()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Manager.onFinish(id: Int) {
|
private fun handleModule(src: InputStream, file: Uri) {
|
||||||
remove(id)
|
val input = ZipInputStream(src.buffered())
|
||||||
APKInstall.install(context, file.toFile())
|
val output = ZipOutputStream(file.outputStream().buffered())
|
||||||
|
|
||||||
|
withStreams(input, output) { zin, zout ->
|
||||||
|
zout.putNextEntry(ZipEntry("META-INF/"))
|
||||||
|
zout.putNextEntry(ZipEntry("META-INF/com/"))
|
||||||
|
zout.putNextEntry(ZipEntry("META-INF/com/google/"))
|
||||||
|
zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
|
||||||
|
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
|
||||||
|
assets.open("module_installer.sh").copyTo(zout)
|
||||||
|
|
||||||
|
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
|
||||||
|
zout.write("#MAGISK\n".toByteArray())
|
||||||
|
|
||||||
|
zin.forEach { entry ->
|
||||||
|
val path = entry.name
|
||||||
|
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
|
||||||
|
zout.putNextEntry(ZipEntry(path))
|
||||||
|
if (!entry.isDirectory) {
|
||||||
|
zin.copyTo(zout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Customize finish notification
|
private class TeeOutputStream(
|
||||||
|
private val o1: OutputStream,
|
||||||
override fun Notification.Builder.setIntent(subject: Subject)
|
private val o2: OutputStream
|
||||||
= when (subject) {
|
) : OutputStream() {
|
||||||
is Magisk -> setIntent(subject)
|
override fun write(b: Int) {
|
||||||
is Module -> setIntent(subject)
|
o1.write(b)
|
||||||
is Manager -> setIntent(subject)
|
o2.write(b)
|
||||||
|
}
|
||||||
|
override fun write(b: ByteArray?, off: Int, len: Int) {
|
||||||
|
o1.write(b, off, len)
|
||||||
|
o2.write(b, off, len)
|
||||||
|
}
|
||||||
|
override fun close() {
|
||||||
|
o1.close()
|
||||||
|
o2.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Notification.Builder.setIntent(subject: Magisk)
|
|
||||||
= when (val action = subject.action) {
|
|
||||||
Uninstall -> setContentIntent(FlashFragment.uninstallIntent(context, subject.file))
|
|
||||||
is Flash -> setContentIntent(FlashFragment.flashIntent(context, subject.file, action is Secondary))
|
|
||||||
is Patch -> setContentIntent(FlashFragment.patchIntent(context, subject.file, action.fileUri))
|
|
||||||
else -> setContentIntent(Intent())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Notification.Builder.setIntent(subject: Module)
|
|
||||||
= when (subject.action) {
|
|
||||||
is Flash -> setContentIntent(FlashFragment.installIntent(context, subject.file))
|
|
||||||
else -> setContentIntent(Intent())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Notification.Builder.setIntent(subject: Manager)
|
|
||||||
= setContentIntent(APKInstall.installIntent(context, subject.file.toFile()))
|
|
||||||
|
|
||||||
private fun Notification.Builder.setContentIntent(intent: Intent) =
|
|
||||||
setContentIntent(
|
|
||||||
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
|
||||||
)
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val SUBJECT_KEY = "subject"
|
||||||
|
private const val REQUEST_CODE = 1
|
||||||
|
|
||||||
private fun intent(context: Context, subject: Subject) =
|
fun observeProgress(owner: LifecycleOwner, callback: (Float, Subject) -> Unit) {
|
||||||
context.intent<DownloadService>().putExtra(ACTION_KEY, subject)
|
progressBroadcast.value = null
|
||||||
|
progressBroadcast.observe(owner) {
|
||||||
fun pendingIntent(context: Context, subject: Subject): PendingIntent {
|
val (progress, subject) = it ?: return@observe
|
||||||
return if (Build.VERSION.SDK_INT >= 26) {
|
callback(progress, subject)
|
||||||
PendingIntent.getForegroundService(context, nextInt(),
|
|
||||||
intent(context, subject), PendingIntent.FLAG_UPDATE_CURRENT)
|
|
||||||
} else {
|
|
||||||
PendingIntent.getService(context, nextInt(),
|
|
||||||
intent(context, subject), PendingIntent.FLAG_UPDATE_CURRENT)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start(context: Context, subject: Subject) {
|
private fun intent(context: Context, subject: Subject) =
|
||||||
val app = context.applicationContext
|
context.intent<DownloadService>().putExtra(SUBJECT_KEY, subject)
|
||||||
if (Build.VERSION.SDK_INT >= 26) {
|
|
||||||
app.startForegroundService(intent(app, subject))
|
@SuppressLint("InlinedApi")
|
||||||
|
fun getPendingIntent(context: Context, subject: Subject): PendingIntent {
|
||||||
|
val flag = FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT or FLAG_ONE_SHOT
|
||||||
|
val intent = intent(context, subject)
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
getForegroundService(context, REQUEST_CODE, intent, flag)
|
||||||
} else {
|
} else {
|
||||||
app.startService(intent(app, subject))
|
getService(context, REQUEST_CODE, intent, flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
fun start(activity: BaseActivity, subject: Subject) {
|
||||||
|
activity.withPermission(Manifest.permission.POST_NOTIFICATIONS) {
|
||||||
|
// Always download regardless of notification permission status
|
||||||
|
val app = activity.applicationContext
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
app.startForegroundService(intent(app, subject))
|
||||||
|
} else {
|
||||||
|
app.startService(intent(app, subject))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.download
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.core.net.toFile
|
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
|
||||||
import com.topjohnwu.magisk.DynAPK
|
|
||||||
import com.topjohnwu.magisk.R
|
|
||||||
import com.topjohnwu.magisk.core.Info
|
|
||||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
|
||||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
|
||||||
import com.topjohnwu.magisk.ktx.relaunchApp
|
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
private fun Context.patch(apk: File) {
|
|
||||||
val patched = File(apk.parent, "patched.apk")
|
|
||||||
HideAPK.patch(this, apk.path, patched.path, packageName, applicationInfo.nonLocalizedLabel)
|
|
||||||
apk.delete()
|
|
||||||
patched.renameTo(apk)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun BaseDownloader.notifyHide(id: Int) {
|
|
||||||
update(id) {
|
|
||||||
it.setProgress(0, 0, true)
|
|
||||||
.setContentTitle(getString(R.string.hide_manager_title))
|
|
||||||
.setContentText("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun BaseDownloader.handleAPK(subject: Subject.Manager) {
|
|
||||||
val apk = subject.file.toFile()
|
|
||||||
val id = subject.notifyID()
|
|
||||||
if (isRunningAsStub) {
|
|
||||||
// Move to upgrade location
|
|
||||||
apk.copyTo(DynAPK.update(this), overwrite = true)
|
|
||||||
apk.delete()
|
|
||||||
if (Info.stubChk.version < subject.stub.versionCode) {
|
|
||||||
notifyHide(id)
|
|
||||||
// Also upgrade stub
|
|
||||||
service.fetchFile(subject.stub.link).byteStream().use { it.writeTo(apk) }
|
|
||||||
patch(apk)
|
|
||||||
} else {
|
|
||||||
// Simply relaunch the app
|
|
||||||
stopSelf()
|
|
||||||
relaunchApp(this)
|
|
||||||
}
|
|
||||||
} else if (packageName != BuildConfig.APPLICATION_ID) {
|
|
||||||
notifyHide(id)
|
|
||||||
patch(apk)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.download
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import com.topjohnwu.magisk.ktx.withStreams
|
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.util.zip.ZipEntry
|
|
||||||
import java.util.zip.ZipInputStream
|
|
||||||
import java.util.zip.ZipOutputStream
|
|
||||||
|
|
||||||
fun InputStream.toModule(file: Uri, installer: InputStream) {
|
|
||||||
|
|
||||||
val input = ZipInputStream(buffered())
|
|
||||||
val output = ZipOutputStream(file.outputStream().buffered())
|
|
||||||
|
|
||||||
withStreams(input, output) { zin, zout ->
|
|
||||||
zout.putNextEntry(ZipEntry("META-INF/"))
|
|
||||||
zout.putNextEntry(ZipEntry("META-INF/com/"))
|
|
||||||
zout.putNextEntry(ZipEntry("META-INF/com/google/"))
|
|
||||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
|
|
||||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
|
|
||||||
installer.copyTo(zout)
|
|
||||||
|
|
||||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
|
|
||||||
zout.write("#MAGISK\n".toByteArray(charset("UTF-8")))
|
|
||||||
|
|
||||||
var off = -1
|
|
||||||
var entry: ZipEntry? = zin.nextEntry
|
|
||||||
while (entry != null) {
|
|
||||||
if (off < 0) {
|
|
||||||
off = entry.name.indexOf('/') + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
val path = entry.name.substring(off)
|
|
||||||
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
|
|
||||||
zout.putNextEntry(ZipEntry(path))
|
|
||||||
if (!entry.isDirectory) {
|
|
||||||
zin.copyTo(zout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
entry = zin.nextEntry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package com.topjohnwu.magisk.core.download
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.core.base.BaseService
|
||||||
|
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.view.Notifications
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
open class NotificationService : BaseService() {
|
||||||
|
|
||||||
|
private val notifications = HashMap<Int, Notification.Builder>().synchronized()
|
||||||
|
protected val hasNotifications get() = notifications.isNotEmpty()
|
||||||
|
|
||||||
|
protected val service get() = ServiceLocator.networkService
|
||||||
|
|
||||||
|
private var attachedNotificationId = 0
|
||||||
|
|
||||||
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
|
super.onTaskRemoved(rootIntent)
|
||||||
|
notifications.forEach { Notifications.mgr.cancel(it.key) }
|
||||||
|
notifications.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun ResponseBody.toProgressStream(subject: Subject): InputStream {
|
||||||
|
val max = contentLength()
|
||||||
|
val total = max.toFloat() / 1048576
|
||||||
|
val id = subject.notifyId
|
||||||
|
|
||||||
|
notifyUpdate(id) { it.setContentTitle(subject.title) }
|
||||||
|
|
||||||
|
return ProgressInputStream(byteStream()) {
|
||||||
|
val progress = it.toFloat() / 1048576
|
||||||
|
notifyUpdate(id) { notification ->
|
||||||
|
if (max > 0) {
|
||||||
|
broadcast(progress / total, subject)
|
||||||
|
notification
|
||||||
|
.setProgress(max.toInt(), it.toInt(), false)
|
||||||
|
.setContentText("%.2f / %.2f MB".format(progress, total))
|
||||||
|
} else {
|
||||||
|
broadcast(-1f, subject)
|
||||||
|
notification.setContentText("%.2f MB / ??".format(progress))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
|
||||||
|
val notification = notifyRemove(id)?.also(editor) ?: return -1
|
||||||
|
val newId = Notifications.nextId()
|
||||||
|
Notifications.mgr.notify(newId, notification.build())
|
||||||
|
return newId
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun notifyFail(subject: Subject) = finalNotify(subject.notifyId) {
|
||||||
|
broadcast(-2f, subject)
|
||||||
|
it.setContentText(getString(R.string.download_file_error))
|
||||||
|
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||||
|
.setOngoing(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun notifyFinish(subject: Subject) = finalNotify(subject.notifyId) {
|
||||||
|
broadcast(1f, subject)
|
||||||
|
it.setContentTitle(subject.title)
|
||||||
|
.setContentText(getString(R.string.download_complete))
|
||||||
|
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
|
.setProgress(0, 0, false)
|
||||||
|
.setOngoing(false)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
subject.pendingIntent(this)?.let { intent -> it.setContentIntent(intent) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun attachNotification(id: Int, notification: Notification) {
|
||||||
|
attachedNotificationId = id
|
||||||
|
startForeground(id, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun maybeDetachNotification(id: Int) : Boolean {
|
||||||
|
if (attachedNotificationId != id) return false
|
||||||
|
if (hasNotifications) {
|
||||||
|
val (anotherId, notification) = notifications.entries.first()
|
||||||
|
// Attaching a new notification will remove the current showing one
|
||||||
|
attachNotification(anotherId, notification.build())
|
||||||
|
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 notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit = {}) {
|
||||||
|
fun create() = Notifications.startProgress("")
|
||||||
|
|
||||||
|
val wasEmpty = !hasNotifications
|
||||||
|
val notification = notifications.getOrPut(id, ::create).also(editor).build()
|
||||||
|
if (wasEmpty)
|
||||||
|
attachNotification(id, notification)
|
||||||
|
else
|
||||||
|
Notifications.mgr.notify(id, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun notifyRemove(id: Int): Notification.Builder? {
|
||||||
|
val n = notifications.remove(id)
|
||||||
|
if (n == null || !maybeDetachNotification(id))
|
||||||
|
Notifications.mgr.cancel(id)
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
protected val progressBroadcast = MutableLiveData<Pair<Float, Subject>?>()
|
||||||
|
|
||||||
|
private fun broadcast(progress: Float, subject: Subject) {
|
||||||
|
progressBroadcast.postValue(progress to subject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,109 +1,84 @@
|
|||||||
package com.topjohnwu.magisk.core.download
|
package com.topjohnwu.magisk.core.download
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Parcelable
|
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.ktx.cachedFile
|
||||||
import com.topjohnwu.magisk.core.model.MagiskJson
|
import com.topjohnwu.magisk.core.model.MagiskJson
|
||||||
import com.topjohnwu.magisk.core.model.ManagerJson
|
|
||||||
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.ktx.get
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
private fun cachedFile(name: String) = get<Context>().cachedFile(name).apply { delete() }.toUri()
|
private fun cachedFile(name: String) = AppContext.cachedFile(name).apply { delete() }.toUri()
|
||||||
|
|
||||||
|
enum class Action {
|
||||||
|
Flash,
|
||||||
|
Download
|
||||||
|
}
|
||||||
|
|
||||||
sealed class Subject : Parcelable {
|
sealed class Subject : Parcelable {
|
||||||
|
|
||||||
abstract val url: String
|
abstract val url: String
|
||||||
abstract val file: Uri
|
abstract val file: Uri
|
||||||
abstract val action: Action
|
|
||||||
abstract val title: String
|
abstract val title: String
|
||||||
|
abstract val notifyId: Int
|
||||||
|
open val autoLaunch: Boolean get() = true
|
||||||
|
open val postDownload: (() -> Unit)? get() = null
|
||||||
|
|
||||||
|
abstract fun pendingIntent(context: Context): PendingIntent?
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class Module(
|
class Module(
|
||||||
val module: OnlineModule,
|
val module: OnlineModule,
|
||||||
override val action: Action
|
val action: Action,
|
||||||
|
override val notifyId: Int = Notifications.nextId()
|
||||||
) : Subject() {
|
) : Subject() {
|
||||||
override val url: String get() = module.zip_url
|
override val url: String get() = module.zipUrl
|
||||||
override val title: String get() = module.downloadFilename
|
override val title: String get() = module.downloadFilename
|
||||||
|
override val autoLaunch: Boolean get() = action == Action.Flash
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
override val file by lazy {
|
override val file by lazy {
|
||||||
MediaStoreUtils.getFile(title).uri
|
MediaStoreUtils.getFile(title).uri
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun pendingIntent(context: Context) =
|
||||||
|
FlashFragment.installIntent(context, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class Manager(
|
class App(
|
||||||
private val app: ManagerJson = Info.remote.app,
|
private val json: MagiskJson = Info.remote.magisk,
|
||||||
val stub: StubJson = Info.remote.stub
|
override val notifyId: Int = Notifications.nextId()
|
||||||
) : Subject() {
|
) : Subject() {
|
||||||
override val action get() = Action.Download
|
override val title: String get() = "Magisk-${json.version}(${json.versionCode})"
|
||||||
override val title: String get() = "MagiskManager-${app.version}(${app.versionCode})"
|
override val url: String get() = json.link
|
||||||
override val url: String get() = app.link
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
override val file by lazy {
|
override val file by lazy {
|
||||||
cachedFile("manager.apk")
|
cachedFile("manager.apk")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@IgnoredOnParcel
|
||||||
|
override var postDownload: (() -> Unit)? = null
|
||||||
|
|
||||||
|
@IgnoredOnParcel
|
||||||
|
var intent: Intent? = null
|
||||||
|
override fun pendingIntent(context: Context) = intent?.toPending(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class Magisk : Subject() {
|
@SuppressLint("InlinedApi")
|
||||||
|
protected fun Intent.toPending(context: Context): PendingIntent {
|
||||||
val magisk: MagiskJson = Info.remote.magisk
|
return PendingIntent.getActivity(context, notifyId, this,
|
||||||
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT)
|
||||||
@Parcelize
|
|
||||||
private class Internal(
|
|
||||||
override val action: Action
|
|
||||||
) : Magisk() {
|
|
||||||
override val url: String get() = magisk.link
|
|
||||||
override val title: String get() = "Magisk-${magisk.version}(${magisk.versionCode})"
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
override val file by lazy {
|
|
||||||
cachedFile("magisk.zip")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
private class Uninstall : Magisk() {
|
|
||||||
override val action get() = Action.Uninstall
|
|
||||||
override val url: String get() = Info.remote.uninstaller.link
|
|
||||||
override val title: String get() = "uninstall.zip"
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
override val file by lazy {
|
|
||||||
cachedFile(title)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
private class Download : Magisk() {
|
|
||||||
override val action get() = Action.Download
|
|
||||||
override val url: String get() = magisk.link
|
|
||||||
override val title: String get() = "Magisk-${magisk.version}(${magisk.versionCode}).zip"
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
override val file by lazy {
|
|
||||||
MediaStoreUtils.getFile(title).uri
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
operator fun invoke(config: Action) = when (config) {
|
|
||||||
Action.Download -> Download()
|
|
||||||
Action.Uninstall -> Uninstall()
|
|
||||||
Action.EnvFix, is Action.Flash, is Action.Patch -> Internal(config)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
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() }
|
||||||
|
}
|
||||||
76
app/src/main/java/com/topjohnwu/magisk/core/ktx/XJVM.kt
Normal file
76
app/src/main/java/com/topjohnwu/magisk/core/ktx/XJVM.kt
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package com.topjohnwu.magisk.core.ktx
|
||||||
|
|
||||||
|
import androidx.collection.SparseArrayCompat
|
||||||
|
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flatMapMerge
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.lang.reflect.Field
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
|
inline fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
|
||||||
|
var entry: ZipEntry? = nextEntry
|
||||||
|
while (entry != null) {
|
||||||
|
callback(entry)
|
||||||
|
entry = nextEntry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <In : InputStream, Out : OutputStream> withStreams(
|
||||||
|
inStream: In,
|
||||||
|
outStream: Out,
|
||||||
|
withBoth: (In, Out) -> Unit
|
||||||
|
) {
|
||||||
|
inStream.use { reader ->
|
||||||
|
outStream.use { writer ->
|
||||||
|
withBoth(reader, writer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun InputStream.copyAndClose(out: OutputStream) = withStreams(this, out) { i, o -> i.copyTo(o) }
|
||||||
|
|
||||||
|
fun InputStream.writeTo(file: File) = copyAndClose(file.outputStream())
|
||||||
|
|
||||||
|
operator fun <E> SparseArrayCompat<E>.set(key: Int, value: E) {
|
||||||
|
put(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> MutableList<T>.synchronized(): MutableList<T> = Collections.synchronizedList(this)
|
||||||
|
|
||||||
|
fun <T> MutableSet<T>.synchronized(): MutableSet<T> = Collections.synchronizedSet(this)
|
||||||
|
|
||||||
|
fun <K, V> MutableMap<K, V>.synchronized(): MutableMap<K, V> = Collections.synchronizedMap(this)
|
||||||
|
|
||||||
|
fun Class<*>.reflectField(name: String): Field =
|
||||||
|
getDeclaredField(name).apply { isAccessible = true }
|
||||||
|
|
||||||
|
inline fun <T, R> Flow<T>.concurrentMap(crossinline transform: suspend (T) -> R): Flow<R> {
|
||||||
|
return flatMapMerge { value ->
|
||||||
|
flow { emit(transform(value)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Long.toTime(format: DateFormat) = format.format(this).orEmpty()
|
||||||
|
|
||||||
|
// Some devices don't allow filenames containing ":"
|
||||||
|
val timeFormatStandard by lazy {
|
||||||
|
SimpleDateFormat(
|
||||||
|
"yyyy-MM-dd'T'HH.mm.ss",
|
||||||
|
currentLocale
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val timeDateFormat: DateFormat by lazy {
|
||||||
|
DateFormat.getDateTimeInstance(
|
||||||
|
DateFormat.DEFAULT,
|
||||||
|
DateFormat.DEFAULT,
|
||||||
|
currentLocale
|
||||||
|
)
|
||||||
|
}
|
||||||
16
app/src/main/java/com/topjohnwu/magisk/core/ktx/XSU.kt
Normal file
16
app/src/main/java/com/topjohnwu/magisk/core/ktx/XSU.kt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package com.topjohnwu.magisk.core.ktx
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.core.Config
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
fun reboot(reason: String = if (Config.recovery) "recovery" else "") {
|
||||||
|
if (reason == "recovery") {
|
||||||
|
// KEYCODE_POWER = 26, hide incorrect "Factory data reset" message
|
||||||
|
Shell.cmd("/system/bin/input keyevent 26").submit()
|
||||||
|
}
|
||||||
|
Shell.cmd("/system/bin/svc power reboot $reason || /system/bin/reboot $reason").submit()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun Shell.Job.await() = withContext(Dispatchers.IO) { exec() }
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.magiskdb
|
|
||||||
|
|
||||||
import androidx.annotation.StringDef
|
|
||||||
|
|
||||||
abstract class BaseDao {
|
|
||||||
|
|
||||||
object Table {
|
|
||||||
const val POLICY = "policies"
|
|
||||||
const val LOG = "logs"
|
|
||||||
const val SETTINGS = "settings"
|
|
||||||
const val STRINGS = "strings"
|
|
||||||
}
|
|
||||||
|
|
||||||
@StringDef(Table.POLICY, Table.LOG, Table.SETTINGS, Table.STRINGS)
|
|
||||||
@Retention(AnnotationRetention.SOURCE)
|
|
||||||
annotation class TableStrict
|
|
||||||
|
|
||||||
@TableStrict
|
|
||||||
abstract val table: String
|
|
||||||
|
|
||||||
inline fun <reified Builder : Query.Builder> buildQuery(builder: Builder.() -> Unit = {}) =
|
|
||||||
Builder::class.java.newInstance()
|
|
||||||
.apply { table = this@BaseDao.table }
|
|
||||||
.apply(builder)
|
|
||||||
.toString()
|
|
||||||
.let { Query(it) }
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.magiskdb
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import com.topjohnwu.magisk.core.Const
|
|
||||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
|
||||||
import com.topjohnwu.magisk.core.model.su.toMap
|
|
||||||
import com.topjohnwu.magisk.core.model.su.toPolicy
|
|
||||||
import com.topjohnwu.magisk.ktx.now
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
|
|
||||||
class PolicyDao(
|
|
||||||
private val context: Context
|
|
||||||
) : BaseDao() {
|
|
||||||
|
|
||||||
override val table: String = Table.POLICY
|
|
||||||
|
|
||||||
suspend fun deleteOutdated() = buildQuery<Delete> {
|
|
||||||
condition {
|
|
||||||
greaterThan("until", "0")
|
|
||||||
and {
|
|
||||||
lessThan("until", TimeUnit.MILLISECONDS.toSeconds(now).toString())
|
|
||||||
}
|
|
||||||
or {
|
|
||||||
lessThan("until", "0")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.commit()
|
|
||||||
|
|
||||||
suspend fun delete(packageName: String) = buildQuery<Delete> {
|
|
||||||
condition {
|
|
||||||
equals("package_name", packageName)
|
|
||||||
}
|
|
||||||
}.commit()
|
|
||||||
|
|
||||||
suspend fun delete(uid: Int) = buildQuery<Delete> {
|
|
||||||
condition {
|
|
||||||
equals("uid", uid)
|
|
||||||
}
|
|
||||||
}.commit()
|
|
||||||
|
|
||||||
suspend fun fetch(uid: Int) = buildQuery<Select> {
|
|
||||||
condition {
|
|
||||||
equals("uid", uid)
|
|
||||||
}
|
|
||||||
}.query().first().toPolicyOrNull()
|
|
||||||
|
|
||||||
suspend fun update(policy: SuPolicy) = buildQuery<Replace> {
|
|
||||||
values(policy.toMap())
|
|
||||||
}.commit()
|
|
||||||
|
|
||||||
suspend fun <R: Any> fetchAll(mapper: (SuPolicy) -> R) = buildQuery<Select> {
|
|
||||||
condition {
|
|
||||||
equals("uid/100000", Const.USER_ID)
|
|
||||||
}
|
|
||||||
}.query {
|
|
||||||
it.toPolicyOrNull()?.let(mapper)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Map<String, String>.toPolicyOrNull(): SuPolicy? {
|
|
||||||
return runCatching { toPolicy(context.packageManager) }.getOrElse {
|
|
||||||
Timber.e(it)
|
|
||||||
if (it is PackageManager.NameNotFoundException) {
|
|
||||||
val uid = getOrElse("uid") { null } ?: return null
|
|
||||||
GlobalScope.launch {
|
|
||||||
delete(uid.toInt())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.magiskdb
|
|
||||||
|
|
||||||
import androidx.annotation.StringDef
|
|
||||||
import com.topjohnwu.magisk.ktx.await
|
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class Query(private val _query: String) {
|
|
||||||
val query get() = "magisk --sqlite '$_query'"
|
|
||||||
|
|
||||||
interface Builder {
|
|
||||||
val requestType: String
|
|
||||||
var table: String
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend inline fun <R : Any> query(crossinline mapper: (Map<String, String>) -> R?): List<R> =
|
|
||||||
withContext(Dispatchers.Default) {
|
|
||||||
Shell.su(query).await().out.map { line ->
|
|
||||||
async {
|
|
||||||
line.split("\\|".toRegex())
|
|
||||||
.map { it.split("=", limit = 2) }
|
|
||||||
.filter { it.size == 2 }
|
|
||||||
.map { it[0] to it[1] }
|
|
||||||
.toMap()
|
|
||||||
.let(mapper)
|
|
||||||
}
|
|
||||||
}.awaitAll().filterNotNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend inline fun query() = query { it }
|
|
||||||
|
|
||||||
suspend inline fun commit() = Shell.su(query).to(null).await()
|
|
||||||
}
|
|
||||||
|
|
||||||
class Delete : Query.Builder {
|
|
||||||
override val requestType: String = "DELETE FROM"
|
|
||||||
override var table = ""
|
|
||||||
|
|
||||||
private var condition = ""
|
|
||||||
|
|
||||||
fun condition(builder: Condition.() -> Unit) {
|
|
||||||
condition = Condition().apply(builder).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return listOf(requestType, table, condition).joinToString(" ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Select : Query.Builder {
|
|
||||||
override val requestType: String get() = "SELECT $fields FROM"
|
|
||||||
override lateinit var table: String
|
|
||||||
|
|
||||||
private var fields = "*"
|
|
||||||
private var condition = ""
|
|
||||||
private var orderField = ""
|
|
||||||
|
|
||||||
fun fields(vararg newFields: String) {
|
|
||||||
if (newFields.isEmpty()) {
|
|
||||||
fields = "*"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fields = newFields.joinToString(", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun condition(builder: Condition.() -> Unit) {
|
|
||||||
condition = Condition().apply(builder).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun orderBy(field: String, @OrderStrict order: String) {
|
|
||||||
orderField = "ORDER BY $field $order"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return listOf(requestType, table, condition, orderField).joinToString(" ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Replace : Insert() {
|
|
||||||
override val requestType: String = "REPLACE INTO"
|
|
||||||
}
|
|
||||||
|
|
||||||
open class Insert : Query.Builder {
|
|
||||||
override val requestType: String = "INSERT INTO"
|
|
||||||
override lateinit var table: String
|
|
||||||
|
|
||||||
private val keys get() = _values.keys.joinToString(",")
|
|
||||||
private val values get() = _values.values.joinToString(",") {
|
|
||||||
when (it) {
|
|
||||||
is Boolean -> if (it) "1" else "0"
|
|
||||||
is Number -> it.toString()
|
|
||||||
else -> "\"$it\""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private var _values: Map<String, Any> = mapOf()
|
|
||||||
|
|
||||||
fun values(vararg pairs: Pair<String, Any>) {
|
|
||||||
_values = pairs.toMap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun values(values: Map<String, Any>) {
|
|
||||||
_values = values
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return listOf(requestType, table, "($keys) VALUES($values)").joinToString(" ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Condition {
|
|
||||||
|
|
||||||
private val conditionWord = "WHERE %s"
|
|
||||||
private var condition: String = ""
|
|
||||||
|
|
||||||
fun equals(field: String, value: Any) {
|
|
||||||
condition = when (value) {
|
|
||||||
is String -> "$field=\"$value\""
|
|
||||||
else -> "$field=$value"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun greaterThan(field: String, value: String) {
|
|
||||||
condition = "$field > $value"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun lessThan(field: String, value: String) {
|
|
||||||
condition = "$field < $value"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun greaterOrEqualTo(field: String, value: String) {
|
|
||||||
condition = "$field >= $value"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun lessOrEqualTo(field: String, value: String) {
|
|
||||||
condition = "$field <= $value"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun and(builder: Condition.() -> Unit) {
|
|
||||||
condition = "($condition AND ${Condition().apply(builder).condition})"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun or(builder: Condition.() -> Unit) {
|
|
||||||
condition = "($condition OR ${Condition().apply(builder).condition})"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return conditionWord.format(condition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object Order {
|
|
||||||
const val ASC = "ASC"
|
|
||||||
const val DESC = "DESC"
|
|
||||||
}
|
|
||||||
|
|
||||||
@StringDef(Order.ASC, Order.DESC)
|
|
||||||
@Retention(AnnotationRetention.SOURCE)
|
|
||||||
annotation class OrderStrict
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.magiskdb
|
|
||||||
|
|
||||||
class SettingsDao : BaseDao() {
|
|
||||||
|
|
||||||
override val table = Table.SETTINGS
|
|
||||||
|
|
||||||
suspend fun delete(key: String) = buildQuery<Delete> {
|
|
||||||
condition { equals("key", key) }
|
|
||||||
}.commit()
|
|
||||||
|
|
||||||
suspend fun put(key: String, value: Int) = buildQuery<Replace> {
|
|
||||||
values("key" to key, "value" to value)
|
|
||||||
}.commit()
|
|
||||||
|
|
||||||
suspend fun fetch(key: String, default: Int = -1) = buildQuery<Select> {
|
|
||||||
fields("value")
|
|
||||||
condition { equals("key", key) }
|
|
||||||
}.query {
|
|
||||||
it["value"]?.toIntOrNull()
|
|
||||||
}.firstOrNull() ?: default
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.magiskdb
|
|
||||||
|
|
||||||
class StringDao : BaseDao() {
|
|
||||||
|
|
||||||
override val table = Table.STRINGS
|
|
||||||
|
|
||||||
suspend fun delete(key: String) = buildQuery<Delete> {
|
|
||||||
condition { equals("key", key) }
|
|
||||||
}.commit()
|
|
||||||
|
|
||||||
suspend fun put(key: String, value: String) = buildQuery<Replace> {
|
|
||||||
values("key" to key, "value" to value)
|
|
||||||
}.commit()
|
|
||||||
|
|
||||||
suspend fun fetch(key: String, default: String = "") = buildQuery<Select> {
|
|
||||||
fields("value")
|
|
||||||
condition { equals("key", key) }
|
|
||||||
}.query {
|
|
||||||
it["value"]
|
|
||||||
}.firstOrNull() ?: default
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -6,56 +6,24 @@ import kotlinx.parcelize.Parcelize
|
|||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class UpdateInfo(
|
data class UpdateInfo(
|
||||||
val app: ManagerJson = ManagerJson(),
|
|
||||||
val uninstaller: UninstallerJson = UninstallerJson(),
|
|
||||||
val magisk: MagiskJson = MagiskJson(),
|
val magisk: MagiskJson = MagiskJson(),
|
||||||
val stub: StubJson = StubJson()
|
|
||||||
)
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class UninstallerJson(
|
|
||||||
val link: String = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class MagiskJson(
|
|
||||||
val version: String = "",
|
|
||||||
val versionCode: Int = -1,
|
|
||||||
val link: String = "",
|
|
||||||
val note: String = "",
|
|
||||||
val md5: String = ""
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class ManagerJson(
|
data class MagiskJson(
|
||||||
val version: String = "",
|
val version: String = "",
|
||||||
val versionCode: Int = -1,
|
val versionCode: Int = -1,
|
||||||
val link: String = "",
|
val link: String = "",
|
||||||
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 id: String,
|
val version: String,
|
||||||
val last_update: Long,
|
val versionCode: Int,
|
||||||
val prop_url: String,
|
val zipUrl: String,
|
||||||
val zip_url: String,
|
val changelog: String,
|
||||||
val notes_url: String
|
|
||||||
)
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class RepoJson(
|
|
||||||
val name: String,
|
|
||||||
val last_update: Long,
|
|
||||||
val modules: List<ModuleJson>
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
|
|||||||
@@ -1,42 +1,52 @@
|
|||||||
package com.topjohnwu.magisk.core.model.module
|
package com.topjohnwu.magisk.core.model.module
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonDataException
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.io.SuFile
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
data class LocalModule(
|
||||||
|
private val path: String,
|
||||||
|
) : Module() {
|
||||||
|
private val svc get() = ServiceLocator.networkService
|
||||||
|
|
||||||
class LocalModule(path: String) : Module() {
|
|
||||||
override var id: String = ""
|
override var id: String = ""
|
||||||
override var name: String = ""
|
override var name: String = ""
|
||||||
override var author: String = ""
|
|
||||||
override var version: String = ""
|
override var version: String = ""
|
||||||
override var versionCode: Int = -1
|
override var versionCode: Int = -1
|
||||||
override var description: String = ""
|
var author: String = ""
|
||||||
|
var description: String = ""
|
||||||
|
var updateInfo: OnlineModule? = null
|
||||||
|
var outdated = false
|
||||||
|
|
||||||
private val removeFile = SuFile(path, "remove")
|
private var updateUrl: String = ""
|
||||||
private val disableFile = SuFile(path, "disable")
|
private val removeFile = RootUtils.fs.getFile(path, "remove")
|
||||||
private val updateFile = SuFile(path, "update")
|
private val disableFile = RootUtils.fs.getFile(path, "disable")
|
||||||
private val ruleFile = SuFile(path, "sepolicy.rule")
|
private val updateFile = RootUtils.fs.getFile(path, "update")
|
||||||
|
private val riruFolder = RootUtils.fs.getFile(path, "riru")
|
||||||
|
private val zygiskFolder = RootUtils.fs.getFile(path, "zygisk")
|
||||||
|
private val unloaded = RootUtils.fs.getFile(zygiskFolder, "unloaded")
|
||||||
|
|
||||||
val updated: Boolean get() = updateFile.exists()
|
val updated: Boolean get() = updateFile.exists()
|
||||||
|
val isRiru: Boolean get() = (id == "riru-core") || riruFolder.exists()
|
||||||
|
val isZygisk: Boolean get() = zygiskFolder.exists()
|
||||||
|
val zygiskUnloaded: Boolean get() = unloaded.exists()
|
||||||
|
|
||||||
var enable: Boolean
|
var enable: Boolean
|
||||||
get() = !disableFile.exists()
|
get() = !disableFile.exists()
|
||||||
set(enable) {
|
set(enable) {
|
||||||
val dir = "$PERSIST/$id"
|
|
||||||
if (enable) {
|
if (enable) {
|
||||||
disableFile.delete()
|
disableFile.delete()
|
||||||
if (Const.Version.atLeast_21_2())
|
Shell.cmd("copy_preinit_files").submit()
|
||||||
Shell.su("copy_sepolicy_rules").submit()
|
|
||||||
else
|
|
||||||
Shell.su("mkdir -p $dir", "cp -af $ruleFile $dir").submit()
|
|
||||||
} else {
|
} else {
|
||||||
!disableFile.createNewFile()
|
!disableFile.createNewFile()
|
||||||
if (Const.Version.atLeast_21_2())
|
Shell.cmd("copy_preinit_files").submit()
|
||||||
Shell.su("copy_sepolicy_rules").submit()
|
|
||||||
else
|
|
||||||
Shell.su("rm -rf $dir").submit()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,23 +54,42 @@ class LocalModule(path: String) : Module() {
|
|||||||
get() = removeFile.exists()
|
get() = removeFile.exists()
|
||||||
set(remove) {
|
set(remove) {
|
||||||
if (remove) {
|
if (remove) {
|
||||||
|
if (updateFile.exists()) return
|
||||||
removeFile.createNewFile()
|
removeFile.createNewFile()
|
||||||
if (Const.Version.atLeast_21_2())
|
Shell.cmd("copy_preinit_files").submit()
|
||||||
Shell.su("copy_sepolicy_rules").submit()
|
|
||||||
else
|
|
||||||
Shell.su("rm -rf $PERSIST/$id").submit()
|
|
||||||
} else {
|
} else {
|
||||||
!removeFile.delete()
|
removeFile.delete()
|
||||||
if (Const.Version.atLeast_21_2())
|
Shell.cmd("copy_preinit_files").submit()
|
||||||
Shell.su("copy_sepolicy_rules").submit()
|
|
||||||
else
|
|
||||||
Shell.su("cp -af $ruleFile $PERSIST/$id").submit()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throws(NumberFormatException::class)
|
||||||
|
private fun parseProps(props: List<String>) {
|
||||||
|
for (line in props) {
|
||||||
|
val prop = line.split("=".toRegex(), 2).map { it.trim() }
|
||||||
|
if (prop.size != 2)
|
||||||
|
continue
|
||||||
|
|
||||||
|
val key = prop[0]
|
||||||
|
val value = prop[1]
|
||||||
|
if (key.isEmpty() || key[0] == '#')
|
||||||
|
continue
|
||||||
|
|
||||||
|
when (key) {
|
||||||
|
"id" -> id = value
|
||||||
|
"name" -> name = value
|
||||||
|
"version" -> version = value
|
||||||
|
"versionCode" -> versionCode = value.toInt()
|
||||||
|
"author" -> author = value
|
||||||
|
"description" -> description = value
|
||||||
|
"updateJson" -> updateUrl = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
runCatching {
|
runCatching {
|
||||||
parseProps(Shell.su("dos2unix < $path/module.prop").exec().out)
|
parseProps(Shell.cmd("dos2unix < $path/module.prop").exec().out)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id.isEmpty()) {
|
if (id.isEmpty()) {
|
||||||
@@ -73,17 +102,35 @@ class LocalModule(path: String) : Module() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun fetch(): Boolean {
|
||||||
|
if (updateUrl.isEmpty())
|
||||||
|
return false
|
||||||
|
|
||||||
|
try {
|
||||||
|
val json = svc.fetchModuleJson(updateUrl)
|
||||||
|
updateInfo = OnlineModule(this, json)
|
||||||
|
outdated = json.versionCode > versionCode
|
||||||
|
return true
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.w(e)
|
||||||
|
} catch (e: JsonDataException) {
|
||||||
|
Timber.w(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private val PERSIST get() = "${Const.MAGISKTMP}/mirror/persist/magisk"
|
fun loaded() = RootUtils.fs.getFile(Const.MAGISK_PATH).exists()
|
||||||
|
|
||||||
suspend fun installed() = withContext(Dispatchers.IO) {
|
suspend fun installed() = withContext(Dispatchers.IO) {
|
||||||
SuFile(Const.MAGISK_PATH)
|
RootUtils.fs.getFile(Const.MAGISK_PATH)
|
||||||
.listFiles { _, name -> name != "lost+found" && name != ".core" }
|
.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.toLowerCase() }
|
.sortedBy { it.name.lowercase(Locale.ROOT) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,37 +5,10 @@ abstract class Module : Comparable<Module> {
|
|||||||
protected set
|
protected set
|
||||||
abstract var name: String
|
abstract var name: String
|
||||||
protected set
|
protected set
|
||||||
abstract var author: String
|
|
||||||
protected set
|
|
||||||
abstract var version: String
|
abstract var version: String
|
||||||
protected set
|
protected set
|
||||||
abstract var versionCode: Int
|
abstract var versionCode: Int
|
||||||
protected set
|
protected set
|
||||||
abstract var description: String
|
|
||||||
protected set
|
|
||||||
|
|
||||||
@Throws(NumberFormatException::class)
|
override operator fun compareTo(other: Module) = id.compareTo(other.id)
|
||||||
protected fun parseProps(props: List<String>) {
|
|
||||||
for (line in props) {
|
|
||||||
val prop = line.split("=".toRegex(), 2).map { it.trim() }
|
|
||||||
if (prop.size != 2)
|
|
||||||
continue
|
|
||||||
|
|
||||||
val key = prop[0]
|
|
||||||
val value = prop[1]
|
|
||||||
if (key.isEmpty() || key[0] == '#')
|
|
||||||
continue
|
|
||||||
|
|
||||||
when (key) {
|
|
||||||
"id" -> id = value
|
|
||||||
"name" -> name = value
|
|
||||||
"version" -> version = value
|
|
||||||
"versionCode" -> versionCode = value.toInt()
|
|
||||||
"author" -> author = value
|
|
||||||
"description" -> description = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override operator fun compareTo(other: Module) = name.compareTo(other.name, true)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,66 +1,27 @@
|
|||||||
package com.topjohnwu.magisk.core.model.module
|
package com.topjohnwu.magisk.core.model.module
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.room.Entity
|
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
import com.topjohnwu.magisk.core.model.ModuleJson
|
import com.topjohnwu.magisk.core.model.ModuleJson
|
||||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
|
||||||
import com.topjohnwu.magisk.ktx.get
|
|
||||||
import com.topjohnwu.magisk.ktx.legalFilename
|
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.text.DateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Entity(tableName = "modules")
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class OnlineModule(
|
data class OnlineModule(
|
||||||
@PrimaryKey override var id: String,
|
override var id: String,
|
||||||
override var name: String = "",
|
override var name: String,
|
||||||
override var author: String = "",
|
override var version: String,
|
||||||
override var version: String = "",
|
override var versionCode: Int,
|
||||||
override var versionCode: Int = -1,
|
val zipUrl: String,
|
||||||
override var description: String = "",
|
val changelog: String,
|
||||||
val last_update: Long,
|
|
||||||
val prop_url: String,
|
|
||||||
val zip_url: String,
|
|
||||||
val notes_url: String
|
|
||||||
) : Module(), Parcelable {
|
) : Module(), Parcelable {
|
||||||
|
constructor(local: LocalModule, json: ModuleJson) :
|
||||||
|
this(local.id, local.name, json.version, json.versionCode, json.zipUrl, json.changelog)
|
||||||
|
|
||||||
private val svc: NetworkService get() = get()
|
|
||||||
|
|
||||||
constructor(info: ModuleJson) : this(
|
|
||||||
id = info.id,
|
|
||||||
last_update = info.last_update,
|
|
||||||
prop_url = info.prop_url,
|
|
||||||
zip_url = info.zip_url,
|
|
||||||
notes_url = info.notes_url
|
|
||||||
)
|
|
||||||
|
|
||||||
val lastUpdate get() = Date(last_update)
|
|
||||||
val lastUpdateString get() = DATE_FORMAT.format(lastUpdate)
|
|
||||||
val downloadFilename get() = "$name-$version($versionCode).zip".legalFilename()
|
val downloadFilename get() = "$name-$version($versionCode).zip".legalFilename()
|
||||||
|
|
||||||
suspend fun notes() = svc.fetchString(notes_url)
|
private fun String.legalFilename() = replace(" ", "_")
|
||||||
|
.replace("'", "").replace("\"", "")
|
||||||
@Throws(IllegalRepoException::class)
|
.replace("$", "").replace("`", "")
|
||||||
suspend fun load() {
|
.replace("*", "").replace("/", "_")
|
||||||
try {
|
.replace("#", "").replace("@", "")
|
||||||
val rawProps = svc.fetchString(prop_url)
|
.replace("\\", "_")
|
||||||
val props = rawProps.split("\\n".toRegex()).dropLastWhile { it.isEmpty() }
|
|
||||||
parseProps(props)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw IllegalRepoException("Repo [$id] parse error:", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (versionCode < 0) {
|
|
||||||
throw IllegalRepoException("Repo [$id] does not contain versionCode")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class IllegalRepoException(msg: String, cause: Throwable? = null) : Exception(msg, cause)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val DATE_FORMAT = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,73 @@
|
|||||||
package com.topjohnwu.magisk.core.model.su
|
package com.topjohnwu.magisk.core.model.su
|
||||||
|
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.Ignore
|
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.ALLOW
|
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||||
import com.topjohnwu.magisk.ktx.now
|
|
||||||
import com.topjohnwu.magisk.ktx.timeFormatTime
|
|
||||||
import com.topjohnwu.magisk.ktx.toTime
|
|
||||||
|
|
||||||
@Entity(tableName = "logs")
|
@Entity(tableName = "logs")
|
||||||
data class SuLog(
|
class SuLog(
|
||||||
val fromUid: Int,
|
val fromUid: Int,
|
||||||
val toUid: Int,
|
val toUid: Int,
|
||||||
val fromPid: Int,
|
val fromPid: Int,
|
||||||
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 time: Long = -1
|
val target: Int,
|
||||||
|
val context: String,
|
||||||
|
val gids: String,
|
||||||
|
val time: Long = System.currentTimeMillis()
|
||||||
) {
|
) {
|
||||||
@PrimaryKey(autoGenerate = true) var id: Int = 0
|
@PrimaryKey(autoGenerate = true) var id: Int = 0
|
||||||
@Ignore val timeString = time.toTime(timeFormatTime)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun SuPolicy.toLog(
|
fun PackageManager.createSuLog(
|
||||||
|
info: PackageInfo,
|
||||||
toUid: Int,
|
toUid: Int,
|
||||||
fromPid: Int,
|
fromPid: Int,
|
||||||
command: String
|
command: String,
|
||||||
) = SuLog(uid, toUid, fromPid, packageName, appName, command, policy == ALLOW, now)
|
policy: Int,
|
||||||
|
target: Int,
|
||||||
|
context: String,
|
||||||
|
gids: String,
|
||||||
|
): SuLog {
|
||||||
|
val appInfo = info.applicationInfo
|
||||||
|
return SuLog(
|
||||||
|
fromUid = appInfo.uid,
|
||||||
|
toUid = toUid,
|
||||||
|
fromPid = fromPid,
|
||||||
|
packageName = getNameForUid(appInfo.uid)!!,
|
||||||
|
appName = appInfo.getLabel(this),
|
||||||
|
command = command,
|
||||||
|
action = policy,
|
||||||
|
target = target,
|
||||||
|
context = context,
|
||||||
|
gids = gids,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createSuLog(
|
||||||
|
fromUid: Int,
|
||||||
|
toUid: Int,
|
||||||
|
fromPid: Int,
|
||||||
|
command: String,
|
||||||
|
policy: Int,
|
||||||
|
target: Int,
|
||||||
|
context: String,
|
||||||
|
gids: String,
|
||||||
|
): SuLog {
|
||||||
|
return SuLog(
|
||||||
|
fromUid = fromUid,
|
||||||
|
toUid = toUid,
|
||||||
|
fromPid = fromPid,
|
||||||
|
packageName = "[UID] $fromUid",
|
||||||
|
appName = "[UID] $fromUid",
|
||||||
|
command = command,
|
||||||
|
action = policy,
|
||||||
|
target = target,
|
||||||
|
context = context,
|
||||||
|
gids = gids,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,70 +1,22 @@
|
|||||||
package com.topjohnwu.magisk.core.model.su
|
package com.topjohnwu.magisk.core.model.su
|
||||||
|
|
||||||
import android.content.pm.PackageManager
|
class SuPolicy(val uid: Int) {
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.INTERACTIVE
|
|
||||||
import com.topjohnwu.magisk.ktx.getLabel
|
|
||||||
|
|
||||||
|
|
||||||
data class SuPolicy(
|
|
||||||
var uid: Int,
|
|
||||||
val packageName: String,
|
|
||||||
val appName: String,
|
|
||||||
val icon: Drawable,
|
|
||||||
var policy: Int = INTERACTIVE,
|
|
||||||
var until: Long = -1L,
|
|
||||||
val logging: Boolean = true,
|
|
||||||
val notification: Boolean = true
|
|
||||||
) {
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val INTERACTIVE = 0
|
const val INTERACTIVE = 0
|
||||||
const val DENY = 1
|
const val DENY = 1
|
||||||
const val ALLOW = 2
|
const val ALLOW = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
var policy: Int = INTERACTIVE
|
||||||
|
var until: Long = -1L
|
||||||
|
var logging: Boolean = true
|
||||||
|
var notification: Boolean = true
|
||||||
|
|
||||||
fun SuPolicy.toMap() = mapOf(
|
fun toMap(): MutableMap<String, Any> = mutableMapOf(
|
||||||
"uid" to uid,
|
"uid" to uid,
|
||||||
"package_name" to packageName,
|
"policy" to policy,
|
||||||
"policy" to policy,
|
"until" to until,
|
||||||
"until" to until,
|
"logging" to logging,
|
||||||
"logging" to logging,
|
"notification" to notification
|
||||||
"notification" to notification
|
|
||||||
)
|
|
||||||
|
|
||||||
@Throws(PackageManager.NameNotFoundException::class)
|
|
||||||
fun Map<String, String>.toPolicy(pm: PackageManager): SuPolicy {
|
|
||||||
val uid = get("uid")?.toIntOrNull() ?: -1
|
|
||||||
val packageName = get("package_name").orEmpty()
|
|
||||||
val info = pm.getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES)
|
|
||||||
|
|
||||||
if (info.uid != uid)
|
|
||||||
throw PackageManager.NameNotFoundException()
|
|
||||||
|
|
||||||
return SuPolicy(
|
|
||||||
uid = uid,
|
|
||||||
packageName = packageName,
|
|
||||||
appName = info.getLabel(pm),
|
|
||||||
icon = info.loadIcon(pm),
|
|
||||||
policy = get("policy")?.toIntOrNull() ?: INTERACTIVE,
|
|
||||||
until = get("until")?.toLongOrNull() ?: -1L,
|
|
||||||
logging = get("logging")?.toIntOrNull() != 0,
|
|
||||||
notification = get("notification")?.toIntOrNull() != 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(PackageManager.NameNotFoundException::class)
|
|
||||||
fun Int.toPolicy(pm: PackageManager, policy: Int = INTERACTIVE): SuPolicy {
|
|
||||||
val pkg = pm.getPackagesForUid(this)?.firstOrNull()
|
|
||||||
?: throw PackageManager.NameNotFoundException()
|
|
||||||
val info = pm.getApplicationInfo(pkg, PackageManager.GET_UNINSTALLED_PACKAGES)
|
|
||||||
return SuPolicy(
|
|
||||||
uid = info.uid,
|
|
||||||
packageName = pkg,
|
|
||||||
appName = info.getLabel(pm),
|
|
||||||
icon = info.loadIcon(pm),
|
|
||||||
policy = policy
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,47 @@
|
|||||||
package com.topjohnwu.magisk.data.repository
|
package com.topjohnwu.magisk.core.repository
|
||||||
|
|
||||||
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
|
import com.topjohnwu.magisk.core.data.magiskdb.SettingsDao
|
||||||
import com.topjohnwu.magisk.core.magiskdb.StringDao
|
import com.topjohnwu.magisk.core.data.magiskdb.StringDao
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlin.properties.ReadWriteProperty
|
import kotlin.properties.ReadWriteProperty
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
interface DBConfig {
|
interface DBConfig {
|
||||||
val settingsDao: SettingsDao
|
val settingsDB: SettingsDao
|
||||||
val stringDao: StringDao
|
val stringDB: StringDao
|
||||||
|
val coroutineScope: CoroutineScope
|
||||||
|
|
||||||
fun dbSettings(
|
fun dbSettings(
|
||||||
name: String,
|
name: String,
|
||||||
default: Int
|
default: Int
|
||||||
) = DBSettingsValue(name, default)
|
) = IntDBProperty(name, default)
|
||||||
|
|
||||||
fun dbSettings(
|
fun dbSettings(
|
||||||
name: String,
|
name: String,
|
||||||
default: Boolean
|
default: Boolean
|
||||||
) = DBBoolSettings(name, default)
|
) = BoolDBProperty(name, default)
|
||||||
|
|
||||||
fun dbStrings(
|
fun dbStrings(
|
||||||
name: String,
|
name: String,
|
||||||
default: String,
|
default: String,
|
||||||
sync: Boolean = false
|
sync: Boolean = false
|
||||||
) = DBStringsValue(name, default, sync)
|
) = StringDBProperty(name, default, sync)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class DBSettingsValue(
|
class IntDBProperty(
|
||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: Int
|
private val default: Int
|
||||||
) : ReadWriteProperty<DBConfig, Int> {
|
) : ReadWriteProperty<DBConfig, Int> {
|
||||||
|
|
||||||
private var value: Int? = null
|
var value: Int? = null
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Int {
|
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Int {
|
||||||
if (value == null)
|
if (value == null)
|
||||||
value = runBlocking {
|
value = runBlocking { thisRef.settingsDB.fetch(name, default) }
|
||||||
thisRef.settingsDao.fetch(name, default)
|
|
||||||
}
|
|
||||||
return value as Int
|
return value as Int
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,18 +49,18 @@ class DBSettingsValue(
|
|||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
this.value = value
|
this.value = value
|
||||||
}
|
}
|
||||||
GlobalScope.launch {
|
thisRef.coroutineScope.launch {
|
||||||
thisRef.settingsDao.put(name, value)
|
thisRef.settingsDB.put(name, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DBBoolSettings(
|
open class BoolDBProperty(
|
||||||
name: String,
|
name: String,
|
||||||
default: Boolean
|
default: Boolean
|
||||||
) : ReadWriteProperty<DBConfig, Boolean> {
|
) : ReadWriteProperty<DBConfig, Boolean> {
|
||||||
|
|
||||||
val base = DBSettingsValue(name, if (default) 1 else 0)
|
val base = IntDBProperty(name, if (default) 1 else 0)
|
||||||
|
|
||||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Boolean =
|
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Boolean =
|
||||||
base.getValue(thisRef, property) != 0
|
base.getValue(thisRef, property) != 0
|
||||||
@@ -70,7 +69,18 @@ class DBBoolSettings(
|
|||||||
base.setValue(thisRef, property, if (value) 1 else 0)
|
base.setValue(thisRef, property, if (value) 1 else 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
class DBStringsValue(
|
class BoolDBPropertyNoWrite(
|
||||||
|
name: String,
|
||||||
|
default: Boolean
|
||||||
|
) : BoolDBProperty(name, default) {
|
||||||
|
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Boolean) {
|
||||||
|
synchronized(base) {
|
||||||
|
base.value = if (value) 1 else 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StringDBProperty(
|
||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: String,
|
private val default: String,
|
||||||
private val sync: Boolean
|
private val sync: Boolean
|
||||||
@@ -82,7 +92,7 @@ class DBStringsValue(
|
|||||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): String {
|
override fun getValue(thisRef: DBConfig, property: KProperty<*>): String {
|
||||||
if (value == null)
|
if (value == null)
|
||||||
value = runBlocking {
|
value = runBlocking {
|
||||||
thisRef.stringDao.fetch(name, default)
|
thisRef.stringDB.fetch(name, default)
|
||||||
}
|
}
|
||||||
return value!!
|
return value!!
|
||||||
}
|
}
|
||||||
@@ -94,21 +104,21 @@ class DBStringsValue(
|
|||||||
if (value.isEmpty()) {
|
if (value.isEmpty()) {
|
||||||
if (sync) {
|
if (sync) {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
thisRef.stringDao.delete(name)
|
thisRef.stringDB.delete(name)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
GlobalScope.launch {
|
thisRef.coroutineScope.launch {
|
||||||
thisRef.stringDao.delete(name)
|
thisRef.stringDB.delete(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (sync) {
|
if (sync) {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
thisRef.stringDao.put(name, value)
|
thisRef.stringDB.put(name, value)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
GlobalScope.launch {
|
thisRef.coroutineScope.launch {
|
||||||
thisRef.stringDao.put(name, value)
|
thisRef.stringDB.put(name, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
package com.topjohnwu.magisk.data.repository
|
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.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.data.database.SuLogDao
|
|
||||||
import com.topjohnwu.magisk.ktx.await
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
|
|
||||||
|
|
||||||
@@ -27,14 +28,18 @@ class LogRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Shell.su("cat ${Const.MAGISK_LOG}").to(list).await()
|
if (Info.env.isActive) {
|
||||||
|
Shell.cmd("cat ${Const.MAGISK_LOG} || logcat -d -s Magisk").to(list).await()
|
||||||
|
} else {
|
||||||
|
Shell.cmd("logcat -d").to(list).await()
|
||||||
|
}
|
||||||
return list.buf.toString()
|
return list.buf.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun clearLogs() = logDao.deleteAll()
|
suspend fun clearLogs() = logDao.deleteAll()
|
||||||
|
|
||||||
fun clearMagiskLogs(cb: (Shell.Result) -> Unit) =
|
fun clearMagiskLogs(cb: (Shell.Result) -> Unit) =
|
||||||
Shell.su("echo -n > ${Const.MAGISK_LOG}").submit(cb)
|
Shell.cmd("echo -n > ${Const.MAGISK_LOG}").submit(cb)
|
||||||
|
|
||||||
suspend fun insert(log: SuLog) = logDao.insert(log)
|
suspend fun insert(log: SuLog) = logDao.insert(log)
|
||||||
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.topjohnwu.magisk.core.repository
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.core.Config
|
||||||
|
import com.topjohnwu.magisk.core.Config.Value.BETA_CHANNEL
|
||||||
|
import com.topjohnwu.magisk.core.Config.Value.CANARY_CHANNEL
|
||||||
|
import com.topjohnwu.magisk.core.Config.Value.CUSTOM_CHANNEL
|
||||||
|
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.STABLE_CHANNEL
|
||||||
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
import com.topjohnwu.magisk.core.data.GithubPageServices
|
||||||
|
import com.topjohnwu.magisk.core.data.RawServices
|
||||||
|
import retrofit2.HttpException
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class NetworkService(
|
||||||
|
private val pages: GithubPageServices,
|
||||||
|
private val raw: RawServices
|
||||||
|
) {
|
||||||
|
suspend fun fetchUpdate() = safe {
|
||||||
|
var info = when (Config.updateChannel) {
|
||||||
|
DEFAULT_CHANNEL, STABLE_CHANNEL -> fetchStableUpdate()
|
||||||
|
BETA_CHANNEL -> fetchBetaUpdate()
|
||||||
|
CANARY_CHANNEL -> fetchCanaryUpdate()
|
||||||
|
DEBUG_CHANNEL -> fetchDebugUpdate()
|
||||||
|
CUSTOM_CHANNEL -> fetchCustomUpdate(Config.customChannelUrl)
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
if (info.magisk.versionCode < Info.env.versionCode &&
|
||||||
|
Config.updateChannel == DEFAULT_CHANNEL) {
|
||||||
|
Config.updateChannel = BETA_CHANNEL
|
||||||
|
info = fetchBetaUpdate()
|
||||||
|
}
|
||||||
|
info
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateInfo
|
||||||
|
private suspend fun fetchStableUpdate() = pages.fetchUpdateJSON("stable.json")
|
||||||
|
private suspend fun fetchBetaUpdate() = pages.fetchUpdateJSON("beta.json")
|
||||||
|
private suspend fun fetchCanaryUpdate() = pages.fetchUpdateJSON("canary.json")
|
||||||
|
private suspend fun fetchDebugUpdate() = pages.fetchUpdateJSON("debug.json")
|
||||||
|
private suspend fun fetchCustomUpdate(url: String) = pages.fetchUpdateJSON(url)
|
||||||
|
|
||||||
|
private inline fun <T> safe(factory: () -> T): T? {
|
||||||
|
return try {
|
||||||
|
if (Info.isConnected.value == true)
|
||||||
|
factory()
|
||||||
|
else
|
||||||
|
null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <T> wrap(factory: () -> T): T {
|
||||||
|
return try {
|
||||||
|
factory()
|
||||||
|
} catch (e: HttpException) {
|
||||||
|
throw IOException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch files
|
||||||
|
suspend fun fetchFile(url: String) = wrap { raw.fetchFile(url) }
|
||||||
|
suspend fun fetchString(url: String) = wrap { raw.fetchString(url) }
|
||||||
|
suspend fun fetchModuleJson(url: String) = wrap { raw.fetchModuleJson(url) }
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
package com.topjohnwu.magisk.core.repository
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import kotlin.properties.ReadWriteProperty
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
interface PreferenceConfig {
|
||||||
|
|
||||||
|
val context: Context
|
||||||
|
|
||||||
|
val fileName: String
|
||||||
|
get() = "${context.packageName}_preferences"
|
||||||
|
|
||||||
|
val prefs: SharedPreferences
|
||||||
|
get() = context.getSharedPreferences(fileName, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
fun preferenceStrInt(
|
||||||
|
name: String,
|
||||||
|
default: Int,
|
||||||
|
commit: Boolean = false
|
||||||
|
) = object: ReadWriteProperty<PreferenceConfig, Int> {
|
||||||
|
val base = StringProperty(name, default.toString(), commit)
|
||||||
|
override fun getValue(thisRef: PreferenceConfig, property: KProperty<*>): Int =
|
||||||
|
base.getValue(thisRef, property).toInt()
|
||||||
|
|
||||||
|
override fun setValue(thisRef: PreferenceConfig, property: KProperty<*>, value: Int) =
|
||||||
|
base.setValue(thisRef, property, value.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun preference(
|
||||||
|
name: String,
|
||||||
|
default: Boolean,
|
||||||
|
commit: Boolean = false
|
||||||
|
) = BooleanProperty(name, default, commit)
|
||||||
|
|
||||||
|
fun preference(
|
||||||
|
name: String,
|
||||||
|
default: Float,
|
||||||
|
commit: Boolean = false
|
||||||
|
) = FloatProperty(name, default, commit)
|
||||||
|
|
||||||
|
fun preference(
|
||||||
|
name: String,
|
||||||
|
default: Int,
|
||||||
|
commit: Boolean = false
|
||||||
|
) = IntProperty(name, default, commit)
|
||||||
|
|
||||||
|
fun preference(
|
||||||
|
name: String,
|
||||||
|
default: Long,
|
||||||
|
commit: Boolean = false
|
||||||
|
) = LongProperty(name, default, commit)
|
||||||
|
|
||||||
|
fun preference(
|
||||||
|
name: String,
|
||||||
|
default: String,
|
||||||
|
commit: Boolean = false
|
||||||
|
) = StringProperty(name, default, commit)
|
||||||
|
|
||||||
|
fun preference(
|
||||||
|
name: String,
|
||||||
|
default: Set<String>,
|
||||||
|
commit: Boolean = false
|
||||||
|
) = StringSetProperty(name, default, commit)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class PreferenceProperty {
|
||||||
|
|
||||||
|
fun SharedPreferences.Editor.put(name: String, value: Boolean) = putBoolean(name, value)
|
||||||
|
fun SharedPreferences.Editor.put(name: String, value: Float) = putFloat(name, value)
|
||||||
|
fun SharedPreferences.Editor.put(name: String, value: Int) = putInt(name, value)
|
||||||
|
fun SharedPreferences.Editor.put(name: String, value: Long) = putLong(name, value)
|
||||||
|
fun SharedPreferences.Editor.put(name: String, value: String) = putString(name, value)
|
||||||
|
fun SharedPreferences.Editor.put(name: String, value: Set<String>) = putStringSet(name, value)
|
||||||
|
|
||||||
|
fun SharedPreferences.get(name: String, value: Boolean) = getBoolean(name, value)
|
||||||
|
fun SharedPreferences.get(name: String, value: Float) = getFloat(name, value)
|
||||||
|
fun SharedPreferences.get(name: String, value: Int) = getInt(name, value)
|
||||||
|
fun SharedPreferences.get(name: String, value: Long) = getLong(name, value)
|
||||||
|
fun SharedPreferences.get(name: String, value: String) = getString(name, value) ?: value
|
||||||
|
fun SharedPreferences.get(name: String, value: Set<String>) = getStringSet(name, value) ?: value
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class BooleanProperty(
|
||||||
|
private val name: String,
|
||||||
|
private val default: Boolean,
|
||||||
|
private val commit: Boolean
|
||||||
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Boolean> {
|
||||||
|
|
||||||
|
override operator fun getValue(
|
||||||
|
thisRef: PreferenceConfig,
|
||||||
|
property: KProperty<*>
|
||||||
|
): Boolean {
|
||||||
|
val prefName = name.ifBlank { property.name }
|
||||||
|
return thisRef.prefs.get(prefName, default)
|
||||||
|
}
|
||||||
|
|
||||||
|
override operator fun setValue(
|
||||||
|
thisRef: PreferenceConfig,
|
||||||
|
property: KProperty<*>,
|
||||||
|
value: Boolean
|
||||||
|
) {
|
||||||
|
val prefName = name.ifBlank { property.name }
|
||||||
|
thisRef.prefs.edit(commit) { put(prefName, value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FloatProperty(
|
||||||
|
private val name: String,
|
||||||
|
private val default: Float,
|
||||||
|
private val commit: Boolean
|
||||||
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Float> {
|
||||||
|
|
||||||
|
override operator fun getValue(
|
||||||
|
thisRef: PreferenceConfig,
|
||||||
|
property: KProperty<*>
|
||||||
|
): Float {
|
||||||
|
val prefName = name.ifBlank { property.name }
|
||||||
|
return thisRef.prefs.get(prefName, default)
|
||||||
|
}
|
||||||
|
|
||||||
|
override operator fun setValue(
|
||||||
|
thisRef: PreferenceConfig,
|
||||||
|
property: KProperty<*>,
|
||||||
|
value: Float
|
||||||
|
) {
|
||||||
|
val prefName = name.ifBlank { property.name }
|
||||||
|
thisRef.prefs.edit(commit) { put(prefName, value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IntProperty(
|
||||||
|
private val name: String,
|
||||||
|
private val default: Int,
|
||||||
|
private val commit: Boolean
|
||||||
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Int> {
|
||||||
|
|
||||||
|
override operator fun getValue(
|
||||||
|
thisRef: PreferenceConfig,
|
||||||
|
property: KProperty<*>
|
||||||
|
): Int {
|
||||||
|
val prefName = name.ifBlank { property.name }
|
||||||
|
return thisRef.prefs.get(prefName, default)
|
||||||
|
}
|
||||||
|
|
||||||
|
override operator fun setValue(
|
||||||
|
thisRef: PreferenceConfig,
|
||||||
|
property: KProperty<*>,
|
||||||
|
value: Int
|
||||||
|
) {
|
||||||
|
val prefName = name.ifBlank { property.name }
|
||||||
|
thisRef.prefs.edit(commit) { put(prefName, value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LongProperty(
|
||||||
|
private val name: String,
|
||||||
|
private val default: Long,
|
||||||
|
private val commit: Boolean
|
||||||
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Long> {
|
||||||
|
|
||||||
|
override operator fun getValue(
|
||||||
|
thisRef: PreferenceConfig,
|
||||||
|
property: KProperty<*>
|
||||||
|
): Long {
|
||||||
|
val prefName = name.ifBlank { property.name }
|
||||||
|
return thisRef.prefs.get(prefName, default)
|
||||||
|
}
|
||||||
|
|
||||||
|
override operator fun setValue(
|
||||||
|
thisRef: PreferenceConfig,
|
||||||
|
property: KProperty<*>,
|
||||||
|
value: Long
|
||||||
|
) {
|
||||||
|
val prefName = name.ifBlank { property.name }
|
||||||
|
thisRef.prefs.edit(commit) { put(prefName, value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StringProperty(
|
||||||
|
private val name: String,
|
||||||
|
private val default: String,
|
||||||
|
private val commit: Boolean
|
||||||
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, String> {
|
||||||
|
|
||||||
|
override operator fun getValue(
|
||||||
|
thisRef: PreferenceConfig,
|
||||||
|
property: KProperty<*>
|
||||||
|
): String {
|
||||||
|
val prefName = name.ifBlank { property.name }
|
||||||
|
return thisRef.prefs.get(prefName, default)
|
||||||
|
}
|
||||||
|
|
||||||
|
override operator fun setValue(
|
||||||
|
thisRef: PreferenceConfig,
|
||||||
|
property: KProperty<*>,
|
||||||
|
value: String
|
||||||
|
) {
|
||||||
|
val prefName = name.ifBlank { property.name }
|
||||||
|
thisRef.prefs.edit(commit) { put(prefName, value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StringSetProperty(
|
||||||
|
private val name: String,
|
||||||
|
private val default: Set<String>,
|
||||||
|
private val commit: Boolean
|
||||||
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Set<String>> {
|
||||||
|
|
||||||
|
override operator fun getValue(
|
||||||
|
thisRef: PreferenceConfig,
|
||||||
|
property: KProperty<*>
|
||||||
|
): Set<String> {
|
||||||
|
val prefName = name.ifBlank { property.name }
|
||||||
|
return thisRef.prefs.get(prefName, default)
|
||||||
|
}
|
||||||
|
|
||||||
|
override operator fun setValue(
|
||||||
|
thisRef: PreferenceConfig,
|
||||||
|
property: KProperty<*>,
|
||||||
|
value: Set<String>
|
||||||
|
) {
|
||||||
|
val prefName = name.ifBlank { property.name }
|
||||||
|
thisRef.prefs.edit(commit) { put(prefName, value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,18 @@
|
|||||||
package com.topjohnwu.magisk.core.su
|
package com.topjohnwu.magisk.core.su
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Process
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
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.intent
|
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.toLog
|
import com.topjohnwu.magisk.core.model.su.createSuLog
|
||||||
import com.topjohnwu.magisk.core.model.su.toPolicy
|
import kotlinx.coroutines.runBlocking
|
||||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
|
||||||
import com.topjohnwu.magisk.ktx.get
|
|
||||||
import com.topjohnwu.magisk.ktx.startActivity
|
|
||||||
import com.topjohnwu.magisk.ktx.startActivityWithRoot
|
|
||||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
object SuCallbackHandler {
|
object SuCallbackHandler {
|
||||||
@@ -29,9 +20,8 @@ object SuCallbackHandler {
|
|||||||
const val REQUEST = "request"
|
const val REQUEST = "request"
|
||||||
const val LOG = "log"
|
const val LOG = "log"
|
||||||
const val NOTIFY = "notify"
|
const val NOTIFY = "notify"
|
||||||
const val TEST = "test"
|
|
||||||
|
|
||||||
operator fun invoke(context: Context, action: String?, data: Bundle?) {
|
fun run(context: Context, action: String?, data: Bundle?) {
|
||||||
data ?: return
|
data ?: return
|
||||||
|
|
||||||
// Debug messages
|
// Debug messages
|
||||||
@@ -45,94 +35,68 @@ object SuCallbackHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
when (action) {
|
when (action) {
|
||||||
REQUEST -> handleRequest(context, data)
|
|
||||||
LOG -> handleLogging(context, data)
|
LOG -> handleLogging(context, data)
|
||||||
NOTIFY -> handleNotify(context, data)
|
NOTIFY -> handleNotify(context, data)
|
||||||
TEST -> {
|
|
||||||
val mode = data.getInt("mode", 2)
|
|
||||||
Shell.su(
|
|
||||||
"magisk --connect-mode $mode",
|
|
||||||
"magisk --use-broadcast"
|
|
||||||
).submit()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Any?.toInt(): Int? {
|
// https://android.googlesource.com/platform/frameworks/base/+/547bf5487d52b93c9fe183aa6d56459c170b17a4
|
||||||
return when (this) {
|
private fun Bundle.getIntComp(key: String, defaultValue: Int): Int {
|
||||||
is Number -> this.toInt()
|
val value = get(key) ?: return defaultValue
|
||||||
else -> null
|
return when (value) {
|
||||||
}
|
is Int -> value
|
||||||
}
|
is Long -> value.toInt()
|
||||||
|
else -> defaultValue
|
||||||
private fun handleRequest(context: Context, data: Bundle) {
|
|
||||||
val intent = context.intent<SuRequestActivity>()
|
|
||||||
.setAction(REQUEST)
|
|
||||||
.putExtras(data)
|
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
|
||||||
if (Build.VERSION.SDK_INT >= 29) {
|
|
||||||
// Android Q does not allow starting activity from background
|
|
||||||
intent.startActivityWithRoot()
|
|
||||||
} else {
|
|
||||||
intent.startActivity(context)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLogging(context: Context, data: Bundle) {
|
private fun handleLogging(context: Context, data: Bundle) {
|
||||||
val fromUid = data["from.uid"].toInt() ?: return
|
val fromUid = data.getIntComp("from.uid", -1)
|
||||||
if (fromUid == Process.myUid())
|
val notify = data.getBoolean("notify", true)
|
||||||
return
|
val policy = data.getIntComp("policy", SuPolicy.ALLOW)
|
||||||
|
val toUid = data.getIntComp("to.uid", -1)
|
||||||
|
val pid = data.getIntComp("pid", -1)
|
||||||
|
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 notify = data.getBoolean("notify", true)
|
val log = runCatching {
|
||||||
val allow = data["policy"].toInt() ?: return
|
pm.getPackageInfo(fromUid, pid)?.let {
|
||||||
|
pm.createSuLog(it, toUid, pid, command, policy, target, seContext, gids)
|
||||||
val policy = runCatching { fromUid.toPolicy(pm, allow) }.getOrElse { return }
|
}
|
||||||
|
}.getOrNull() ?: createSuLog(fromUid, toUid, pid, command, policy, target, seContext, gids)
|
||||||
|
|
||||||
if (notify)
|
if (notify)
|
||||||
notify(context, policy)
|
notify(context, log.action == SuPolicy.ALLOW, log.appName)
|
||||||
|
|
||||||
val toUid = data["to.uid"].toInt() ?: return
|
runBlocking { ServiceLocator.logRepo.insert(log) }
|
||||||
val pid = data["pid"].toInt() ?: return
|
|
||||||
|
|
||||||
val command = data.getString("command") ?: return
|
|
||||||
val log = policy.toLog(
|
|
||||||
toUid = toUid,
|
|
||||||
fromPid = pid,
|
|
||||||
command = command
|
|
||||||
)
|
|
||||||
|
|
||||||
val logRepo = get<LogRepository>()
|
|
||||||
GlobalScope.launch {
|
|
||||||
logRepo.insert(log)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleNotify(context: Context, data: Bundle) {
|
private fun handleNotify(context: Context, data: Bundle) {
|
||||||
val fromUid = data["from.uid"].toInt() ?: return
|
val uid = data.getIntComp("from.uid", -1)
|
||||||
if (fromUid == Process.myUid())
|
val pid = data.getIntComp("pid", -1)
|
||||||
return
|
val policy = data.getIntComp("policy", SuPolicy.ALLOW)
|
||||||
|
|
||||||
val pm = context.packageManager
|
val pm = context.packageManager
|
||||||
val allow = data["policy"].toInt() ?: return
|
|
||||||
|
|
||||||
runCatching {
|
val appName = runCatching {
|
||||||
val policy = fromUid.toPolicy(pm, allow)
|
pm.getPackageInfo(uid, pid)?.applicationInfo?.getLabel(pm)
|
||||||
if (policy.policy >= 0)
|
}.getOrNull() ?: "[UID] $uid"
|
||||||
notify(context, policy)
|
|
||||||
}
|
notify(context, policy == SuPolicy.ALLOW, appName)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notify(context: Context, policy: SuPolicy) {
|
private fun notify(context: Context, granted: Boolean, appName: String) {
|
||||||
if (policy.notification && Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
|
if (Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
|
||||||
val resId = if (policy.policy == SuPolicy.ALLOW)
|
val resId = if (granted)
|
||||||
R.string.su_allow_toast
|
R.string.su_allow_toast
|
||||||
else
|
else
|
||||||
R.string.su_deny_toast
|
R.string.su_deny_toast
|
||||||
|
|
||||||
Utils.toast(context.getString(resId, policy.appName), Toast.LENGTH_SHORT)
|
context.toast(context.getString(resId, appName), Toast.LENGTH_SHORT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,31 @@
|
|||||||
package com.topjohnwu.magisk.core.su
|
package com.topjohnwu.magisk.core.su
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.LocalSocket
|
|
||||||
import android.net.LocalSocketAddress
|
|
||||||
import androidx.collection.ArrayMap
|
|
||||||
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.Const
|
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
||||||
import com.topjohnwu.magisk.core.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.core.model.su.toPolicy
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.magisk.ktx.now
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.*
|
import java.io.DataOutputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.concurrent.TimeUnit.SECONDS
|
|
||||||
|
|
||||||
class SuRequestHandler(
|
class SuRequestHandler(
|
||||||
private val pm: PackageManager,
|
val pm: PackageManager,
|
||||||
private val policyDB: PolicyDao
|
private val policyDB: PolicyDao
|
||||||
) : Closeable {
|
) {
|
||||||
|
|
||||||
private lateinit var output: DataOutputStream
|
private lateinit var output: File
|
||||||
lateinit var policy: SuPolicy
|
private lateinit var policy: SuPolicy
|
||||||
|
lateinit var pkgInfo: PackageInfo
|
||||||
private set
|
private set
|
||||||
|
|
||||||
// Return true to indicate undetermined policy, require user interaction
|
// Return true to indicate undetermined policy, require user interaction
|
||||||
@@ -33,8 +34,10 @@ class SuRequestHandler(
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
// Never allow com.topjohnwu.magisk (could be malware)
|
// Never allow com.topjohnwu.magisk (could be malware)
|
||||||
if (policy.packageName == BuildConfig.APPLICATION_ID)
|
if (pkgInfo.packageName == BuildConfig.APPLICATION_ID) {
|
||||||
|
Shell.cmd("(pm uninstall ${BuildConfig.APPLICATION_ID} >/dev/null 2>&1)&").exec()
|
||||||
return false
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
when (Config.suAutoResponse) {
|
when (Config.suAutoResponse) {
|
||||||
Config.Value.SU_AUTO_DENY -> {
|
Config.Value.SU_AUTO_DENY -> {
|
||||||
@@ -50,90 +53,51 @@ class SuRequestHandler(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun <T> Deferred<T>.timedAwait() : T? {
|
private suspend fun init(intent: Intent): Boolean {
|
||||||
return withTimeoutOrNull(SECONDS.toMillis(1)) {
|
val uid = intent.getIntExtra("uid", -1)
|
||||||
await()
|
val pid = intent.getIntExtra("pid", -1)
|
||||||
|
val fifo = intent.getStringExtra("fifo")
|
||||||
|
if (uid <= 0 || pid <= 0 || fifo == null) {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
output = File(fifo)
|
||||||
|
policy = SuPolicy(uid)
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun close() {
|
|
||||||
if (::output.isInitialized)
|
|
||||||
output.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SuRequestError : IOException()
|
|
||||||
|
|
||||||
private suspend fun init(intent: Intent) = withContext(Dispatchers.IO) {
|
|
||||||
try {
|
try {
|
||||||
val uid: Int
|
pkgInfo = pm.getPackageInfo(uid, pid) ?: PackageInfo().apply {
|
||||||
if (Const.Version.atLeast_21_0()) {
|
val name = pm.getNameForUid(uid) ?: throw PackageManager.NameNotFoundException()
|
||||||
val name = intent.getStringExtra("fifo") ?: throw SuRequestError()
|
// We only fill in sharedUserId and leave other fields uninitialized
|
||||||
uid = intent.getIntExtra("uid", -1).also { if (it < 0) throw SuRequestError() }
|
sharedUserId = name.split(":")[0]
|
||||||
output = DataOutputStream(FileOutputStream(name).buffered())
|
|
||||||
} else {
|
|
||||||
val name = intent.getStringExtra("socket") ?: throw SuRequestError()
|
|
||||||
val socket = LocalSocket()
|
|
||||||
socket.connect(LocalSocketAddress(name, LocalSocketAddress.Namespace.ABSTRACT))
|
|
||||||
output = DataOutputStream(BufferedOutputStream(socket.outputStream))
|
|
||||||
val input = DataInputStream(BufferedInputStream(socket.inputStream))
|
|
||||||
val map = async { input.readRequest() }.timedAwait() ?: throw SuRequestError()
|
|
||||||
uid = map["uid"]?.toIntOrNull() ?: throw SuRequestError()
|
|
||||||
}
|
|
||||||
policy = uid.toPolicy(pm)
|
|
||||||
true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
when (e) {
|
|
||||||
is IOException, is PackageManager.NameNotFoundException -> {
|
|
||||||
Timber.e(e)
|
|
||||||
runCatching { close() }
|
|
||||||
false
|
|
||||||
}
|
|
||||||
else -> throw e // Unexpected error
|
|
||||||
}
|
}
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
Timber.e(e)
|
||||||
|
respond(SuPolicy.DENY, -1)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
return output.canWrite()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun respond(action: Int, time: Int) {
|
suspend fun respond(action: Int, time: Int) {
|
||||||
val until = if (time > 0)
|
val until = if (time > 0)
|
||||||
TimeUnit.MILLISECONDS.toSeconds(now) + TimeUnit.MINUTES.toSeconds(time.toLong())
|
TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) +
|
||||||
|
TimeUnit.MINUTES.toSeconds(time.toLong())
|
||||||
else
|
else
|
||||||
time.toLong()
|
time.toLong()
|
||||||
|
|
||||||
policy.policy = action
|
policy.policy = action
|
||||||
policy.until = until
|
policy.until = until
|
||||||
policy.uid = policy.uid % 100000 + Const.USER_ID * 100000
|
|
||||||
|
|
||||||
GlobalScope.launch(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 {
|
}
|
||||||
runCatching { close() }
|
if (until >= 0) {
|
||||||
if (until >= 0)
|
policyDB.update(policy)
|
||||||
policyDB.update(policy)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun DataInputStream.readRequest(): Map<String, String> {
|
|
||||||
fun readString(): String {
|
|
||||||
val len = readInt()
|
|
||||||
val buf = ByteArray(len)
|
|
||||||
readFully(buf)
|
|
||||||
return String(buf, Charsets.UTF_8)
|
|
||||||
}
|
|
||||||
val ret = ArrayMap<String, String>()
|
|
||||||
while (true) {
|
|
||||||
val name = readString()
|
|
||||||
if (name == "eof")
|
|
||||||
break
|
|
||||||
ret[name] = readString()
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
package com.topjohnwu.magisk.core.tasks
|
package com.topjohnwu.magisk.core.tasks
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.os.postDelayed
|
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.ktx.writeTo
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
||||||
import com.topjohnwu.magisk.core.utils.unzip
|
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
import com.topjohnwu.magisk.core.utils.unzip
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.KoinComponent
|
|
||||||
import org.koin.core.inject
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
@@ -23,63 +20,51 @@ open class FlashZip(
|
|||||||
private val mUri: Uri,
|
private val mUri: Uri,
|
||||||
private val console: MutableList<String>,
|
private val console: MutableList<String>,
|
||||||
private val logs: MutableList<String>
|
private val logs: MutableList<String>
|
||||||
): KoinComponent {
|
) {
|
||||||
|
|
||||||
val context: Context by inject()
|
private val installDir = File(AppContext.cacheDir, "flash")
|
||||||
private val installFolder = File(context.cacheDir, "flash").apply {
|
private lateinit var zipFile: File
|
||||||
if (!exists()) mkdirs()
|
|
||||||
}
|
|
||||||
private val tmpFile: File = File(installFolder, "install.zip")
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun unzipAndCheck(): Boolean {
|
|
||||||
val parentFile = tmpFile.parentFile ?: return false
|
|
||||||
tmpFile.unzip(parentFile, "META-INF/com/google/android", true)
|
|
||||||
|
|
||||||
val updaterScript = File(parentFile, "updater-script")
|
|
||||||
return Shell
|
|
||||||
.su("grep -q '#MAGISK' $updaterScript")
|
|
||||||
.exec()
|
|
||||||
.isSuccess
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun flash(): Boolean {
|
private fun flash(): Boolean {
|
||||||
console.add("- Copying zip to temp directory")
|
installDir.deleteRecursively()
|
||||||
|
installDir.mkdirs()
|
||||||
|
|
||||||
runCatching {
|
zipFile = if (mUri.scheme == "file") {
|
||||||
mUri.inputStream().writeTo(tmpFile)
|
mUri.toFile()
|
||||||
}.getOrElse {
|
} else {
|
||||||
when (it) {
|
File(installDir, "install.zip").also {
|
||||||
is FileNotFoundException -> console.add("! Invalid Uri")
|
console.add("- Copying zip to temp directory")
|
||||||
is IOException -> console.add("! Cannot copy to cache")
|
try {
|
||||||
|
mUri.inputStream().writeTo(it)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
when (e) {
|
||||||
|
is FileNotFoundException -> console.add("! Invalid Uri")
|
||||||
|
else -> console.add("! Cannot copy to cache")
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw it
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val isMagiskModule = runCatching {
|
val isValid = runCatching {
|
||||||
unzipAndCheck()
|
zipFile.unzip(installDir, "META-INF/com/google/android", true)
|
||||||
|
val script = File(installDir, "updater-script")
|
||||||
|
script.readText().contains("#MAGISK")
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
console.add("! Unzip error")
|
console.add("! Unzip error")
|
||||||
throw it
|
throw it
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isMagiskModule) {
|
if (!isValid) {
|
||||||
console.add("! This zip is not a Magisk Module!")
|
console.add("! This zip is not a Magisk module!")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
console.add("- Installing ${mUri.displayName}")
|
console.add("- Installing ${mUri.displayName}")
|
||||||
|
|
||||||
val parentFile = tmpFile.parent ?: return false
|
return Shell.cmd("sh $installDir/update-binary dummy 1 \'$zipFile\'")
|
||||||
|
.to(console, logs).exec().isSuccess
|
||||||
return Shell
|
|
||||||
.su(
|
|
||||||
"cd $parentFile",
|
|
||||||
"BOOTMODE=true sh update-binary dummy 1 $tmpFile"
|
|
||||||
)
|
|
||||||
.to(console, logs)
|
|
||||||
.exec().isSuccess
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open suspend fun exec() = withContext(Dispatchers.IO) {
|
open suspend fun exec() = withContext(Dispatchers.IO) {
|
||||||
@@ -94,25 +79,7 @@ open class FlashZip(
|
|||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
false
|
false
|
||||||
} finally {
|
} finally {
|
||||||
Shell.su("cd /", "rm -rf ${tmpFile.parent} ${Const.TMP_FOLDER_PATH}").submit()
|
Shell.cmd("cd /", "rm -rf $installDir ${Const.TMPDIR}").submit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Uninstall(
|
|
||||||
uri: Uri,
|
|
||||||
console: MutableList<String>,
|
|
||||||
log: MutableList<String>
|
|
||||||
) : FlashZip(uri, console, log) {
|
|
||||||
|
|
||||||
override suspend fun exec(): Boolean {
|
|
||||||
val success = super.exec()
|
|
||||||
if (success) {
|
|
||||||
UiThreadHandler.handler.postDelayed(3000) {
|
|
||||||
Shell.su("pm uninstall " + context.packageName).exec()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return success
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,45 @@
|
|||||||
package com.topjohnwu.magisk.core.tasks
|
package com.topjohnwu.magisk.core.tasks
|
||||||
|
|
||||||
import android.app.ProgressDialog
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build.VERSION.SDK_INT
|
|
||||||
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.DynAPK
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.*
|
import com.topjohnwu.magisk.StubApk
|
||||||
|
import com.topjohnwu.magisk.core.Config
|
||||||
|
import com.topjohnwu.magisk.core.Const
|
||||||
|
import com.topjohnwu.magisk.core.Provider
|
||||||
|
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.data.repository.NetworkService
|
import com.topjohnwu.magisk.signing.JarMap
|
||||||
import com.topjohnwu.magisk.ktx.inject
|
import com.topjohnwu.magisk.signing.SignApk
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
import com.topjohnwu.magisk.utils.APKInstall
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import com.topjohnwu.signing.JarMap
|
|
||||||
import com.topjohnwu.signing.SignApk
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.Runnable
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.io.OutputStream
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
|
import kotlin.random.asKotlinRandom
|
||||||
|
|
||||||
object HideAPK {
|
object HideAPK {
|
||||||
|
|
||||||
private const val ALPHA = "abcdefghijklmnopqrstuvwxyz"
|
private const val ALPHA = "abcdefghijklmnopqrstuvwxyz"
|
||||||
private const val ALPHADOTS = "$ALPHA....."
|
private const val ALPHADOTS = "$ALPHA....."
|
||||||
private const val APP_NAME = "Magisk Manager"
|
|
||||||
private const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
private const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
||||||
|
|
||||||
// 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: NetworkService by inject()
|
|
||||||
private val Context.APK_URI get() = Provider.APK_URI(packageName)
|
|
||||||
private val Context.PREFS_URI get() = Provider.PREFS_URI(packageName)
|
|
||||||
|
|
||||||
private fun genPackageName(): String {
|
private fun genPackageName(): String {
|
||||||
val random = SecureRandom()
|
val random = SecureRandom()
|
||||||
@@ -49,7 +48,7 @@ object HideAPK {
|
|||||||
var next: Char
|
var next: Char
|
||||||
var prev = 0.toChar()
|
var prev = 0.toChar()
|
||||||
for (i in 0 until len) {
|
for (i in 0 until len) {
|
||||||
next = if (prev == '.' || prev == 0.toChar() || i == len - 1) {
|
next = if (prev == '.' || i == 0 || i == len - 1) {
|
||||||
ALPHA[random.nextInt(ALPHA.length)]
|
ALPHA[random.nextInt(ALPHA.length)]
|
||||||
} else {
|
} else {
|
||||||
ALPHADOTS[random.nextInt(ALPHADOTS.length)]
|
ALPHADOTS[random.nextInt(ALPHADOTS.length)]
|
||||||
@@ -59,134 +58,213 @@ object HideAPK {
|
|||||||
}
|
}
|
||||||
if (!builder.contains('.')) {
|
if (!builder.contains('.')) {
|
||||||
// Pick a random index and set it as dot
|
// Pick a random index and set it as dot
|
||||||
val idx = random.nextInt(len - 1)
|
val idx = random.nextInt(len - 2)
|
||||||
builder[idx] = '.'
|
builder[idx + 1] = '.'
|
||||||
}
|
}
|
||||||
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: String, out: String,
|
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 origLabel = info.applicationInfo.nonLocalizedLabel.toString()
|
||||||
try {
|
try {
|
||||||
val jar = JarMap.open(apk)
|
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, APP_NAME to label.toString()))
|
if (!xml.patchStrings {
|
||||||
return false
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// Write apk changes
|
// Write apk changes
|
||||||
jar.getOutputStream(je).write(xml.bytes)
|
jar.getOutputStream(je).use { it.write(xml.bytes) }
|
||||||
val keys = Keygen(context)
|
val keys = Keygen()
|
||||||
SignApk.sign(keys.cert, keys.key, jar, FileOutputStream(out))
|
SignApk.sign(keys.cert, keys.key, jar, out)
|
||||||
|
return true
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun patchAndHide(context: Context, label: String): Boolean {
|
private fun launchApp(activity: Activity, pkg: String) {
|
||||||
val dlStub = !isRunningAsStub && SDK_INT >= 28 && Const.Version.atLeast_20_2()
|
val intent = activity.packageManager.getLaunchIntentForPackage(pkg) ?: return
|
||||||
val src = if (dlStub) {
|
val self = activity.packageName
|
||||||
val stub = File(context.cacheDir, "stub.apk")
|
val flag = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
try {
|
activity.grantUriPermission(pkg, Provider.preferencesUri(self), flag)
|
||||||
svc.fetchFile(Info.remote.stub.link).byteStream().use {
|
intent.putExtra(Const.Key.PREV_PKG, self)
|
||||||
it.writeTo(stub)
|
activity.startActivity(intent)
|
||||||
}
|
activity.finish()
|
||||||
} catch (e: IOException) {
|
}
|
||||||
Timber.e(e)
|
|
||||||
return false
|
private fun patchAndHide(activity: Activity, label: String, onFailure: Runnable): Boolean {
|
||||||
}
|
val stub = File(activity.cacheDir, "stub.apk")
|
||||||
stub.path
|
try {
|
||||||
} else {
|
activity.assets.open("stub.apk").writeTo(stub)
|
||||||
context.packageCodePath
|
} catch (e: IOException) {
|
||||||
|
Timber.e(e)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a new random package name and signature
|
// Generate a new random package name and signature
|
||||||
val repack = File(context.cacheDir, "patched.apk")
|
val repack = File(activity.cacheDir, "patched.apk")
|
||||||
val pkg = genPackageName()
|
val pkg = genPackageName()
|
||||||
Config.keyStoreRaw = ""
|
Config.keyStoreRaw = ""
|
||||||
|
|
||||||
if (!patch(context, src, repack.path, pkg, label))
|
if (!patch(activity, stub, FileOutputStream(repack), pkg, label))
|
||||||
return false
|
return false
|
||||||
|
|
||||||
// Install the application
|
// Install and auto launch app
|
||||||
if (!Shell.su("adb_pm_install $repack").exec().isSuccess)
|
val session = APKInstall.startSession(activity, pkg, onFailure) {
|
||||||
return false
|
launchApp(activity, pkg)
|
||||||
|
|
||||||
context.apply {
|
|
||||||
val intent = packageManager.getLaunchIntentForPackage(pkg) ?: return false
|
|
||||||
Config.suManager = pkg
|
|
||||||
grantUriPermission(pkg, APK_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
grantUriPermission(pkg, PREFS_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
intent.putExtra(Const.Key.PREV_PKG, packageName)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Config.suManager = pkg
|
||||||
|
val cmd = "adb_pm_install $repack $pkg"
|
||||||
|
if (Shell.cmd(cmd).exec().isSuccess) return true
|
||||||
|
|
||||||
|
try {
|
||||||
|
session.install(activity, repack)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e(e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
session.waitIntent()?.let { activity.startActivity(it) } ?: return false
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
fun hide(context: Context, label: String) {
|
suspend fun hide(activity: Activity, label: String) {
|
||||||
val dialog = ProgressDialog.show(context, context.getString(R.string.hide_manager_title), "", true)
|
val dialog = android.app.ProgressDialog(activity).apply {
|
||||||
GlobalScope.launch {
|
setTitle(activity.getString(R.string.hide_app_title))
|
||||||
val result = withContext(Dispatchers.IO) {
|
isIndeterminate = true
|
||||||
patchAndHide(context, label)
|
setCancelable(false)
|
||||||
}
|
show()
|
||||||
if (!result) {
|
|
||||||
Utils.toast(R.string.hide_manager_fail_toast, Toast.LENGTH_LONG)
|
|
||||||
dialog.dismiss()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
val onFailure = Runnable {
|
||||||
|
dialog.dismiss()
|
||||||
private suspend fun downloadAndRestore(context: Context): Boolean {
|
activity.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||||
val apk = if (isRunningAsStub) {
|
|
||||||
DynAPK.current(context)
|
|
||||||
} else {
|
|
||||||
File(context.cacheDir, "manager.apk").also { apk ->
|
|
||||||
try {
|
|
||||||
svc.fetchFile(Info.remote.app.link).byteStream().use {
|
|
||||||
it.writeTo(apk)
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Timber.e(e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
val success = withContext(Dispatchers.IO) {
|
||||||
if (!Shell.su("adb_pm_install $apk").exec().isSuccess)
|
patchAndHide(activity, label, onFailure)
|
||||||
return false
|
|
||||||
|
|
||||||
context.apply {
|
|
||||||
val intent = packageManager.getLaunchIntentForPackage(APPLICATION_ID) ?: return false
|
|
||||||
Config.suManager = ""
|
|
||||||
grantUriPermission(APPLICATION_ID, APK_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
grantUriPermission(APPLICATION_ID, PREFS_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
intent.putExtra(Const.Key.PREV_PKG, packageName)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
}
|
||||||
|
if (!success) onFailure.run()
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
fun restore(context: Context) {
|
suspend fun restore(activity: Activity) {
|
||||||
val dialog = ProgressDialog.show(context, context.getString(R.string.restore_img_msg), "", true)
|
val dialog = android.app.ProgressDialog(activity).apply {
|
||||||
GlobalScope.launch {
|
setTitle(activity.getString(R.string.restore_img_msg))
|
||||||
val result = withContext(Dispatchers.IO) {
|
isIndeterminate = true
|
||||||
downloadAndRestore(context)
|
setCancelable(false)
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
val onFailure = Runnable {
|
||||||
|
dialog.dismiss()
|
||||||
|
activity.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||||
|
}
|
||||||
|
val apk = StubApk.current(activity)
|
||||||
|
val session = APKInstall.startSession(activity, APPLICATION_ID, onFailure) {
|
||||||
|
launchApp(activity, APPLICATION_ID)
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
Config.suManager = ""
|
||||||
|
val cmd = "adb_pm_install $apk $APPLICATION_ID"
|
||||||
|
if (Shell.cmd(cmd).await().isSuccess) return
|
||||||
|
val success = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
session.install(activity, apk)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e(e)
|
||||||
|
return@withContext false
|
||||||
}
|
}
|
||||||
if (!result) {
|
session.waitIntent()?.let { activity.startActivity(it) } ?: return@withContext false
|
||||||
Utils.toast(R.string.restore_manager_fail_toast, Toast.LENGTH_LONG)
|
return@withContext true
|
||||||
dialog.dismiss()
|
}
|
||||||
|
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,34 +1,36 @@
|
|||||||
package com.topjohnwu.magisk.core.tasks
|
package com.topjohnwu.magisk.core.tasks
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.system.ErrnoException
|
||||||
|
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 androidx.localbroadcastmanager.content.LocalBroadcastManager
|
|
||||||
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.core.AppApkPath
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
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.data.repository.NetworkService
|
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||||
import com.topjohnwu.magisk.di.Protected
|
|
||||||
import com.topjohnwu.magisk.events.dialog.EnvFixDialog
|
|
||||||
import com.topjohnwu.magisk.ktx.reboot
|
|
||||||
import com.topjohnwu.magisk.ktx.withStreams
|
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import com.topjohnwu.signing.SignBoot
|
|
||||||
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
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
import com.topjohnwu.superuser.io.SuFile
|
import com.topjohnwu.superuser.nio.ExtendedFile
|
||||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
import com.topjohnwu.superuser.nio.FileSystemManager
|
||||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import net.jpountz.lz4.LZ4FrameInputStream
|
import net.jpountz.lz4.LZ4FrameInputStream
|
||||||
@@ -36,257 +38,430 @@ import org.kamranzafar.jtar.TarEntry
|
|||||||
import org.kamranzafar.jtar.TarHeader
|
import org.kamranzafar.jtar.TarHeader
|
||||||
import org.kamranzafar.jtar.TarInputStream
|
import org.kamranzafar.jtar.TarInputStream
|
||||||
import org.kamranzafar.jtar.TarOutputStream
|
import org.kamranzafar.jtar.TarOutputStream
|
||||||
import org.koin.core.KoinComponent
|
|
||||||
import org.koin.core.get
|
|
||||||
import org.koin.core.inject
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
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.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
abstract class MagiskInstallImpl : KoinComponent {
|
abstract class MagiskInstallImpl protected constructor(
|
||||||
|
protected val console: MutableList<String> = NOPList.getInstance(),
|
||||||
|
private val logs: MutableList<String> = NOPList.getInstance()
|
||||||
|
) {
|
||||||
|
|
||||||
protected lateinit var installDir: File
|
protected lateinit var installDir: ExtendedFile
|
||||||
private lateinit var srcBoot: String
|
private lateinit var srcBoot: ExtendedFile
|
||||||
private lateinit var zipUri: Uri
|
|
||||||
|
|
||||||
protected val console: MutableList<String>
|
private val shell = Shell.getShell()
|
||||||
private val logs: MutableList<String>
|
private val service get() = ServiceLocator.networkService
|
||||||
private var tarOut: TarOutputStream? = null
|
protected val context get() = ServiceLocator.deContext
|
||||||
|
private val useRootDir = shell.isRoot && Info.noDataExec
|
||||||
|
|
||||||
private val service: NetworkService by inject()
|
private val rootFS get() = RootUtils.fs
|
||||||
protected val context: Context by inject()
|
private val localFS get() = FileSystemManager.getLocal()
|
||||||
|
|
||||||
protected constructor() {
|
|
||||||
console = NOPList.getInstance()
|
|
||||||
logs = NOPList.getInstance()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected constructor(zip: Uri, out: MutableList<String>, err: MutableList<String>) {
|
|
||||||
console = out
|
|
||||||
logs = err
|
|
||||||
zipUri = zip
|
|
||||||
installDir = File(get<Context>(Protected).filesDir.parent, "install")
|
|
||||||
"rm -rf $installDir".sh()
|
|
||||||
installDir.mkdirs()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findImage(): Boolean {
|
private fun findImage(): Boolean {
|
||||||
srcBoot = "find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
|
val bootPath = "RECOVERYMODE=${Config.recovery} find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
|
||||||
if (srcBoot.isEmpty()) {
|
if (bootPath.isEmpty()) {
|
||||||
console.add("! Unable to detect target image")
|
console.add("! Unable to detect target image")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
console.add("- Target image: $srcBoot")
|
srcBoot = rootFS.getFile(bootPath)
|
||||||
|
console.add("- Target image: $bootPath")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findSecondaryImage(): Boolean {
|
private fun findSecondary(): Boolean {
|
||||||
val slot = "echo \$SLOT".fsh()
|
val slot = "echo \$SLOT".fsh()
|
||||||
val target = if (slot == "_a") "_b" else "_a"
|
val target = if (slot == "_a") "_b" else "_a"
|
||||||
console.add("- Target slot: $target")
|
console.add("- Target slot: $target")
|
||||||
srcBoot = arrayOf(
|
val bootPath = arrayOf(
|
||||||
"SLOT=$target",
|
"SLOT=$target",
|
||||||
"find_boot_image",
|
"find_boot_image",
|
||||||
"SLOT=$slot",
|
"SLOT=$slot",
|
||||||
"echo \"\$BOOTIMAGE\"").fsh()
|
"echo \"\$BOOTIMAGE\"").fsh()
|
||||||
if (srcBoot.isEmpty()) {
|
if (bootPath.isEmpty()) {
|
||||||
console.add("! Unable to detect target image")
|
console.add("! Unable to detect target image")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
console.add("- Target image: $srcBoot")
|
srcBoot = rootFS.getFile(bootPath)
|
||||||
|
console.add("- Target image: $bootPath")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
private fun extractFiles(): Boolean {
|
||||||
private fun extractZip(): Boolean {
|
console.add("- Device platform: ${Const.CPU_ABI}")
|
||||||
val arch = if (Build.VERSION.SDK_INT >= 21) {
|
console.add("- Installing: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
||||||
val abis = listOf(*Build.SUPPORTED_ABIS)
|
|
||||||
if (abis.contains("x86")) "x86" else "arm"
|
|
||||||
} else {
|
|
||||||
if (Build.CPU_ABI == "x86") "x86" else "arm"
|
|
||||||
}
|
|
||||||
|
|
||||||
console.add("- Device platform: " + Build.CPU_ABI)
|
installDir = localFS.getFile(context.filesDir.parent, "install")
|
||||||
console.add("- Magisk Manager: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
installDir.deleteRecursively()
|
||||||
console.add("- Install target: ${Info.remote.magisk.version} (${Info.remote.magisk.versionCode})")
|
installDir.mkdirs()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ZipInputStream(zipUri.inputStream().buffered()).use { zi ->
|
// Extract binaries
|
||||||
lateinit var ze: ZipEntry
|
if (isRunningAsStub) {
|
||||||
while (zi.nextEntry?.let { ze = it } != null) {
|
val zf = ZipFile(StubApk.current(context))
|
||||||
if (ze.isDirectory)
|
|
||||||
continue
|
// Also extract magisk32 on non 64-bit only 64-bit devices
|
||||||
var name: String? = null
|
val is32lib = Const.CPU_ABI_32?.let {
|
||||||
val names = arrayOf("$arch/", "common/", "META-INF/com/google/android/update-binary")
|
{ entry: ZipEntry -> entry.name == "lib/$it/libmagisk32.so" }
|
||||||
for (n in names) {
|
} ?: { false }
|
||||||
ze.name.run {
|
|
||||||
if (startsWith(n)) {
|
zf.entries().asSequence().filter {
|
||||||
name = substring(lastIndexOf('/') + 1)
|
!it.isDirectory && (it.name.startsWith("lib/${Const.CPU_ABI}/") || is32lib(it))
|
||||||
}
|
}.forEach {
|
||||||
}
|
val n = it.name.substring(it.name.lastIndexOf('/') + 1)
|
||||||
name ?: continue
|
val name = n.substring(3, n.length - 3)
|
||||||
break
|
val dest = File(installDir, name)
|
||||||
}
|
zf.getInputStream(it).writeTo(dest)
|
||||||
if (name == null && ze.name.startsWith("chromeos/"))
|
dest.setExecutable(true)
|
||||||
name = ze.name
|
}
|
||||||
name?.also {
|
zf.close()
|
||||||
val dest = if (installDir is SuFile)
|
} else {
|
||||||
SuFile(installDir, it)
|
val info = context.applicationInfo
|
||||||
else
|
var libs = File(info.nativeLibraryDir).listFiles { _, name ->
|
||||||
File(installDir, it)
|
name.startsWith("lib") && name.endsWith(".so")
|
||||||
dest.parentFile!!.mkdirs()
|
} ?: emptyArray()
|
||||||
SuFileOutputStream(dest).use { s -> zi.copyTo(s) }
|
|
||||||
} ?: continue
|
// Also symlink magisk32 on non 64-bit only 64-bit devices
|
||||||
|
val lib32 = info.javaClass.getDeclaredField("secondaryNativeLibraryDir")
|
||||||
|
.get(info) as String?
|
||||||
|
if (lib32 != null) {
|
||||||
|
libs += File(lib32, "libmagisk32.so")
|
||||||
|
}
|
||||||
|
|
||||||
|
for (lib in libs) {
|
||||||
|
val name = lib.name.substring(3, lib.name.length - 3)
|
||||||
|
Os.symlink(lib.path, "$installDir/$name")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
|
||||||
console.add("! Cannot unzip zip")
|
// Extract scripts
|
||||||
|
for (script in listOf("util_functions.sh", "boot_patch.sh", "addon.d.sh", "stub.apk")) {
|
||||||
|
val dest = File(installDir, script)
|
||||||
|
context.assets.open(script).writeTo(dest)
|
||||||
|
}
|
||||||
|
// Extract chromeos tools
|
||||||
|
File(installDir, "chromeos").mkdir()
|
||||||
|
for (file in listOf("futility", "kernel_data_key.vbprivk", "kernel.keyblock")) {
|
||||||
|
val name = "chromeos/$file"
|
||||||
|
val dest = File(installDir, name)
|
||||||
|
context.assets.open(name).writeTo(dest)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
console.add("! Unable to extract files")
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val init64 = SuFile.open(installDir, "magiskinit64")
|
if (useRootDir) {
|
||||||
if (Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_64_BIT_ABIS.isNotEmpty()) {
|
// Move everything to tmpfs to workaround Samsung bullshit
|
||||||
init64.renameTo(SuFile.open(installDir, "magiskinit"))
|
rootFS.getFile(Const.TMPDIR).also {
|
||||||
} else {
|
arrayOf(
|
||||||
init64.delete()
|
"rm -rf $it",
|
||||||
|
"mkdir -p $it",
|
||||||
|
"cp_readlink $installDir $it",
|
||||||
|
"rm -rf $installDir"
|
||||||
|
).sh()
|
||||||
|
installDir = it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"cd $installDir; chmod 755 *".sh()
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun newEntry(name: String, size: Long): TarEntry {
|
private fun InputStream.copyAndCloseOut(out: OutputStream) = out.use { copyTo(it) }
|
||||||
|
|
||||||
|
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 LZ4InputStream(s: InputStream) : LZ4FrameInputStream(s) {
|
||||||
|
// Workaround bug in LZ4FrameInputStream
|
||||||
|
override fun available() = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private class NoBootException : IOException()
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun handleTar(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() =
|
fun decompressedStream(): InputStream {
|
||||||
if (entry.name.contains(".lz4")) LZ4FrameInputStream(tarIn) else tarIn
|
return if (entry.name.endsWith(".lz4")) LZ4InputStream(tarIn) else tarIn
|
||||||
|
}
|
||||||
|
|
||||||
while (tarIn.nextEntry?.let { entry = it } != null) {
|
while (tarIn.nextEntry?.let { entry = it } != null) {
|
||||||
if (entry.name.contains("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 = File(installDir, name)
|
val extract = installDir.getChildFile(name)
|
||||||
FileOutputStream(extract).use { decompressedStream().copyTo(it) }
|
decompressedStream().copyAndCloseOut(extract.newOutputStream())
|
||||||
} else if (entry.name.contains("vbmeta.img")) {
|
} else if (entry.name.contains("vbmeta.img")) {
|
||||||
val rawData = ByteArrayOutputStream().let {
|
val rawData = decompressedStream().readBytes()
|
||||||
decompressedStream().copyTo(it)
|
// Valid vbmeta.img should be at least 256 bytes
|
||||||
it.toByteArray()
|
if (rawData.size < 256)
|
||||||
}
|
continue
|
||||||
// Valid vbmeta.img should be at least 256 bytes
|
|
||||||
if (rawData.size < 256)
|
|
||||||
continue
|
|
||||||
|
|
||||||
// Patch flags to AVB_VBMETA_IMAGE_FLAGS_VERIFICATION_DISABLED
|
// Patch flags to AVB_VBMETA_IMAGE_FLAGS_HASHTREE_DISABLED |
|
||||||
console.add("-- Patching: vbmeta.img")
|
// AVB_VBMETA_IMAGE_FLAGS_VERIFICATION_DISABLED
|
||||||
ByteBuffer.wrap(rawData).putInt(120, 2)
|
console.add("-- Patching: vbmeta.img")
|
||||||
tarOut.putNextEntry(newEntry("vbmeta.img", rawData.size.toLong()))
|
ByteBuffer.wrap(rawData).putInt(120, 3)
|
||||||
tarOut.write(rawData)
|
tarOut.putNextEntry(newTarEntry("vbmeta.img", rawData.size.toLong()))
|
||||||
} else {
|
tarOut.write(rawData)
|
||||||
console.add("-- Copying: ${entry.name}")
|
// vbmeta partition exist, disable boot vbmeta patch
|
||||||
tarOut.putNextEntry(entry)
|
Info.patchBootVbmeta = false
|
||||||
tarIn.copyTo(tarOut, bufferSize = 1024 * 1024)
|
} else if (entry.name.contains("userdata.img")) {
|
||||||
}
|
continue
|
||||||
}
|
|
||||||
val boot = SuFile.open(installDir, "boot.img")
|
|
||||||
val recovery = SuFile.open(installDir, "recovery.img")
|
|
||||||
if (recovery.exists() && boot.exists()) {
|
|
||||||
// Install Magisk to recovery
|
|
||||||
srcBoot = recovery.path
|
|
||||||
// Repack boot image to prevent restore
|
|
||||||
arrayOf(
|
|
||||||
"./magiskboot unpack boot.img",
|
|
||||||
"./magiskboot repack boot.img",
|
|
||||||
"./magiskboot cleanup",
|
|
||||||
"mv new-boot.img boot.img").sh()
|
|
||||||
SuFileInputStream(boot).use {
|
|
||||||
tarOut.putNextEntry(newEntry("boot.img", boot.length()))
|
|
||||||
it.copyTo(tarOut)
|
|
||||||
}
|
|
||||||
boot.delete()
|
|
||||||
} else {
|
} else {
|
||||||
if (!boot.exists()) {
|
console.add("-- Copying: ${entry.name}")
|
||||||
console.add("! No boot image found")
|
tarOut.putNextEntry(entry)
|
||||||
throw IOException()
|
tarIn.copyTo(tarOut, bufferSize = 1024 * 1024)
|
||||||
}
|
|
||||||
srcBoot = boot.path
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tarOut
|
|
||||||
|
val boot = installDir.getChildFile("boot.img")
|
||||||
|
val initBoot = installDir.getChildFile("init_boot.img")
|
||||||
|
val recovery = installDir.getChildFile("recovery.img")
|
||||||
|
|
||||||
|
fun ExtendedFile.copyToTar() {
|
||||||
|
newInputStream().use {
|
||||||
|
tarOut.putNextEntry(newTarEntry(name, length()))
|
||||||
|
it.copyTo(tarOut)
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
throw NoBootException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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()
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
src.reset()
|
src.reset()
|
||||||
|
|
||||||
val alpha = "abcdefghijklmnopqrstuvwxyz"
|
val alpha = "abcdefghijklmnopqrstuvwxyz"
|
||||||
val alphaNum = "$alpha${alpha.toUpperCase(Locale.ROOT)}0123456789"
|
val alphaNum = "$alpha${alpha.uppercase(Locale.ROOT)}0123456789"
|
||||||
val random = SecureRandom()
|
val random = SecureRandom()
|
||||||
val suffix = StringBuilder()
|
val filename = StringBuilder("magisk_patched-${BuildConfig.VERSION_CODE}_").run {
|
||||||
for (i in 1..5) {
|
for (i in 1..5) {
|
||||||
suffix.append(alphaNum[random.nextInt(alphaNum.length)])
|
append(alphaNum[random.nextInt(alphaNum.length)])
|
||||||
|
}
|
||||||
|
toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
val filename = "magisk_patched_$suffix"
|
srcBoot = if (tarMagic.contentEquals("ustar".toByteArray())) {
|
||||||
outStream = if (magic.contentEquals("ustar".toByteArray())) {
|
// tar file
|
||||||
outFile = MediaStoreUtils.getFile("$filename.tar", true)
|
outFile = MediaStoreUtils.getFile("$filename.tar", true)
|
||||||
handleTar(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 = File(installDir, "boot.img").path
|
|
||||||
console.add("- Copying image to cache")
|
|
||||||
FileOutputStream(srcBoot).use { src.copyTo(it) }
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output file
|
// Output file
|
||||||
try {
|
try {
|
||||||
val patched = SuFile.open(installDir, "new-boot.img")
|
val newBoot = installDir.getChildFile("new-boot.img")
|
||||||
if (outStream is TarOutputStream) {
|
if (outStream is TarOutputStream) {
|
||||||
val name = if (srcBoot.contains("recovery")) "recovery.img" else "boot.img"
|
val name = with(srcBoot.path) {
|
||||||
outStream.putNextEntry(newEntry(name, patched.length()))
|
when {
|
||||||
|
contains("recovery") -> "recovery.img"
|
||||||
|
contains("init_boot") -> "init_boot.img"
|
||||||
|
else -> "boot.img"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outStream.putNextEntry(newTarEntry(name, newBoot.length()))
|
||||||
}
|
}
|
||||||
withStreams(SuFileInputStream(patched), outStream) { src, out -> src.copyTo(out) }
|
newBoot.newInputStream().copyAndClose(outStream)
|
||||||
patched.delete()
|
newBoot.delete()
|
||||||
|
|
||||||
console.add("")
|
console.add("")
|
||||||
console.add("****************************")
|
console.add("****************************")
|
||||||
@@ -295,188 +470,168 @@ abstract class MagiskInstallImpl : KoinComponent {
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fix up binaries
|
||||||
|
srcBoot.delete()
|
||||||
|
"cp_readlink $installDir".sh()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun patchBoot(): Boolean {
|
private fun patchBoot(): Boolean {
|
||||||
var srcNand = ""
|
val newBoot = installDir.getChildFile("new-boot.img")
|
||||||
if ("[ -c $srcBoot ] && nanddump -f boot.img $srcBoot".sh().isSuccess) {
|
if (!useRootDir) {
|
||||||
srcNand = srcBoot
|
// Create output files before hand
|
||||||
srcBoot = File(installDir, "boot.img").path
|
newBoot.createNewFile()
|
||||||
|
File(installDir, "stock_boot.img").createNewFile()
|
||||||
}
|
}
|
||||||
|
|
||||||
var isSigned: Boolean
|
val cmds = arrayOf(
|
||||||
|
"cd $installDir",
|
||||||
|
"KEEPFORCEENCRYPT=${Config.keepEnc} " +
|
||||||
|
"KEEPVERITY=${Config.keepVerity} " +
|
||||||
|
"PATCHVBMETAFLAG=${Info.patchBootVbmeta} " +
|
||||||
|
"RECOVERYMODE=${Config.recovery} " +
|
||||||
|
"LEGACYSAR=${Info.legacySAR} " +
|
||||||
|
"sh boot_patch.sh $srcBoot")
|
||||||
|
val isSuccess = cmds.sh().isSuccess
|
||||||
|
|
||||||
|
shell.newJob().add("./magiskboot cleanup", "cd /").exec()
|
||||||
|
|
||||||
|
return isSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun flashBoot() = "direct_install $installDir $srcBoot".sh().isSuccess
|
||||||
|
|
||||||
|
private fun postOTA(): Boolean {
|
||||||
try {
|
try {
|
||||||
SuFileInputStream(srcBoot).use {
|
val bootctl = File.createTempFile("bootctl", null, context.cacheDir)
|
||||||
isSigned = SignBoot.verifySignature(it, null)
|
context.assets.open("bootctl").writeTo(bootctl)
|
||||||
if (isSigned) {
|
"post_ota $bootctl".sh()
|
||||||
console.add("- Boot image is signed with AVB 1.0")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
console.add("! Unable to check signature")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!("KEEPFORCEENCRYPT=${Config.keepEnc} KEEPVERITY=${Config.keepVerity} " +
|
|
||||||
"RECOVERYMODE=${Config.recovery} sh update-binary " +
|
|
||||||
"sh boot_patch.sh $srcBoot").sh().isSuccess) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (srcNand.isNotEmpty()) {
|
|
||||||
srcBoot = srcNand
|
|
||||||
}
|
|
||||||
|
|
||||||
val job = Shell.sh(
|
|
||||||
"./magiskboot cleanup",
|
|
||||||
"mv bin/busybox busybox",
|
|
||||||
"rm -rf magisk.apk bin boot.img update-binary",
|
|
||||||
"cd /")
|
|
||||||
|
|
||||||
val patched = File(installDir, "new-boot.img")
|
|
||||||
if (isSigned) {
|
|
||||||
console.add("- Signing boot image with verity keys")
|
|
||||||
val signed = File(installDir, "signed.img")
|
|
||||||
try {
|
|
||||||
withStreams(SuFileInputStream(patched), signed.outputStream().buffered()) {
|
|
||||||
input, out -> SignBoot.doSignature(null, null, input, out, "/boot")
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
console.add("! Unable to sign image")
|
|
||||||
Timber.e(e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
job.add("mv -f $signed $patched")
|
|
||||||
}
|
|
||||||
job.exec()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copySepolicyRules(): Boolean {
|
|
||||||
if (Info.remote.magisk.versionCode >= 21100) return true
|
|
||||||
// Copy existing rules for migration
|
|
||||||
"copy_sepolicy_rules".sh()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun flashBoot(): Boolean {
|
|
||||||
if (!"direct_install $installDir $srcBoot".sh().isSuccess)
|
|
||||||
return false
|
|
||||||
"run_migrations".sh()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun postOTA(): Boolean {
|
|
||||||
val bootctl = SuFile("/data/adb/bootctl")
|
|
||||||
try {
|
|
||||||
withStreams(service.fetchBootctl().byteStream(), SuFileOutputStream(bootctl)) {
|
|
||||||
it, out -> it.copyTo(out)
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
console.add("! Unable to download bootctl")
|
console.add("! Unable to download bootctl")
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
"post_ota ${bootctl.parent}".sh()
|
|
||||||
|
|
||||||
console.add("***************************************")
|
console.add("***************************************")
|
||||||
console.add(" Next reboot will boot to second slot!")
|
console.add(" Next reboot will boot to second slot!")
|
||||||
console.add("***************************************")
|
console.add("***************************************")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.sh() = Shell.sh(this).to(console, logs).exec()
|
private fun Array<String>.eq() = shell.newJob().add(*this).to(console, logs).enqueue()
|
||||||
private fun Array<String>.sh() = Shell.sh(*this).to(console, logs).exec()
|
private fun String.sh() = shell.newJob().add(this).to(console, logs).exec()
|
||||||
private fun String.fsh() = ShellUtils.fastCmd(this)
|
private fun Array<String>.sh() = shell.newJob().add(*this).to(console, logs).exec()
|
||||||
private fun Array<String>.fsh() = ShellUtils.fastCmd(*this)
|
private fun String.fsh() = ShellUtils.fastCmd(shell, this)
|
||||||
|
private fun Array<String>.fsh() = ShellUtils.fastCmd(shell, *this)
|
||||||
|
|
||||||
protected fun doPatchFile(patchFile: Uri) = extractZip() && handleFile(patchFile)
|
protected fun patchFile(file: Uri) = extractFiles() && handleFile(file)
|
||||||
|
|
||||||
protected fun direct() = findImage() && extractZip() && patchBoot() &&
|
protected fun direct() = findImage() && extractFiles() && patchBoot() && flashBoot()
|
||||||
copySepolicyRules() && flashBoot()
|
|
||||||
|
|
||||||
protected suspend fun secondSlot() = findSecondaryImage() && extractZip() &&
|
protected fun secondSlot() =
|
||||||
patchBoot() && copySepolicyRules() && flashBoot() && postOTA()
|
findSecondary() && extractFiles() && patchBoot() && flashBoot() && postOTA()
|
||||||
|
|
||||||
protected fun fixEnv(zip: Uri): Boolean {
|
protected fun fixEnv() = extractFiles() && "fix_env $installDir".sh().isSuccess
|
||||||
installDir = SuFile("/data/adb/magisk")
|
|
||||||
Shell.su("rm -rf /data/adb/magisk/*").exec()
|
protected fun uninstall() = "run_uninstaller $AppApkPath".sh().isSuccess
|
||||||
zipUri = zip
|
|
||||||
return extractZip() && Shell.su("fix_env").exec().isSuccess
|
|
||||||
}
|
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
protected abstract suspend fun operations(): Boolean
|
protected abstract suspend fun operations(): Boolean
|
||||||
|
|
||||||
open suspend fun exec() = withContext(Dispatchers.IO) { operations() }
|
open suspend fun exec(): Boolean {
|
||||||
|
if (haveActiveSession.getAndSet(true))
|
||||||
|
return false
|
||||||
|
val result = withContext(Dispatchers.IO) { operations() }
|
||||||
|
haveActiveSession.set(false)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private var haveActiveSession = AtomicBoolean(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class MagiskInstaller(
|
abstract class MagiskInstaller(
|
||||||
file: Uri,
|
|
||||||
console: MutableList<String>,
|
console: MutableList<String>,
|
||||||
logs: MutableList<String>
|
logs: MutableList<String>
|
||||||
) : MagiskInstallImpl(file, console, logs) {
|
) : MagiskInstallImpl(console, logs) {
|
||||||
|
|
||||||
override suspend fun exec(): Boolean {
|
override suspend fun exec(): Boolean {
|
||||||
val success = super.exec()
|
val success = super.exec()
|
||||||
if (success) {
|
if (success) {
|
||||||
console.add("- All done!")
|
console.add("- All done!")
|
||||||
} else {
|
} else {
|
||||||
Shell.sh("rm -rf $installDir").submit()
|
Shell.cmd("rm -rf $installDir").submit()
|
||||||
console.add("! Installation failed")
|
console.add("! Installation failed")
|
||||||
}
|
}
|
||||||
return success
|
return success
|
||||||
}
|
}
|
||||||
|
|
||||||
class Patch(
|
class Patch(
|
||||||
file: Uri,
|
|
||||||
private val uri: Uri,
|
private val uri: Uri,
|
||||||
console: MutableList<String>,
|
console: MutableList<String>,
|
||||||
logs: MutableList<String>
|
logs: MutableList<String>
|
||||||
) : MagiskInstaller(file, console, logs) {
|
) : MagiskInstaller(console, logs) {
|
||||||
override suspend fun operations() = doPatchFile(uri)
|
override suspend fun operations() = patchFile(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
class SecondSlot(
|
class SecondSlot(
|
||||||
file: Uri,
|
|
||||||
console: MutableList<String>,
|
console: MutableList<String>,
|
||||||
logs: MutableList<String>
|
logs: MutableList<String>
|
||||||
) : MagiskInstaller(file, console, logs) {
|
) : MagiskInstaller(console, logs) {
|
||||||
override suspend fun operations() = secondSlot()
|
override suspend fun operations() = secondSlot()
|
||||||
}
|
}
|
||||||
|
|
||||||
class Direct(
|
class Direct(
|
||||||
file: Uri,
|
|
||||||
console: MutableList<String>,
|
console: MutableList<String>,
|
||||||
logs: MutableList<String>
|
logs: MutableList<String>
|
||||||
) : MagiskInstaller(file, console, logs) {
|
) : MagiskInstaller(console, logs) {
|
||||||
override suspend fun operations() = direct()
|
override suspend fun operations() = direct()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
class Emulator(
|
||||||
|
console: MutableList<String>,
|
||||||
|
logs: MutableList<String>
|
||||||
|
) : MagiskInstaller(console, logs) {
|
||||||
|
override suspend fun operations() = fixEnv()
|
||||||
|
}
|
||||||
|
|
||||||
class EnvFixTask(
|
class Uninstall(
|
||||||
private val zip: Uri
|
console: MutableList<String>,
|
||||||
) : MagiskInstallImpl() {
|
logs: MutableList<String>
|
||||||
override suspend fun operations() = fixEnv(zip)
|
) : MagiskInstallImpl(console, logs) {
|
||||||
|
override suspend fun operations() = uninstall()
|
||||||
|
|
||||||
override suspend fun exec(): Boolean {
|
override suspend fun exec(): Boolean {
|
||||||
val success = super.exec()
|
val success = super.exec()
|
||||||
LocalBroadcastManager.getInstance(context).sendBroadcast(Intent(EnvFixDialog.DISMISS))
|
if (success) {
|
||||||
Utils.toast(
|
UiThreadHandler.handler.postDelayed(3000) {
|
||||||
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
|
Shell.cmd("pm uninstall ${context.packageName}").exec()
|
||||||
Toast.LENGTH_LONG
|
}
|
||||||
)
|
}
|
||||||
if (success)
|
return success
|
||||||
UiThreadHandler.handler.postDelayed(5000) { reboot() }
|
}
|
||||||
return success
|
}
|
||||||
|
|
||||||
|
class FixEnv(private val callback: () -> Unit) : MagiskInstallImpl() {
|
||||||
|
override suspend fun operations() = fixEnv()
|
||||||
|
|
||||||
|
override suspend fun exec(): Boolean {
|
||||||
|
val success = super.exec()
|
||||||
|
callback()
|
||||||
|
context.toast(
|
||||||
|
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
)
|
||||||
|
if (success)
|
||||||
|
UiThreadHandler.handler.postDelayed(5000) { reboot() }
|
||||||
|
return success
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.tasks
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
|
||||||
import com.topjohnwu.magisk.data.database.RepoDao
|
|
||||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
|
||||||
import com.topjohnwu.magisk.ktx.synchronized
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class RepoUpdater(
|
|
||||||
private val svc: NetworkService,
|
|
||||||
private val repoDB: RepoDao
|
|
||||||
) {
|
|
||||||
|
|
||||||
suspend fun run(forced: Boolean) = withContext(Dispatchers.IO) {
|
|
||||||
val cachedMap = HashMap<String, Date>().also { map ->
|
|
||||||
repoDB.getModuleStubs().forEach { map[it.id] = Date(it.last_update) }
|
|
||||||
}.synchronized()
|
|
||||||
svc.fetchRepoInfo()?.let { info ->
|
|
||||||
coroutineScope {
|
|
||||||
info.modules.forEach {
|
|
||||||
launch {
|
|
||||||
val lastUpdated = cachedMap.remove(it.id)
|
|
||||||
if (forced || lastUpdated?.before(Date(it.last_update)) != false) {
|
|
||||||
try {
|
|
||||||
val repo = OnlineModule(it).apply { load() }
|
|
||||||
repoDB.addModule(repo)
|
|
||||||
} catch (e: OnlineModule.IllegalRepoException) {
|
|
||||||
Timber.e(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
repoDB.removeModules(cachedMap.keys)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@ import java.io.ByteArrayOutputStream
|
|||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder.LITTLE_ENDIAN
|
import java.nio.ByteOrder.LITTLE_ENDIAN
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class AXML(b: ByteArray) {
|
class AXML(b: ByteArray) {
|
||||||
|
|
||||||
@@ -30,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 {
|
||||||
@@ -43,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
|
||||||
@@ -58,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
|
||||||
@@ -104,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,31 +1,24 @@
|
|||||||
package com.topjohnwu.magisk.core.utils
|
package com.topjohnwu.magisk.core.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
|
import androidx.biometric.BiometricManager.Authenticators
|
||||||
import androidx.biometric.BiometricPrompt
|
import androidx.biometric.BiometricPrompt
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import org.koin.core.KoinComponent
|
|
||||||
import org.koin.core.get
|
|
||||||
|
|
||||||
object BiometricHelper: KoinComponent {
|
class BiometricHelper(context: Context) {
|
||||||
|
|
||||||
private val mgr by lazy { BiometricManager.from(get()) }
|
private val mgr = BiometricManager.from(context)
|
||||||
|
|
||||||
val isSupported get() = when (mgr.canAuthenticate()) {
|
val isSupported get() = when (mgr.canAuthenticate(Authenticators.BIOMETRIC_WEAK)) {
|
||||||
BiometricManager.BIOMETRIC_SUCCESS -> true
|
BiometricManager.BIOMETRIC_SUCCESS -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
val isEnabled: Boolean get() {
|
val isEnabled get() = isSupported && Config.suBiometric
|
||||||
val enabled = Config.suBiometric
|
|
||||||
if (enabled && !isSupported) {
|
|
||||||
Config.suBiometric = false
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
fun authenticate(
|
fun authenticate(
|
||||||
activity: FragmentActivity,
|
activity: FragmentActivity,
|
||||||
@@ -49,7 +42,7 @@ object BiometricHelper: KoinComponent {
|
|||||||
)
|
)
|
||||||
val info = BiometricPrompt.PromptInfo.Builder()
|
val info = BiometricPrompt.PromptInfo.Builder()
|
||||||
.setConfirmationRequired(true)
|
.setConfirmationRequired(true)
|
||||||
.setDeviceCredentialAllowed(false)
|
.setAllowedAuthenticators(Authenticators.BIOMETRIC_WEAK)
|
||||||
.setTitle(activity.getString(R.string.authenticate))
|
.setTitle(activity.getString(R.string.authenticate))
|
||||||
.setNegativeButtonText(activity.getString(android.R.string.cancel))
|
.setNegativeButtonText(activity.getString(android.R.string.cancel))
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.topjohnwu.magisk.core.utils
|
||||||
|
|
||||||
|
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.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class DispatcherExecutor(dispatcher: CoroutineDispatcher) : AbstractExecutorService() {
|
||||||
|
|
||||||
|
private val job = SupervisorJob()
|
||||||
|
private val scope = CoroutineScope(job + dispatcher)
|
||||||
|
private val latch = CountDownLatch(1)
|
||||||
|
|
||||||
|
init {
|
||||||
|
job.invokeOnCompletion { latch.countDown() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun execute(command: Runnable) {
|
||||||
|
scope.launch {
|
||||||
|
command.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shutdown() = job.cancel()
|
||||||
|
|
||||||
|
override fun shutdownNow(): List<Runnable> {
|
||||||
|
job.cancel()
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isShutdown() = job.isCancelled
|
||||||
|
|
||||||
|
override fun isTerminated() = job.isCancelled && job.isCompleted
|
||||||
|
|
||||||
|
override fun awaitTermination(timeout: Long, unit: TimeUnit) = latch.await(timeout, unit)
|
||||||
|
}
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.utils
|
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import java.util.concurrent.*
|
|
||||||
|
|
||||||
class IODispatcherExecutor : AbstractExecutorService() {
|
|
||||||
|
|
||||||
private val job = SupervisorJob().apply { invokeOnCompletion { future.run() } }
|
|
||||||
private val scope = CoroutineScope(job + Dispatchers.IO)
|
|
||||||
private val future = FutureTask(Callable { true })
|
|
||||||
|
|
||||||
override fun execute(command: Runnable) {
|
|
||||||
scope.launch {
|
|
||||||
command.run()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun shutdown() = job.cancel()
|
|
||||||
|
|
||||||
override fun shutdownNow(): List<Runnable> {
|
|
||||||
job.cancel()
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isShutdown() = job.isCancelled
|
|
||||||
|
|
||||||
override fun isTerminated() = job.isCancelled && job.isCompleted
|
|
||||||
|
|
||||||
override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean {
|
|
||||||
return try {
|
|
||||||
future.get(timeout, unit)
|
|
||||||
} catch (e: TimeoutException) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +1,22 @@
|
|||||||
package com.topjohnwu.magisk.core.utils
|
package com.topjohnwu.magisk.core.utils
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Base64OutputStream
|
import android.util.Base64OutputStream
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.signing.CryptoUtils.readCertificate
|
|
||||||
import com.topjohnwu.signing.CryptoUtils.readPrivateKey
|
|
||||||
import com.topjohnwu.signing.KeyData
|
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
import org.bouncycastle.asn1.x500.X500Name
|
||||||
|
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||||
|
import org.bouncycastle.cert.X509v3CertificateBuilder
|
||||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
|
|
||||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.security.KeyPairGenerator
|
import java.security.KeyPairGenerator
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
import java.security.MessageDigest
|
|
||||||
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
|
||||||
|
|
||||||
@@ -29,63 +25,21 @@ private interface CertKeyProvider {
|
|||||||
val key: PrivateKey
|
val key: PrivateKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
class Keygen : CertKeyProvider {
|
||||||
class Keygen(context: Context) : CertKeyProvider {
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ALIAS = "magisk"
|
private const val ALIAS = "magisk"
|
||||||
private val PASSWORD get() = "magisk".toCharArray()
|
private val PASSWORD get() = "magisk".toCharArray()
|
||||||
private const val TESTKEY_CERT = "61ed377e85d386a8dfee6b864bd85b0bfaa5af81"
|
|
||||||
private const val DNAME = "C=US,ST=California,L=Mountain View,O=Google Inc.,OU=Android,CN=Android"
|
private const val DNAME = "C=US,ST=California,L=Mountain View,O=Google Inc.,OU=Android,CN=Android"
|
||||||
private const val BASE64_FLAG = Base64.NO_PADDING or Base64.NO_WRAP
|
private const val BASE64_FLAG = Base64.NO_PADDING or Base64.NO_WRAP
|
||||||
}
|
}
|
||||||
|
|
||||||
private val start = Calendar.getInstance().apply { add(Calendar.MONTH, -3) }
|
private val start = Calendar.getInstance().apply { add(Calendar.MONTH, -3) }
|
||||||
private val end = start.apply { add(Calendar.YEAR, 30) }
|
private val end = (start.clone() as Calendar).apply { add(Calendar.YEAR, 30) }
|
||||||
|
|
||||||
override val cert get() = provider.cert
|
private val ks = init()
|
||||||
override val key get() = provider.key
|
override val cert = ks.getCertificate(ALIAS) as X509Certificate
|
||||||
|
override val key = ks.getKey(ALIAS, PASSWORD) as PrivateKey
|
||||||
private val provider: CertKeyProvider
|
|
||||||
|
|
||||||
inner class KeyStoreProvider :
|
|
||||||
CertKeyProvider {
|
|
||||||
private val ks by lazy { init() }
|
|
||||||
override val cert by lazy { ks.getCertificate(ALIAS) as X509Certificate }
|
|
||||||
override val key by lazy { ks.getKey(
|
|
||||||
ALIAS,
|
|
||||||
PASSWORD
|
|
||||||
) as PrivateKey }
|
|
||||||
}
|
|
||||||
|
|
||||||
class TestProvider : CertKeyProvider {
|
|
||||||
override val cert by lazy {
|
|
||||||
readCertificate(ByteArrayInputStream(KeyData.testCert()))
|
|
||||||
}
|
|
||||||
override val key by lazy {
|
|
||||||
readPrivateKey(ByteArrayInputStream(KeyData.testKey()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
val pm = context.packageManager
|
|
||||||
val info = pm.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES)
|
|
||||||
val sig = info.signatures[0]
|
|
||||||
val digest = MessageDigest.getInstance("SHA1")
|
|
||||||
val chksum = digest.digest(sig.toByteArray())
|
|
||||||
|
|
||||||
val sb = StringBuilder()
|
|
||||||
for (b in chksum) {
|
|
||||||
sb.append("%02x".format(0xFF and b.toInt()))
|
|
||||||
}
|
|
||||||
|
|
||||||
provider = if (sb.toString() == TESTKEY_CERT) {
|
|
||||||
// The app was signed by the test key, continue to use it (legacy mode)
|
|
||||||
TestProvider()
|
|
||||||
} else {
|
|
||||||
KeyStoreProvider()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun init(): KeyStore {
|
private fun init(): KeyStore {
|
||||||
val raw = Config.keyStoreRaw
|
val raw = Config.keyStoreRaw
|
||||||
@@ -93,12 +47,8 @@ class Keygen(context: Context) : CertKeyProvider {
|
|||||||
if (raw.isEmpty()) {
|
if (raw.isEmpty()) {
|
||||||
ks.load(null)
|
ks.load(null)
|
||||||
} else {
|
} else {
|
||||||
GZIPInputStream(Base64.decode(raw,
|
GZIPInputStream(Base64.decode(raw, BASE64_FLAG).inputStream()).use {
|
||||||
BASE64_FLAG
|
ks.load(it, PASSWORD)
|
||||||
).inputStream()).use {
|
|
||||||
ks.load(it,
|
|
||||||
PASSWORD
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,22 +59,19 @@ class Keygen(context: Context) : CertKeyProvider {
|
|||||||
// Generate new private key and certificate
|
// Generate new private key and certificate
|
||||||
val kp = KeyPairGenerator.getInstance("RSA").apply { initialize(4096) }.genKeyPair()
|
val kp = KeyPairGenerator.getInstance("RSA").apply { initialize(4096) }.genKeyPair()
|
||||||
val dname = X500Name(DNAME)
|
val dname = X500Name(DNAME)
|
||||||
val builder = JcaX509v3CertificateBuilder(dname, BigInteger(160, Random()),
|
val builder = X509v3CertificateBuilder(
|
||||||
start.time, end.time, dname, kp.public)
|
dname, BigInteger(160, Random()),
|
||||||
|
start.time, end.time, Locale.ROOT, dname,
|
||||||
|
SubjectPublicKeyInfo.getInstance(kp.public.encoded)
|
||||||
|
)
|
||||||
val signer = JcaContentSignerBuilder("SHA1WithRSA").build(kp.private)
|
val signer = JcaContentSignerBuilder("SHA1WithRSA").build(kp.private)
|
||||||
val cert = JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
val cert = JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
||||||
|
|
||||||
// Store them into keystore
|
// Store them into keystore
|
||||||
ks.setKeyEntry(
|
ks.setKeyEntry(ALIAS, kp.private, PASSWORD, arrayOf(cert))
|
||||||
ALIAS, kp.private,
|
|
||||||
PASSWORD, arrayOf(cert))
|
|
||||||
val bytes = ByteArrayOutputStream()
|
val bytes = ByteArrayOutputStream()
|
||||||
GZIPOutputStream(Base64OutputStream(bytes,
|
GZIPOutputStream(Base64OutputStream(bytes, BASE64_FLAG)).use {
|
||||||
BASE64_FLAG
|
ks.store(it, PASSWORD)
|
||||||
)).use {
|
|
||||||
ks.store(it,
|
|
||||||
PASSWORD
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Config.keyStoreRaw = bytes.toString("UTF-8")
|
Config.keyStoreRaw = bytes.toString("UTF-8")
|
||||||
|
|
||||||
|
|||||||
@@ -3,21 +3,16 @@
|
|||||||
package com.topjohnwu.magisk.core.utils
|
package com.topjohnwu.magisk.core.utils
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.res.AssetManager
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.util.DisplayMetrics
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.core.ActivityTracker
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.ResMgr
|
import com.topjohnwu.magisk.core.createNewResources
|
||||||
import com.topjohnwu.magisk.core.addAssetPath
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
import com.topjohnwu.magisk.ktx.langTagToLocale
|
|
||||||
import com.topjohnwu.magisk.ktx.toLangTag
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.Comparator
|
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
var currentLocale: Locale = Locale.getDefault()
|
var currentLocale: Locale = Locale.getDefault()
|
||||||
|
|
||||||
@@ -30,11 +25,13 @@ suspend fun availableLocales() = cachedLocales ?:
|
|||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
val compareId = R.string.app_changelog
|
val compareId = R.string.app_changelog
|
||||||
|
|
||||||
// Create a completely new resource to prevent cross talk over app's configs
|
// Create a completely new resource to prevent cross talk over active configs
|
||||||
val asset = AssetManager::class.java.newInstance().apply { addAssetPath(ResMgr.apk) }
|
val res = createNewResources()
|
||||||
val config = Configuration(ResMgr.resource.configuration)
|
|
||||||
val metrics = DisplayMetrics().apply { setTo(ResMgr.resource.displayMetrics) }
|
fun changeLocale(locale: Locale) {
|
||||||
val res = Resources(asset, metrics, config)
|
res.configuration.setLocale(locale)
|
||||||
|
res.updateConfiguration(res.configuration, res.displayMetrics)
|
||||||
|
}
|
||||||
|
|
||||||
val locales = ArrayList<String>().apply {
|
val locales = ArrayList<String>().apply {
|
||||||
// Add default locale
|
// Add default locale
|
||||||
@@ -45,19 +42,17 @@ withContext(Dispatchers.Default) {
|
|||||||
add("pt-BR")
|
add("pt-BR")
|
||||||
|
|
||||||
// Then add all supported locales
|
// Then add all supported locales
|
||||||
addAll(res.assets.locales)
|
addAll(Resources.getSystem().assets.locales)
|
||||||
}.map {
|
}.map {
|
||||||
it.langTagToLocale()
|
Locale.forLanguageTag(it)
|
||||||
}.distinctBy {
|
}.distinctBy {
|
||||||
config.setLocale(it)
|
changeLocale(it)
|
||||||
res.updateConfiguration(config, metrics)
|
|
||||||
res.getString(compareId)
|
res.getString(compareId)
|
||||||
}.sortedWith(Comparator { a, b ->
|
}.sortedWith { a, b ->
|
||||||
a.getDisplayName(a).compareTo(b.getDisplayName(b), true)
|
a.getDisplayName(a).compareTo(b.getDisplayName(b), true)
|
||||||
})
|
}
|
||||||
|
|
||||||
config.setLocale(defaultLocale)
|
changeLocale(defaultLocale)
|
||||||
res.updateConfiguration(config, metrics)
|
|
||||||
val defName = res.getString(R.string.system_default)
|
val defName = res.getString(R.string.system_default)
|
||||||
|
|
||||||
val names = ArrayList<String>(locales.size + 1)
|
val names = ArrayList<String>(locales.size + 1)
|
||||||
@@ -68,23 +63,26 @@ withContext(Dispatchers.Default) {
|
|||||||
|
|
||||||
locales.forEach { locale ->
|
locales.forEach { locale ->
|
||||||
names.add(locale.getDisplayName(locale))
|
names.add(locale.getDisplayName(locale))
|
||||||
values.add(locale.toLangTag())
|
values.add(locale.toLanguageTag())
|
||||||
}
|
}
|
||||||
|
|
||||||
(names.toTypedArray() to values.toTypedArray()).also { cachedLocales = it }
|
(names.toTypedArray() to values.toTypedArray()).also { cachedLocales = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Resources.updateConfig(config: Configuration = configuration) {
|
fun Resources.setConfig(config: Configuration) {
|
||||||
config.setLocale(currentLocale)
|
config.setLocale(currentLocale)
|
||||||
updateConfiguration(config, displayMetrics)
|
updateConfiguration(config, displayMetrics)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Resources.syncLocale() = setConfig(configuration)
|
||||||
|
|
||||||
fun refreshLocale() {
|
fun refreshLocale() {
|
||||||
val localeConfig = Config.locale
|
val localeConfig = Config.locale
|
||||||
currentLocale = when {
|
currentLocale = when {
|
||||||
localeConfig.isEmpty() -> defaultLocale
|
localeConfig.isEmpty() -> defaultLocale
|
||||||
else -> localeConfig.langTagToLocale()
|
else -> Locale.forLanguageTag(localeConfig)
|
||||||
}
|
}
|
||||||
Locale.setDefault(currentLocale)
|
Locale.setDefault(currentLocale)
|
||||||
ResMgr.resource.updateConfig()
|
AppContext.resources.syncLocale()
|
||||||
|
ActivityTracker.foreground?.recreate()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package com.topjohnwu.magisk.core.utils
|
package com.topjohnwu.magisk.core.utils
|
||||||
|
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
@@ -13,7 +11,7 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.ktx.get
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -24,7 +22,7 @@ import kotlin.experimental.and
|
|||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
object MediaStoreUtils {
|
object MediaStoreUtils {
|
||||||
|
|
||||||
private val cr: ContentResolver by lazy { get<Context>().contentResolver }
|
private val cr get() = AppContext.contentResolver
|
||||||
|
|
||||||
@get:RequiresApi(api = 29)
|
@get:RequiresApi(api = 29)
|
||||||
private val tableUri
|
private val tableUri
|
||||||
@@ -89,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()
|
||||||
@@ -104,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,79 @@
|
|||||||
|
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.ktx.registerRuntimeReceiver
|
||||||
|
|
||||||
|
typealias ConnectionCallback = (Boolean) -> Unit
|
||||||
|
|
||||||
|
class NetworkObserver(
|
||||||
|
context: Context,
|
||||||
|
private val callback: ConnectionCallback
|
||||||
|
): 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)
|
||||||
|
callback(true)
|
||||||
|
}
|
||||||
|
override fun onLost(network: Network) {
|
||||||
|
activeList.remove(network)
|
||||||
|
callback(!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()) {
|
||||||
|
callback(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() {
|
||||||
|
callback(manager.getNetworkCapabilities(manager.activeNetwork)
|
||||||
|
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) ?: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun observe(context: Context, callback: ConnectionCallback): NetworkObserver {
|
||||||
|
return NetworkObserver(context, callback).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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user