mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-08-14 07:07:27 +00:00
Compare commits
820 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 |
17
.github/ISSUE_TEMPLATE/bug_report.md
vendored
17
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,19 +1,18 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
title: ""
|
||||
labels: ""
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
## 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, upload the install logs. Please also upload the `boot.img` or `recovery.img` that you are using for patching.
|
||||
|
||||
@@ -31,7 +30,7 @@ Without following the rules above, your issue will be closed without explanation
|
||||
|
||||
-->
|
||||
|
||||
Device:
|
||||
Device:
|
||||
Android version:
|
||||
Magisk version name:
|
||||
Magisk version code:
|
||||
Magisk version name:
|
||||
Magisk version code:
|
||||
|
19
.github/ccache.sh
vendored
19
.github/ccache.sh
vendored
@@ -1,19 +0,0 @@
|
||||
OS=$(uname)
|
||||
CCACHE_VER=4.4
|
||||
|
||||
case $OS in
|
||||
Darwin )
|
||||
brew install ccache
|
||||
ln -s $(which ccache) ./ccache
|
||||
;;
|
||||
Linux )
|
||||
sudo apt-get install -y ccache
|
||||
ln -s $(which ccache) ./ccache
|
||||
;;
|
||||
* )
|
||||
curl -OL https://github.com/ccache/ccache/releases/download/v${CCACHE_VER}/ccache-${CCACHE_VER}-windows-64.zip
|
||||
unzip -j ccache-*-windows-64.zip '*/ccache.exe'
|
||||
;;
|
||||
esac
|
||||
mkdir ./.ccache
|
||||
./ccache -o compiler_check='%compiler% -dumpmachine; %compiler% -dumpversion'
|
101
.github/workflows/build.yml
vendored
101
.github/workflows/build.yml
vendored
@@ -2,17 +2,17 @@ name: Magisk Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'app/**'
|
||||
- 'native/**'
|
||||
- 'stub/**'
|
||||
- 'buildSrc/**'
|
||||
- 'build.py'
|
||||
- 'gradle.properties'
|
||||
- '.github/workflows/build.yml'
|
||||
- "app/**"
|
||||
- "native/**"
|
||||
- "stub/**"
|
||||
- "buildSrc/**"
|
||||
- "build.py"
|
||||
- "gradle.properties"
|
||||
- ".github/workflows/build.yml"
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -22,33 +22,35 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
env:
|
||||
NDK_CCACHE: ${{ github.workspace }}/ccache
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
submodules: "recursive"
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: "temurin"
|
||||
java-version: "17"
|
||||
|
||||
- name: Set up Python 3
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
python-version: "3.x"
|
||||
|
||||
- name: Set up ccache
|
||||
run: bash .github/ccache.sh
|
||||
- name: Set up sccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
with:
|
||||
variant: sccache
|
||||
key: ${{ runner.os }}-${{ github.sha }}
|
||||
restore-keys: ${{ runner.os }}
|
||||
max-size: 10000M
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -58,10 +60,9 @@ jobs:
|
||||
restore-keys: ${{ runner.os }}-gradle-
|
||||
|
||||
- name: Cache build cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/.ccache
|
||||
~/.gradle/caches/build-cache-*
|
||||
key: ${{ runner.os }}-build-cache-${{ github.sha }}
|
||||
restore-keys: ${{ runner.os }}-build-cache-
|
||||
@@ -71,13 +72,11 @@ jobs:
|
||||
|
||||
- name: Build release
|
||||
run: |
|
||||
./ccache -zp
|
||||
python build.py -vr all
|
||||
|
||||
- name: Build debug
|
||||
run: |
|
||||
python build.py -v all
|
||||
./ccache -s
|
||||
|
||||
- name: Stop gradle daemon
|
||||
run: ./gradlew --stop
|
||||
@@ -85,7 +84,51 @@ jobs:
|
||||
# Only upload artifacts built on Linux
|
||||
- name: Upload build artifact
|
||||
if: runner.os == 'Linux'
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ github.sha }}
|
||||
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 }}
|
||||
|
43
.gitmodules
vendored
43
.gitmodules
vendored
@@ -1,45 +1,48 @@
|
||||
[submodule "selinux"]
|
||||
path = native/jni/external/selinux
|
||||
path = native/src/external/selinux
|
||||
url = https://github.com/topjohnwu/selinux.git
|
||||
[submodule "busybox"]
|
||||
path = native/jni/external/busybox
|
||||
path = native/src/external/busybox
|
||||
url = https://github.com/topjohnwu/ndk-busybox.git
|
||||
[submodule "dtc"]
|
||||
path = native/jni/external/dtc
|
||||
path = native/src/external/dtc
|
||||
url = https://github.com/dgibson/dtc.git
|
||||
[submodule "lz4"]
|
||||
path = native/jni/external/lz4
|
||||
path = native/src/external/lz4
|
||||
url = https://github.com/lz4/lz4.git
|
||||
[submodule "bzip2"]
|
||||
path = native/jni/external/bzip2
|
||||
path = native/src/external/bzip2
|
||||
url = https://github.com/nemequ/bzip2.git
|
||||
[submodule "xz"]
|
||||
path = native/jni/external/xz
|
||||
path = native/src/external/xz
|
||||
url = https://github.com/xz-mirror/xz.git
|
||||
[submodule "nanopb"]
|
||||
path = native/jni/external/nanopb
|
||||
path = native/src/external/nanopb
|
||||
url = https://github.com/nanopb/nanopb.git
|
||||
[submodule "mincrypt"]
|
||||
path = native/jni/external/mincrypt
|
||||
url = https://github.com/topjohnwu/mincrypt.git
|
||||
[submodule "pcre"]
|
||||
path = native/jni/external/pcre
|
||||
path = native/src/external/pcre
|
||||
url = https://android.googlesource.com/platform/external/pcre
|
||||
[submodule "xhook"]
|
||||
path = native/jni/external/xhook
|
||||
url = https://github.com/iqiyi/xHook.git
|
||||
[submodule "libcxx"]
|
||||
path = native/jni/external/libcxx
|
||||
path = native/src/external/libcxx
|
||||
url = https://github.com/topjohnwu/libcxx.git
|
||||
[submodule "zlib"]
|
||||
path = native/jni/external/zlib
|
||||
path = native/src/external/zlib
|
||||
url = https://android.googlesource.com/platform/external/zlib
|
||||
[submodule "parallel-hashmap"]
|
||||
path = native/jni/external/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"]
|
||||
path = tools/termux-elf-cleaner
|
||||
url = https://github.com/termux/termux-elf-cleaner.git
|
||||
[submodule "zopfli"]
|
||||
path = native/jni/external/zopfli
|
||||
url = https://github.com/google/zopfli.git
|
||||
|
31
README.MD
31
README.MD
@@ -6,7 +6,7 @@
|
||||
|
||||
## Introduction
|
||||
|
||||
Magisk is a suite of open source software for customizing Android, supporting devices higher than Android 5.0.<br>
|
||||
Magisk is a suite of open source software for customizing Android, supporting devices higher than Android 6.0.<br>
|
||||
Some highlight features:
|
||||
|
||||
- **MagiskSU**: Provide root access for applications
|
||||
@@ -18,42 +18,25 @@ Some highlight features:
|
||||
|
||||
[Github](https://github.com/topjohnwu/Magisk/) is the only source where you can get official Magisk information and downloads.
|
||||
|
||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v24.1)
|
||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v24.2)
|
||||
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-debug.apk)
|
||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v26.1)
|
||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v26.1)
|
||||
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-release.apk)
|
||||
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-debug.apk)
|
||||
|
||||
## Useful Links
|
||||
|
||||
- [Installation Instruction](https://topjohnwu.github.io/Magisk/install.html)
|
||||
- [Building and Development](https://topjohnwu.github.io/Magisk/build.html)
|
||||
- [Magisk Documentation](https://topjohnwu.github.io/Magisk/)
|
||||
- [Magisk Troubleshoot Wiki](https://www.didgeridoohan.com/magisk/HomePage) (by [@Didgeridoohan](https://github.com/Didgeridoohan))
|
||||
|
||||
## Bug Reports
|
||||
|
||||
**Only bug reports from Canary builds will be accepted.**
|
||||
**Only bug reports from Debug builds will be accepted.**
|
||||
|
||||
For installation issues, upload both boot image and install logs.<br>
|
||||
For Magisk issues, upload boot logcat or dmesg.<br>
|
||||
For Magisk app crashes, record and upload the logcat when the crash occurs.
|
||||
|
||||
## Building and Development
|
||||
|
||||
- Magisk builds on any OS Android Studio supports. Install Android Studio and do the initial setups.
|
||||
- Clone sources: `git clone --recurse-submodules https://github.com/topjohnwu/Magisk.git`
|
||||
- Install Python 3.6+ \
|
||||
(Windows only: select **'Add Python to PATH'** in installer, and run `pip install colorama` after install)
|
||||
- Configure to use the JDK bundled in Android Studio:
|
||||
- macOS: `export JAVA_HOME="/Applications/Android Studio.app/Contents/jre/Contents/Home"`
|
||||
- Linux: `export PATH="/path/to/androidstudio/jre/bin:$PATH"`
|
||||
- Windows: Add `C:\Path\To\Android Studio\jre\bin` to environment variable `PATH`
|
||||
- Set environment variable `ANDROID_SDK_ROOT` to the Android SDK folder (can be found in Android Studio settings)
|
||||
- Run `./build.py ndk` to let the script download and install NDK for you
|
||||
- To start building, run `build.py` to see your options. \
|
||||
For each action, use `-h` to access help (e.g. `./build.py all -h`)
|
||||
- To start development, open the project with Android Studio. The IDE can be used for both app (Kotlin/Java) and native (C++/C) sources.
|
||||
- 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
|
||||
|
||||
Default string resources for the Magisk app and its stub APK are located here:
|
||||
|
7
app/.gitignore
vendored
7
app/.gitignore
vendored
@@ -3,10 +3,9 @@
|
||||
/local.properties
|
||||
.idea/
|
||||
/build
|
||||
app/release
|
||||
*.hprof
|
||||
.externalNativeBuild/
|
||||
*.apk
|
||||
src/main/assets
|
||||
src/main/jniLibs
|
||||
src/main/resources
|
||||
src/*/assets
|
||||
src/*/jniLibs
|
||||
src/*/resources
|
||||
|
@@ -19,12 +19,17 @@ kapt {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.topjohnwu.magisk"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.topjohnwu.magisk"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
versionName = Config.version
|
||||
versionCode = Config.versionCode
|
||||
ndk.abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
ndk {
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
debugSymbolLevel = "FULL"
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -37,11 +42,13 @@ android {
|
||||
|
||||
buildFeatures {
|
||||
dataBinding = true
|
||||
aidl = true
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/*"
|
||||
excludes += "/META-INF/versions/**"
|
||||
excludes += "/org/bouncycastle/**"
|
||||
excludes += "/kotlin/**"
|
||||
excludes += "/kotlinx/**"
|
||||
@@ -50,14 +57,6 @@ android {
|
||||
excludes += "/*.bin"
|
||||
excludes += "/*.json"
|
||||
}
|
||||
jniLibs {
|
||||
keepDebugSymbols += "**/*.so"
|
||||
}
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
freeCompilerArgs = listOf("-Xjvm-default=enable")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,55 +73,49 @@ dependencies {
|
||||
implementation("com.github.topjohnwu:jtar:1.0.0")
|
||||
implementation("com.github.topjohnwu:indeterminate-checkbox:1.0.7")
|
||||
implementation("com.github.topjohnwu:lz4-java:1.7.1")
|
||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
||||
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
|
||||
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0")
|
||||
implementation("dev.rikka.rikkax.insets:insets:1.1.1")
|
||||
implementation("dev.rikka.rikkax.recyclerview:recyclerview-ktx:1.3.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 vBAdapt = "4.0.0"
|
||||
val bindingAdapter = "me.tatarka.bindingcollectionadapter2:bindingcollectionadapter"
|
||||
implementation("${bindingAdapter}:${vBAdapt}")
|
||||
implementation("${bindingAdapter}-recyclerview:${vBAdapt}")
|
||||
|
||||
val vLibsu = "4.0.2"
|
||||
val vLibsu = "5.2.0"
|
||||
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 vRetrofit = "2.9.0"
|
||||
implementation("com.squareup.retrofit2:retrofit:${vRetrofit}")
|
||||
implementation("com.squareup.retrofit2:converter-moshi:${vRetrofit}")
|
||||
implementation("com.squareup.retrofit2:converter-scalars:${vRetrofit}")
|
||||
|
||||
val vOkHttp = "4.9.3"
|
||||
val vOkHttp = "4.11.0"
|
||||
implementation("com.squareup.okhttp3:okhttp:${vOkHttp}")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:${vOkHttp}")
|
||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:${vOkHttp}")
|
||||
|
||||
val vMoshi = "1.13.0"
|
||||
val vMoshi = "1.15.0"
|
||||
implementation("com.squareup.moshi:moshi:${vMoshi}")
|
||||
kapt("com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}")
|
||||
|
||||
val vRoom = "2.4.1"
|
||||
val vRoom = "2.6.0-beta01"
|
||||
implementation("androidx.room:room-runtime:${vRoom}")
|
||||
implementation("androidx.room:room-ktx:${vRoom}")
|
||||
kapt("androidx.room:room-compiler:${vRoom}")
|
||||
|
||||
val vNav = "2.5.0-alpha01"
|
||||
val vNav = "2.7.1"
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:${vNav}")
|
||||
implementation("androidx.navigation:navigation-ui-ktx:${vNav}")
|
||||
|
||||
implementation("androidx.biometric:biometric:1.1.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
implementation("androidx.appcompat:appcompat:1.4.1")
|
||||
implementation("androidx.preference:preference:1.2.0")
|
||||
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
||||
implementation("androidx.fragment:fragment-ktx:1.4.1")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("androidx.recyclerview:recyclerview:1.3.1")
|
||||
implementation("androidx.fragment:fragment-ktx:1.6.1")
|
||||
implementation("androidx.transition:transition:1.4.1")
|
||||
implementation("androidx.core:core-ktx:1.7.0")
|
||||
implementation("androidx.core:core-splashscreen:1.0.0-beta01")
|
||||
implementation("com.google.android.material:material:1.5.0")
|
||||
implementation("androidx.core:core-ktx:1.10.1")
|
||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||
implementation("com.google.android.material:material:1.9.0")
|
||||
}
|
||||
|
44
app/proguard-rules.pro
vendored
44
app/proguard-rules.pro
vendored
@@ -1,21 +1,3 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /Users/topjohnwu/Library/Android/sdk/tools/proguard/proguard-android.txt
|
||||
# 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 *;
|
||||
#}
|
||||
|
||||
# Parcelable
|
||||
-keepclassmembers class * implements android.os.Parcelable {
|
||||
public static final ** CREATOR;
|
||||
@@ -26,12 +8,23 @@
|
||||
public static void check*(...);
|
||||
public static void throw*(...);
|
||||
}
|
||||
-assumenosideeffects class java.util.Objects {
|
||||
public static ** requireNonNull(...);
|
||||
}
|
||||
-assumenosideeffects public class kotlin.coroutines.jvm.internal.DebugMetadataKt {
|
||||
private static ** getDebugMetadataAnnotation(...) return null;
|
||||
}
|
||||
|
||||
# Stub
|
||||
-keep class com.topjohnwu.magisk.core.App { <init>(java.lang.Object); }
|
||||
-keepclassmembers class androidx.appcompat.app.AppCompatDelegateImpl {
|
||||
boolean mActivityHandlesUiModeChecked;
|
||||
boolean mActivityHandlesUiMode;
|
||||
boolean mActivityHandlesConfigFlagsChecked;
|
||||
int mActivityHandlesConfigFlags;
|
||||
}
|
||||
|
||||
# main
|
||||
-keep,allowoptimization public class com.topjohnwu.magisk.signing.SignBoot {
|
||||
public static void main(java.lang.String[]);
|
||||
}
|
||||
|
||||
# Strip Timber verbose and debug logging
|
||||
@@ -40,6 +33,17 @@
|
||||
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
|
||||
-repackageclasses 'a'
|
||||
-allowaccessmodification
|
||||
|
@@ -5,11 +5,5 @@ plugins {
|
||||
setupCommon()
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
consumerProguardFiles("proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api("io.michaelrocks:paranoid-core:0.3.7")
|
||||
namespace = "com.topjohnwu.shared"
|
||||
}
|
||||
|
21
app/shared/proguard-rules.pro
vendored
21
app/shared/proguard-rules.pro
vendored
@@ -1,21 +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
|
@@ -1,14 +1,18 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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.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.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"
|
||||
|
@@ -2,9 +2,6 @@ package com.topjohnwu.magisk;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import io.michaelrocks.paranoid.Obfuscate;
|
||||
|
||||
@Obfuscate
|
||||
public class ProviderInstaller {
|
||||
|
||||
public static boolean install(Context context) {
|
||||
|
@@ -1,20 +1,25 @@
|
||||
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;
|
||||
|
||||
import io.michaelrocks.paranoid.Obfuscate;
|
||||
|
||||
@Obfuscate
|
||||
public class StubApk {
|
||||
private static File dynDir;
|
||||
private static Method addAssetPath;
|
||||
@@ -22,7 +27,7 @@ public class StubApk {
|
||||
private static File getDynDir(ApplicationInfo info) {
|
||||
if (dynDir == null) {
|
||||
final String dataDir;
|
||||
if (SDK_INT >= 24) {
|
||||
if (SDK_INT >= Build.VERSION_CODES.N) {
|
||||
// Use device protected path to allow directBootAware
|
||||
dataDir = info.deviceProtectedDataDir;
|
||||
} else {
|
||||
@@ -50,12 +55,33 @@ public class StubApk {
|
||||
return new File(getDynDir(info), "update.apk");
|
||||
}
|
||||
|
||||
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) {}
|
||||
@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) {
|
||||
|
@@ -25,9 +25,6 @@ import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.michaelrocks.paranoid.Obfuscate;
|
||||
|
||||
@Obfuscate
|
||||
public final class APKInstall {
|
||||
|
||||
public static void transfer(InputStream in, OutputStream out) throws IOException {
|
||||
@@ -39,6 +36,16 @@ public final class APKInstall {
|
||||
}
|
||||
}
|
||||
|
||||
public static void registerReceiver(
|
||||
Context context, BroadcastReceiver receiver, IntentFilter filter) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// noinspection InlinedApi
|
||||
context.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED);
|
||||
} else {
|
||||
context.registerReceiver(receiver, filter);
|
||||
}
|
||||
}
|
||||
|
||||
public static Session startSession(Context context) {
|
||||
return startSession(context, null, null, null);
|
||||
}
|
||||
@@ -51,9 +58,9 @@ public final class APKInstall {
|
||||
// If pkg is not null, look for package added event
|
||||
var filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
|
||||
filter.addDataScheme("package");
|
||||
context.registerReceiver(receiver, filter);
|
||||
registerReceiver(context, receiver, filter);
|
||||
}
|
||||
context.registerReceiver(receiver, new IntentFilter(receiver.sessionId));
|
||||
registerReceiver(context, receiver, new IntentFilter(receiver.sessionId));
|
||||
return receiver;
|
||||
}
|
||||
|
||||
@@ -94,27 +101,25 @@ public final class APKInstall {
|
||||
} 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);
|
||||
break;
|
||||
case STATUS_SUCCESS:
|
||||
case STATUS_PENDING_USER_ACTION ->
|
||||
userAction = intent.getParcelableExtra(Intent.EXTRA_INTENT);
|
||||
case STATUS_SUCCESS -> {
|
||||
if (packageName == null) {
|
||||
onSuccess(context);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
default -> {
|
||||
int id = intent.getIntExtra(EXTRA_SESSION_ID, 0);
|
||||
if (id > 0) {
|
||||
var installer = context.getPackageManager().getPackageInstaller();
|
||||
var info = installer.getSessionInfo(id);
|
||||
if (info != null) {
|
||||
installer.abandonSession(info.getSessionId());
|
||||
}
|
||||
var installer = context.getPackageManager().getPackageInstaller();
|
||||
try {
|
||||
installer.abandonSession(id);
|
||||
} catch (SecurityException ignored) {
|
||||
}
|
||||
if (onFailure != null) {
|
||||
onFailure.run();
|
||||
}
|
||||
context.getApplicationContext().unregisterReceiver(this);
|
||||
}
|
||||
}
|
||||
latch.countDown();
|
||||
}
|
||||
|
@@ -1,22 +1,23 @@
|
||||
package com.topjohnwu.magisk.utils;
|
||||
|
||||
import android.os.Process;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.Enumeration;
|
||||
|
||||
import dalvik.system.DexClassLoader;
|
||||
import dalvik.system.BaseDexClassLoader;
|
||||
|
||||
public class DynamicClassLoader extends DexClassLoader {
|
||||
|
||||
private static final ClassLoader base = Object.class.getClassLoader();
|
||||
public class DynamicClassLoader extends BaseDexClassLoader {
|
||||
|
||||
public DynamicClassLoader(File apk) {
|
||||
super(apk.getPath(), apk.getParent(), null, base);
|
||||
this(apk, getSystemClassLoader());
|
||||
}
|
||||
|
||||
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
|
||||
@@ -28,7 +29,7 @@ public class DynamicClassLoader extends DexClassLoader {
|
||||
|
||||
try {
|
||||
// Then check boot classpath
|
||||
return base.loadClass(name);
|
||||
return getSystemClassLoader().loadClass(name);
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
try {
|
||||
// Next try current dex
|
||||
@@ -46,7 +47,7 @@ public class DynamicClassLoader extends DexClassLoader {
|
||||
|
||||
@Override
|
||||
public URL getResource(String name) {
|
||||
URL resource = base.getResource(name);
|
||||
URL resource = getSystemClassLoader().getResource(name);
|
||||
if (resource != null)
|
||||
return resource;
|
||||
resource = findResource(name);
|
||||
@@ -58,7 +59,7 @@ public class DynamicClassLoader extends DexClassLoader {
|
||||
|
||||
@Override
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@@ -1,16 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.topjohnwu.magisk">
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
<permission
|
||||
android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
|
||||
android:protectionLevel="signature"
|
||||
tools:node="remove" />
|
||||
|
||||
<uses-permission
|
||||
android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
|
||||
tools:node="remove" />
|
||||
|
||||
<application
|
||||
android:name=".core.App"
|
||||
android:extractNativeLibs="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:multiArch="true"
|
||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
|
||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning"
|
||||
tools:remove="android:appComponentFactory">
|
||||
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
@@ -29,7 +35,6 @@
|
||||
<activity
|
||||
android:name=".ui.surequest.SuRequestActivity"
|
||||
android:directBootAware="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false"
|
||||
android:taskAffinity=""
|
||||
tools:ignore="AppLinkUrlError">
|
||||
@@ -41,7 +46,6 @@
|
||||
|
||||
<receiver
|
||||
android:name=".core.Receiver"
|
||||
android:directBootAware="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
||||
@@ -58,7 +62,8 @@
|
||||
|
||||
<service
|
||||
android:name=".core.download.DownloadService"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service
|
||||
android:name=".core.JobService"
|
||||
|
@@ -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();
|
||||
}
|
@@ -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,13 +5,14 @@ import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.OnRebindCallback
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.navigation.NavDirections
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.ktx.startAnimations
|
||||
|
||||
abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHolder {
|
||||
|
||||
@@ -20,11 +21,12 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
||||
protected abstract val layoutRes: Int
|
||||
|
||||
private val navigation get() = activity?.navigation
|
||||
open val snackbarView: View? get() = null
|
||||
open val snackbarAnchorView: View? get() = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
startObserveEvents()
|
||||
startObserveLiveData()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
@@ -36,9 +38,17 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
||||
it.setVariable(BR.viewModel, viewModel)
|
||||
it.lifecycleOwner = viewLifecycleOwner
|
||||
}
|
||||
if (this is MenuProvider) {
|
||||
activity?.addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.STARTED)
|
||||
}
|
||||
savedInstanceState?.let { viewModel.onRestoreState(it) }
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
viewModel.onSaveState(outState)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
activity?.supportActionBar?.subtitle = null
|
||||
@@ -70,7 +80,10 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.requestRefresh()
|
||||
viewModel.let {
|
||||
if (it is AsyncLoadViewModel)
|
||||
it.startLoading()
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun onPreBind(binding: Binding) {
|
||||
@@ -78,7 +91,6 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
||||
}
|
||||
|
||||
fun NavDirections.navigate() {
|
||||
navigation?.navigate(this)
|
||||
navigation?.currentDestination?.getAction(actionId)?.let { navigation!!.navigate(this) }
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,79 +1,32 @@
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
import android.Manifest.permission.*
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.databinding.Observable
|
||||
import android.os.Bundle
|
||||
import androidx.databinding.PropertyChangeRegistry
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.NavDirections
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.databinding.ObservableHost
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.events.BackPressEvent
|
||||
import com.topjohnwu.magisk.events.DialogBuilder
|
||||
import com.topjohnwu.magisk.events.DialogEvent
|
||||
import com.topjohnwu.magisk.events.NavigationEvent
|
||||
import com.topjohnwu.magisk.events.PermissionEvent
|
||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
abstract class BaseViewModel(
|
||||
initialState: State = State.LOADING
|
||||
) : ViewModel(), ObservableHost {
|
||||
abstract class BaseViewModel : ViewModel(), ObservableHost {
|
||||
|
||||
override var callbacks: PropertyChangeRegistry? = null
|
||||
|
||||
enum class State {
|
||||
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
|
||||
private val _viewEvents = MutableLiveData<ViewEvent>()
|
||||
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
|
||||
|
||||
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()
|
||||
}
|
||||
open fun onSaveState(state: Bundle) {}
|
||||
open fun onRestoreState(state: Bundle) {}
|
||||
open fun onNetworkChanged(network: Boolean) {}
|
||||
|
||||
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
||||
PermissionEvent(permission, callback).publish()
|
||||
@@ -100,15 +53,25 @@ abstract class BaseViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
@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 <Event : ViewEvent> Event.publish() {
|
||||
fun ViewEvent.publish() {
|
||||
_viewEvents.postValue(this)
|
||||
}
|
||||
|
||||
fun <Event : ViewEventWithScope> Event.publish() {
|
||||
scope = viewModelScope
|
||||
_viewEvents.postValue(this)
|
||||
fun DialogBuilder.show() {
|
||||
DialogEvent(this).publish()
|
||||
}
|
||||
|
||||
fun NavDirections.navigate(pop: Boolean = false) {
|
||||
|
@@ -20,12 +20,14 @@ abstract class NavigationActivity<Binding : ViewDataBinding> : UIActivity<Bindin
|
||||
val navigation: NavController get() = navHostFragment.navController
|
||||
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
return currentFragment?.onKeyEvent(event) == true || super.dispatchKeyEvent(event)
|
||||
return if (binded && currentFragment?.onKeyEvent(event) == true) true else super.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (currentFragment?.onBackPressed()?.not() == true) {
|
||||
super.onBackPressed()
|
||||
if (binded) {
|
||||
if (currentFragment?.onBackPressed() == false) {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -5,13 +5,18 @@ 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
|
||||
@@ -22,12 +27,13 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
||||
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 {
|
||||
val theme = Config.darkTheme
|
||||
AppCompatDelegate.setDefaultNightMode(theme)
|
||||
AppCompatDelegate.setDefaultNightMode(Config.darkTheme)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -36,7 +42,7 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
startObserveEvents()
|
||||
startObserveLiveData()
|
||||
|
||||
// We need to set the window background explicitly since for whatever reason it's not
|
||||
// propagated upstream
|
||||
@@ -84,7 +90,10 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.requestRefresh()
|
||||
viewModel.let {
|
||||
if (it is AsyncLoadViewModel)
|
||||
it.startLoading()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEventDispatched(event: ViewEvent) = when (event) {
|
||||
@@ -93,3 +102,14 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
/**
|
||||
* Class for passing events from ViewModels to Activities/Fragments
|
||||
@@ -9,10 +8,6 @@ import kotlinx.coroutines.CoroutineScope
|
||||
*/
|
||||
abstract class ViewEvent
|
||||
|
||||
abstract class ViewEventWithScope: ViewEvent() {
|
||||
lateinit var scope: CoroutineScope
|
||||
}
|
||||
|
||||
interface ContextExecutor {
|
||||
operator fun invoke(context: Context)
|
||||
}
|
||||
|
@@ -1,15 +1,24 @@
|
||||
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 {
|
||||
interface ViewModelHolder : LifecycleOwner, ViewModelStoreOwner {
|
||||
|
||||
val viewModel: BaseViewModel
|
||||
|
||||
fun startObserveEvents() {
|
||||
viewModel.viewEvents.observe(this) {
|
||||
onEventDispatched(it)
|
||||
}
|
||||
fun startObserveLiveData() {
|
||||
viewModel.viewEvents.observe(this, this::onEventDispatched)
|
||||
Info.isConnected.observe(this, viewModel::onNetworkChanged)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -17,3 +26,24 @@ interface ViewModelHolder : LifecycleOwner {
|
||||
*/
|
||||
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]
|
||||
}
|
||||
|
@@ -1,20 +1,26 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.utils.*
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.utils.DispatcherExecutor
|
||||
import com.topjohnwu.magisk.core.utils.ProcessLifecycle
|
||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||
import com.topjohnwu.magisk.core.utils.ShellInit
|
||||
import com.topjohnwu.magisk.core.utils.refreshLocale
|
||||
import com.topjohnwu.magisk.core.utils.setConfig
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import timber.log.Timber
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
open class App() : Application() {
|
||||
@@ -22,9 +28,9 @@ open class App() : Application() {
|
||||
constructor(o: Any) : this() {
|
||||
val data = StubApk.Data(o)
|
||||
// Add the root service name mapping
|
||||
data.classToComponent[RootRegistry::class.java.name] = data.rootService.name
|
||||
data.classToComponent[RootUtils::class.java.name] = data.rootService.name
|
||||
// Send back the actual root service class
|
||||
data.rootService = RootRegistry::class.java
|
||||
data.rootService = RootUtils::class.java
|
||||
Info.stub = data
|
||||
}
|
||||
|
||||
@@ -38,43 +44,44 @@ open class App() : Application() {
|
||||
}
|
||||
|
||||
override fun attachBaseContext(context: Context) {
|
||||
Shell.setDefaultBuilder(Shell.Builder.create()
|
||||
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
||||
.setInitializers(ShellInit::class.java)
|
||||
.setTimeout(2))
|
||||
Shell.EXECUTOR = DispatcherExecutor(Dispatchers.IO)
|
||||
|
||||
// Get the actual ContextImpl
|
||||
val app: Application
|
||||
val base: Context
|
||||
if (context is Application) {
|
||||
app = context
|
||||
base = context.baseContext
|
||||
AppApkPath = StubApk.current(base).path
|
||||
} else {
|
||||
app = this
|
||||
base = context
|
||||
AppApkPath = base.packageResourcePath
|
||||
}
|
||||
super.attachBaseContext(base)
|
||||
ServiceLocator.context = base
|
||||
app.registerActivityLifecycleCallbacks(ActivityTracker)
|
||||
|
||||
Shell.setDefaultBuilder(Shell.Builder.create()
|
||||
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
||||
.setInitializers(ShellInit::class.java)
|
||||
.setContext(base)
|
||||
.setTimeout(2))
|
||||
Shell.EXECUTOR = DispatcherExecutor(Dispatchers.IO)
|
||||
RootUtils.bindTask = RootService.bindOrTask(
|
||||
intent<RootUtils>(),
|
||||
UiThreadHandler.executor,
|
||||
RootUtils.Connection
|
||||
)
|
||||
// Pre-heat the shell ASAP
|
||||
Shell.getShell(null) {}
|
||||
|
||||
refreshLocale()
|
||||
AppApkPath = if (isRunningAsStub) {
|
||||
StubApk.current(base).path
|
||||
} else {
|
||||
base.packageResourcePath
|
||||
}
|
||||
|
||||
base.resources.patch()
|
||||
app.registerActivityLifecycleCallbacks(ActivityTracker)
|
||||
resources.patch()
|
||||
Notifications.setup()
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
RootRegistry.bindTask = RootService.bindOrTask(
|
||||
intent<RootRegistry>(),
|
||||
UiThreadHandler.executor,
|
||||
RootRegistry.Connection
|
||||
)
|
||||
ProcessLifecycle.init(this)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
@@ -86,20 +93,21 @@ open class App() : Application() {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
object ActivityTracker : Application.ActivityLifecycleCallbacks {
|
||||
|
||||
val foreground: Activity? get() = ref.get()
|
||||
|
||||
@Volatile
|
||||
var foreground: Activity? = null
|
||||
private var ref = WeakReference<Activity>(null)
|
||||
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
if (activity is SuRequestActivity) return
|
||||
foreground = activity
|
||||
ref = WeakReference(activity)
|
||||
}
|
||||
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
if (activity is SuRequestActivity) return
|
||||
foreground = null
|
||||
ref.clear()
|
||||
}
|
||||
|
||||
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
||||
|
@@ -1,34 +1,33 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Xml
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.edit
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||
import com.topjohnwu.magisk.core.repository.BoolDBPropertyNoWrite
|
||||
import com.topjohnwu.magisk.core.repository.DBConfig
|
||||
import com.topjohnwu.magisk.core.repository.PreferenceConfig
|
||||
import com.topjohnwu.magisk.core.utils.refreshLocale
|
||||
import com.topjohnwu.magisk.data.preference.PreferenceModel
|
||||
import com.topjohnwu.magisk.data.repository.DBBoolSettingsNoWrite
|
||||
import com.topjohnwu.magisk.data.repository.DBConfig
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.ui.theme.Theme
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
object Config : PreferenceModel, DBConfig {
|
||||
object Config : PreferenceConfig, DBConfig {
|
||||
|
||||
override val stringDB get() = ServiceLocator.stringDB
|
||||
override val settingsDB get() = ServiceLocator.settingsDB
|
||||
override val context get() = ServiceLocator.deContext
|
||||
override val coroutineScope get() = GlobalScope
|
||||
|
||||
@get:SuppressLint("ApplySharedPref")
|
||||
val prefsFile: File get() {
|
||||
// Flush prefs to disk
|
||||
prefs.edit().apply {
|
||||
remove(Key.ASKED_HOME)
|
||||
}.commit()
|
||||
return File("${context.filesDir.parent}/shared_prefs", "${fileName}.xml")
|
||||
private val prefsFile = File("${context.filesDir.parent}/shared_prefs", "${fileName}.xml")
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
fun getPrefsFile(): File {
|
||||
prefs.edit().remove(Key.ASKED_HOME).commit()
|
||||
return prefsFile
|
||||
}
|
||||
|
||||
object Key {
|
||||
@@ -70,6 +69,7 @@ object Config : PreferenceModel, DBConfig {
|
||||
const val BETA_CHANNEL = 1
|
||||
const val CUSTOM_CHANNEL = 2
|
||||
const val CANARY_CHANNEL = 3
|
||||
const val DEBUG_CHANNEL = 4
|
||||
|
||||
// root access mode
|
||||
const val ROOT_ACCESS_DISABLED = 0
|
||||
@@ -106,6 +106,8 @@ object Config : PreferenceModel, DBConfig {
|
||||
|
||||
private val defaultChannel =
|
||||
if (BuildConfig.DEBUG)
|
||||
Value.DEBUG_CHANNEL
|
||||
else if (Const.APP_IS_CANARY)
|
||||
Value.CANARY_CHANNEL
|
||||
else
|
||||
Value.DEFAULT_CHANNEL
|
||||
@@ -131,7 +133,15 @@ object Config : PreferenceModel, DBConfig {
|
||||
var themeOrdinal by preference(Key.THEME_ORDINAL, Theme.Piplup.ordinal)
|
||||
var suReAuth by preference(Key.SU_REAUTH, false)
|
||||
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 showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
|
||||
|
||||
@@ -149,7 +159,7 @@ object Config : PreferenceModel, DBConfig {
|
||||
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
|
||||
var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
|
||||
var zygisk by dbSettings(Key.ZYGISK, false)
|
||||
var denyList by DBBoolSettingsNoWrite(Key.DENYLIST, false)
|
||||
var denyList by BoolDBPropertyNoWrite(Key.DENYLIST, false)
|
||||
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
||||
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
|
||||
|
||||
@@ -158,9 +168,8 @@ object Config : PreferenceModel, DBConfig {
|
||||
fun load(pkg: String?) {
|
||||
// Only try to load prefs when fresh install and a previous package name is set
|
||||
if (pkg != null && prefs.all.isEmpty()) runCatching {
|
||||
context.contentResolver.openInputStream(Provider.PREFS_URI(pkg))?.use {
|
||||
prefs.edit { parsePrefs(it) }
|
||||
}
|
||||
context.contentResolver.openInputStream(Provider.preferencesUri(pkg))?.writeTo(prefsFile)
|
||||
return
|
||||
}
|
||||
|
||||
prefs.edit {
|
||||
@@ -169,57 +178,10 @@ object Config : PreferenceModel, DBConfig {
|
||||
suBiometric = true
|
||||
remove(SU_FINGERPRINT)
|
||||
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())
|
||||
else if (it.toInt() > Value.CANARY_CHANNEL)
|
||||
putString(Key.UPDATE_CHANNEL, Value.CANARY_CHANNEL.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun SharedPreferences.Editor.parsePrefs(input: InputStream) {
|
||||
runCatching {
|
||||
val parser = Xml.newPullParser()
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
|
||||
parser.setInput(input, "UTF-8")
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.START_TAG, null, "map")
|
||||
while (parser.next() != XmlPullParser.END_TAG) {
|
||||
if (parser.eventType != XmlPullParser.START_TAG)
|
||||
continue
|
||||
val key: String = parser.getAttributeValue(null, "name")
|
||||
fun value() = parser.getAttributeValue(null, "value")!!
|
||||
when (parser.name) {
|
||||
"string" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "string")
|
||||
putString(key, parser.nextText())
|
||||
parser.require(XmlPullParser.END_TAG, null, "string")
|
||||
}
|
||||
"boolean" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "boolean")
|
||||
putBoolean(key, value().toBoolean())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "boolean")
|
||||
}
|
||||
"int" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "int")
|
||||
putInt(key, value().toInt())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "int")
|
||||
}
|
||||
"long" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "long")
|
||||
putLong(key, value().toLong())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "long")
|
||||
}
|
||||
"float" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "int")
|
||||
putFloat(key, value().toFloat())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "int")
|
||||
}
|
||||
else -> parser.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -15,8 +15,7 @@ object Const {
|
||||
else Build.SUPPORTED_32_BIT_ABIS.firstOrNull()
|
||||
|
||||
// Paths
|
||||
lateinit var MAGISKTMP: String
|
||||
val MAGISK_PATH get() = "$MAGISKTMP/modules"
|
||||
const val MAGISK_PATH = "/data/adb/modules"
|
||||
const val TMPDIR = "/dev/tmp"
|
||||
const val MAGISK_LOG = "/cache/magisk.log"
|
||||
|
||||
@@ -25,11 +24,11 @@ object Const {
|
||||
val APP_IS_CANARY get() = Version.isCanary(BuildConfig.VERSION_CODE)
|
||||
|
||||
object Version {
|
||||
const val MIN_VERSION = "v21.0"
|
||||
const val MIN_VERCODE = 21000
|
||||
const val MIN_VERSION = "v22.0"
|
||||
const val MIN_VERCODE = 22000
|
||||
|
||||
fun atLeast_21_2() = Info.env.versionCode >= 21200 || isCanary()
|
||||
fun atLeast_24_0() = Info.env.versionCode >= 24000 || isCanary()
|
||||
fun atLeast_25_0() = Info.env.versionCode >= 25000 || isCanary()
|
||||
fun isCanary() = isCanary(Info.env.versionCode)
|
||||
|
||||
fun isCanary(ver: Int) = ver > 0 && ver % 100 != 0
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
@@ -13,45 +12,49 @@ import android.content.res.Resources
|
||||
import android.util.DisplayMetrics
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.ktx.unwrap
|
||||
import com.topjohnwu.magisk.core.utils.syncLocale
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
|
||||
lateinit var AppApkPath: String
|
||||
|
||||
fun AssetManager.addAssetPath(path: String) = StubApk.addAssetPath(this, path)
|
||||
|
||||
fun Context.wrap(): Context = if (this is PatchedContext) this else PatchedContext(this)
|
||||
|
||||
private class PatchedContext(base: Context) : ContextWrapper(base) {
|
||||
init { base.resources.patch() }
|
||||
override fun getClassLoader() = javaClass.classLoader!!
|
||||
override fun createConfigurationContext(config: Configuration) =
|
||||
super.createConfigurationContext(config).wrap()
|
||||
}
|
||||
fun Resources.addAssetPath(path: String) = StubApk.addAssetPath(this, path)
|
||||
|
||||
fun Resources.patch(): Resources {
|
||||
syncLocale()
|
||||
if (isRunningAsStub)
|
||||
assets.addAssetPath(AppApkPath)
|
||||
addAssetPath(AppApkPath)
|
||||
syncLocale()
|
||||
return this
|
||||
}
|
||||
|
||||
fun Context.patch(): Context {
|
||||
unwrap().resources.patch()
|
||||
return this
|
||||
}
|
||||
|
||||
// Wrapping is only necessary for ContextThemeWrapper to support configuration overrides
|
||||
fun Context.wrap(): Context {
|
||||
patch()
|
||||
return object : ContextWrapper(this) {
|
||||
override fun createConfigurationContext(config: Configuration): Context {
|
||||
return super.createConfigurationContext(config).wrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createNewResources(): Resources {
|
||||
val asset = AssetManager::class.java.newInstance()
|
||||
asset.addAssetPath(AppApkPath)
|
||||
val config = Configuration(AppContext.resources.configuration)
|
||||
val metrics = DisplayMetrics()
|
||||
metrics.setTo(AppContext.resources.displayMetrics)
|
||||
return Resources(asset, metrics, config)
|
||||
val res = Resources(asset, metrics, config)
|
||||
res.addAssetPath(AppApkPath)
|
||||
return res
|
||||
}
|
||||
|
||||
fun Class<*>.cmp(pkg: String) =
|
||||
ComponentName(pkg, Info.stub?.classToComponent?.get(name) ?: name)
|
||||
|
||||
inline fun <reified T> Activity.redirect() = Intent(intent)
|
||||
.setComponent(T::class.java.cmp(packageName))
|
||||
.setFlags(0)
|
||||
|
||||
inline fun <reified T> Context.intent() = Intent().setComponent(T::class.java.cmp(packageName))
|
||||
|
||||
// Keep a reference to these resources to prevent it from
|
||||
|
@@ -1,16 +1,15 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.os.Build
|
||||
import androidx.databinding.ObservableBoolean
|
||||
import androidx.lifecycle.LiveData
|
||||
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.utils.net.NetworkObserver
|
||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
import com.topjohnwu.magisk.ktx.getProperty
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||
import com.topjohnwu.magisk.core.utils.NetworkObserver
|
||||
import com.topjohnwu.superuser.ShellUtils.fastCmd
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
|
||||
val isRunningAsStub get() = Info.stub != null
|
||||
|
||||
@@ -29,6 +28,7 @@ object Info {
|
||||
// Device state
|
||||
@JvmStatic val env by lazy { loadState() }
|
||||
@JvmField var isSAR = false
|
||||
var legacySAR = false
|
||||
var isAB = false
|
||||
@JvmField val isZygiskEnabled = System.getenv("ZYGISK_ENABLED") == "1"
|
||||
@JvmStatic val isFDE get() = crypto == "block"
|
||||
@@ -36,6 +36,7 @@ object Info {
|
||||
@JvmField var vbmeta = false
|
||||
var crypto = ""
|
||||
var noDataExec = false
|
||||
var isRooted = false
|
||||
|
||||
@JvmField var hasGMS = true
|
||||
val isSamsung = Build.MANUFACTURER.equals("samsung", ignoreCase = true)
|
||||
@@ -43,28 +44,39 @@ object Info {
|
||||
getProperty("ro.kernel.qemu", "0") == "1" ||
|
||||
getProperty("ro.boot.qemu", "0") == "1"
|
||||
|
||||
val isConnected by lazy {
|
||||
ObservableBoolean(false).also { field ->
|
||||
val isConnected: LiveData<Boolean> by lazy {
|
||||
MutableLiveData(false).also { field ->
|
||||
NetworkObserver.observe(AppContext) {
|
||||
UiThreadHandler.run { field.set(it) }
|
||||
remote = EMPTY_REMOTE
|
||||
field.postValue(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadState() = Env(
|
||||
fastCmd("magisk -v").split(":".toRegex())[0],
|
||||
runCatching { fastCmd("magisk -V").toInt() }.getOrDefault(-1)
|
||||
)
|
||||
val showSuperUser: Boolean get() {
|
||||
return env.isActive && (Const.USER_ID == 0
|
||||
|| Config.suMultiuserMode == Config.Value.MULTIUSER_MODE_USER)
|
||||
}
|
||||
|
||||
private fun loadState(): Env {
|
||||
val v = fastCmd("magisk -v").split(":".toRegex())
|
||||
return Env(
|
||||
v[0], v.size >= 3 && v[2] == "D",
|
||||
runCatching { fastCmd("magisk -V").toInt() }.getOrDefault(-1)
|
||||
)
|
||||
}
|
||||
|
||||
class Env(
|
||||
val versionString: String = "",
|
||||
val isDebug: Boolean = false,
|
||||
code: Int = -1
|
||||
) {
|
||||
val versionCode = when {
|
||||
code < Const.Version.MIN_VERCODE -> -1
|
||||
else -> if (Shell.rootAccess()) code else -1
|
||||
isRooted -> code
|
||||
else -> -1
|
||||
}
|
||||
val isUnsupported = code > 0 && code < Const.Version.MIN_VERCODE
|
||||
val isActive = versionCode >= 0
|
||||
val isActive = versionCode > 0
|
||||
}
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ 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.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -33,7 +33,7 @@ class JobService : BaseJobService() {
|
||||
svc.fetchUpdate()?.let {
|
||||
Info.remote = it
|
||||
if (Info.env.isActive && BuildConfig.VERSION_CODE < it.magisk.versionCode)
|
||||
Notifications.updateAvailable(this)
|
||||
Notifications.updateAvailable()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,22 +1,13 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
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 android.os.Bundle
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
||||
import com.topjohnwu.magisk.core.base.BaseProvider
|
||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
||||
import java.io.File
|
||||
|
||||
class Provider : ContentProvider() {
|
||||
|
||||
override fun attachInfo(context: Context, info: ProviderInfo) {
|
||||
super.attachInfo(context.wrap(), info)
|
||||
}
|
||||
class Provider : BaseProvider() {
|
||||
|
||||
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
|
||||
SuCallbackHandler.run(context!!, method, extras)
|
||||
@@ -25,24 +16,13 @@ class Provider : ContentProvider() {
|
||||
|
||||
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
|
||||
return when (uri.encodedPath ?: return null) {
|
||||
"/apk_file" -> ParcelFileDescriptor.open(File(context!!.packageCodePath), MODE_READ_ONLY)
|
||||
"/prefs_file" -> ParcelFileDescriptor.open(Config.prefsFile, MODE_READ_ONLY)
|
||||
"/prefs_file" -> ParcelFileDescriptor.open(Config.getPrefsFile(), MODE_READ_ONLY)
|
||||
else -> super.openFile(uri, mode)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun APK_URI(pkg: String) =
|
||||
Uri.Builder().scheme("content").authority("$pkg.provider").path("apk_file").build()
|
||||
|
||||
fun PREFS_URI(pkg: String) =
|
||||
fun preferencesUri(pkg: String): Uri =
|
||||
Uri.Builder().scheme("content").authority("$pkg.provider").path("prefs_file").build()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@@ -1,10 +1,10 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.topjohnwu.magisk.core.base.BaseReceiver
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import com.topjohnwu.superuser.Shell
|
||||
@@ -26,8 +26,9 @@ open class Receiver : BaseReceiver() {
|
||||
return if (uid == -1) null else uid
|
||||
}
|
||||
|
||||
override fun onReceive(context: ContextWrapper, intent: Intent?) {
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
intent ?: return
|
||||
super.onReceive(context, intent)
|
||||
|
||||
fun rmPolicy(uid: Int) = GlobalScope.launch {
|
||||
policyDB.delete(uid)
|
||||
@@ -50,7 +51,7 @@ open class Receiver : BaseReceiver() {
|
||||
@Suppress("DEPRECATION")
|
||||
val installer = context.packageManager.getInstallerPackageName(context.packageName)
|
||||
if (installer == context.packageName) {
|
||||
Notifications.updateDone(context)
|
||||
Notifications.updateDone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,25 +1,33 @@
|
||||
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.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts.GetContent
|
||||
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.ktx.reflectField
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.utils.RequestInstall
|
||||
import com.topjohnwu.magisk.core.utils.UninstallPackage
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import com.topjohnwu.magisk.ktx.reflectField
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
interface ContentResultCallback: ActivityResultCallback<Uri>, Parcelable {
|
||||
fun onActivityLaunch() {}
|
||||
// Make the result type explicitly non-null
|
||||
override fun onActivityResult(result: Uri)
|
||||
}
|
||||
|
||||
abstract class BaseActivity : AppCompatActivity() {
|
||||
|
||||
@@ -28,26 +36,27 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
permissionCallback?.invoke(it)
|
||||
permissionCallback = null
|
||||
}
|
||||
|
||||
private var installCallback: ((Boolean) -> Unit)? = null
|
||||
private val requestInstall = registerForActivityResult(RequestInstall()) {
|
||||
permissionCallback?.invoke(it)
|
||||
permissionCallback = null
|
||||
installCallback?.invoke(it)
|
||||
installCallback = null
|
||||
}
|
||||
|
||||
private var contentCallback: ((Uri) -> Unit)? = null
|
||||
private var contentCallback: ContentResultCallback? = null
|
||||
private val getContent = registerForActivityResult(GetContent()) {
|
||||
if (it != null) contentCallback?.invoke(it)
|
||||
if (it != null) contentCallback?.onActivityResult(it)
|
||||
contentCallback = null
|
||||
}
|
||||
|
||||
private var uninstallLatch = CountDownLatch(1)
|
||||
private val uninstallPkg = registerForActivityResult(UninstallPackage()) {
|
||||
uninstallLatch.countDown()
|
||||
private val mReferrerField by lazy(LazyThreadSafetyMode.NONE) {
|
||||
Activity::class.java.reflectField("mReferrer")
|
||||
}
|
||||
|
||||
override fun applyOverrideConfiguration(config: Configuration?) {
|
||||
// Force applying our preferred local
|
||||
config?.setLocale(currentLocale)
|
||||
super.applyOverrideConfiguration(config)
|
||||
val realCallingPackage: String? get() {
|
||||
callingPackage?.let { return it }
|
||||
mReferrerField.get(this)?.let { return it as String }
|
||||
return null
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
@@ -59,36 +68,50 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
// Overwrite private members to avoid nasty "false" stack traces being logged
|
||||
val delegate = delegate
|
||||
val clz = delegate.javaClass
|
||||
clz.reflectField("mActivityHandlesUiModeChecked").set(delegate, true)
|
||||
clz.reflectField("mActivityHandlesUiMode").set(delegate, false)
|
||||
clz.reflectField("mActivityHandlesConfigFlagsChecked").set(delegate, true)
|
||||
clz.reflectField("mActivityHandlesConfigFlags").set(delegate, 0)
|
||||
}
|
||||
contentCallback = savedInstanceState?.getParcelable(CONTENT_CALLBACK_KEY)
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
contentCallback?.let {
|
||||
outState.putParcelable(CONTENT_CALLBACK_KEY, it)
|
||||
}
|
||||
}
|
||||
|
||||
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
||||
if (permission == WRITE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= 30) {
|
||||
// We do not need external rw on 30+
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
|
||||
permission == WRITE_EXTERNAL_STORAGE) {
|
||||
// We do not need external rw on R+
|
||||
callback(true)
|
||||
return
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU &&
|
||||
permission == POST_NOTIFICATIONS) {
|
||||
// All apps have notification permissions before T
|
||||
callback(true)
|
||||
return
|
||||
}
|
||||
permissionCallback = callback
|
||||
if (permission == REQUEST_INSTALL_PACKAGES) {
|
||||
installCallback = callback
|
||||
requestInstall.launch(Unit)
|
||||
} else {
|
||||
permissionCallback = callback
|
||||
requestPermission.launch(permission)
|
||||
}
|
||||
}
|
||||
|
||||
fun getContent(type: String, callback: (Uri) -> Unit) {
|
||||
fun getContent(type: String, callback: ContentResultCallback) {
|
||||
contentCallback = callback
|
||||
getContent.launch(type)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun uninstallAndWait(pkg: String) {
|
||||
uninstallLatch = CountDownLatch(1)
|
||||
uninstallPkg.launch(pkg)
|
||||
uninstallLatch.await(3, TimeUnit.SECONDS)
|
||||
try {
|
||||
getContent.launch(type)
|
||||
callback.onActivityLaunch()
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
toast(R.string.app_not_found, Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
|
||||
override fun recreate() {
|
||||
@@ -100,4 +123,8 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
startActivity(Intent(intent).setFlags(0))
|
||||
finish()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTENT_CALLBACK_KEY = "content_callback"
|
||||
}
|
||||
}
|
||||
|
@@ -2,10 +2,10 @@ package com.topjohnwu.magisk.core.base
|
||||
|
||||
import android.app.job.JobService
|
||||
import android.content.Context
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import com.topjohnwu.magisk.core.patch
|
||||
|
||||
abstract class BaseJobService : JobService() {
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base.wrap())
|
||||
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,15 +2,13 @@ package com.topjohnwu.magisk.core.base
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import androidx.annotation.CallSuper
|
||||
import com.topjohnwu.magisk.core.patch
|
||||
|
||||
abstract class BaseReceiver : BroadcastReceiver() {
|
||||
|
||||
final override fun onReceive(context: Context, intent: Intent?) {
|
||||
onReceive(context.wrap() as ContextWrapper, intent)
|
||||
@CallSuper
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
context.patch()
|
||||
}
|
||||
|
||||
abstract fun onReceive(context: ContextWrapper, intent: Intent?)
|
||||
}
|
||||
|
@@ -2,10 +2,13 @@ package com.topjohnwu.magisk.core.base
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import com.topjohnwu.magisk.core.patch
|
||||
|
||||
abstract class BaseService : Service() {
|
||||
open class BaseService : Service() {
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base.wrap())
|
||||
super.attachBaseContext(base.patch())
|
||||
}
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.data.network
|
||||
package com.topjohnwu.magisk.core.data
|
||||
|
||||
import com.topjohnwu.magisk.core.model.BranchInfo
|
||||
import com.topjohnwu.magisk.core.model.ModuleJson
|
||||
@@ -12,15 +12,12 @@ private const val FILE = "file"
|
||||
|
||||
interface GithubPageServices {
|
||||
|
||||
@GET("{$FILE}")
|
||||
suspend fun fetchUpdateJSON(@Path(FILE) file: String): UpdateInfo
|
||||
@GET
|
||||
suspend fun fetchUpdateJSON(@Url file: String): UpdateInfo
|
||||
}
|
||||
|
||||
interface RawServices {
|
||||
|
||||
@GET
|
||||
suspend fun fetchCustomUpdate(@Url url: String): UpdateInfo
|
||||
|
||||
@GET
|
||||
@Streaming
|
||||
suspend fun fetchFile(@Url url: String): ResponseBody
|
@@ -1,15 +1,27 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
package com.topjohnwu.magisk.core.data
|
||||
|
||||
import androidx.room.*
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.topjohnwu.magisk.core.model.su.SuLog
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
|
||||
@Database(version = 1, entities = [SuLog::class], exportSchema = false)
|
||||
@Database(version = 2, entities = [SuLog::class], exportSchema = false)
|
||||
abstract class SuLogDatabase : RoomDatabase() {
|
||||
|
||||
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
|
@@ -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,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
package com.topjohnwu.magisk.core.di
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.Moshi
|
@@ -1,25 +1,18 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
package com.topjohnwu.magisk.core.di
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.text.method.LinkMovementMethod
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.ViewModelStoreOwner
|
||||
import androidx.room.Room
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
|
||||
import com.topjohnwu.magisk.core.magiskdb.StringDao
|
||||
import com.topjohnwu.magisk.data.database.SuLogDatabase
|
||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||
import com.topjohnwu.magisk.ktx.deviceProtectedContext
|
||||
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
|
||||
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
|
||||
|
||||
@@ -31,6 +24,7 @@ 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()
|
||||
@@ -47,32 +41,13 @@ object ServiceLocator {
|
||||
NetworkService(
|
||||
createApiService(retrofit, Const.Url.GITHUB_PAGE_URL),
|
||||
createApiService(retrofit, Const.Url.GITHUB_RAW_URL),
|
||||
createApiService(retrofit, Const.Url.GITHUB_API_URL)
|
||||
)
|
||||
}
|
||||
|
||||
object VMFactory : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return when (modelClass) {
|
||||
HomeViewModel::class.java -> HomeViewModel(networkService)
|
||||
LogViewModel::class.java -> LogViewModel(logRepo)
|
||||
SuperuserViewModel::class.java -> SuperuserViewModel(policyDB)
|
||||
InstallViewModel::class.java -> InstallViewModel(networkService)
|
||||
SuRequestViewModel::class.java -> SuRequestViewModel(policyDB, timeoutPrefs)
|
||||
else -> modelClass.newInstance()
|
||||
} as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified VM : ViewModel> ViewModelStoreOwner.viewModel() =
|
||||
lazy(LazyThreadSafetyMode.NONE) {
|
||||
ViewModelProvider(this, ServiceLocator.VMFactory)[VM::class.java]
|
||||
}
|
||||
|
||||
private fun createSuLogDatabase(context: Context) =
|
||||
Room.databaseBuilder(context, SuLogDatabase::class.java, "sulogs.db")
|
||||
.addMigrations(SuLogDatabase.MIGRATION_1_2)
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.*
|
||||
@@ -11,25 +12,28 @@ import androidx.core.net.toFile
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.*
|
||||
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.isRunningAsStub
|
||||
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.ktx.copyAndClose
|
||||
import com.topjohnwu.magisk.ktx.forEach
|
||||
import com.topjohnwu.magisk.ktx.withStreams
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
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
|
||||
|
||||
@@ -43,14 +47,12 @@ class DownloadService : NotificationService() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
private fun download(subject: Subject) {
|
||||
update(subject.notifyId)
|
||||
val coroutineScope = CoroutineScope(job + Dispatchers.IO)
|
||||
coroutineScope.launch {
|
||||
notifyUpdate(subject.notifyId)
|
||||
CoroutineScope(job + Dispatchers.IO).launch {
|
||||
try {
|
||||
val stream = service.fetchFile(subject.url).toProgressStream(subject)
|
||||
when (subject) {
|
||||
@@ -59,7 +61,7 @@ class DownloadService : NotificationService() {
|
||||
}
|
||||
val activity = ActivityTracker.foreground
|
||||
if (activity != null && subject.autoLaunch) {
|
||||
remove(subject.notifyId)
|
||||
notifyRemove(subject.notifyId)
|
||||
subject.pendingIntent(activity)?.send()
|
||||
} else {
|
||||
notifyFinish(subject)
|
||||
@@ -74,9 +76,9 @@ class DownloadService : NotificationService() {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleApp(stream: InputStream, subject: Subject.App) {
|
||||
private fun handleApp(stream: InputStream, subject: Subject.App) {
|
||||
fun writeTee(output: OutputStream) {
|
||||
val uri = MediaStoreUtils.getFile("${subject.title}.apk").uri
|
||||
val uri = MediaStoreUtils.getFile("${subject.title}.apk").uri
|
||||
val external = uri.outputStream()
|
||||
stream.copyAndClose(TeeOutputStream(external, output))
|
||||
}
|
||||
@@ -87,35 +89,34 @@ class DownloadService : NotificationService() {
|
||||
// Download full APK to stub update path
|
||||
writeTee(updateApk.outputStream())
|
||||
|
||||
if (Info.stub!!.version < subject.stub.versionCode) {
|
||||
val zf = ZipFile(updateApk)
|
||||
val prop = Properties()
|
||||
prop.load(ByteArrayInputStream(zf.comment.toByteArray()))
|
||||
val stubVersion = prop.getProperty("stubVersion").toIntOrNull() ?: -1
|
||||
if (Info.stub!!.version < stubVersion) {
|
||||
// Also upgrade stub
|
||||
update(subject.notifyId) {
|
||||
notifyUpdate(subject.notifyId) {
|
||||
it.setProgress(0, 0, true)
|
||||
.setContentTitle(getString(R.string.hide_app_title))
|
||||
.setContentText("")
|
||||
}
|
||||
|
||||
// Download
|
||||
// Extract stub
|
||||
val apk = subject.file.toFile()
|
||||
service.fetchFile(subject.stub.link).byteStream().writeTo(apk)
|
||||
zf.getInputStream(zf.getEntry("assets/stub.apk")).writeTo(apk)
|
||||
zf.close()
|
||||
|
||||
// Patch and install
|
||||
val session = APKInstall.startSession(this)
|
||||
session.openStream(this).use {
|
||||
val label = applicationInfo.nonLocalizedLabel
|
||||
if (!HideAPK.patch(this, apk, it, packageName, label)) {
|
||||
throw IOException("HideAPK patch error")
|
||||
}
|
||||
}
|
||||
subject.intent = HideAPK.upgrade(this, apk)
|
||||
?: throw IOException("HideAPK patch error")
|
||||
apk.delete()
|
||||
subject.intent = session.waitIntent()
|
||||
} 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 = Notifications.selfLaunchIntent(this)
|
||||
subject.intent = selfLaunchIntent()
|
||||
subject.postDownload = { Runtime.getRuntime().exit(0) }
|
||||
}
|
||||
return
|
||||
@@ -196,19 +197,23 @@ class DownloadService : NotificationService() {
|
||||
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 >= 26) {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
getForegroundService(context, REQUEST_CODE, intent, flag)
|
||||
} else {
|
||||
getService(context, REQUEST_CODE, intent, flag)
|
||||
}
|
||||
}
|
||||
|
||||
fun start(context: Context, subject: Subject) {
|
||||
val app = context.applicationContext
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
app.startForegroundService(intent(app, subject))
|
||||
} else {
|
||||
app.startService(intent(app, subject))
|
||||
@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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,13 +2,13 @@ package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
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.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.ktx.synchronized
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import okhttp3.ResponseBody
|
||||
import java.io.InputStream
|
||||
@@ -20,7 +20,7 @@ open class NotificationService : BaseService() {
|
||||
|
||||
protected val service get() = ServiceLocator.networkService
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
private var attachedNotificationId = 0
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
@@ -33,11 +33,11 @@ open class NotificationService : BaseService() {
|
||||
val total = max.toFloat() / 1048576
|
||||
val id = subject.notifyId
|
||||
|
||||
update(id) { it.setContentTitle(subject.title) }
|
||||
notifyUpdate(id) { it.setContentTitle(subject.title) }
|
||||
|
||||
return ProgressInputStream(byteStream()) {
|
||||
val progress = it.toFloat() / 1048576
|
||||
update(id) { notification ->
|
||||
notifyUpdate(id) { notification ->
|
||||
if (max > 0) {
|
||||
broadcast(progress / total, subject)
|
||||
notification
|
||||
@@ -52,7 +52,7 @@ open class NotificationService : BaseService() {
|
||||
}
|
||||
|
||||
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
|
||||
val notification = remove(id)?.also(editor) ?: return -1
|
||||
val notification = notifyRemove(id)?.also(editor) ?: return -1
|
||||
val newId = Notifications.nextId()
|
||||
Notifications.mgr.notify(newId, notification.build())
|
||||
return newId
|
||||
@@ -76,29 +76,44 @@ open class NotificationService : BaseService() {
|
||||
subject.pendingIntent(this)?.let { intent -> it.setContentIntent(intent) }
|
||||
}
|
||||
|
||||
private fun create() = Notifications.progress(this, "")
|
||||
private fun attachNotification(id: Int, notification: Notification) {
|
||||
attachedNotificationId = id
|
||||
startForeground(id, notification)
|
||||
}
|
||||
|
||||
private fun updateForeground() {
|
||||
private fun maybeDetachNotification(id: Int) : Boolean {
|
||||
if (attachedNotificationId != id) return false
|
||||
if (hasNotifications) {
|
||||
val (id, notification) = notifications.entries.first()
|
||||
startForeground(id, notification.build())
|
||||
} else {
|
||||
stopForeground(false)
|
||||
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 update(id: Int, editor: (Notification.Builder) -> Unit = {}) {
|
||||
protected fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit = {}) {
|
||||
fun create() = Notifications.startProgress("")
|
||||
|
||||
val wasEmpty = !hasNotifications
|
||||
val notification = notifications.getOrPut(id, ::create).also(editor)
|
||||
val notification = notifications.getOrPut(id, ::create).also(editor).build()
|
||||
if (wasEmpty)
|
||||
updateForeground()
|
||||
attachNotification(id, notification)
|
||||
else
|
||||
Notifications.mgr.notify(id, notification.build())
|
||||
Notifications.mgr.notify(id, notification)
|
||||
}
|
||||
|
||||
protected fun remove(id: Int): Notification.Builder? {
|
||||
val n = notifications.remove(id)?.also { updateForeground() }
|
||||
Notifications.mgr.cancel(id)
|
||||
protected fun notifyRemove(id: Int): Notification.Builder? {
|
||||
val n = notifications.remove(id)
|
||||
if (n == null || !maybeDetachNotification(id))
|
||||
Notifications.mgr.cancel(id)
|
||||
return n
|
||||
}
|
||||
|
||||
|
@@ -8,12 +8,11 @@ import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import androidx.core.net.toUri
|
||||
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.StubJson
|
||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
import com.topjohnwu.magisk.ktx.cachedFile
|
||||
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
@@ -59,7 +58,6 @@ sealed class Subject : Parcelable {
|
||||
@Parcelize
|
||||
class App(
|
||||
private val json: MagiskJson = Info.remote.magisk,
|
||||
val stub: StubJson = Info.remote.stub,
|
||||
override val notifyId: Int = Notifications.nextId()
|
||||
) : Subject() {
|
||||
override val title: String get() = "Magisk-${json.version}(${json.versionCode})"
|
||||
|
152
app/src/main/java/com/topjohnwu/magisk/core/ktx/XAndroid.kt
Normal file
152
app/src/main/java/com/topjohnwu/magisk/core/ktx/XAndroid.kt
Normal file
@@ -0,0 +1,152 @@
|
||||
package com.topjohnwu.magisk.core.ktx
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.*
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.AdaptiveIconDrawable
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Process
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.getSystemService
|
||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import java.io.File
|
||||
import kotlin.String
|
||||
|
||||
fun Context.rawResource(id: Int) = resources.openRawResource(id)
|
||||
|
||||
fun Context.getBitmap(id: Int): Bitmap {
|
||||
var drawable = AppCompatResources.getDrawable(this, id)!!
|
||||
if (drawable is BitmapDrawable)
|
||||
return drawable.bitmap
|
||||
if (SDK_INT >= Build.VERSION_CODES.O && drawable is AdaptiveIconDrawable) {
|
||||
drawable = LayerDrawable(arrayOf(drawable.background, drawable.foreground))
|
||||
}
|
||||
val bitmap = Bitmap.createBitmap(
|
||||
drawable.intrinsicWidth, drawable.intrinsicHeight,
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
val canvas = Canvas(bitmap)
|
||||
drawable.setBounds(0, 0, canvas.width, canvas.height)
|
||||
drawable.draw(canvas)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
val Context.deviceProtectedContext: Context get() =
|
||||
if (SDK_INT >= Build.VERSION_CODES.N) {
|
||||
createDeviceProtectedStorageContext()
|
||||
} else { this }
|
||||
|
||||
fun Context.cachedFile(name: String) = File(cacheDir, name)
|
||||
|
||||
fun ApplicationInfo.getLabel(pm: PackageManager): String {
|
||||
runCatching {
|
||||
if (labelRes > 0) {
|
||||
val res = pm.getResourcesForApplication(this)
|
||||
val config = Configuration()
|
||||
config.setLocale(currentLocale)
|
||||
res.updateConfiguration(config, res.displayMetrics)
|
||||
return res.getString(labelRes)
|
||||
}
|
||||
}
|
||||
|
||||
return loadLabel(pm).toString()
|
||||
}
|
||||
|
||||
fun Context.unwrap(): Context {
|
||||
var context = this
|
||||
while (context is ContextWrapper)
|
||||
context = context.baseContext
|
||||
return context
|
||||
}
|
||||
|
||||
fun Activity.hideKeyboard() {
|
||||
val view = currentFocus ?: return
|
||||
getSystemService<InputMethodManager>()
|
||||
?.hideSoftInputFromWindow(view.windowToken, 0)
|
||||
view.clearFocus()
|
||||
}
|
||||
|
||||
val View.activity: Activity get() {
|
||||
var context = context
|
||||
while(true) {
|
||||
if (context !is ContextWrapper)
|
||||
error("View is not attached to activity")
|
||||
if (context is Activity)
|
||||
return context
|
||||
context = context.baseContext
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
fun getProperty(key: String, def: String): String {
|
||||
runCatching {
|
||||
val clazz = Class.forName("android.os.SystemProperties")
|
||||
val get = clazz.getMethod("get", String::class.java, String::class.java)
|
||||
return get.invoke(clazz, key, def) as String
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
@Throws(PackageManager.NameNotFoundException::class)
|
||||
fun PackageManager.getPackageInfo(uid: Int, pid: Int): PackageInfo? {
|
||||
val flag = PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||
val pkgs = getPackagesForUid(uid) ?: throw PackageManager.NameNotFoundException()
|
||||
if (pkgs.size > 1) {
|
||||
if (pid <= 0) {
|
||||
return null
|
||||
}
|
||||
// Try to find package name from PID
|
||||
val proc = RootUtils.obj?.getAppProcess(pid)
|
||||
if (proc == null) {
|
||||
if (uid == Process.SHELL_UID) {
|
||||
// It is possible that some apps installed are sharing UID with shell.
|
||||
// We will not be able to find a package from the active process list,
|
||||
// because the client is forked from ADB shell, not any app process.
|
||||
return getPackageInfo("com.android.shell", flag)
|
||||
}
|
||||
} else if (uid == proc.uid) {
|
||||
return getPackageInfo(proc.pkgList[0], flag)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
if (pkgs.size == 1) {
|
||||
return getPackageInfo(pkgs[0], flag)
|
||||
}
|
||||
throw PackageManager.NameNotFoundException()
|
||||
}
|
||||
|
||||
fun Context.registerRuntimeReceiver(receiver: BroadcastReceiver, filter: IntentFilter) {
|
||||
APKInstall.registerReceiver(this, receiver, filter)
|
||||
}
|
||||
|
||||
fun Context.selfLaunchIntent(): Intent {
|
||||
val pm = packageManager
|
||||
val intent = pm.getLaunchIntentForPackage(packageName)!!
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
return intent
|
||||
}
|
||||
|
||||
fun Context.toast(msg: CharSequence, duration: Int) {
|
||||
UiThreadHandler.run { Toast.makeText(this, msg, duration).show() }
|
||||
}
|
||||
|
||||
fun Context.toast(resId: Int, duration: Int) {
|
||||
UiThreadHandler.run { Toast.makeText(this, resId, duration).show() }
|
||||
}
|
@@ -1,13 +1,17 @@
|
||||
package com.topjohnwu.magisk.ktx
|
||||
package com.topjohnwu.magisk.core.ktx
|
||||
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import timber.log.Timber
|
||||
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.*
|
||||
import java.util.Collections
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
@@ -45,8 +49,28 @@ fun <T> MutableSet<T>.synchronized(): MutableSet<T> = Collections.synchronizedSe
|
||||
|
||||
fun <K, V> MutableMap<K, V>.synchronized(): MutableMap<K, V> = Collections.synchronizedMap(this)
|
||||
|
||||
fun SimpleDateFormat.parseOrNull(date: String) =
|
||||
runCatching { parse(date) }.onFailure { Timber.e(it) }.getOrNull()
|
||||
|
||||
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,63 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||
import com.topjohnwu.magisk.core.model.su.toPolicy
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
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 : 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(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(AppContext.packageManager) }.getOrElse {
|
||||
Timber.w(it)
|
||||
val uid = getOrElse("uid") { 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.cmd(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.cmd(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
|
||||
|
||||
}
|
@@ -7,7 +7,6 @@ import kotlinx.parcelize.Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class UpdateInfo(
|
||||
val magisk: MagiskJson = MagiskJson(),
|
||||
val stub: StubJson = StubJson()
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
@@ -19,13 +18,6 @@ data class MagiskJson(
|
||||
val note: String = ""
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StubJson(
|
||||
val versionCode: Int = -1,
|
||||
val link: String = ""
|
||||
) : Parcelable
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ModuleJson(
|
||||
val version: String,
|
||||
|
@@ -2,9 +2,9 @@ package com.topjohnwu.magisk.core.model.module
|
||||
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
@@ -26,13 +26,12 @@ data class LocalModule(
|
||||
var outdated = false
|
||||
|
||||
private var updateUrl: String = ""
|
||||
private val removeFile = SuFile(path, "remove")
|
||||
private val disableFile = SuFile(path, "disable")
|
||||
private val updateFile = SuFile(path, "update")
|
||||
private val ruleFile = SuFile(path, "sepolicy.rule")
|
||||
private val riruFolder = SuFile(path, "riru")
|
||||
private val zygiskFolder = SuFile(path, "zygisk")
|
||||
private val unloaded = SuFile(zygiskFolder, "unloaded")
|
||||
private val removeFile = RootUtils.fs.getFile(path, "remove")
|
||||
private val disableFile = RootUtils.fs.getFile(path, "disable")
|
||||
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 isRiru: Boolean get() = (id == "riru-core") || riruFolder.exists()
|
||||
@@ -42,19 +41,12 @@ data class LocalModule(
|
||||
var enable: Boolean
|
||||
get() = !disableFile.exists()
|
||||
set(enable) {
|
||||
val dir = "$PERSIST/$id"
|
||||
if (enable) {
|
||||
disableFile.delete()
|
||||
if (Const.Version.atLeast_21_2())
|
||||
Shell.cmd("copy_sepolicy_rules").submit()
|
||||
else
|
||||
Shell.cmd("mkdir -p $dir", "cp -af $ruleFile $dir").submit()
|
||||
Shell.cmd("copy_preinit_files").submit()
|
||||
} else {
|
||||
!disableFile.createNewFile()
|
||||
if (Const.Version.atLeast_21_2())
|
||||
Shell.cmd("copy_sepolicy_rules").submit()
|
||||
else
|
||||
Shell.cmd("rm -rf $dir").submit()
|
||||
Shell.cmd("copy_preinit_files").submit()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,16 +56,10 @@ data class LocalModule(
|
||||
if (remove) {
|
||||
if (updateFile.exists()) return
|
||||
removeFile.createNewFile()
|
||||
if (Const.Version.atLeast_21_2())
|
||||
Shell.cmd("copy_sepolicy_rules").submit()
|
||||
else
|
||||
Shell.cmd("rm -rf $PERSIST/$id").submit()
|
||||
Shell.cmd("copy_preinit_files").submit()
|
||||
} else {
|
||||
removeFile.delete()
|
||||
if (Const.Version.atLeast_21_2())
|
||||
Shell.cmd("copy_sepolicy_rules").submit()
|
||||
else
|
||||
Shell.cmd("cp -af $ruleFile $PERSIST/$id").submit()
|
||||
Shell.cmd("copy_preinit_files").submit()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,13 +122,13 @@ data class LocalModule(
|
||||
|
||||
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) {
|
||||
SuFile(Const.MAGISK_PATH)
|
||||
RootUtils.fs.getFile(Const.MAGISK_PATH)
|
||||
.listFiles()
|
||||
.orEmpty()
|
||||
.filter { !it.isFile }
|
||||
.filter { !it.isFile && !it.isHidden }
|
||||
.map { LocalModule("${Const.MAGISK_PATH}/${it.name}") }
|
||||
.sortedBy { it.name.lowercase(Locale.ROOT) }
|
||||
}
|
||||
|
@@ -1,19 +1,73 @@
|
||||
package com.topjohnwu.magisk.core.model.su
|
||||
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.topjohnwu.magisk.ktx.now
|
||||
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||
|
||||
@Entity(tableName = "logs")
|
||||
data class SuLog(
|
||||
class SuLog(
|
||||
val fromUid: Int,
|
||||
val toUid: Int,
|
||||
val fromPid: Int,
|
||||
val packageName: String,
|
||||
val appName: String,
|
||||
val command: String,
|
||||
val action: Boolean,
|
||||
val time: Long = now
|
||||
val action: Int,
|
||||
val target: Int,
|
||||
val context: String,
|
||||
val gids: String,
|
||||
val time: Long = System.currentTimeMillis()
|
||||
) {
|
||||
@PrimaryKey(autoGenerate = true) var id: Int = 0
|
||||
}
|
||||
|
||||
fun PackageManager.createSuLog(
|
||||
info: PackageInfo,
|
||||
toUid: Int,
|
||||
fromPid: Int,
|
||||
command: String,
|
||||
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,85 +1,22 @@
|
||||
@file:SuppressLint("InlinedApi")
|
||||
|
||||
package com.topjohnwu.magisk.core.model.su
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Drawable
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.INTERACTIVE
|
||||
import com.topjohnwu.magisk.ktx.getLabel
|
||||
|
||||
data class SuPolicy(
|
||||
val 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
|
||||
) {
|
||||
|
||||
class SuPolicy(val uid: Int) {
|
||||
companion object {
|
||||
const val INTERACTIVE = 0
|
||||
const val DENY = 1
|
||||
const val ALLOW = 2
|
||||
}
|
||||
|
||||
fun toLog(toUid: Int, fromPid: Int, command: String) = SuLog(
|
||||
uid, toUid, fromPid, packageName, appName,
|
||||
command, policy == ALLOW)
|
||||
var policy: Int = INTERACTIVE
|
||||
var until: Long = -1L
|
||||
var logging: Boolean = true
|
||||
var notification: Boolean = true
|
||||
|
||||
fun toMap() = mapOf(
|
||||
fun toMap(): MutableMap<String, Any> = mutableMapOf(
|
||||
"uid" to uid,
|
||||
"package_name" to packageName,
|
||||
"policy" to policy,
|
||||
"until" to until,
|
||||
"logging" to logging,
|
||||
"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.MATCH_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.MATCH_UNINSTALLED_PACKAGES)
|
||||
return SuPolicy(
|
||||
uid = info.uid,
|
||||
packageName = pkg,
|
||||
appName = info.getLabel(pm),
|
||||
icon = info.loadIcon(pm),
|
||||
policy = policy
|
||||
)
|
||||
}
|
||||
|
||||
fun Int.toUidPolicy(pm: PackageManager, policy: Int): SuPolicy {
|
||||
return SuPolicy(
|
||||
uid = this,
|
||||
packageName = "[UID] $this",
|
||||
appName = "[UID] $this",
|
||||
icon = pm.defaultActivityIcon,
|
||||
policy = policy
|
||||
)
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
package com.topjohnwu.magisk.core.repository
|
||||
|
||||
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
|
||||
import com.topjohnwu.magisk.core.magiskdb.StringDao
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import com.topjohnwu.magisk.core.data.magiskdb.SettingsDao
|
||||
import com.topjohnwu.magisk.core.data.magiskdb.StringDao
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
@@ -11,26 +11,27 @@ import kotlin.reflect.KProperty
|
||||
interface DBConfig {
|
||||
val settingsDB: SettingsDao
|
||||
val stringDB: StringDao
|
||||
val coroutineScope: CoroutineScope
|
||||
|
||||
fun dbSettings(
|
||||
name: String,
|
||||
default: Int
|
||||
) = DBSettingsValue(name, default)
|
||||
) = IntDBProperty(name, default)
|
||||
|
||||
fun dbSettings(
|
||||
name: String,
|
||||
default: Boolean
|
||||
) = DBBoolSettings(name, default)
|
||||
) = BoolDBProperty(name, default)
|
||||
|
||||
fun dbStrings(
|
||||
name: String,
|
||||
default: String,
|
||||
sync: Boolean = false
|
||||
) = DBStringsValue(name, default, sync)
|
||||
) = StringDBProperty(name, default, sync)
|
||||
|
||||
}
|
||||
|
||||
class DBSettingsValue(
|
||||
class IntDBProperty(
|
||||
private val name: String,
|
||||
private val default: Int
|
||||
) : ReadWriteProperty<DBConfig, Int> {
|
||||
@@ -48,18 +49,18 @@ class DBSettingsValue(
|
||||
synchronized(this) {
|
||||
this.value = value
|
||||
}
|
||||
GlobalScope.launch {
|
||||
thisRef.coroutineScope.launch {
|
||||
thisRef.settingsDB.put(name, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class DBBoolSettings(
|
||||
open class BoolDBProperty(
|
||||
name: String,
|
||||
default: 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 =
|
||||
base.getValue(thisRef, property) != 0
|
||||
@@ -68,10 +69,10 @@ open class DBBoolSettings(
|
||||
base.setValue(thisRef, property, if (value) 1 else 0)
|
||||
}
|
||||
|
||||
class DBBoolSettingsNoWrite(
|
||||
class BoolDBPropertyNoWrite(
|
||||
name: String,
|
||||
default: Boolean
|
||||
) : DBBoolSettings(name, default) {
|
||||
) : BoolDBProperty(name, default) {
|
||||
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Boolean) {
|
||||
synchronized(base) {
|
||||
base.value = if (value) 1 else 0
|
||||
@@ -79,7 +80,7 @@ class DBBoolSettingsNoWrite(
|
||||
}
|
||||
}
|
||||
|
||||
class DBStringsValue(
|
||||
class StringDBProperty(
|
||||
private val name: String,
|
||||
private val default: String,
|
||||
private val sync: Boolean
|
||||
@@ -106,7 +107,7 @@ class DBStringsValue(
|
||||
thisRef.stringDB.delete(name)
|
||||
}
|
||||
} else {
|
||||
GlobalScope.launch {
|
||||
thisRef.coroutineScope.launch {
|
||||
thisRef.stringDB.delete(name)
|
||||
}
|
||||
}
|
||||
@@ -116,7 +117,7 @@ class DBStringsValue(
|
||||
thisRef.stringDB.put(name, value)
|
||||
}
|
||||
} else {
|
||||
GlobalScope.launch {
|
||||
thisRef.coroutineScope.launch {
|
||||
thisRef.stringDB.put(name, value)
|
||||
}
|
||||
}
|
@@ -1,10 +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.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.data.database.SuLogDao
|
||||
import com.topjohnwu.magisk.ktx.await
|
||||
import com.topjohnwu.superuser.Shell
|
||||
|
||||
|
@@ -1,35 +1,34 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
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.data.network.GithubApiServices
|
||||
import com.topjohnwu.magisk.data.network.GithubPageServices
|
||||
import com.topjohnwu.magisk.data.network.RawServices
|
||||
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,
|
||||
private val api: GithubApiServices
|
||||
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 == DEFAULT_CHANNEL) {
|
||||
Config.updateChannel = BETA_CHANNEL
|
||||
info = fetchBetaUpdate()
|
||||
}
|
||||
@@ -40,11 +39,15 @@ class NetworkService(
|
||||
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 fetchCustomUpdate(url: String) = raw.fetchCustomUpdate(url)
|
||||
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 {
|
||||
factory()
|
||||
if (Info.isConnected.value == true)
|
||||
factory()
|
||||
else
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
null
|
@@ -1,11 +1,73 @@
|
||||
package com.topjohnwu.magisk.data.preference
|
||||
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
|
||||
|
||||
abstract class Property {
|
||||
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)
|
||||
@@ -27,10 +89,10 @@ class BooleanProperty(
|
||||
private val name: String,
|
||||
private val default: Boolean,
|
||||
private val commit: Boolean
|
||||
) : Property(), ReadWriteProperty<PreferenceModel, Boolean> {
|
||||
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Boolean> {
|
||||
|
||||
override operator fun getValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>
|
||||
): Boolean {
|
||||
val prefName = name.ifBlank { property.name }
|
||||
@@ -38,7 +100,7 @@ class BooleanProperty(
|
||||
}
|
||||
|
||||
override operator fun setValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>,
|
||||
value: Boolean
|
||||
) {
|
||||
@@ -51,10 +113,10 @@ class FloatProperty(
|
||||
private val name: String,
|
||||
private val default: Float,
|
||||
private val commit: Boolean
|
||||
) : Property(), ReadWriteProperty<PreferenceModel, Float> {
|
||||
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Float> {
|
||||
|
||||
override operator fun getValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>
|
||||
): Float {
|
||||
val prefName = name.ifBlank { property.name }
|
||||
@@ -62,7 +124,7 @@ class FloatProperty(
|
||||
}
|
||||
|
||||
override operator fun setValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>,
|
||||
value: Float
|
||||
) {
|
||||
@@ -75,10 +137,10 @@ class IntProperty(
|
||||
private val name: String,
|
||||
private val default: Int,
|
||||
private val commit: Boolean
|
||||
) : Property(), ReadWriteProperty<PreferenceModel, Int> {
|
||||
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Int> {
|
||||
|
||||
override operator fun getValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>
|
||||
): Int {
|
||||
val prefName = name.ifBlank { property.name }
|
||||
@@ -86,7 +148,7 @@ class IntProperty(
|
||||
}
|
||||
|
||||
override operator fun setValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>,
|
||||
value: Int
|
||||
) {
|
||||
@@ -99,10 +161,10 @@ class LongProperty(
|
||||
private val name: String,
|
||||
private val default: Long,
|
||||
private val commit: Boolean
|
||||
) : Property(), ReadWriteProperty<PreferenceModel, Long> {
|
||||
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Long> {
|
||||
|
||||
override operator fun getValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>
|
||||
): Long {
|
||||
val prefName = name.ifBlank { property.name }
|
||||
@@ -110,7 +172,7 @@ class LongProperty(
|
||||
}
|
||||
|
||||
override operator fun setValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>,
|
||||
value: Long
|
||||
) {
|
||||
@@ -123,10 +185,10 @@ class StringProperty(
|
||||
private val name: String,
|
||||
private val default: String,
|
||||
private val commit: Boolean
|
||||
) : Property(), ReadWriteProperty<PreferenceModel, String> {
|
||||
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, String> {
|
||||
|
||||
override operator fun getValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>
|
||||
): String {
|
||||
val prefName = name.ifBlank { property.name }
|
||||
@@ -134,7 +196,7 @@ class StringProperty(
|
||||
}
|
||||
|
||||
override operator fun setValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>,
|
||||
value: String
|
||||
) {
|
||||
@@ -147,10 +209,10 @@ class StringSetProperty(
|
||||
private val name: String,
|
||||
private val default: Set<String>,
|
||||
private val commit: Boolean
|
||||
) : Property(), ReadWriteProperty<PreferenceModel, Set<String>> {
|
||||
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Set<String>> {
|
||||
|
||||
override operator fun getValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>
|
||||
): Set<String> {
|
||||
val prefName = name.ifBlank { property.name }
|
||||
@@ -158,7 +220,7 @@ class StringSetProperty(
|
||||
}
|
||||
|
||||
override operator fun setValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>,
|
||||
value: Set<String>
|
||||
) {
|
@@ -6,13 +6,13 @@ import android.widget.Toast
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
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.toPolicy
|
||||
import com.topjohnwu.magisk.core.model.su.toUidPolicy
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import com.topjohnwu.magisk.core.model.su.createSuLog
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import timber.log.Timber
|
||||
|
||||
object SuCallbackHandler {
|
||||
@@ -53,59 +53,50 @@ object SuCallbackHandler {
|
||||
private fun handleLogging(context: Context, data: Bundle) {
|
||||
val fromUid = data.getIntComp("from.uid", -1)
|
||||
val notify = data.getBoolean("notify", true)
|
||||
val allow = data.getIntComp("policy", SuPolicy.ALLOW)
|
||||
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 policy = runCatching {
|
||||
fromUid.toPolicy(pm, allow)
|
||||
}.getOrElse {
|
||||
GlobalScope.launch { ServiceLocator.policyDB.delete(fromUid) }
|
||||
fromUid.toUidPolicy(pm, allow)
|
||||
}
|
||||
val log = runCatching {
|
||||
pm.getPackageInfo(fromUid, pid)?.let {
|
||||
pm.createSuLog(it, toUid, pid, command, policy, target, seContext, gids)
|
||||
}
|
||||
}.getOrNull() ?: createSuLog(fromUid, toUid, pid, command, policy, target, seContext, gids)
|
||||
|
||||
if (notify)
|
||||
notify(context, policy)
|
||||
notify(context, log.action == SuPolicy.ALLOW, log.appName)
|
||||
|
||||
val toUid = data.getIntComp("to.uid", -1)
|
||||
val pid = data.getIntComp("pid", -1)
|
||||
|
||||
val command = data.getString("command", "")
|
||||
val log = policy.toLog(
|
||||
toUid = toUid,
|
||||
fromPid = pid,
|
||||
command = command
|
||||
)
|
||||
|
||||
GlobalScope.launch {
|
||||
ServiceLocator.logRepo.insert(log)
|
||||
}
|
||||
runBlocking { ServiceLocator.logRepo.insert(log) }
|
||||
}
|
||||
|
||||
private fun handleNotify(context: Context, data: Bundle) {
|
||||
val fromUid = data.getIntComp("from.uid", -1)
|
||||
val allow = data.getIntComp("policy", SuPolicy.ALLOW)
|
||||
val uid = data.getIntComp("from.uid", -1)
|
||||
val pid = data.getIntComp("pid", -1)
|
||||
val policy = data.getIntComp("policy", SuPolicy.ALLOW)
|
||||
|
||||
val pm = context.packageManager
|
||||
|
||||
val appName = runCatching {
|
||||
pm.getPackageInfo(uid, pid)?.applicationInfo?.getLabel(pm)
|
||||
}.getOrNull() ?: "[UID] $uid"
|
||||
|
||||
val policy = runCatching {
|
||||
fromUid.toPolicy(pm, allow)
|
||||
}.getOrElse {
|
||||
GlobalScope.launch { ServiceLocator.policyDB.delete(fromUid) }
|
||||
fromUid.toUidPolicy(pm, allow)
|
||||
}
|
||||
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 (Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
|
||||
val resId = if (policy.policy == SuPolicy.ALLOW)
|
||||
val resId = if (granted)
|
||||
R.string.su_allow_toast
|
||||
else
|
||||
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,32 +1,31 @@
|
||||
package com.topjohnwu.magisk.core.su
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.ktx.getPackageInfo
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||
import com.topjohnwu.magisk.core.model.su.toPolicy
|
||||
import com.topjohnwu.magisk.ktx.now
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.Closeable
|
||||
import java.io.DataOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class SuRequestHandler(
|
||||
private val pm: PackageManager,
|
||||
val pm: PackageManager,
|
||||
private val policyDB: PolicyDao
|
||||
) : Closeable {
|
||||
) {
|
||||
|
||||
private lateinit var output: DataOutputStream
|
||||
lateinit var policy: SuPolicy
|
||||
private lateinit var output: File
|
||||
private lateinit var policy: SuPolicy
|
||||
lateinit var pkgInfo: PackageInfo
|
||||
private set
|
||||
|
||||
// Return true to indicate undetermined policy, require user interaction
|
||||
@@ -35,8 +34,8 @@ class SuRequestHandler(
|
||||
return false
|
||||
|
||||
// Never allow com.topjohnwu.magisk (could be malware)
|
||||
if (policy.packageName == BuildConfig.APPLICATION_ID) {
|
||||
Shell.cmd("(pm uninstall ${BuildConfig.APPLICATION_ID})& >/dev/null 2>&1").exec()
|
||||
if (pkgInfo.packageName == BuildConfig.APPLICATION_ID) {
|
||||
Shell.cmd("(pm uninstall ${BuildConfig.APPLICATION_ID} >/dev/null 2>&1)&").exec()
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -54,52 +53,50 @@ class SuRequestHandler(
|
||||
return true
|
||||
}
|
||||
|
||||
@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 {
|
||||
val name = intent.getStringExtra("fifo") ?: throw SuRequestError()
|
||||
val uid = intent.getIntExtra("uid", -1).also { if (it < 0) throw SuRequestError() }
|
||||
output = DataOutputStream(FileOutputStream(name).buffered())
|
||||
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
|
||||
}
|
||||
private suspend fun init(intent: Intent): Boolean {
|
||||
val uid = intent.getIntExtra("uid", -1)
|
||||
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)
|
||||
try {
|
||||
pkgInfo = pm.getPackageInfo(uid, pid) ?: PackageInfo().apply {
|
||||
val name = pm.getNameForUid(uid) ?: throw PackageManager.NameNotFoundException()
|
||||
// We only fill in sharedUserId and leave other fields uninitialized
|
||||
sharedUserId = name.split(":")[0]
|
||||
}
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
Timber.e(e)
|
||||
respond(SuPolicy.DENY, -1)
|
||||
return false
|
||||
}
|
||||
return output.canWrite()
|
||||
}
|
||||
|
||||
fun respond(action: Int, time: Int) {
|
||||
suspend fun respond(action: Int, time: Int) {
|
||||
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
|
||||
time.toLong()
|
||||
|
||||
policy.policy = action
|
||||
policy.until = until
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
output.writeInt(policy.policy)
|
||||
output.flush()
|
||||
DataOutputStream(FileOutputStream(output)).use {
|
||||
it.writeInt(policy.policy)
|
||||
it.flush()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
} finally {
|
||||
runCatching { close() }
|
||||
if (until >= 0)
|
||||
policyDB.update(policy)
|
||||
}
|
||||
if (until >= 0) {
|
||||
policyDB.update(policy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -3,11 +3,11 @@ package com.topjohnwu.magisk.core.tasks
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toFile
|
||||
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.inputStream
|
||||
import com.topjohnwu.magisk.core.utils.unzip
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@@ -4,22 +4,21 @@ import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
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.Keygen
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.ktx.await
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import com.topjohnwu.magisk.signing.JarMap
|
||||
import com.topjohnwu.magisk.signing.SignApk
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Runnable
|
||||
@@ -30,6 +29,7 @@ import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.security.SecureRandom
|
||||
import kotlin.random.asKotlinRandom
|
||||
|
||||
object HideAPK {
|
||||
|
||||
@@ -39,8 +39,7 @@ object HideAPK {
|
||||
|
||||
// Some arbitrary limit
|
||||
const val MAX_LABEL_LENGTH = 32
|
||||
|
||||
private val svc get() = ServiceLocator.networkService
|
||||
const val PLACEHOLDER = "COMPONENT_PLACEHOLDER"
|
||||
|
||||
private fun genPackageName(): String {
|
||||
val random = SecureRandom()
|
||||
@@ -65,24 +64,91 @@ object HideAPK {
|
||||
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,
|
||||
apk: File, out: OutputStream,
|
||||
pkg: String, label: CharSequence
|
||||
): Boolean {
|
||||
val info = context.packageManager.getPackageArchiveInfo(apk.path, 0) ?: return false
|
||||
val name = info.applicationInfo.nonLocalizedLabel.toString()
|
||||
val origLabel = info.applicationInfo.nonLocalizedLabel.toString()
|
||||
try {
|
||||
JarMap.open(apk, true).use { jar ->
|
||||
val je = jar.getJarEntry(ANDROID_MANIFEST)
|
||||
val xml = AXML(jar.getRawData(je))
|
||||
val generator = classNameGenerator()
|
||||
|
||||
if (!xml.findAndPatch(APPLICATION_ID to pkg, name to label.toString()))
|
||||
if (!xml.patchStrings {
|
||||
for (i in it.indices) {
|
||||
val s = it[i]
|
||||
if (s.contains(APPLICATION_ID)) {
|
||||
it[i] = s.replace(APPLICATION_ID, pkg)
|
||||
} else if (s.contains(PLACEHOLDER)) {
|
||||
it[i] = generator.next()
|
||||
} else if (s == origLabel) {
|
||||
it[i] = label.toString()
|
||||
}
|
||||
}
|
||||
}) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Write apk changes
|
||||
jar.getOutputStream(je).use { it.write(xml.bytes) }
|
||||
val keys = Keygen(context)
|
||||
val keys = Keygen()
|
||||
SignApk.sign(keys.cert, keys.key, jar, out)
|
||||
return true
|
||||
}
|
||||
@@ -94,26 +160,21 @@ object HideAPK {
|
||||
|
||||
private fun launchApp(activity: Activity, pkg: String) {
|
||||
val intent = activity.packageManager.getLaunchIntentForPackage(pkg) ?: return
|
||||
Config.suManager = if (pkg == APPLICATION_ID) "" else pkg
|
||||
val self = activity.packageName
|
||||
val flag = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
activity.grantUriPermission(pkg, Provider.APK_URI(self), flag)
|
||||
activity.grantUriPermission(pkg, Provider.PREFS_URI(self), flag)
|
||||
activity.grantUriPermission(pkg, Provider.preferencesUri(self), flag)
|
||||
intent.putExtra(Const.Key.PREV_PKG, self)
|
||||
activity.startActivity(intent)
|
||||
activity.finish()
|
||||
}
|
||||
|
||||
private suspend fun patchAndHide(activity: Activity, label: String, onFailure: Runnable): Boolean {
|
||||
private fun patchAndHide(activity: Activity, label: String, onFailure: Runnable): Boolean {
|
||||
val stub = File(activity.cacheDir, "stub.apk")
|
||||
try {
|
||||
svc.fetchFile(Info.remote.stub.link).byteStream().writeTo(stub)
|
||||
activity.assets.open("stub.apk").writeTo(stub)
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
stub.createNewFile()
|
||||
val cmd = "\$MAGISKBIN/magiskinit -x manager ${stub.path}"
|
||||
if (!Shell.cmd(cmd).exec().isSuccess)
|
||||
return false
|
||||
return false
|
||||
}
|
||||
|
||||
// Generate a new random package name and signature
|
||||
@@ -129,8 +190,9 @@ object HideAPK {
|
||||
launchApp(activity, pkg)
|
||||
}
|
||||
|
||||
val cmd = "adb_pm_install $repack ${activity.applicationInfo.uid}"
|
||||
if (Shell.su(cmd).exec().isSuccess) return true
|
||||
Config.suManager = pkg
|
||||
val cmd = "adb_pm_install $repack $pkg"
|
||||
if (Shell.cmd(cmd).exec().isSuccess) return true
|
||||
|
||||
try {
|
||||
session.install(activity, repack)
|
||||
@@ -152,7 +214,7 @@ object HideAPK {
|
||||
}
|
||||
val onFailure = Runnable {
|
||||
dialog.dismiss()
|
||||
Utils.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||
activity.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||
}
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
patchAndHide(activity, label, onFailure)
|
||||
@@ -170,15 +232,16 @@ object HideAPK {
|
||||
}
|
||||
val onFailure = Runnable {
|
||||
dialog.dismiss()
|
||||
Utils.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||
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()
|
||||
}
|
||||
val cmd = "adb_pm_install $apk ${activity.applicationInfo.uid}"
|
||||
if (Shell.su(cmd).await().isSuccess) return
|
||||
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)
|
||||
@@ -191,4 +254,17 @@ object HideAPK {
|
||||
}
|
||||
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,30 +1,36 @@
|
||||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import android.net.Uri
|
||||
import android.system.ErrnoException
|
||||
import android.system.Os
|
||||
import android.system.OsConstants
|
||||
import android.system.OsConstants.O_WRONLY
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.os.postDelayed
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.*
|
||||
import com.topjohnwu.magisk.core.AppApkPath
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.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.inputStream
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.ktx.reboot
|
||||
import com.topjohnwu.magisk.ktx.withStreams
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import com.topjohnwu.magisk.signing.SignBoot
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import com.topjohnwu.superuser.internal.NOPList
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
||||
import com.topjohnwu.superuser.nio.ExtendedFile
|
||||
import com.topjohnwu.superuser.nio.FileSystemManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.jpountz.lz4.LZ4FrameInputStream
|
||||
@@ -37,29 +43,34 @@ import java.io.*
|
||||
import java.nio.ByteBuffer
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
abstract class MagiskInstallImpl protected constructor(
|
||||
protected val console: MutableList<String> = NOPList.getInstance(),
|
||||
private val logs: MutableList<String> = NOPList.getInstance()
|
||||
) {
|
||||
|
||||
protected var installDir = File("xxx")
|
||||
private lateinit var srcBoot: File
|
||||
protected lateinit var installDir: ExtendedFile
|
||||
private lateinit var srcBoot: ExtendedFile
|
||||
|
||||
private val shell = Shell.getShell()
|
||||
private val service get() = ServiceLocator.networkService
|
||||
protected val context get() = ServiceLocator.deContext
|
||||
private val useRootDir = shell.isRoot && Info.noDataExec
|
||||
|
||||
private val rootFS get() = RootUtils.fs
|
||||
private val localFS get() = FileSystemManager.getLocal()
|
||||
|
||||
private fun findImage(): Boolean {
|
||||
val bootPath = "find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
|
||||
val bootPath = "RECOVERYMODE=${Config.recovery} find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
|
||||
if (bootPath.isEmpty()) {
|
||||
console.add("! Unable to detect target image")
|
||||
return false
|
||||
}
|
||||
srcBoot = SuFile(bootPath)
|
||||
srcBoot = rootFS.getFile(bootPath)
|
||||
console.add("- Target image: $bootPath")
|
||||
return true
|
||||
}
|
||||
@@ -77,7 +88,7 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
console.add("! Unable to detect target image")
|
||||
return false
|
||||
}
|
||||
srcBoot = SuFile(bootPath)
|
||||
srcBoot = rootFS.getFile(bootPath)
|
||||
console.add("- Target image: $bootPath")
|
||||
return true
|
||||
}
|
||||
@@ -86,7 +97,7 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
console.add("- Device platform: ${Const.CPU_ABI}")
|
||||
console.add("- Installing: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
||||
|
||||
installDir = File(context.filesDir.parent, "install")
|
||||
installDir = localFS.getFile(context.filesDir.parent, "install")
|
||||
installDir.deleteRecursively()
|
||||
installDir.mkdirs()
|
||||
|
||||
@@ -107,7 +118,9 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
val name = n.substring(3, n.length - 3)
|
||||
val dest = File(installDir, name)
|
||||
zf.getInputStream(it).writeTo(dest)
|
||||
dest.setExecutable(true)
|
||||
}
|
||||
zf.close()
|
||||
} else {
|
||||
val info = context.applicationInfo
|
||||
var libs = File(info.nativeLibraryDir).listFiles { _, name ->
|
||||
@@ -115,7 +128,8 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
} ?: emptyArray()
|
||||
|
||||
// Also symlink magisk32 on non 64-bit only 64-bit devices
|
||||
val lib32 = info.javaClass.getDeclaredField("secondaryNativeLibraryDir").get(info) as String?
|
||||
val lib32 = info.javaClass.getDeclaredField("secondaryNativeLibraryDir")
|
||||
.get(info) as String?
|
||||
if (lib32 != null) {
|
||||
libs += File(lib32, "libmagisk32.so")
|
||||
}
|
||||
@@ -127,7 +141,7 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
}
|
||||
|
||||
// Extract scripts
|
||||
for (script in listOf("util_functions.sh", "boot_patch.sh", "addon.d.sh")) {
|
||||
for (script in listOf("util_functions.sh", "boot_patch.sh", "addon.d.sh", "stub.apk")) {
|
||||
val dest = File(installDir, script)
|
||||
context.assets.open(script).writeTo(dest)
|
||||
}
|
||||
@@ -146,7 +160,7 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
|
||||
if (useRootDir) {
|
||||
// Move everything to tmpfs to workaround Samsung bullshit
|
||||
SuFile(Const.TMPDIR).also {
|
||||
rootFS.getFile(Const.TMPDIR).also {
|
||||
arrayOf(
|
||||
"rm -rf $it",
|
||||
"mkdir -p $it",
|
||||
@@ -160,105 +174,211 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
return true
|
||||
}
|
||||
|
||||
// Optimization for SuFile I/O streams to skip an internal trial and error
|
||||
private fun installDirFile(name: String): File {
|
||||
return if (useRootDir)
|
||||
SuFile(installDir, name)
|
||||
else
|
||||
File(installDir, name)
|
||||
}
|
||||
|
||||
private fun InputStream.cleanPump(out: OutputStream) = withStreams(this, out) { src, _ ->
|
||||
src.copyTo(out)
|
||||
}
|
||||
private fun InputStream.copyAndCloseOut(out: OutputStream) = out.use { copyTo(it) }
|
||||
|
||||
private fun newTarEntry(name: String, size: Long): TarEntry {
|
||||
console.add("-- Writing: $name")
|
||||
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)
|
||||
private fun processTar(input: InputStream, output: OutputStream): OutputStream {
|
||||
private fun processTar(tarIn: TarInputStream, tarOut: TarOutputStream): ExtendedFile {
|
||||
console.add("- Processing tar file")
|
||||
val tarOut = TarOutputStream(output)
|
||||
TarInputStream(input).use { tarIn ->
|
||||
lateinit var entry: TarEntry
|
||||
lateinit var entry: TarEntry
|
||||
|
||||
fun decompressedStream(): InputStream {
|
||||
val src = if (entry.name.endsWith(".lz4")) LZ4FrameInputStream(tarIn) else tarIn
|
||||
return object : FilterInputStream(src) {
|
||||
override fun available() = 0 /* Workaround bug in LZ4FrameInputStream */
|
||||
override fun close() { /* Never close src stream */ }
|
||||
}
|
||||
}
|
||||
fun decompressedStream(): InputStream {
|
||||
return if (entry.name.endsWith(".lz4")) LZ4InputStream(tarIn) else tarIn
|
||||
}
|
||||
|
||||
while (tarIn.nextEntry?.let { entry = it } != null) {
|
||||
if (entry.name.startsWith("boot.img") ||
|
||||
(Config.recovery && entry.name.contains("recovery.img"))) {
|
||||
val name = entry.name.replace(".lz4", "")
|
||||
console.add("-- Extracting: $name")
|
||||
while (tarIn.nextEntry?.let { entry = it } != null) {
|
||||
if (entry.name.startsWith("boot.img") ||
|
||||
entry.name.startsWith("init_boot.img") ||
|
||||
(Config.recovery && entry.name.contains("recovery.img"))) {
|
||||
val name = entry.name.replace(".lz4", "")
|
||||
console.add("-- Extracting: $name")
|
||||
|
||||
val extract = installDirFile(name)
|
||||
decompressedStream().cleanPump(SuFileOutputStream.open(extract))
|
||||
} else if (entry.name.contains("vbmeta.img")) {
|
||||
val rawData = decompressedStream().readBytes()
|
||||
// Valid vbmeta.img should be at least 256 bytes
|
||||
if (rawData.size < 256)
|
||||
continue
|
||||
val extract = installDir.getChildFile(name)
|
||||
decompressedStream().copyAndCloseOut(extract.newOutputStream())
|
||||
} else if (entry.name.contains("vbmeta.img")) {
|
||||
val rawData = decompressedStream().readBytes()
|
||||
// Valid vbmeta.img should be at least 256 bytes
|
||||
if (rawData.size < 256)
|
||||
continue
|
||||
|
||||
// Patch flags to AVB_VBMETA_IMAGE_FLAGS_HASHTREE_DISABLED |
|
||||
// AVB_VBMETA_IMAGE_FLAGS_VERIFICATION_DISABLED
|
||||
console.add("-- Patching: vbmeta.img")
|
||||
ByteBuffer.wrap(rawData).putInt(120, 3)
|
||||
tarOut.putNextEntry(newTarEntry("vbmeta.img", rawData.size.toLong()))
|
||||
tarOut.write(rawData)
|
||||
} else {
|
||||
console.add("-- Copying: ${entry.name}")
|
||||
tarOut.putNextEntry(entry)
|
||||
tarIn.copyTo(tarOut, bufferSize = 1024 * 1024)
|
||||
}
|
||||
// Patch flags to AVB_VBMETA_IMAGE_FLAGS_HASHTREE_DISABLED |
|
||||
// AVB_VBMETA_IMAGE_FLAGS_VERIFICATION_DISABLED
|
||||
console.add("-- Patching: vbmeta.img")
|
||||
ByteBuffer.wrap(rawData).putInt(120, 3)
|
||||
tarOut.putNextEntry(newTarEntry("vbmeta.img", rawData.size.toLong()))
|
||||
tarOut.write(rawData)
|
||||
} else if (entry.name.contains("userdata.img")) {
|
||||
continue
|
||||
} else {
|
||||
console.add("-- Copying: ${entry.name}")
|
||||
tarOut.putNextEntry(entry)
|
||||
tarIn.copyTo(tarOut, bufferSize = 1024 * 1024)
|
||||
}
|
||||
}
|
||||
val boot = installDirFile("boot.img")
|
||||
val recovery = installDirFile("recovery.img")
|
||||
if (Config.recovery && recovery.exists() && boot.exists()) {
|
||||
// Install to recovery
|
||||
srcBoot = recovery
|
||||
// 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()
|
||||
SuFileInputStream.open(boot).use {
|
||||
tarOut.putNextEntry(newTarEntry("boot.img", boot.length()))
|
||||
|
||||
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)
|
||||
}
|
||||
boot.delete()
|
||||
delete()
|
||||
}
|
||||
|
||||
// Patch priority: recovery > init_boot > boot
|
||||
return when {
|
||||
recovery.exists() -> {
|
||||
if (boot.exists()) {
|
||||
// Repack boot image to prevent auto restore
|
||||
arrayOf(
|
||||
"cd $installDir",
|
||||
"chmod -R 755 .",
|
||||
"./magiskboot unpack boot.img",
|
||||
"./magiskboot repack boot.img",
|
||||
"cat new-boot.img > boot.img",
|
||||
"./magiskboot cleanup",
|
||||
"rm -f new-boot.img",
|
||||
"cd /").sh()
|
||||
boot.copyToTar()
|
||||
}
|
||||
recovery
|
||||
}
|
||||
initBoot.exists() -> {
|
||||
if (boot.exists())
|
||||
boot.copyToTar()
|
||||
initBoot
|
||||
}
|
||||
boot.exists() -> boot
|
||||
else -> throw NoBootException()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun processZip(zipIn: ZipInputStream): ExtendedFile {
|
||||
console.add("- Processing zip file")
|
||||
val boot = installDir.getChildFile("boot.img")
|
||||
val initBoot = installDir.getChildFile("init_boot.img")
|
||||
lateinit var entry: ZipEntry
|
||||
while (zipIn.nextEntry?.also { entry = it } != null) {
|
||||
if (entry.isDirectory) continue
|
||||
when (entry.name.substringAfterLast('/')) {
|
||||
"payload.bin" -> {
|
||||
try {
|
||||
return processPayload(zipIn)
|
||||
} catch (e: IOException) {
|
||||
// No boot image in payload.bin, continue to find boot images
|
||||
}
|
||||
}
|
||||
"init_boot.img" -> {
|
||||
console.add("- Extracting init_boot.img")
|
||||
zipIn.copyAndCloseOut(initBoot.newOutputStream())
|
||||
return initBoot
|
||||
}
|
||||
"boot.img" -> {
|
||||
console.add("- Extracting boot.img")
|
||||
zipIn.copyAndCloseOut(boot.newOutputStream())
|
||||
// Don't return here since there might be an init_boot.img
|
||||
}
|
||||
}
|
||||
}
|
||||
if (boot.exists()) {
|
||||
return boot
|
||||
} else {
|
||||
if (!boot.exists()) {
|
||||
console.add("! No boot image found")
|
||||
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 buf = ByteBuffer.allocate(1024 * 1024)
|
||||
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.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()
|
||||
}
|
||||
srcBoot = boot
|
||||
val boot = installDir.getChildFile("boot.img")
|
||||
val initBoot = installDir.getChildFile("init_boot.img")
|
||||
return when {
|
||||
initBoot.exists() -> {
|
||||
console.add("-- Extract init_boot.img")
|
||||
initBoot
|
||||
}
|
||||
boot.exists() -> {
|
||||
console.add("-- Extract boot.img")
|
||||
boot
|
||||
}
|
||||
else -> {
|
||||
throw NoBootException()
|
||||
}
|
||||
}
|
||||
} catch (e: ErrnoException) {
|
||||
throw IOException(e)
|
||||
} finally {
|
||||
fifo?.delete()
|
||||
}
|
||||
return tarOut
|
||||
}
|
||||
|
||||
private fun handleFile(uri: Uri): Boolean {
|
||||
val outStream: OutputStream
|
||||
var outFile: MediaStoreUtils.UriFile? = null
|
||||
val outFile: MediaStoreUtils.UriFile
|
||||
|
||||
// Process input file
|
||||
try {
|
||||
uri.inputStream().buffered().use { src ->
|
||||
src.mark(500)
|
||||
val magic = ByteArray(5)
|
||||
if (src.skip(257) != 257L || src.read(magic) != magic.size) {
|
||||
val magic = ByteArray(4)
|
||||
val tarMagic = ByteArray(5)
|
||||
if (src.read(magic) != magic.size || src.skip(253) != 253L ||
|
||||
src.read(tarMagic) != tarMagic.size
|
||||
) {
|
||||
console.add("! Invalid input file")
|
||||
return false
|
||||
}
|
||||
@@ -274,40 +394,69 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
toString()
|
||||
}
|
||||
|
||||
outStream = if (magic.contentEquals("ustar".toByteArray())) {
|
||||
srcBoot = if (tarMagic.contentEquals("ustar".toByteArray())) {
|
||||
// tar file
|
||||
outFile = MediaStoreUtils.getFile("$filename.tar", true)
|
||||
processTar(src, outFile!!.uri.outputStream())
|
||||
outStream = TarOutputStream(outFile.uri.outputStream())
|
||||
|
||||
try {
|
||||
processTar(TarInputStream(src), outStream)
|
||||
} catch (e: IOException) {
|
||||
outStream.close()
|
||||
outFile.delete()
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
// raw image
|
||||
srcBoot = installDirFile("boot.img")
|
||||
console.add("- Copying image to cache")
|
||||
src.cleanPump(SuFileOutputStream.open(srcBoot))
|
||||
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) {
|
||||
if (e is NoBootException)
|
||||
console.add("! No boot image found")
|
||||
console.add("! Process error")
|
||||
outFile?.delete()
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
// Patch file
|
||||
if (!patchBoot()) {
|
||||
outFile!!.delete()
|
||||
outFile.delete()
|
||||
return false
|
||||
}
|
||||
|
||||
// Output file
|
||||
try {
|
||||
val newBoot = installDirFile("new-boot.img")
|
||||
val newBoot = installDir.getChildFile("new-boot.img")
|
||||
if (outStream is TarOutputStream) {
|
||||
val name = if (srcBoot.path.contains("recovery")) "recovery.img" else "boot.img"
|
||||
val name = with(srcBoot.path) {
|
||||
when {
|
||||
contains("recovery") -> "recovery.img"
|
||||
contains("init_boot") -> "init_boot.img"
|
||||
else -> "boot.img"
|
||||
}
|
||||
}
|
||||
outStream.putNextEntry(newTarEntry(name, newBoot.length()))
|
||||
}
|
||||
SuFileInputStream.open(newBoot).cleanPump(outStream)
|
||||
newBoot.newInputStream().copyAndClose(outStream)
|
||||
newBoot.delete()
|
||||
|
||||
console.add("")
|
||||
@@ -317,7 +466,7 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
console.add("****************************")
|
||||
} catch (e: IOException) {
|
||||
console.add("! Failed to output to $outFile")
|
||||
outFile!!.delete()
|
||||
outFile.delete()
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
@@ -330,23 +479,7 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
}
|
||||
|
||||
private fun patchBoot(): Boolean {
|
||||
var isSigned = false
|
||||
if (srcBoot.let { it !is SuFile || !it.isCharacter }) {
|
||||
try {
|
||||
SuFileInputStream.open(srcBoot).use {
|
||||
if (SignBoot.verifySignature(it, null)) {
|
||||
isSigned = true
|
||||
console.add("- Boot image is signed with AVB 1.0")
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
console.add("! Unable to check signature")
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
val newBoot = installDirFile("new-boot.img")
|
||||
val newBoot = installDir.getChildFile("new-boot.img")
|
||||
if (!useRootDir) {
|
||||
// Create output files before hand
|
||||
newBoot.createNewFile()
|
||||
@@ -359,31 +492,13 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
"KEEPVERITY=${Config.keepVerity} " +
|
||||
"PATCHVBMETAFLAG=${Config.patchVbmeta} " +
|
||||
"RECOVERYMODE=${Config.recovery} " +
|
||||
"LEGACYSAR=${Info.legacySAR} " +
|
||||
"sh boot_patch.sh $srcBoot")
|
||||
val isSuccess = cmds.sh().isSuccess
|
||||
|
||||
if (!cmds.sh().isSuccess)
|
||||
return false
|
||||
shell.newJob().add("./magiskboot cleanup", "cd /").exec()
|
||||
|
||||
val job = shell.newJob().add("./magiskboot cleanup", "cd /")
|
||||
|
||||
if (isSigned) {
|
||||
console.add("- Signing boot image with verity keys")
|
||||
val signed = File.createTempFile("signed", ".img", context.cacheDir)
|
||||
try {
|
||||
val src = SuFileInputStream.open(newBoot).buffered()
|
||||
val out = signed.outputStream().buffered()
|
||||
withStreams(src, out) { _, _ ->
|
||||
SignBoot.doSignature(null, null, src, out, "/boot")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
console.add("! Unable to sign image")
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
job.add("cat $signed > $newBoot", "rm -f $signed")
|
||||
}
|
||||
job.exec()
|
||||
return true
|
||||
return isSuccess
|
||||
}
|
||||
|
||||
private fun flashBoot() = "direct_install $installDir $srcBoot".sh().isSuccess
|
||||
@@ -405,16 +520,17 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
return true
|
||||
}
|
||||
|
||||
private fun Array<String>.eq() = shell.newJob().add(*this).to(console, logs).enqueue()
|
||||
private fun String.sh() = shell.newJob().add(this).to(console, logs).exec()
|
||||
private fun Array<String>.sh() = shell.newJob().add(*this).to(console, logs).exec()
|
||||
private fun String.fsh() = ShellUtils.fastCmd(shell, this)
|
||||
private fun Array<String>.fsh() = ShellUtils.fastCmd(shell, *this)
|
||||
|
||||
protected fun doPatchFile(patchFile: Uri) = extractFiles() && handleFile(patchFile)
|
||||
protected fun patchFile(file: Uri) = extractFiles() && handleFile(file)
|
||||
|
||||
protected fun direct() = findImage() && extractFiles() && patchBoot() && flashBoot()
|
||||
|
||||
protected suspend fun secondSlot() =
|
||||
protected fun secondSlot() =
|
||||
findSecondary() && extractFiles() && patchBoot() && flashBoot() && postOTA()
|
||||
|
||||
protected fun fixEnv() = extractFiles() && "fix_env $installDir".sh().isSuccess
|
||||
@@ -425,20 +541,15 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
protected abstract suspend fun operations(): Boolean
|
||||
|
||||
open suspend fun exec(): Boolean {
|
||||
synchronized(Companion) {
|
||||
if (haveActiveSession)
|
||||
return false
|
||||
haveActiveSession = true
|
||||
}
|
||||
if (haveActiveSession.getAndSet(true))
|
||||
return false
|
||||
val result = withContext(Dispatchers.IO) { operations() }
|
||||
synchronized(Companion) {
|
||||
haveActiveSession = false
|
||||
}
|
||||
haveActiveSession.set(false)
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var haveActiveSession = false
|
||||
private var haveActiveSession = AtomicBoolean(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,7 +574,7 @@ abstract class MagiskInstaller(
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>
|
||||
) : MagiskInstaller(console, logs) {
|
||||
override suspend fun operations() = doPatchFile(uri)
|
||||
override suspend fun operations() = patchFile(uri)
|
||||
}
|
||||
|
||||
class SecondSlot(
|
||||
@@ -510,7 +621,7 @@ abstract class MagiskInstaller(
|
||||
override suspend fun exec(): Boolean {
|
||||
val success = super.exec()
|
||||
callback()
|
||||
Utils.toast(
|
||||
context.toast(
|
||||
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
|
@@ -4,7 +4,6 @@ import java.io.ByteArrayOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder.LITTLE_ENDIAN
|
||||
import java.nio.charset.Charset
|
||||
import java.util.*
|
||||
|
||||
class AXML(b: ByteArray) {
|
||||
|
||||
@@ -30,7 +29,7 @@ class AXML(b: ByteArray) {
|
||||
* Followed by an array of uint32_t with size = number of strings
|
||||
* 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)
|
||||
|
||||
fun findStringPool(): Int {
|
||||
@@ -43,7 +42,6 @@ class AXML(b: ByteArray) {
|
||||
return -1
|
||||
}
|
||||
|
||||
var patch = false
|
||||
val start = findStringPool()
|
||||
if (start < 0)
|
||||
return false
|
||||
@@ -58,34 +56,26 @@ class AXML(b: ByteArray) {
|
||||
val dataOff = start + intBuf.get()
|
||||
intBuf.get()
|
||||
|
||||
val strings = ArrayList<String>(count)
|
||||
// Read and patch all strings
|
||||
loop@ for (i in 0 until count) {
|
||||
val strList = ArrayList<String>(count)
|
||||
// Collect all strings in the pool
|
||||
for (i in 0 until count) {
|
||||
val off = dataOff + intBuf.get()
|
||||
val len = buffer.getShort(off)
|
||||
val str = 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)
|
||||
strList.add(String(bytes, off + 2, len * 2, UTF_16LE))
|
||||
}
|
||||
|
||||
if (!patch)
|
||||
return false
|
||||
val strArr = strList.toTypedArray()
|
||||
patchFn(strArr)
|
||||
|
||||
// Write everything before string data, will patch values later
|
||||
val baos = RawByteStream()
|
||||
baos.write(bytes, 0, dataOff)
|
||||
|
||||
// Write string data
|
||||
val strList = IntArray(count)
|
||||
val offList = IntArray(count)
|
||||
for (i in 0 until count) {
|
||||
strList[i] = baos.size() - dataOff
|
||||
val str = strings[i]
|
||||
offList[i] = baos.size() - dataOff
|
||||
val str = strArr[i]
|
||||
baos.write(str.length.toShortBytes())
|
||||
baos.write(str.toByteArray(UTF_16LE))
|
||||
// Null terminate
|
||||
@@ -104,7 +94,7 @@ class AXML(b: ByteArray) {
|
||||
// Patch index table
|
||||
newBuffer.position(start + STRING_INDICES_OFF)
|
||||
val newIntBuf = newBuffer.asIntBuffer()
|
||||
strList.forEach { newIntBuf.put(it) }
|
||||
offList.forEach { newIntBuf.put(it) }
|
||||
|
||||
// Write the rest of the chunks
|
||||
val nextOff = start + size
|
||||
|
@@ -1,30 +1,24 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.content.Context
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricManager.Authenticators
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
|
||||
object BiometricHelper {
|
||||
class BiometricHelper(context: Context) {
|
||||
|
||||
private val mgr by lazy { BiometricManager.from(AppContext) }
|
||||
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
|
||||
else -> false
|
||||
}
|
||||
|
||||
val isEnabled: Boolean get() {
|
||||
val enabled = Config.suBiometric
|
||||
if (enabled && !isSupported) {
|
||||
Config.suBiometric = false
|
||||
return false
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
val isEnabled get() = isSupported && Config.suBiometric
|
||||
|
||||
fun authenticate(
|
||||
activity: FragmentActivity,
|
||||
@@ -48,7 +42,7 @@ object BiometricHelper {
|
||||
)
|
||||
val info = BiometricPrompt.PromptInfo.Builder()
|
||||
.setConfirmationRequired(true)
|
||||
.setDeviceCredentialAllowed(false)
|
||||
.setAllowedAuthenticators(Authenticators.BIOMETRIC_WEAK)
|
||||
.setTitle(activity.getString(R.string.authenticate))
|
||||
.setNegativeButtonText(activity.getString(android.R.string.cancel))
|
||||
.build()
|
||||
|
@@ -1,6 +1,10 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Runnable
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.AbstractExecutorService
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
@@ -1,26 +1,22 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Base64
|
||||
import android.util.Base64OutputStream
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.signing.CryptoUtils.readCertificate
|
||||
import com.topjohnwu.magisk.signing.CryptoUtils.readPrivateKey
|
||||
import com.topjohnwu.magisk.signing.KeyData
|
||||
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.JcaX509v3CertificateBuilder
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.math.BigInteger
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.KeyStore
|
||||
import java.security.MessageDigest
|
||||
import java.security.PrivateKey
|
||||
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.GZIPOutputStream
|
||||
|
||||
@@ -29,13 +25,11 @@ private interface CertKeyProvider {
|
||||
val key: PrivateKey
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
class Keygen(context: Context) : CertKeyProvider {
|
||||
class Keygen : CertKeyProvider {
|
||||
|
||||
companion object {
|
||||
private const val ALIAS = "magisk"
|
||||
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 BASE64_FLAG = Base64.NO_PADDING or Base64.NO_WRAP
|
||||
}
|
||||
@@ -43,49 +37,9 @@ class Keygen(context: Context) : CertKeyProvider {
|
||||
private val start = Calendar.getInstance().apply { add(Calendar.MONTH, -3) }
|
||||
private val end = (start.clone() as Calendar).apply { add(Calendar.YEAR, 30) }
|
||||
|
||||
override val cert get() = provider.cert
|
||||
override val key get() = provider.key
|
||||
|
||||
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 val ks = init()
|
||||
override val cert = ks.getCertificate(ALIAS) as X509Certificate
|
||||
override val key = ks.getKey(ALIAS, PASSWORD) as PrivateKey
|
||||
|
||||
private fun init(): KeyStore {
|
||||
val raw = Config.keyStoreRaw
|
||||
@@ -93,12 +47,8 @@ class Keygen(context: Context) : CertKeyProvider {
|
||||
if (raw.isEmpty()) {
|
||||
ks.load(null)
|
||||
} else {
|
||||
GZIPInputStream(Base64.decode(raw,
|
||||
BASE64_FLAG
|
||||
).inputStream()).use {
|
||||
ks.load(it,
|
||||
PASSWORD
|
||||
)
|
||||
GZIPInputStream(Base64.decode(raw, BASE64_FLAG).inputStream()).use {
|
||||
ks.load(it, PASSWORD)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,22 +59,19 @@ class Keygen(context: Context) : CertKeyProvider {
|
||||
// Generate new private key and certificate
|
||||
val kp = KeyPairGenerator.getInstance("RSA").apply { initialize(4096) }.genKeyPair()
|
||||
val dname = X500Name(DNAME)
|
||||
val builder = JcaX509v3CertificateBuilder(dname, BigInteger(160, Random()),
|
||||
start.time, end.time, dname, kp.public)
|
||||
val builder = X509v3CertificateBuilder(
|
||||
dname, BigInteger(160, Random()),
|
||||
start.time, end.time, Locale.ROOT, dname,
|
||||
SubjectPublicKeyInfo.getInstance(kp.public.encoded)
|
||||
)
|
||||
val signer = JcaContentSignerBuilder("SHA1WithRSA").build(kp.private)
|
||||
val cert = JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
||||
|
||||
// Store them into keystore
|
||||
ks.setKeyEntry(
|
||||
ALIAS, kp.private,
|
||||
PASSWORD, arrayOf(cert))
|
||||
ks.setKeyEntry(ALIAS, kp.private, PASSWORD, arrayOf(cert))
|
||||
val bytes = ByteArrayOutputStream()
|
||||
GZIPOutputStream(Base64OutputStream(bytes,
|
||||
BASE64_FLAG
|
||||
)).use {
|
||||
ks.store(it,
|
||||
PASSWORD
|
||||
)
|
||||
GZIPOutputStream(Base64OutputStream(bytes, BASE64_FLAG)).use {
|
||||
ks.store(it, PASSWORD)
|
||||
}
|
||||
Config.keyStoreRaw = bytes.toString("UTF-8")
|
||||
|
||||
|
@@ -6,13 +6,13 @@ import android.annotation.SuppressLint
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.ActivityTracker
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.createNewResources
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
var currentLocale: Locale = Locale.getDefault()
|
||||
|
||||
@@ -28,6 +28,11 @@ withContext(Dispatchers.Default) {
|
||||
// Create a completely new resource to prevent cross talk over active configs
|
||||
val res = createNewResources()
|
||||
|
||||
fun changeLocale(locale: Locale) {
|
||||
res.configuration.setLocale(locale)
|
||||
res.updateConfiguration(res.configuration, res.displayMetrics)
|
||||
}
|
||||
|
||||
val locales = ArrayList<String>().apply {
|
||||
// Add default locale
|
||||
add("en")
|
||||
@@ -41,13 +46,13 @@ withContext(Dispatchers.Default) {
|
||||
}.map {
|
||||
Locale.forLanguageTag(it)
|
||||
}.distinctBy {
|
||||
res.setLocale(it)
|
||||
changeLocale(it)
|
||||
res.getString(compareId)
|
||||
}.sortedWith { a, b ->
|
||||
a.getDisplayName(a).compareTo(b.getDisplayName(b), true)
|
||||
}
|
||||
|
||||
res.setLocale(defaultLocale)
|
||||
changeLocale(defaultLocale)
|
||||
val defName = res.getString(R.string.system_default)
|
||||
|
||||
val names = ArrayList<String>(locales.size + 1)
|
||||
@@ -71,11 +76,6 @@ fun Resources.setConfig(config: Configuration) {
|
||||
|
||||
fun Resources.syncLocale() = setConfig(configuration)
|
||||
|
||||
fun Resources.setLocale(locale: Locale) {
|
||||
configuration.setLocale(locale)
|
||||
updateConfiguration(configuration, displayMetrics)
|
||||
}
|
||||
|
||||
fun refreshLocale() {
|
||||
val localeConfig = Config.locale
|
||||
currentLocale = when {
|
||||
@@ -84,4 +84,5 @@ fun refreshLocale() {
|
||||
}
|
||||
Locale.setDefault(currentLocale)
|
||||
AppContext.resources.syncLocale()
|
||||
ActivityTracker.foreground?.recreate()
|
||||
}
|
||||
|
@@ -11,7 +11,7 @@ import androidx.annotation.RequiresApi
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
@@ -87,7 +87,7 @@ object MediaStoreUtils {
|
||||
|
||||
@Throws(IOException::class)
|
||||
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
|
||||
val parent = File(Environment.getExternalStorageDirectory(), relativePath)
|
||||
parent.mkdirs()
|
||||
@@ -102,6 +102,8 @@ object MediaStoreUtils {
|
||||
|
||||
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() {
|
||||
if (scheme == "file") {
|
||||
// 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);
|
||||
}
|
||||
}
|
@@ -25,7 +25,7 @@ class RequestInstall : ActivityResultContract<Unit, Boolean>() {
|
||||
context: Context,
|
||||
input: Unit
|
||||
): SynchronousResult<Boolean>? {
|
||||
if (Build.VERSION.SDK_INT < 26)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
|
||||
return SynchronousResult(true)
|
||||
if (context.packageManager.canRequestPackageInstalls())
|
||||
return SynchronousResult(true)
|
||||
|
@@ -1,54 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class RootRegistry(stub: Any?) : RootService() {
|
||||
|
||||
constructor() : this(null) {
|
||||
// Always log full stack trace with Timber
|
||||
Timber.plant(Timber.DebugTree())
|
||||
Thread.setDefaultUncaughtExceptionHandler { _, e ->
|
||||
Timber.e(e)
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
|
||||
private val className: String? = stub?.javaClass?.name
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
// TODO: PLACEHOLDER
|
||||
return Binder()
|
||||
}
|
||||
|
||||
override fun getComponentName(): ComponentName {
|
||||
return ComponentName(packageName, className ?: javaClass.name)
|
||||
}
|
||||
|
||||
// TODO: PLACEHOLDER
|
||||
object Connection : CountDownLatch(1), ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||
Timber.d("onServiceConnected")
|
||||
countDown()
|
||||
}
|
||||
override fun onNullBinding(name: ComponentName) {
|
||||
Timber.d("onServiceConnected")
|
||||
countDown()
|
||||
}
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
bind(Intent().setComponent(name), this)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
var bindTask: Shell.Task? = null
|
||||
}
|
||||
}
|
139
app/src/main/java/com/topjohnwu/magisk/core/utils/RootUtils.kt
Normal file
139
app/src/main/java/com/topjohnwu/magisk/core/utils/RootUtils.kt
Normal file
@@ -0,0 +1,139 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import android.system.Os
|
||||
import androidx.core.content.getSystemService
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
import com.topjohnwu.superuser.nio.FileSystemManager
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.concurrent.locks.AbstractQueuedSynchronizer
|
||||
|
||||
class RootUtils(stub: Any?) : RootService() {
|
||||
|
||||
private val className: String = stub?.javaClass?.name ?: javaClass.name
|
||||
private lateinit var am: ActivityManager
|
||||
|
||||
constructor() : this(null) {
|
||||
Timber.plant(object : Timber.DebugTree() {
|
||||
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||
super.log(priority, "Magisk", message, t)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
am = getSystemService()!!
|
||||
}
|
||||
|
||||
override fun getComponentName(): ComponentName {
|
||||
return ComponentName(packageName, className)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return object : IRootUtils.Stub() {
|
||||
override fun getAppProcess(pid: Int) = safe(null) { getAppProcessImpl(pid) }
|
||||
override fun getFileSystem(): IBinder = FileSystemManager.getService()
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <T> safe(default: T, block: () -> T): T {
|
||||
return try {
|
||||
block()
|
||||
} catch (e: Throwable) {
|
||||
// The process died unexpectedly
|
||||
Timber.e(e)
|
||||
default
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAppProcessImpl(_pid: Int): ActivityManager.RunningAppProcessInfo? {
|
||||
val procList = am.runningAppProcesses
|
||||
var pid = _pid
|
||||
while (pid > 1) {
|
||||
val proc = procList.find { it.pid == pid }
|
||||
if (proc != null)
|
||||
return proc
|
||||
|
||||
// Stop find when root process
|
||||
if (Os.stat("/proc/$pid").st_uid == 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Find PPID
|
||||
File("/proc/$pid/status").useLines {
|
||||
val line = it.find { l -> l.startsWith("PPid:") } ?: return null
|
||||
pid = line.substring(5).trim().toInt()
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
object Connection : AbstractQueuedSynchronizer(), ServiceConnection {
|
||||
init {
|
||||
state = 1
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||
Timber.d("onServiceConnected")
|
||||
IRootUtils.Stub.asInterface(service).let {
|
||||
obj = it
|
||||
fs = FileSystemManager.getRemote(it.fileSystem)
|
||||
}
|
||||
releaseShared(1)
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
state = 1
|
||||
obj = null
|
||||
bind(Intent().setComponent(name), this)
|
||||
}
|
||||
|
||||
override fun tryAcquireShared(acquires: Int) = if (state == 0) 1 else -1
|
||||
|
||||
override fun tryReleaseShared(releases: Int): Boolean {
|
||||
// Decrement count; signal when transition to zero
|
||||
while (true) {
|
||||
val c = state
|
||||
if (c == 0)
|
||||
return false
|
||||
val n = c - 1
|
||||
if (compareAndSetState(c, n))
|
||||
return n == 0
|
||||
}
|
||||
}
|
||||
|
||||
fun await() {
|
||||
if (!Info.isRooted)
|
||||
return
|
||||
if (!ShellUtils.onMainThread()) {
|
||||
acquireSharedInterruptibly(1)
|
||||
} else if (state != 0) {
|
||||
throw IllegalStateException("Cannot await on the main thread")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
var bindTask: Shell.Task? = null
|
||||
var fs: FileSystemManager = FileSystemManager.getLocal()
|
||||
get() {
|
||||
Connection.await()
|
||||
return field
|
||||
}
|
||||
private set
|
||||
var obj: IRootUtils? = null
|
||||
get() {
|
||||
Connection.await()
|
||||
return field
|
||||
}
|
||||
private set
|
||||
}
|
||||
}
|
@@ -7,10 +7,10 @@ import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.ktx.cachedFile
|
||||
import com.topjohnwu.magisk.ktx.deviceProtectedContext
|
||||
import com.topjohnwu.magisk.ktx.rawResource
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import com.topjohnwu.magisk.core.ktx.cachedFile
|
||||
import com.topjohnwu.magisk.core.ktx.deviceProtectedContext
|
||||
import com.topjohnwu.magisk.core.ktx.rawResource
|
||||
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import java.io.File
|
||||
@@ -19,8 +19,9 @@ import java.util.jar.JarFile
|
||||
class ShellInit : Shell.Initializer() {
|
||||
override fun onInit(context: Context, shell: Shell): Boolean {
|
||||
if (shell.isRoot) {
|
||||
RootRegistry.bindTask?.let { shell.execTask(it) }
|
||||
RootRegistry.bindTask = null
|
||||
Info.isRooted = true
|
||||
RootUtils.bindTask?.let { shell.execTask(it) }
|
||||
RootUtils.bindTask = null
|
||||
}
|
||||
shell.newJob().apply {
|
||||
add("export ASH_STANDALONE=1")
|
||||
@@ -40,7 +41,7 @@ class ShellInit : Shell.Initializer() {
|
||||
}
|
||||
|
||||
if (shell.isRoot) {
|
||||
add("export MAGISKTMP=\$(magisk --path)/.magisk")
|
||||
add("export MAGISKTMP=\$(magisk --path)")
|
||||
// Test if we can properly execute stuff in /data
|
||||
Info.noDataExec = !shell.newJob().add("$localBB sh -c \"$localBB true\"").exec().isSuccess
|
||||
}
|
||||
@@ -48,12 +49,12 @@ class ShellInit : Shell.Initializer() {
|
||||
if (Info.noDataExec) {
|
||||
// Copy it out of /data to workaround Samsung bullshit
|
||||
add(
|
||||
"if [ -x \$MAGISKTMP/busybox/busybox ]; then",
|
||||
" cp -af $localBB \$MAGISKTMP/busybox/busybox",
|
||||
" exec \$MAGISKTMP/busybox/busybox sh",
|
||||
"if [ -x \$MAGISKTMP/.magisk/busybox/busybox ]; then",
|
||||
" cp -af $localBB \$MAGISKTMP/.magisk/busybox/busybox",
|
||||
" exec \$MAGISKTMP/.magisk/busybox/busybox sh",
|
||||
"else",
|
||||
" cp -af $localBB /dev/.busybox",
|
||||
" exec /dev/.busybox sh",
|
||||
" cp -af $localBB /dev/busybox",
|
||||
" exec /dev/busybox sh",
|
||||
"fi"
|
||||
)
|
||||
} else {
|
||||
@@ -72,12 +73,12 @@ class ShellInit : Shell.Initializer() {
|
||||
fun getVar(name: String) = fastCmd("echo \$$name")
|
||||
fun getBool(name: String) = getVar(name).toBoolean()
|
||||
|
||||
Const.MAGISKTMP = getVar("MAGISKTMP")
|
||||
Info.isSAR = getBool("SYSTEM_ROOT")
|
||||
Info.ramdisk = getBool("RAMDISKEXIST")
|
||||
Info.vbmeta = getBool("VBMETAEXIST")
|
||||
Info.isAB = getBool("ISAB")
|
||||
Info.crypto = getVar("CRYPTOTYPE")
|
||||
Info.legacySAR = getBool("LEGACYSAR")
|
||||
|
||||
// Default presets
|
||||
Config.recovery = getBool("RECOVERYMODE")
|
||||
|
@@ -1,21 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
|
||||
class UninstallPackage : ActivityResultContract<String, Boolean>() {
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun createIntent(context: Context, input: String): Intent {
|
||||
val uri = Uri.Builder().scheme("package").opaquePart(input).build()
|
||||
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, uri)
|
||||
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true)
|
||||
return intent
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?) =
|
||||
resultCode == Activity.RESULT_OK
|
||||
}
|
@@ -1,7 +1,5 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
@@ -31,12 +29,12 @@ fun InputStream.unzip(folder: File, path: String, junkPath: Boolean) {
|
||||
else
|
||||
entry.name
|
||||
|
||||
var dest = File(folder, name)
|
||||
if (!dest.parentFile!!.exists() && !dest.parentFile!!.mkdirs()) {
|
||||
dest = SuFile(folder, name)
|
||||
dest.parentFile!!.mkdirs()
|
||||
val dest = File(folder, name)
|
||||
dest.parentFile!!.let {
|
||||
if (!it.exists())
|
||||
it.mkdirs()
|
||||
}
|
||||
SuFileOutputStream.open(dest).use { out -> zin.copyTo(out) }
|
||||
dest.outputStream().use { out -> zin.copyTo(out) }
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw IOException(e)
|
||||
|
@@ -1,49 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.utils.net
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import androidx.collection.ArraySet
|
||||
|
||||
@TargetApi(21)
|
||||
open class LollipopNetworkObserver(
|
||||
context: Context,
|
||||
callback: ConnectionCallback
|
||||
): NetworkObserver(context, callback) {
|
||||
|
||||
private val networkCallback = NetCallback()
|
||||
|
||||
init {
|
||||
val request = NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.build()
|
||||
manager.registerNetworkCallback(request, networkCallback)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun getCurrentState() {
|
||||
callback(manager.activeNetworkInfo?.isConnected ?: false)
|
||||
}
|
||||
|
||||
override fun stopObserving() {
|
||||
manager.unregisterNetworkCallback(networkCallback)
|
||||
}
|
||||
|
||||
private inner class NetCallback : ConnectivityManager.NetworkCallback() {
|
||||
|
||||
private val activeList = ArraySet<Network>()
|
||||
|
||||
override fun onAvailable(network: Network) {
|
||||
activeList.add(network)
|
||||
callback(true)
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
activeList.remove(network)
|
||||
callback(!activeList.isEmpty())
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,53 +0,0 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.topjohnwu.magisk.core.utils.net
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.PowerManager
|
||||
import androidx.core.content.getSystemService
|
||||
|
||||
@TargetApi(23)
|
||||
class MarshmallowNetworkObserver(
|
||||
context: Context,
|
||||
callback: ConnectionCallback
|
||||
): LollipopNetworkObserver(context, callback) {
|
||||
|
||||
private val receiver = IdleBroadcastReceiver()
|
||||
|
||||
init {
|
||||
val filter = IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)
|
||||
app.registerReceiver(receiver, filter)
|
||||
}
|
||||
|
||||
override fun stopObserving() {
|
||||
super.stopObserving()
|
||||
app.unregisterReceiver(receiver)
|
||||
}
|
||||
|
||||
override fun getCurrentState() {
|
||||
callback(manager.getNetworkCapabilities(manager.activeNetwork)
|
||||
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false)
|
||||
}
|
||||
|
||||
private inner class IdleBroadcastReceiver: BroadcastReceiver() {
|
||||
|
||||
private fun Context.isIdleMode(): Boolean {
|
||||
val pwm = getSystemService<PowerManager>() ?: return true
|
||||
val isIgnoringOptimizations = pwm.isIgnoringBatteryOptimizations(packageName)
|
||||
return pwm.isDeviceIdleMode && !isIgnoringOptimizations
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (context.isIdleMode()) {
|
||||
callback(false)
|
||||
} else {
|
||||
getCurrentState()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,29 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.utils.net
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.getSystemService
|
||||
|
||||
typealias ConnectionCallback = (Boolean) -> Unit
|
||||
|
||||
abstract class NetworkObserver(
|
||||
context: Context,
|
||||
protected val callback: ConnectionCallback
|
||||
) {
|
||||
|
||||
protected val app: Context = context.applicationContext
|
||||
protected val manager = context.getSystemService<ConnectivityManager>()!!
|
||||
|
||||
protected abstract fun stopObserving()
|
||||
protected abstract fun getCurrentState()
|
||||
|
||||
companion object {
|
||||
fun observe(context: Context, callback: ConnectionCallback): NetworkObserver {
|
||||
val observer: NetworkObserver = if (Build.VERSION.SDK_INT >= 23)
|
||||
MarshmallowNetworkObserver(context, callback)
|
||||
else LollipopNetworkObserver(context, callback)
|
||||
return observer.apply { getCurrentState() }
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,67 +0,0 @@
|
||||
package com.topjohnwu.magisk.data.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
interface PreferenceModel {
|
||||
|
||||
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<PreferenceModel, Int> {
|
||||
val base = StringProperty(name, default.toString(), commit)
|
||||
override fun getValue(thisRef: PreferenceModel, property: KProperty<*>): Int =
|
||||
base.getValue(thisRef, property).toInt()
|
||||
|
||||
override fun setValue(thisRef: PreferenceModel, 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)
|
||||
|
||||
}
|
@@ -1,13 +0,0 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
|
||||
|
||||
open class BindingBoundAdapter : BindingRecyclerViewAdapter<RvItem>() {
|
||||
|
||||
override fun onBindBinding(binding: ViewDataBinding, variableId: Int, layoutRes: Int, position: Int, item: RvItem) {
|
||||
super.onBindBinding(binding, variableId, layoutRes, position, item)
|
||||
|
||||
item.onBindingBound(binding)
|
||||
}
|
||||
}
|
@@ -8,10 +8,7 @@ import android.text.Spanned
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import android.widget.*
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.cardview.widget.CardView
|
||||
@@ -29,7 +26,8 @@ import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.utils.TextHolder
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import com.topjohnwu.widget.IndeterminateCheckBox
|
||||
import kotlin.math.roundToInt
|
||||
@@ -289,3 +287,13 @@ fun TextView.setTextColorAttr(attr: Int) {
|
||||
context.theme.resolveAttribute(attr, tv, true)
|
||||
setTextColor(tv.data)
|
||||
}
|
||||
|
||||
@BindingAdapter("android:text")
|
||||
fun TextView.setText(text: TextHolder) {
|
||||
this.text = text.getText(context.resources)
|
||||
}
|
||||
|
||||
@BindingAdapter("items", "layout")
|
||||
fun Spinner.setAdapter(items: Array<Any>, layoutRes: Int) {
|
||||
adapter = ArrayAdapter(context, layoutRes, items)
|
||||
}
|
||||
|
@@ -1,38 +1,53 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.databinding.ListChangeRegistry
|
||||
import androidx.databinding.ObservableList
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListUpdateCallback
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.AbstractList
|
||||
|
||||
/**
|
||||
* @param callback The callback that controls the behavior of the DiffObservableList.
|
||||
* @param detectMoves True if DiffUtil should try to detect moved items, false otherwise.
|
||||
*/
|
||||
open class DiffObservableList<T>(
|
||||
private val callback: Callback<T>,
|
||||
private val detectMoves: Boolean = true
|
||||
) : AbstractList<T>(), ObservableList<T> {
|
||||
// Only expose the immutable List types
|
||||
interface DiffList<T : DiffItem<*>> : List<T> {
|
||||
fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult
|
||||
|
||||
protected var list: MutableList<T> = ArrayList()
|
||||
@MainThread
|
||||
fun update(newItems: List<T>, diffResult: DiffUtil.DiffResult)
|
||||
|
||||
@WorkerThread
|
||||
suspend fun update(newItems: List<T>)
|
||||
}
|
||||
|
||||
interface FilterList<T : DiffItem<*>> : List<T> {
|
||||
fun filter(filter: (T) -> Boolean)
|
||||
|
||||
@MainThread
|
||||
fun set(newItems: List<T>)
|
||||
}
|
||||
|
||||
fun <T : DiffItem<*>> diffList(): DiffList<T> = DiffObservableList()
|
||||
|
||||
fun <T : DiffItem<*>> filterList(scope: CoroutineScope): FilterList<T> =
|
||||
FilterableDiffObservableList(scope)
|
||||
|
||||
private open class DiffObservableList<T : DiffItem<*>>
|
||||
: AbstractList<T>(), ObservableList<T>, DiffList<T>, ListUpdateCallback {
|
||||
|
||||
protected var list: List<T> = emptyList()
|
||||
private val listeners = ListChangeRegistry()
|
||||
protected val listCallback = ObservableListUpdateCallback()
|
||||
|
||||
override val size: Int get() = list.size
|
||||
|
||||
/**
|
||||
* Calculates the list of update operations that can convert this list into the given one.
|
||||
*
|
||||
* @param newItems The items that this list will be set to.
|
||||
* @return A DiffResult that contains the information about the edit sequence to covert this
|
||||
* list into the given one.
|
||||
*/
|
||||
fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult {
|
||||
val frozenList = ArrayList(list)
|
||||
return doCalculateDiff(frozenList, newItems)
|
||||
override fun get(index: Int) = list[index]
|
||||
|
||||
override fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult {
|
||||
return doCalculateDiff(list, newItems)
|
||||
}
|
||||
|
||||
protected fun doCalculateDiff(oldItems: List<T>, newItems: List<T>): DiffUtil.DiffResult {
|
||||
@@ -41,47 +56,34 @@ open class DiffObservableList<T>(
|
||||
|
||||
override fun getNewListSize() = newItems.size
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = oldItems[oldItemPosition]
|
||||
val newItem = newItems[newItemPosition]
|
||||
return callback.areItemsTheSame(oldItem, newItem)
|
||||
return (oldItem as DiffItem<Any>).itemSameAs(newItem)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = oldItems[oldItemPosition]
|
||||
val newItem = newItems[newItemPosition]
|
||||
return callback.areContentsTheSame(oldItem, newItem)
|
||||
return (oldItem as DiffItem<Any>).contentSameAs(newItem)
|
||||
}
|
||||
}, detectMoves)
|
||||
}, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the contents of this list to the given one using the DiffResults to dispatch change
|
||||
* notifications.
|
||||
*
|
||||
* @param newItems The items to set this list to.
|
||||
* @param diffResult The diff results to dispatch change notifications.
|
||||
*/
|
||||
@MainThread
|
||||
fun update(newItems: List<T>, diffResult: DiffUtil.DiffResult) {
|
||||
list = newItems.toMutableList()
|
||||
diffResult.dispatchUpdatesTo(listCallback)
|
||||
override fun update(newItems: List<T>, diffResult: DiffUtil.DiffResult) {
|
||||
list = ArrayList(newItems)
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets this list to the given items. This is a convenience method for calling [ ][.calculateDiff] followed by [.update].
|
||||
*
|
||||
*
|
||||
* **Warning!** If the lists are large this operation may be too slow for the main thread. In
|
||||
* that case, you should call [.calculateDiff] on a background thread and then
|
||||
* [.update] on the main thread.
|
||||
*
|
||||
* @param newItems The items to set this list to.
|
||||
*/
|
||||
@MainThread
|
||||
fun update(newItems: List<T>) {
|
||||
val diffResult = doCalculateDiff(list, newItems)
|
||||
update(newItems, diffResult)
|
||||
@WorkerThread
|
||||
override suspend fun update(newItems: List<T>) {
|
||||
val diffResult = calculateDiff(newItems)
|
||||
withContext(Dispatchers.Main) {
|
||||
update(newItems, diffResult)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addOnListChangedCallback(listener: ObservableList.OnListChangedCallback<out ObservableList<T>>) {
|
||||
@@ -92,113 +94,63 @@ open class DiffObservableList<T>(
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
override fun get(index: Int) = list[index]
|
||||
|
||||
override fun add(index: Int, element: T) {
|
||||
list.add(index, element)
|
||||
notifyAdd(index, 1)
|
||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||
listeners.notifyChanged(this, position, count)
|
||||
}
|
||||
|
||||
override fun addAll(elements: Collection<T>) = addAll(size, elements)
|
||||
|
||||
override fun addAll(index: Int, elements: Collection<T>): Boolean {
|
||||
val added = list.addAll(index, elements)
|
||||
if (added) {
|
||||
notifyAdd(index, elements.size)
|
||||
}
|
||||
return added
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||
listeners.notifyMoved(this, fromPosition, toPosition, 1)
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
val oldSize = size
|
||||
list.clear()
|
||||
if (oldSize != 0) {
|
||||
notifyRemove(0, oldSize)
|
||||
}
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
modCount += 1
|
||||
listeners.notifyInserted(this, position, count)
|
||||
}
|
||||
|
||||
override fun remove(element: T): Boolean {
|
||||
val index = indexOf(element)
|
||||
return if (index >= 0) {
|
||||
removeAt(index)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeAt(index: Int): T {
|
||||
val element = list.removeAt(index)
|
||||
notifyRemove(index, 1)
|
||||
return element
|
||||
}
|
||||
|
||||
override fun set(index: Int, element: T): T {
|
||||
val old = list.set(index, element)
|
||||
listeners.notifyChanged(this, index, 1)
|
||||
return old
|
||||
}
|
||||
|
||||
private fun notifyAdd(start: Int, count: Int) {
|
||||
listeners.notifyInserted(this, start, count)
|
||||
}
|
||||
|
||||
private fun notifyRemove(start: Int, count: Int) {
|
||||
listeners.notifyRemoved(this, start, count)
|
||||
}
|
||||
|
||||
/**
|
||||
* A Callback class used by DiffUtil while calculating the diff between two lists.
|
||||
*/
|
||||
interface Callback<T> {
|
||||
/**
|
||||
* Called by the DiffUtil to decide whether two object represent the same Item.
|
||||
*
|
||||
*
|
||||
* For example, if your items have unique ids, this method should check their id equality.
|
||||
*
|
||||
* @param oldItem The old item.
|
||||
* @param newItem The new item.
|
||||
* @return True if the two items represent the same object or false if they are different.
|
||||
*/
|
||||
fun areItemsTheSame(oldItem: T, newItem: T): Boolean
|
||||
|
||||
/**
|
||||
* Called by the DiffUtil when it wants to check whether two items have the same data.
|
||||
* DiffUtil uses this information to detect if the contents of an item has changed.
|
||||
*
|
||||
*
|
||||
* DiffUtil uses this method to check equality instead of [Object.equals] so
|
||||
* that you can change its behavior depending on your UI.
|
||||
*
|
||||
*
|
||||
* This method is called only if [.areItemsTheSame] returns `true` for
|
||||
* these items.
|
||||
*
|
||||
* @param oldItem The old item.
|
||||
* @param newItem The new item which replaces the old item.
|
||||
* @return True if the contents of the items are the same or false if they are different.
|
||||
*/
|
||||
fun areContentsTheSame(oldItem: T, newItem: T): Boolean
|
||||
}
|
||||
|
||||
inner class ObservableListUpdateCallback : ListUpdateCallback {
|
||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||
listeners.notifyChanged(this@DiffObservableList, position, count)
|
||||
}
|
||||
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||
listeners.notifyMoved(this@DiffObservableList, fromPosition, toPosition, 1)
|
||||
}
|
||||
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
modCount += 1
|
||||
listeners.notifyInserted(this@DiffObservableList, position, count)
|
||||
}
|
||||
|
||||
override fun onRemoved(position: Int, count: Int) {
|
||||
modCount += 1
|
||||
listeners.notifyRemoved(this@DiffObservableList, position, count)
|
||||
}
|
||||
override fun onRemoved(position: Int, count: Int) {
|
||||
modCount += 1
|
||||
listeners.notifyRemoved(this, position, count)
|
||||
}
|
||||
}
|
||||
|
||||
private class FilterableDiffObservableList<T : DiffItem<*>>(
|
||||
private val scope: CoroutineScope
|
||||
) : DiffObservableList<T>(), FilterList<T> {
|
||||
|
||||
private var sublist: List<T> = emptyList()
|
||||
private var job: Job? = null
|
||||
private var lastFilter: ((T) -> Boolean)? = null
|
||||
|
||||
// ---
|
||||
|
||||
override fun filter(filter: (T) -> Boolean) {
|
||||
lastFilter = filter
|
||||
job?.cancel()
|
||||
job = scope.launch(Dispatchers.Default) {
|
||||
val oldList = sublist
|
||||
val newList = list.filter(filter)
|
||||
val diff = doCalculateDiff(oldList, newList)
|
||||
withContext(Dispatchers.Main) {
|
||||
sublist = newList
|
||||
diff.dispatchUpdatesTo(this@FilterableDiffObservableList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
override fun get(index: Int): T {
|
||||
return sublist[index]
|
||||
}
|
||||
|
||||
override val size: Int
|
||||
get() = sublist.size
|
||||
|
||||
@MainThread
|
||||
override fun set(newItems: List<T>) {
|
||||
onRemoved(0, sublist.size)
|
||||
list = newItems
|
||||
sublist = emptyList()
|
||||
lastFilter?.let { filter(it) }
|
||||
}
|
||||
}
|
||||
|
@@ -1,85 +0,0 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.os.Looper
|
||||
import java.util.*
|
||||
|
||||
class FilterableDiffObservableList<T>(
|
||||
callback: Callback<T>
|
||||
) : DiffObservableList<T>(callback) {
|
||||
|
||||
var filter: ((T) -> Boolean)? = null
|
||||
set(value) {
|
||||
field = value
|
||||
queueUpdate()
|
||||
}
|
||||
@Volatile
|
||||
private var sublist: MutableList<T> = super.list
|
||||
|
||||
// ---
|
||||
|
||||
private val ui by lazy { Handler(Looper.getMainLooper()) }
|
||||
private val handler = Handler(HandlerThread("List${hashCode()}").apply { start() }.looper)
|
||||
private val updater = Runnable {
|
||||
val filter = filter ?: { true }
|
||||
val newList = super.list.filter(filter)
|
||||
val diff = synchronized(this) { doCalculateDiff(sublist, newList) }
|
||||
ui.post {
|
||||
sublist = Collections.synchronizedList(newList)
|
||||
diff.dispatchUpdatesTo(listCallback)
|
||||
}
|
||||
}
|
||||
|
||||
private fun queueUpdate() {
|
||||
handler.removeCallbacks(updater)
|
||||
handler.post(updater)
|
||||
}
|
||||
|
||||
fun hasFilter() = filter != null
|
||||
|
||||
fun filter(switch: (T) -> Boolean) {
|
||||
filter = switch
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
filter = null
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
override fun get(index: Int): T {
|
||||
return sublist.get(index)
|
||||
}
|
||||
|
||||
override fun add(element: T): Boolean {
|
||||
return sublist.add(element)
|
||||
}
|
||||
|
||||
override fun add(index: Int, element: T) {
|
||||
sublist.add(index, element)
|
||||
}
|
||||
|
||||
override fun addAll(elements: Collection<T>): Boolean {
|
||||
return sublist.addAll(elements)
|
||||
}
|
||||
|
||||
override fun addAll(index: Int, elements: Collection<T>): Boolean {
|
||||
return sublist.addAll(index, elements)
|
||||
}
|
||||
|
||||
override fun remove(element: T): Boolean {
|
||||
return sublist.remove(element)
|
||||
}
|
||||
|
||||
override fun removeAt(index: Int): T {
|
||||
return sublist.removeAt(index)
|
||||
}
|
||||
|
||||
override fun set(index: Int, element: T): T {
|
||||
return sublist.set(index, element)
|
||||
}
|
||||
|
||||
override val size: Int
|
||||
get() = sublist.size
|
||||
}
|
@@ -1,35 +0,0 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
|
||||
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
||||
import me.tatarka.bindingcollectionadapter2.OnItemBind
|
||||
|
||||
fun <T : AnyDiffRvItem> diffListOf() =
|
||||
DiffObservableList(DiffRvItem.callback<T>())
|
||||
|
||||
fun <T : AnyDiffRvItem> diffListOf(newItems: List<T>) =
|
||||
DiffObservableList(DiffRvItem.callback<T>()).also { it.update(newItems) }
|
||||
|
||||
fun <T : AnyDiffRvItem> filterableListOf() =
|
||||
FilterableDiffObservableList(DiffRvItem.callback<T>())
|
||||
|
||||
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,162 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import androidx.databinding.ListChangeRegistry
|
||||
import androidx.databinding.ObservableList
|
||||
import androidx.databinding.ObservableList.OnListChangedCallback
|
||||
import java.util.*
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class MergeObservableList<T> : AbstractList<T>(), ObservableList<T> {
|
||||
|
||||
private val lists: MutableList<List<T>> = mutableListOf()
|
||||
private val listeners = ListChangeRegistry()
|
||||
private val callback = Callback<T>()
|
||||
|
||||
override fun addOnListChangedCallback(callback: OnListChangedCallback<out ObservableList<T>>) {
|
||||
listeners.add(callback)
|
||||
}
|
||||
|
||||
override fun removeOnListChangedCallback(callback: OnListChangedCallback<out ObservableList<T>>) {
|
||||
listeners.remove(callback)
|
||||
}
|
||||
|
||||
override fun get(index: Int): T {
|
||||
if (index < 0)
|
||||
throw IndexOutOfBoundsException()
|
||||
var idx = index
|
||||
for (list in lists) {
|
||||
val size = list.size
|
||||
if (idx < size) {
|
||||
return list[idx]
|
||||
}
|
||||
idx -= size
|
||||
}
|
||||
throw IndexOutOfBoundsException()
|
||||
}
|
||||
|
||||
override val size: Int
|
||||
get() = lists.fold(0) { i, it -> i + it.size }
|
||||
|
||||
|
||||
fun insertItem(obj: T): MergeObservableList<T> {
|
||||
val idx = size
|
||||
lists.add(listOf(obj))
|
||||
++modCount
|
||||
listeners.notifyInserted(this, idx, 1)
|
||||
return this
|
||||
}
|
||||
|
||||
fun insertList(list: List<T>): MergeObservableList<T> {
|
||||
val idx = size
|
||||
lists.add(list)
|
||||
++modCount
|
||||
(list as? ObservableList<T>)?.addOnListChangedCallback(callback)
|
||||
if (list.isNotEmpty())
|
||||
listeners.notifyInserted(this, idx, list.size)
|
||||
return this
|
||||
}
|
||||
|
||||
fun removeItem(obj: T): Boolean {
|
||||
var idx = 0
|
||||
for ((i, list) in lists.withIndex()) {
|
||||
if (list !is ObservableList<*>) {
|
||||
if (obj == list[0]) {
|
||||
lists.removeAt(i)
|
||||
++modCount
|
||||
listeners.notifyRemoved(this, idx, 1)
|
||||
return true
|
||||
}
|
||||
}
|
||||
idx += list.size
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun removeList(listToRemove: List<T>): Boolean {
|
||||
var idx = 0
|
||||
for ((i, list) in lists.withIndex()) {
|
||||
if (listToRemove === list) {
|
||||
(list as? ObservableList<T>)?.removeOnListChangedCallback(callback)
|
||||
lists.removeAt(i)
|
||||
++modCount
|
||||
listeners.notifyRemoved(this, idx, list.size)
|
||||
return true
|
||||
}
|
||||
idx += list.size
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
val sz = size
|
||||
for (list in lists) {
|
||||
if (list is ObservableList) {
|
||||
list.removeOnListChangedCallback(callback)
|
||||
}
|
||||
}
|
||||
++modCount
|
||||
lists.clear()
|
||||
if (sz > 0)
|
||||
listeners.notifyRemoved(this, 0, sz)
|
||||
}
|
||||
|
||||
private fun subIndexToIndex(subList: List<*>, index: Int): Int {
|
||||
if (index < 0)
|
||||
throw IndexOutOfBoundsException()
|
||||
var idx = 0
|
||||
for (list in lists) {
|
||||
if (subList === list) {
|
||||
return idx + index
|
||||
}
|
||||
idx += list.size
|
||||
}
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
inner class Callback<T> : OnListChangedCallback<ObservableList<T>>() {
|
||||
override fun onChanged(sender: ObservableList<T>) {
|
||||
++modCount
|
||||
listeners.notifyChanged(this@MergeObservableList)
|
||||
}
|
||||
|
||||
override fun onItemRangeChanged(
|
||||
sender: ObservableList<T>,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
listeners.notifyChanged(this@MergeObservableList,
|
||||
subIndexToIndex(sender, positionStart), itemCount)
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(
|
||||
sender: ObservableList<T>,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
++modCount
|
||||
listeners.notifyInserted(this@MergeObservableList,
|
||||
subIndexToIndex(sender, positionStart), itemCount)
|
||||
}
|
||||
|
||||
override fun onItemRangeMoved(
|
||||
sender: ObservableList<T>,
|
||||
fromPosition: Int,
|
||||
toPosition: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
val idx = subIndexToIndex(sender, 0)
|
||||
listeners.notifyMoved(this@MergeObservableList,
|
||||
idx + fromPosition, idx + toPosition, itemCount)
|
||||
}
|
||||
|
||||
override fun onItemRangeRemoved(
|
||||
sender: ObservableList<T>,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
++modCount
|
||||
listeners.notifyRemoved(this@MergeObservableList,
|
||||
subIndexToIndex(sender, positionStart), itemCount)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,89 +1,35 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.databinding.PropertyChangeRegistry
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.magisk.BR
|
||||
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
||||
|
||||
abstract class RvItem {
|
||||
|
||||
abstract val layoutRes: Int
|
||||
|
||||
@CallSuper
|
||||
open fun bind(binding: ItemBinding<*>) {
|
||||
binding.set(BR.item, layoutRes)
|
||||
}
|
||||
|
||||
/**
|
||||
* This callback is useful if you want to manipulate your views directly.
|
||||
* If you want to use this callback, you must set [me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter]
|
||||
* on your RecyclerView and call it from there. You can use [BindingBoundAdapter] for your convenience.
|
||||
*/
|
||||
open fun onBindingBound(binding: ViewDataBinding) {}
|
||||
}
|
||||
|
||||
interface RvContainer<E> {
|
||||
val item: E
|
||||
}
|
||||
|
||||
interface ComparableRv<T> : Comparable<T> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun comparableEqual(o: Any?) =
|
||||
o != null && o::class == this::class && compareTo(o as T) == 0
|
||||
}
|
||||
|
||||
abstract class DiffRvItem<T> : RvItem() {
|
||||
|
||||
// Defer to contentSameAs by default
|
||||
open fun itemSameAs(other: T) = true
|
||||
|
||||
open fun contentSameAs(other: T) =
|
||||
when (this) {
|
||||
is RvContainer<*> -> item == (other as RvContainer<*>).item
|
||||
is ComparableRv<*> -> comparableEqual(other)
|
||||
else -> this == other
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val callback = object : DiffObservableList.Callback<DiffRvItem<Any>> {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: DiffRvItem<Any>,
|
||||
newItem: DiffRvItem<Any>
|
||||
): Boolean {
|
||||
return oldItem::class == newItem::class && oldItem.itemSameAs(newItem)
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: DiffRvItem<Any>,
|
||||
newItem: DiffRvItem<Any>
|
||||
): Boolean {
|
||||
return oldItem.contentSameAs(newItem)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : AnyDiffRvItem> callback() = callback as DiffObservableList.Callback<T>
|
||||
}
|
||||
}
|
||||
|
||||
typealias AnyDiffRvItem = DiffRvItem<*>
|
||||
|
||||
abstract class ObservableDiffRvItem<T> : DiffRvItem<T>(), ObservableHost {
|
||||
override var callbacks: PropertyChangeRegistry? = null
|
||||
}
|
||||
|
||||
abstract class ObservableRvItem : RvItem(), ObservableHost {
|
||||
override var callbacks: PropertyChangeRegistry? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* This item addresses issues where enclosing recycler has to be invalidated or generally
|
||||
* manipulated with. This shouldn't be however necessary for 99.9% of use-cases. Refrain from using
|
||||
* this item as it provides virtually no additional functionality. Stick with ComparableRvItem.
|
||||
* */
|
||||
|
||||
interface LenientRvItem {
|
||||
fun onBindingBound(binding: ViewDataBinding, recyclerView: RecyclerView)
|
||||
interface ItemWrapper<E> {
|
||||
val item: E
|
||||
}
|
||||
|
||||
interface ViewAwareItem {
|
||||
fun onBind(binding: ViewDataBinding, recyclerView: RecyclerView)
|
||||
}
|
||||
|
||||
interface DiffItem<T : Any> {
|
||||
|
||||
fun itemSameAs(other: T): Boolean {
|
||||
if (this === other) return true
|
||||
return when (this) {
|
||||
is ItemWrapper<*> -> item == (other as ItemWrapper<*>).item
|
||||
is Comparable<*> -> compareValues(this, other as Comparable<*>) == 0
|
||||
else -> this == other
|
||||
}
|
||||
}
|
||||
|
||||
fun contentSameAs(other: T) = true
|
||||
}
|
||||
|
@@ -1,35 +0,0 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
|
||||
|
||||
class RvBindingAdapter<T : RvItem> : BindingRecyclerViewAdapter<T>() {
|
||||
|
||||
private var recyclerView: RecyclerView? = null
|
||||
|
||||
override fun onBindBinding(
|
||||
binding: ViewDataBinding,
|
||||
variableId: Int,
|
||||
layoutRes: Int,
|
||||
position: Int,
|
||||
item: T
|
||||
) {
|
||||
super.onBindBinding(binding, variableId, layoutRes, position, item)
|
||||
|
||||
when (item) {
|
||||
is LenientRvItem -> {
|
||||
val recycler = recyclerView ?: return
|
||||
item.onBindingBound(binding)
|
||||
item.onBindingBound(binding, recycler)
|
||||
}
|
||||
else -> item.onBindingBound(binding)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onAttachedToRecyclerView(recyclerView)
|
||||
this.recyclerView = recyclerView
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,121 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.util.SparseArray
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.BindingAdapter
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ObservableList
|
||||
import androidx.databinding.ObservableList.OnListChangedCallback
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.magisk.BR
|
||||
|
||||
class RvItemAdapter<T: RvItem>(
|
||||
val items: List<T>,
|
||||
val extraBindings: SparseArray<*>?
|
||||
) : RecyclerView.Adapter<RvItemAdapter.ViewHolder>() {
|
||||
|
||||
private var lifecycleOwner: LifecycleOwner? = null
|
||||
private var recyclerView: RecyclerView? = null
|
||||
private val observer by lazy(LazyThreadSafetyMode.NONE) { ListObserver<T>() }
|
||||
|
||||
override fun onAttachedToRecyclerView(rv: RecyclerView) {
|
||||
lifecycleOwner = rv.findViewTreeLifecycleOwner()
|
||||
recyclerView = rv
|
||||
if (items is ObservableList)
|
||||
items.addOnListChangedCallback(observer)
|
||||
}
|
||||
|
||||
override fun onDetachedFromRecyclerView(rv: RecyclerView) {
|
||||
lifecycleOwner = null
|
||||
recyclerView = null
|
||||
if (items is ObservableList)
|
||||
items.removeOnListChangedCallback(observer)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, layoutRes: Int): ViewHolder {
|
||||
val inflator = LayoutInflater.from(parent.context)
|
||||
return ViewHolder(DataBindingUtil.inflate(inflator, layoutRes, parent, false))
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val item = items[position]
|
||||
holder.binding.setVariable(BR.item, item)
|
||||
extraBindings?.let {
|
||||
for (i in 0 until it.size()) {
|
||||
holder.binding.setVariable(it.keyAt(i), it.valueAt(i))
|
||||
}
|
||||
}
|
||||
holder.binding.lifecycleOwner = lifecycleOwner
|
||||
holder.binding.executePendingBindings()
|
||||
recyclerView?.let {
|
||||
if (item is ViewAwareItem)
|
||||
item.onBind(holder.binding, it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun getItemViewType(position: Int) = items[position].layoutRes
|
||||
|
||||
class ViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
inner class ListObserver<T: RvItem> : OnListChangedCallback<ObservableList<T>>() {
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onChanged(sender: ObservableList<T>) {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onItemRangeChanged(
|
||||
sender: ObservableList<T>,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
notifyItemRangeChanged(positionStart, itemCount)
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(
|
||||
sender: ObservableList<T>?,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
notifyItemRangeInserted(positionStart, itemCount)
|
||||
}
|
||||
|
||||
override fun onItemRangeMoved(
|
||||
sender: ObservableList<T>?,
|
||||
fromPosition: Int,
|
||||
toPosition: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
for (i in 0 until itemCount) {
|
||||
notifyItemMoved(fromPosition + i, toPosition + i)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemRangeRemoved(
|
||||
sender: ObservableList<T>?,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
notifyItemRangeRemoved(positionStart, itemCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun bindExtra(body: (SparseArray<Any?>) -> Unit) = SparseArray<Any?>().also(body)
|
||||
|
||||
@BindingAdapter("items", "extraBindings", requireAll = false)
|
||||
fun <T: RvItem> RecyclerView.setAdapter(items: List<T>?, extraBindings: SparseArray<*>?) {
|
||||
if (items != null) {
|
||||
val rva = (adapter as? RvItemAdapter<*>)
|
||||
if (rva == null || rva.items !== items || rva.extraBindings !== extraBindings) {
|
||||
adapter = RvItemAdapter(items, extraBindings)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,12 +1,14 @@
|
||||
package com.topjohnwu.magisk.events.dialog
|
||||
package com.topjohnwu.magisk.dialog
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.UIActivity
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.events.DialogBuilder
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
|
||||
class DarkThemeDialog : DialogEvent() {
|
||||
class DarkThemeDialog : DialogBuilder {
|
||||
|
||||
override fun build(dialog: MagiskDialog) {
|
||||
val activity = dialog.ownerActivity!!
|
||||
@@ -33,6 +35,6 @@ class DarkThemeDialog : DialogEvent() {
|
||||
|
||||
private fun selectTheme(mode: Int, activity: Activity) {
|
||||
Config.darkTheme = mode
|
||||
activity.recreate()
|
||||
(activity as UIActivity<*>).delegate.localNightMode = mode
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.events.dialog
|
||||
package com.topjohnwu.magisk.dialog
|
||||
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
@@ -6,11 +6,12 @@ import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
||||
import com.topjohnwu.magisk.events.DialogBuilder
|
||||
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class EnvFixDialog(private val vm: HomeViewModel) : DialogEvent() {
|
||||
class EnvFixDialog(private val vm: HomeViewModel, private val code: Int) : DialogBuilder {
|
||||
|
||||
override fun build(dialog: MagiskDialog) {
|
||||
dialog.apply {
|
||||
@@ -38,8 +39,10 @@ class EnvFixDialog(private val vm: HomeViewModel) : DialogEvent() {
|
||||
}
|
||||
}
|
||||
|
||||
if (Info.env.versionCode != BuildConfig.VERSION_CODE ||
|
||||
if (code == 2 || // No rules block, module policy not loaded
|
||||
Info.env.versionCode != BuildConfig.VERSION_CODE ||
|
||||
Info.env.versionString != BuildConfig.VERSION_NAME) {
|
||||
dialog.setMessage(R.string.env_full_fix_msg)
|
||||
dialog.setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||
text = android.R.string.ok
|
||||
onClick {
|
@@ -0,0 +1,33 @@
|
||||
package com.topjohnwu.magisk.dialog
|
||||
|
||||
import android.net.Uri
|
||||
import com.topjohnwu.magisk.MainDirections
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.events.DialogBuilder
|
||||
import com.topjohnwu.magisk.ui.module.ModuleViewModel
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
|
||||
class LocalModuleInstallDialog(
|
||||
private val viewModel: ModuleViewModel,
|
||||
private val uri: Uri,
|
||||
private val displayName: String
|
||||
) : DialogBuilder {
|
||||
override fun build(dialog: MagiskDialog) {
|
||||
dialog.apply {
|
||||
setTitle(R.string.confirm_install_title)
|
||||
setMessage(context.getString(R.string.confirm_install, displayName))
|
||||
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||
text = android.R.string.ok
|
||||
onClick {
|
||||
viewModel.apply {
|
||||
MainDirections.actionFlashFragment(Const.Value.FLASH_ZIP, uri).navigate()
|
||||
}
|
||||
}
|
||||
}
|
||||
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||
text = android.R.string.cancel
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,11 +1,11 @@
|
||||
package com.topjohnwu.magisk.events.dialog
|
||||
package com.topjohnwu.magisk.dialog
|
||||
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.download.DownloadService
|
||||
import com.topjohnwu.magisk.core.download.Subject
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
import java.io.File
|
||||
|
||||
@@ -29,7 +29,7 @@ class ManagerInstallDialog : MarkDownDialog() {
|
||||
setCancelable(true)
|
||||
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||
text = R.string.install
|
||||
onClick { DownloadService.start(context, Subject.App()) }
|
||||
onClick { DownloadService.start(activity, Subject.App()) }
|
||||
}
|
||||
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||
text = android.R.string.cancel
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user