mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-08-20 11:17:42 +00:00
Compare commits
863 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5a16418543 | ||
![]() |
7297aba15a | ||
![]() |
bc5d5f9502 | ||
![]() |
1761986c1b | ||
![]() |
1e034e3e0e | ||
![]() |
bbf9756bfa | ||
![]() |
96e559fb0e | ||
![]() |
4c45775131 | ||
![]() |
c072b4254d | ||
![]() |
2dbb812126 | ||
![]() |
be50f17f55 | ||
![]() |
6f77f190f2 | ||
![]() |
6bdc57cbe4 | ||
![]() |
de00f1d5a9 | ||
![]() |
e9b9bf987b | ||
![]() |
f4b6385f9f | ||
![]() |
75d905a56d | ||
![]() |
b1363ee479 | ||
![]() |
51afe43a30 | ||
![]() |
189c03c047 | ||
![]() |
ae9d270a32 | ||
![]() |
e47e869f6b | ||
![]() |
c39038a439 | ||
![]() |
69174e2c13 | ||
![]() |
474268a0af | ||
![]() |
eadb0307fa | ||
![]() |
5a5d0d5d72 | ||
![]() |
a1273bc467 | ||
![]() |
0c28a916be | ||
![]() |
0ba573b789 | ||
![]() |
ec42ee152c | ||
![]() |
abcb487361 | ||
![]() |
d12d9e82f1 | ||
![]() |
275208e81b | ||
![]() |
41226c12b8 | ||
![]() |
f86c66c99d | ||
![]() |
93876b5fd3 | ||
![]() |
b5b14ce343 | ||
![]() |
350d0d600c | ||
![]() |
f924ffcbf3 | ||
![]() |
0f5963f231 | ||
![]() |
1961ff2c40 | ||
![]() |
40003691d6 | ||
![]() |
8290358241 | ||
![]() |
ee34f775c3 | ||
![]() |
feb47cd88c | ||
![]() |
c6efb51f61 | ||
![]() |
a5acf33ccd | ||
![]() |
ab9ee449e4 | ||
![]() |
9571b6f9be | ||
![]() |
207d7fd3f6 | ||
![]() |
bcdcfa1104 | ||
![]() |
e0a4230dac | ||
![]() |
17ba5cba3e | ||
![]() |
f2e109ad7d | ||
![]() |
c83e141a1c | ||
![]() |
6089cc36de | ||
![]() |
9638dc0a66 | ||
![]() |
b191a14a23 | ||
![]() |
cf1bc82537 | ||
![]() |
6141bb5bb3 | ||
![]() |
4d2b62da0d | ||
![]() |
39383229d1 | ||
![]() |
08bfbb154a | ||
![]() |
d390ca2fdf | ||
![]() |
7ad77a14ae | ||
![]() |
f33343b4e6 | ||
![]() |
af65d07456 | ||
![]() |
16d728f379 | ||
![]() |
c97ab690b6 | ||
![]() |
4caed73fe0 | ||
![]() |
4856da1584 | ||
![]() |
0a07018fec | ||
![]() |
64c82e1f2c | ||
![]() |
e8e8afa6c2 | ||
![]() |
af2207433d | ||
![]() |
75ba62d588 | ||
![]() |
606d97ae4d | ||
![]() |
d778b0b0a7 | ||
![]() |
5ee6daf126 | ||
![]() |
43b9a09c9b | ||
![]() |
8475a2bb94 | ||
![]() |
d8692de2f4 | ||
![]() |
33a9abc946 | ||
![]() |
ee943afbc9 | ||
![]() |
1f7c3e9f14 | ||
![]() |
46770db18b | ||
![]() |
92f980601c | ||
![]() |
d0b8c16651 | ||
![]() |
a470ee6f93 | ||
![]() |
ff1c56683d | ||
![]() |
4ee4cbada6 | ||
![]() |
dbc2236dd2 | ||
![]() |
a8c4a33e91 | ||
![]() |
279f955a84 | ||
![]() |
fbd1dbb20c | ||
![]() |
6c09fc2e64 | ||
![]() |
f3304b482c | ||
![]() |
0a85ef61c3 | ||
![]() |
dc26ad7125 | ||
![]() |
24b1c607f3 | ||
![]() |
732a161b67 | ||
![]() |
9c7cf340a1 | ||
![]() |
399b9e5eba | ||
![]() |
5805573625 | ||
![]() |
a6b1149b9f | ||
![]() |
51e985ae7f | ||
![]() |
9929b25339 | ||
![]() |
2359cfc480 | ||
![]() |
81493475f9 | ||
![]() |
0493829231 | ||
![]() |
e2d1952ad9 | ||
![]() |
7450965458 | ||
![]() |
f45384685b | ||
![]() |
8abcccc262 | ||
![]() |
a9c89cbbbb | ||
![]() |
d2eaa6e6c1 | ||
![]() |
53257b6ea1 | ||
![]() |
c874391be4 | ||
![]() |
7e8e013832 | ||
![]() |
037f46f7f0 | ||
![]() |
d3e1c496ca | ||
![]() |
d7d0a44693 | ||
![]() |
9d6f6764cb | ||
![]() |
cb3ab63815 | ||
![]() |
caae932117 | ||
![]() |
e9cf27eb5a | ||
![]() |
6ee6685f4c | ||
![]() |
d15017b777 | ||
![]() |
a9387e63e1 | ||
![]() |
23c1f0111b | ||
![]() |
866386e21f | ||
![]() |
bf10496fa9 | ||
![]() |
607e6547a7 | ||
![]() |
6b21091fe2 | ||
![]() |
e58f98e844 | ||
![]() |
b8cb9cd84d | ||
![]() |
c1038ac6f9 | ||
![]() |
c556dd0aac | ||
![]() |
d2fbcd07b7 | ||
![]() |
bf6359abaa | ||
![]() |
d1621845b8 | ||
![]() |
f33f1d25d0 | ||
![]() |
40f25f4d56 | ||
![]() |
e13775ec2c | ||
![]() |
ee4dad7a13 | ||
![]() |
5e2ef1b7f4 | ||
![]() |
f8c38eab74 | ||
![]() |
305e8b3d14 | ||
![]() |
2a654e5d7f | ||
![]() |
57afae3425 | ||
![]() |
feb44f875e | ||
![]() |
7eebe62bb6 | ||
![]() |
9ea9f01933 | ||
![]() |
665c6bdc4b | ||
![]() |
c79bc83275 | ||
![]() |
c30fbdf145 | ||
![]() |
f12951bd1d | ||
![]() |
52f2e8c4a0 | ||
![]() |
1b2af1ed6d | ||
![]() |
0f9b2a7df8 | ||
![]() |
f2846694e1 | ||
![]() |
e668dbf6f7 | ||
![]() |
d77a368176 | ||
![]() |
ad0da08610 | ||
![]() |
0c52385ad4 | ||
![]() |
5b8b48ccc1 | ||
![]() |
659b9c6fee | ||
![]() |
ec31cab5a7 | ||
![]() |
dd93556ad8 | ||
![]() |
533aeadd38 | ||
![]() |
18d0cedbe2 | ||
![]() |
5a94ef9106 | ||
![]() |
8e8f01f8b5 | ||
![]() |
7087badf87 | ||
![]() |
47d2d4e3a5 | ||
![]() |
6c47d8f556 | ||
![]() |
8c9d0314fb | ||
![]() |
69144942e3 | ||
![]() |
5627053b74 | ||
![]() |
0f666de5e6 | ||
![]() |
eddc862fa3 | ||
![]() |
4327682120 | ||
![]() |
af5bdee78f | ||
![]() |
0e36e86dbf | ||
![]() |
f95478f1f1 | ||
![]() |
9fe8741a02 | ||
![]() |
a5768e02ea | ||
![]() |
f5aaff2b1e | ||
![]() |
655f778171 | ||
![]() |
2e77a426b2 | ||
![]() |
2bcf2e76f1 | ||
![]() |
57bd450798 | ||
![]() |
582cad1b8d | ||
![]() |
6ca2a3d841 | ||
![]() |
91773c3311 | ||
![]() |
dc61033b2c | ||
![]() |
f8d62a4b6c | ||
![]() |
1d2145b1b7 | ||
![]() |
1f7f84b74a | ||
![]() |
cd7a335d0f | ||
![]() |
17569005a4 | ||
![]() |
f36b21bae5 | ||
![]() |
fe1ca52f6d | ||
![]() |
1be647a279 | ||
![]() |
2ea1a47bec | ||
![]() |
2d799dae0d | ||
![]() |
c6408babac | ||
![]() |
a8c1ed8795 | ||
![]() |
e3cb5f8ddd | ||
![]() |
e160e211dd | ||
![]() |
22d05ca399 | ||
![]() |
bd2651057d | ||
![]() |
1610092ec4 | ||
![]() |
b9e6937996 | ||
![]() |
a207f03952 | ||
![]() |
851153dd7c | ||
![]() |
583ffc8177 | ||
![]() |
7518092ad2 | ||
![]() |
8c2ad3883a | ||
![]() |
d364554425 | ||
![]() |
726ffdcd98 | ||
![]() |
f9d22cf8ee | ||
![]() |
ee50da566f | ||
![]() |
9f7d410959 | ||
![]() |
bc94ea4334 | ||
![]() |
c0c9204848 | ||
![]() |
c0d1bf63bc | ||
![]() |
bbda0cdffe | ||
![]() |
7b5ff99cd1 | ||
![]() |
21ddb26db8 | ||
![]() |
7bf2e3875f | ||
![]() |
b136aba1e2 | ||
![]() |
0d84f80b3c | ||
![]() |
af45aeb771 | ||
![]() |
1c5a435e1f | ||
![]() |
0ea1257dcd | ||
![]() |
4c92677b5a | ||
![]() |
979260bd62 | ||
![]() |
f7de649a36 | ||
![]() |
0cf0d2b821 | ||
![]() |
3733c9a091 | ||
![]() |
e9f32e4f68 | ||
![]() |
68c2817d40 | ||
![]() |
83d837d868 | ||
![]() |
093eb15ee1 | ||
![]() |
c6412c1b1b | ||
![]() |
1151393d74 | ||
![]() |
468f3efb13 | ||
![]() |
d6b19b9d4c | ||
![]() |
709f25f600 | ||
![]() |
4b16e4b026 | ||
![]() |
cdfbc02922 | ||
![]() |
d0c9384233 | ||
![]() |
2488668b06 | ||
![]() |
52a98cbd51 | ||
![]() |
1840c4c486 | ||
![]() |
34080f3958 | ||
![]() |
e9b76b6aa5 | ||
![]() |
b7799b53d9 | ||
![]() |
1e206515c7 | ||
![]() |
6bb313184d | ||
![]() |
2763992434 | ||
![]() |
18fe0e6442 | ||
![]() |
a70c73bffd | ||
![]() |
b4ae3493a6 | ||
![]() |
1a16004b20 | ||
![]() |
56707b8119 | ||
![]() |
c3f9533ddc | ||
![]() |
3b3abd63cc | ||
![]() |
411d3ed4e9 | ||
![]() |
f29cc26103 | ||
![]() |
1cd595a598 | ||
![]() |
22e023b58d | ||
![]() |
7be958e35d | ||
![]() |
69b66ef637 | ||
![]() |
daf8653c38 | ||
![]() |
e2545e57cf | ||
![]() |
7cb0909c70 | ||
![]() |
cc5ff36165 | ||
![]() |
18b1ef6c29 | ||
![]() |
7fe012347a | ||
![]() |
5c165c9bb0 | ||
![]() |
6c3519923d | ||
![]() |
9ea859810d | ||
![]() |
8dae7b5451 | ||
![]() |
f827755aaf | ||
![]() |
637a8af234 | ||
![]() |
b0fc580860 | ||
![]() |
9279f30e89 | ||
![]() |
b505819ca2 | ||
![]() |
39d1d23909 | ||
![]() |
69529ac59c | ||
![]() |
a18a440236 | ||
![]() |
aa7846c1c0 | ||
![]() |
24ba4ab95b | ||
![]() |
762b70ba9d | ||
![]() |
41b77e4f25 | ||
![]() |
2087e47300 | ||
![]() |
46ce765860 | ||
![]() |
5117dc1a31 | ||
![]() |
620fd7d124 | ||
![]() |
3e991dc003 | ||
![]() |
15cab86152 | ||
![]() |
aa785b5845 | ||
![]() |
97731a519a | ||
![]() |
b696dae808 | ||
![]() |
732a8260c2 | ||
![]() |
4ff60ef9a9 | ||
![]() |
23b1b69110 | ||
![]() |
3a4fe53f27 | ||
![]() |
e48afff5e8 | ||
![]() |
3f4f4598e8 | ||
![]() |
3921e9cb1b | ||
![]() |
0b987dd0b0 | ||
![]() |
1620e15f99 | ||
![]() |
b089511e91 | ||
![]() |
958788c1aa | ||
![]() |
b5a8a27296 | ||
![]() |
98123775ad | ||
![]() |
c7133974be | ||
![]() |
04324a7ebe | ||
![]() |
f54daa3469 | ||
![]() |
07c22ccd39 | ||
![]() |
e893c13cf1 | ||
![]() |
dba5020e4f | ||
![]() |
87e036a190 | ||
![]() |
3dd94672b0 | ||
![]() |
004b193f69 | ||
![]() |
4417997749 | ||
![]() |
2eef542054 | ||
![]() |
a07d4080b6 | ||
![]() |
b9d0a3b3d4 | ||
![]() |
76405bd984 | ||
![]() |
4e2b88b3d0 | ||
![]() |
7048aa1014 | ||
![]() |
1c2fcd14b5 | ||
![]() |
84e1bd7bc3 | ||
![]() |
362eea741f | ||
![]() |
4de93cfd4b | ||
![]() |
03cee0b8d4 | ||
![]() |
54ecc001f4 | ||
![]() |
5c325d9466 | ||
![]() |
0e851cdcf8 | ||
![]() |
af054e4e31 | ||
![]() |
33fb4653f0 | ||
![]() |
d9f0aed571 | ||
![]() |
98813c24fb | ||
![]() |
3cc81bb3fd | ||
![]() |
366dd52419 | ||
![]() |
fe6b658c02 | ||
![]() |
3cf66d1c57 | ||
![]() |
382568bd3c | ||
![]() |
d130aa02a1 | ||
![]() |
1a1646795f | ||
![]() |
d52ea1b068 | ||
![]() |
e14f7b6908 | ||
![]() |
4709a32641 | ||
![]() |
71b7f52663 | ||
![]() |
981ccabbef | ||
![]() |
9e07eb592c | ||
![]() |
9555380818 | ||
![]() |
f80d5d858e | ||
![]() |
a1ce6f5f12 | ||
![]() |
1aade8f8a8 | ||
![]() |
b9213b7043 | ||
![]() |
4af72324f4 | ||
![]() |
b6ea5b8984 | ||
![]() |
c279e08c88 | ||
![]() |
2717feac21 | ||
![]() |
8adf27859d | ||
![]() |
307cf87215 | ||
![]() |
ca31412c05 | ||
![]() |
f59fbd5dca | ||
![]() |
2285f5e888 | ||
![]() |
da36e5bcd5 | ||
![]() |
4ed9f57fdc | ||
![]() |
ea7be6162f | ||
![]() |
3726eb6032 | ||
![]() |
6e918ffd68 | ||
![]() |
4772868d6a | ||
![]() |
78df677a42 | ||
![]() |
85a4b249b3 | ||
![]() |
d06e9a0b51 | ||
![]() |
5eb774a2ad | ||
![]() |
cbbbbab483 | ||
![]() |
e5641d5bdb | ||
![]() |
a721206c6f | ||
![]() |
c7a27481f9 | ||
![]() |
594c304634 | ||
![]() |
d0ec387c28 | ||
![]() |
7dbfba76bf | ||
![]() |
2a4aa95a6f | ||
![]() |
5520f0fbf7 | ||
![]() |
a1a87c9956 | ||
![]() |
2c53356bfd | ||
![]() |
85d9756f62 | ||
![]() |
79586ece4c | ||
![]() |
6851d11a8e | ||
![]() |
996a857096 | ||
![]() |
d7158131e4 | ||
![]() |
3d3082bc82 | ||
![]() |
744ebca206 | ||
![]() |
92077ebe53 | ||
![]() |
78ca682bc5 | ||
![]() |
af01a36296 | ||
![]() |
97ed1b16d0 | ||
![]() |
508a001753 | ||
![]() |
c1909d520b | ||
![]() |
9b1e173373 | ||
![]() |
4ba365565f | ||
![]() |
ae34659b26 | ||
![]() |
79a85f5937 | ||
![]() |
b249832571 | ||
![]() |
577b5912af | ||
![]() |
9e8c68af12 | ||
![]() |
03418ddcbf | ||
![]() |
220a1c84ce | ||
![]() |
9a4458ffac | ||
![]() |
7a9e6d2ad2 | ||
![]() |
9656cf2f86 | ||
![]() |
584bad5314 | ||
![]() |
459088024f | ||
![]() |
d740bbe058 | ||
![]() |
6ecc04a4df | ||
![]() |
15a7e9af57 | ||
![]() |
0329f00129 | ||
![]() |
cd8a2edefb | ||
![]() |
4318ab5cd2 | ||
![]() |
3517e6d752 | ||
![]() |
67845f9c21 | ||
![]() |
f562710438 | ||
![]() |
e836909c50 | ||
![]() |
7769ba5f54 | ||
![]() |
7fe9db90a1 | ||
![]() |
8f7d6dfb77 | ||
![]() |
2839978cc1 | ||
![]() |
e73f87b758 | ||
![]() |
bd0409fd15 | ||
![]() |
babdfe80cb | ||
![]() |
636223b289 | ||
![]() |
aa0a2f77cf | ||
![]() |
e38f35eab2 | ||
![]() |
cb39514705 | ||
![]() |
78a444d601 | ||
![]() |
37b81ad1f6 | ||
![]() |
7871c2f595 | ||
![]() |
57d83635c6 | ||
![]() |
76fbf4634a | ||
![]() |
7ce4bd3330 | ||
![]() |
ad0e6511e1 | ||
![]() |
a4a734458b | ||
![]() |
f989756b93 | ||
![]() |
5763a3d908 | ||
![]() |
1b745ae1a0 | ||
![]() |
b6d50bea2c | ||
![]() |
831a398bf1 | ||
![]() |
a848783b97 | ||
![]() |
4d876f0145 | ||
![]() |
bdfedea4e0 | ||
![]() |
ea0e3a09ef | ||
![]() |
dadae20960 | ||
![]() |
4ed34cd648 | ||
![]() |
0d38c94c9c | ||
![]() |
2a2a452bd4 | ||
![]() |
13c2695e98 | ||
![]() |
3ff60ed49f | ||
![]() |
bbb1786ec3 | ||
![]() |
4bfd2dac54 | ||
![]() |
857c12372a | ||
![]() |
33f5154269 | ||
![]() |
ed37ddd570 | ||
![]() |
cd5384f13e | ||
![]() |
11b2ddbad8 | ||
![]() |
cf9957ce4d | ||
![]() |
44643ad7b3 | ||
![]() |
1e53a5555e | ||
![]() |
616adc22e1 | ||
![]() |
916e373edb | ||
![]() |
021ae15395 | ||
![]() |
52cf72002a | ||
![]() |
68874bf571 | ||
![]() |
a468fd946d | ||
![]() |
e327565434 | ||
![]() |
c3b4678f6e | ||
![]() |
978216eade | ||
![]() |
44cfe94e4d | ||
![]() |
f9e82c9e8a | ||
![]() |
25b4b107d3 | ||
![]() |
db651fa9ec | ||
![]() |
23ad611566 | ||
![]() |
095d821240 | ||
![]() |
e23f23a8b7 | ||
![]() |
48f829b76e | ||
![]() |
0b82fe197c | ||
![]() |
af99c1b843 | ||
![]() |
c6646efe68 | ||
![]() |
66a7ef5615 | ||
![]() |
9474750bdf | ||
![]() |
e86db0bd61 | ||
![]() |
a29fc11798 | ||
![]() |
a66a3b7438 | ||
![]() |
44029875a6 | ||
![]() |
ccf21b0992 | ||
![]() |
4e14dab60a | ||
![]() |
6e299018a4 | ||
![]() |
555a54ec53 | ||
![]() |
1565bf5442 | ||
![]() |
14b830027b | ||
![]() |
38325e708e | ||
![]() |
646260ad6d | ||
![]() |
d1d26f4481 | ||
![]() |
357d913f18 | ||
![]() |
71b0c8b42b | ||
![]() |
cdc66c1ac8 | ||
![]() |
e9af773901 | ||
![]() |
eadf6e8b96 | ||
![]() |
87bec70d9f | ||
![]() |
3668b28f62 | ||
![]() |
933e4bd163 | ||
![]() |
e3ab9e9a1e | ||
![]() |
58ad2c1416 | ||
![]() |
c5291ad33b | ||
![]() |
77d8445bfd | ||
![]() |
f8395a7dc6 | ||
![]() |
727c70005e | ||
![]() |
38ab6858f0 | ||
![]() |
a54114f149 | ||
![]() |
7a4a5c8992 | ||
![]() |
928a16d8cc | ||
![]() |
3f7f6e619a | ||
![]() |
c2f96975ce | ||
![]() |
8bd4760b00 | ||
![]() |
4f4aeb893d | ||
![]() |
fed4f1b50f | ||
![]() |
e11087cd1a | ||
![]() |
e6eb51551c | ||
![]() |
c5c608f0d3 | ||
![]() |
4737c5117a | ||
![]() |
9806b38d8e | ||
![]() |
6bfe34e5a8 | ||
![]() |
34dd9eb7d6 | ||
![]() |
2d8beabbd4 | ||
![]() |
4d9b7e7114 | ||
![]() |
40aab13601 | ||
![]() |
4c0f72f68f | ||
![]() |
dd565a11ea | ||
![]() |
1735a713cb | ||
![]() |
52ba6d11bc | ||
![]() |
7357a35f8d | ||
![]() |
aeb7fd7cb3 | ||
![]() |
1b4a6850b8 | ||
![]() |
07b45f39df | ||
![]() |
1d0b873950 | ||
![]() |
d449f49d73 | ||
![]() |
e8787b5cfd | ||
![]() |
d17ed2b979 | ||
![]() |
b496923cbb | ||
![]() |
759d196aad | ||
![]() |
a7ab8216ce | ||
![]() |
b9e89a1a2d | ||
![]() |
c7c9fb9576 | ||
![]() |
8b095de04d | ||
![]() |
468325b51a | ||
![]() |
e5058bfb8b | ||
![]() |
d4b9ef736d | ||
![]() |
00d3cb0908 | ||
![]() |
d35072d4e6 | ||
![]() |
1a964e78dd | ||
![]() |
4264ae49c0 | ||
![]() |
f08712cd0a | ||
![]() |
3906fe75dc | ||
![]() |
2497e548c9 | ||
![]() |
e4635684e9 | ||
![]() |
9b61bdfc9a | ||
![]() |
6066b5cf86 | ||
![]() |
5cdf95a4d0 | ||
![]() |
910a36fdc1 | ||
![]() |
8331206acb | ||
![]() |
8423dc8d63 | ||
![]() |
6077c989a7 | ||
![]() |
c97d1044fa | ||
![]() |
f42c089b26 | ||
![]() |
1f8c063dc6 | ||
![]() |
4874520d65 | ||
![]() |
5e53639969 | ||
![]() |
83ab0ca6cd | ||
![]() |
70fd03d5fc | ||
![]() |
2e52875b50 | ||
![]() |
fd9b990ad7 | ||
![]() |
69978a9442 | ||
![]() |
d155da52ce | ||
![]() |
9c5b131913 | ||
![]() |
9d740cec1a | ||
![]() |
c2978eb9c3 | ||
![]() |
38abad1e44 | ||
![]() |
b4863eb51b | ||
![]() |
3817167ba1 | ||
![]() |
d1a35dd2ba | ||
![]() |
26116ac414 | ||
![]() |
0b26882fce | ||
![]() |
a2495fb5fb | ||
![]() |
0beb3bf16a | ||
![]() |
b68658e974 | ||
![]() |
3ae7344747 | ||
![]() |
4eb71830b3 | ||
![]() |
9183a0a6ea | ||
![]() |
bb64ba0ef6 | ||
![]() |
d89a568897 | ||
![]() |
9fd1f41e8b | ||
![]() |
c1ab348673 | ||
![]() |
00247c7901 | ||
![]() |
3c75f474c6 | ||
![]() |
db1f5b0397 | ||
![]() |
db277c3e55 | ||
![]() |
b9c93c66f6 | ||
![]() |
a250e2b56c | ||
![]() |
cd96454886 | ||
![]() |
741b679306 | ||
![]() |
90013e486d | ||
![]() |
4e2ecdb920 | ||
![]() |
6e5df1f06b | ||
![]() |
9469e79e3c | ||
![]() |
db78c20161 | ||
![]() |
1699da1754 | ||
![]() |
754e690274 | ||
![]() |
6f74ed6ceb | ||
![]() |
71205bc530 | ||
![]() |
10e236abdf | ||
![]() |
2248af00f3 | ||
![]() |
7e61716277 | ||
![]() |
50edb8d072 | ||
![]() |
515f81944c | ||
![]() |
46d4708386 | ||
![]() |
aabc36f86b | ||
![]() |
e0d5d90267 | ||
![]() |
482a5b991b | ||
![]() |
20124fe410 | ||
![]() |
f8dcec116a | ||
![]() |
343a339aae | ||
![]() |
42606efe56 | ||
![]() |
cae58c8790 | ||
![]() |
3a39dd4049 | ||
![]() |
89ff3c6572 | ||
![]() |
7bf9c74216 | ||
![]() |
e2f3753551 | ||
![]() |
cacf873645 | ||
![]() |
11e1e7ee36 | ||
![]() |
87801b6f23 | ||
![]() |
7ce4789e17 | ||
![]() |
9dc6d9afce | ||
![]() |
d6a5354bff | ||
![]() |
07af37475b | ||
![]() |
1b9c273b10 | ||
![]() |
262c52db56 | ||
![]() |
eb777296d4 | ||
![]() |
fc70a384d3 | ||
![]() |
34b2f525a3 | ||
![]() |
569e9ad937 | ||
![]() |
c495b3d183 | ||
![]() |
8b16bfbb54 | ||
![]() |
b2f1fd9966 | ||
![]() |
317153c53a | ||
![]() |
fa60daf9b5 | ||
![]() |
aadb2d825c | ||
![]() |
0e7fe537e3 | ||
![]() |
409de3ac44 | ||
![]() |
759055eaa5 | ||
![]() |
9016e6727d | ||
![]() |
a3381da7ed | ||
![]() |
351e094440 | ||
![]() |
2106751ea4 | ||
![]() |
7ae3cd1c43 | ||
![]() |
edfd4dcddf | ||
![]() |
fb89cf1367 | ||
![]() |
b7b345cf8a | ||
![]() |
0be487e47e | ||
![]() |
5471147422 | ||
![]() |
6305159c5e | ||
![]() |
2ed092c9db | ||
![]() |
5c6a7ffa6f | ||
![]() |
9ab7550970 | ||
![]() |
47e7a0a434 | ||
![]() |
4cc5e9f986 | ||
![]() |
6a2ae89846 | ||
![]() |
3c93539e02 | ||
![]() |
05e5ac2ad2 | ||
![]() |
10b1782732 | ||
![]() |
e029994ef8 | ||
![]() |
9679874874 | ||
![]() |
8186f253e8 | ||
![]() |
d4fe8632ec | ||
![]() |
d7776f6597 | ||
![]() |
3219d945f5 | ||
![]() |
8a73a16029 | ||
![]() |
ce90f9b60d | ||
![]() |
bdf54d562f | ||
![]() |
e744cc8ea6 | ||
![]() |
babcf36495 | ||
![]() |
e4094c0caa | ||
![]() |
2e51fe20a1 | ||
![]() |
c29636c452 | ||
![]() |
22017a5543 | ||
![]() |
50e2f33d1c | ||
![]() |
5e6eb8dd01 | ||
![]() |
18acb97dfe | ||
![]() |
bf2f823b8c | ||
![]() |
d0c4226997 | ||
![]() |
4ea8bd0229 | ||
![]() |
ee0d58a9b8 | ||
![]() |
bf04fa134b | ||
![]() |
297662cafb | ||
![]() |
f464a9b269 | ||
![]() |
d19fcd5e21 | ||
![]() |
c0981174a8 | ||
![]() |
0b5f973b31 | ||
![]() |
4159b3871c | ||
![]() |
580c993c0b | ||
![]() |
0cc29350a0 | ||
![]() |
490a784993 | ||
![]() |
9c774f96db | ||
![]() |
99afe7ac07 | ||
![]() |
b3f05fd925 | ||
![]() |
683cfee88b | ||
![]() |
3bcaf0ed5b | ||
![]() |
edb76503d3 | ||
![]() |
484038638f | ||
![]() |
8dfb30fefe | ||
![]() |
2a252d13b8 | ||
![]() |
afa364cfc3 | ||
![]() |
dfa36fb25d | ||
![]() |
c8492b0c58 | ||
![]() |
083ef803fe | ||
![]() |
351f0269ae | ||
![]() |
a29ae15ff7 | ||
![]() |
34dded3b25 | ||
![]() |
975b1a5e36 | ||
![]() |
e11508f84d | ||
![]() |
0772f6dcaf | ||
![]() |
d3fe3a711a | ||
![]() |
756d8356ca | ||
![]() |
42003b4006 | ||
![]() |
dc65a2b884 | ||
![]() |
071ae79fa8 | ||
![]() |
c11ccbae2d | ||
![]() |
6ef86d8d20 | ||
![]() |
985249c3d0 | ||
![]() |
622e09862a | ||
![]() |
7505599ea0 | ||
![]() |
575c417403 | ||
![]() |
9f7a3db8be | ||
![]() |
029422679c | ||
![]() |
05d6d2b51b | ||
![]() |
4cff0384f7 | ||
![]() |
68db366696 | ||
![]() |
358538717c | ||
![]() |
24603b3cef | ||
![]() |
4eb9240806 | ||
![]() |
0469f0b5ae | ||
![]() |
0b8577d02b | ||
![]() |
97135879a1 | ||
![]() |
fef41f68c0 | ||
![]() |
0ac19e3a4e | ||
![]() |
2793d209a4 | ||
![]() |
71e9c044e6 | ||
![]() |
42e5f5150a | ||
![]() |
90545057e9 | ||
![]() |
cffd024e9e | ||
![]() |
8c858592c4 | ||
![]() |
4f1a1879e5 | ||
![]() |
e88eed9a8d | ||
![]() |
9581ae8245 | ||
![]() |
4202b7a9dc | ||
![]() |
b4c398542a | ||
![]() |
081148b2d7 | ||
![]() |
a32c4561ed | ||
![]() |
cc79a96fa3 | ||
![]() |
ff340ce3d8 | ||
![]() |
134508193d | ||
![]() |
c2b74aa83e | ||
![]() |
3358eab991 | ||
![]() |
a609e0aad4 | ||
![]() |
f97866a961 | ||
![]() |
e1987c42c4 | ||
![]() |
18566715e1 | ||
![]() |
79f0f3230c | ||
![]() |
63a89d9f04 | ||
![]() |
f639f39e79 | ||
![]() |
b4099fc5f9 | ||
![]() |
ff2513e276 | ||
![]() |
f24d52436b | ||
![]() |
9de6e8846b | ||
![]() |
01a1213463 | ||
![]() |
f0fbd9214a | ||
![]() |
c4f37c550f | ||
![]() |
448384af06 | ||
![]() |
3f840f53a0 | ||
![]() |
d8718d8ac8 | ||
![]() |
2fb46a11dc | ||
![]() |
9a11412719 | ||
![]() |
98874be171 | ||
![]() |
704f91545e | ||
![]() |
efb3239cbd | ||
![]() |
7e7ddeb9e2 | ||
![]() |
9e8218089b | ||
![]() |
3f660a3963 | ||
![]() |
daeb6711b0 | ||
![]() |
4e1aec28a0 | ||
![]() |
5512917ec1 | ||
![]() |
cd1edc5d56 | ||
![]() |
4f52587586 | ||
![]() |
d7ee4ef5f5 | ||
![]() |
31f88e0f05 | ||
![]() |
9f1740cc4f | ||
![]() |
f2c15c7701 | ||
![]() |
e67d0678f9 | ||
![]() |
b1faa5eed4 | ||
![]() |
7f1f0b9048 | ||
![]() |
183e5f2ecc | ||
![]() |
14efe4939a | ||
![]() |
3dc7d77ea9 | ||
![]() |
0f07bbb3e5 | ||
![]() |
dd5a3416bf | ||
![]() |
2fb49ad780 | ||
![]() |
92f0e53fee | ||
![]() |
876132694d | ||
![]() |
1257ba41c6 | ||
![]() |
2cc71ac7ed | ||
![]() |
753808a4ce | ||
![]() |
32cd694ad5 | ||
![]() |
f008420891 | ||
![]() |
fa8900be65 | ||
![]() |
69c2f407d6 | ||
![]() |
ffcd093db1 | ||
![]() |
8dbf93750f | ||
![]() |
e266a81167 | ||
![]() |
e841aab9e7 | ||
![]() |
49f259065d | ||
![]() |
b10379e700 | ||
![]() |
810d27a618 | ||
![]() |
9b60c005c7 | ||
![]() |
cc6ca0bda2 | ||
![]() |
4512232637 | ||
![]() |
2c092ffdef | ||
![]() |
66406227d6 | ||
![]() |
a11d25bb44 | ||
![]() |
2e58d902b7 | ||
![]() |
237794b05c | ||
![]() |
563a587882 | ||
![]() |
24505cd111 | ||
![]() |
0c681cdab4 | ||
![]() |
13ef3058c6 | ||
![]() |
50b159b43d | ||
![]() |
8c6c328730 | ||
![]() |
c9812ddf08 | ||
![]() |
2ef0449c2c | ||
![]() |
5edc750c47 | ||
![]() |
2f0e396d7f | ||
![]() |
000a163beb | ||
![]() |
80dd37ee31 | ||
![]() |
e0b5645064 | ||
![]() |
e51aacb0b7 | ||
![]() |
2d6af94aa0 |
17
.github/ISSUE_TEMPLATE/bug_report.md
vendored
17
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,19 +1,18 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us improve
|
about: Create a report to help us improve
|
||||||
title: ''
|
title: ""
|
||||||
labels: ''
|
labels: ""
|
||||||
assignees: ''
|
assignees: ""
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|
||||||
## READ BEFORE OPENING ISSUES
|
## READ BEFORE OPENING ISSUES
|
||||||
|
|
||||||
All bug reports require you to **USE CANARY BUILDS**. Please include the version name and version code in the bug report.
|
All bug reports require you to **USE DEBUG BUILDS**. Please include the version name and version code in the bug report.
|
||||||
|
|
||||||
If you experience a bootloop, attach a `dmesg` (kernel logs) when the device refuse to boot. This may very likely require a custom kernel on some devices as `last_kmsg` or `pstore ramoops` are usually not enabled by default. In addition, please also upload the result of `cat /proc/mounts` when your device is working correctly **WITHOUT ROOT**.
|
If you experience a bootloop, attach a `dmesg` (kernel logs) when the device refuse to boot. This may very likely require a custom kernel on some devices as `last_kmsg` or `pstore ramoops` are usually not enabled by default. In addition, please also upload the result of `cat /proc/mounts` when your device is working correctly **WITHOUT MAGISK**.
|
||||||
|
|
||||||
If you experience issues during installation, in recovery, upload the recovery logs, or in Magisk, upload the install logs. Please also upload the `boot.img` or `recovery.img` that you are using for patching.
|
If you experience issues during installation, in recovery, upload the recovery logs, or in Magisk, upload the install logs. Please also upload the `boot.img` or `recovery.img` that you are using for patching.
|
||||||
|
|
||||||
@@ -31,7 +30,7 @@ Without following the rules above, your issue will be closed without explanation
|
|||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
Device:
|
Device:
|
||||||
Android version:
|
Android version:
|
||||||
Magisk version name:
|
Magisk version name:
|
||||||
Magisk version code:
|
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:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ master ]
|
branches: [master]
|
||||||
paths:
|
paths:
|
||||||
- 'app/**'
|
- "app/**"
|
||||||
- 'native/**'
|
- "native/**"
|
||||||
- 'stub/**'
|
- "stub/**"
|
||||||
- 'buildSrc/**'
|
- "buildSrc/**"
|
||||||
- 'build.py'
|
- "build.py"
|
||||||
- 'gradle.properties'
|
- "gradle.properties"
|
||||||
- '.github/workflows/build.yml'
|
- ".github/workflows/build.yml"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [master]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -22,33 +22,35 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
env:
|
|
||||||
NDK_CCACHE: ${{ github.workspace }}/ccache
|
|
||||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out
|
- name: Check out
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: 'recursive'
|
submodules: "recursive"
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: '11'
|
distribution: "temurin"
|
||||||
|
java-version: "17"
|
||||||
|
|
||||||
- name: Set up Python 3
|
- name: Set up Python 3
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.x'
|
python-version: "3.x"
|
||||||
|
|
||||||
- name: Set up ccache
|
- name: Set up sccache
|
||||||
run: bash .github/ccache.sh
|
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
|
- name: Cache Gradle dependencies
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
@@ -58,10 +60,9 @@ jobs:
|
|||||||
restore-keys: ${{ runner.os }}-gradle-
|
restore-keys: ${{ runner.os }}-gradle-
|
||||||
|
|
||||||
- name: Cache build cache
|
- name: Cache build cache
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ github.workspace }}/.ccache
|
|
||||||
~/.gradle/caches/build-cache-*
|
~/.gradle/caches/build-cache-*
|
||||||
key: ${{ runner.os }}-build-cache-${{ github.sha }}
|
key: ${{ runner.os }}-build-cache-${{ github.sha }}
|
||||||
restore-keys: ${{ runner.os }}-build-cache-
|
restore-keys: ${{ runner.os }}-build-cache-
|
||||||
@@ -71,13 +72,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Build release
|
- name: Build release
|
||||||
run: |
|
run: |
|
||||||
./ccache -zp
|
|
||||||
python build.py -vr all
|
python build.py -vr all
|
||||||
|
|
||||||
- name: Build debug
|
- name: Build debug
|
||||||
run: |
|
run: |
|
||||||
python build.py -v all
|
python build.py -v all
|
||||||
./ccache -s
|
|
||||||
|
|
||||||
- name: Stop gradle daemon
|
- name: Stop gradle daemon
|
||||||
run: ./gradlew --stop
|
run: ./gradlew --stop
|
||||||
@@ -85,7 +84,51 @@ jobs:
|
|||||||
# Only upload artifacts built on Linux
|
# Only upload artifacts built on Linux
|
||||||
- name: Upload build artifact
|
- name: Upload build artifact
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: ${{ github.sha }}
|
name: ${{ github.sha }}
|
||||||
path: out
|
path: out
|
||||||
|
|
||||||
|
- name: Upload mapping and native debug symbols
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ github.sha }}-symbols
|
||||||
|
path: app/build/outputs
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Test on ${{ matrix.api }}
|
||||||
|
runs-on: macos-latest
|
||||||
|
needs: build
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
api: [23, 26, 28, 29, 34]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
distribution: "temurin"
|
||||||
|
java-version: "17"
|
||||||
|
|
||||||
|
- name: Set up Python 3
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.x"
|
||||||
|
|
||||||
|
- name: Download build artifacts
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ github.sha }}
|
||||||
|
path: out
|
||||||
|
|
||||||
|
- name: AVD test
|
||||||
|
run: |
|
||||||
|
brew install coreutils
|
||||||
|
scripts/avd_test.sh ${{ matrix.api }}
|
||||||
|
43
.gitmodules
vendored
43
.gitmodules
vendored
@@ -1,45 +1,48 @@
|
|||||||
[submodule "selinux"]
|
[submodule "selinux"]
|
||||||
path = native/jni/external/selinux
|
path = native/src/external/selinux
|
||||||
url = https://github.com/topjohnwu/selinux.git
|
url = https://github.com/topjohnwu/selinux.git
|
||||||
[submodule "busybox"]
|
[submodule "busybox"]
|
||||||
path = native/jni/external/busybox
|
path = native/src/external/busybox
|
||||||
url = https://github.com/topjohnwu/ndk-busybox.git
|
url = https://github.com/topjohnwu/ndk-busybox.git
|
||||||
[submodule "dtc"]
|
[submodule "dtc"]
|
||||||
path = native/jni/external/dtc
|
path = native/src/external/dtc
|
||||||
url = https://github.com/dgibson/dtc.git
|
url = https://github.com/dgibson/dtc.git
|
||||||
[submodule "lz4"]
|
[submodule "lz4"]
|
||||||
path = native/jni/external/lz4
|
path = native/src/external/lz4
|
||||||
url = https://github.com/lz4/lz4.git
|
url = https://github.com/lz4/lz4.git
|
||||||
[submodule "bzip2"]
|
[submodule "bzip2"]
|
||||||
path = native/jni/external/bzip2
|
path = native/src/external/bzip2
|
||||||
url = https://github.com/nemequ/bzip2.git
|
url = https://github.com/nemequ/bzip2.git
|
||||||
[submodule "xz"]
|
[submodule "xz"]
|
||||||
path = native/jni/external/xz
|
path = native/src/external/xz
|
||||||
url = https://github.com/xz-mirror/xz.git
|
url = https://github.com/xz-mirror/xz.git
|
||||||
[submodule "nanopb"]
|
[submodule "nanopb"]
|
||||||
path = native/jni/external/nanopb
|
path = native/src/external/nanopb
|
||||||
url = https://github.com/nanopb/nanopb.git
|
url = https://github.com/nanopb/nanopb.git
|
||||||
[submodule "mincrypt"]
|
|
||||||
path = native/jni/external/mincrypt
|
|
||||||
url = https://github.com/topjohnwu/mincrypt.git
|
|
||||||
[submodule "pcre"]
|
[submodule "pcre"]
|
||||||
path = native/jni/external/pcre
|
path = native/src/external/pcre
|
||||||
url = https://android.googlesource.com/platform/external/pcre
|
url = https://android.googlesource.com/platform/external/pcre
|
||||||
[submodule "xhook"]
|
|
||||||
path = native/jni/external/xhook
|
|
||||||
url = https://github.com/iqiyi/xHook.git
|
|
||||||
[submodule "libcxx"]
|
[submodule "libcxx"]
|
||||||
path = native/jni/external/libcxx
|
path = native/src/external/libcxx
|
||||||
url = https://github.com/topjohnwu/libcxx.git
|
url = https://github.com/topjohnwu/libcxx.git
|
||||||
[submodule "zlib"]
|
[submodule "zlib"]
|
||||||
path = native/jni/external/zlib
|
path = native/src/external/zlib
|
||||||
url = https://android.googlesource.com/platform/external/zlib
|
url = https://android.googlesource.com/platform/external/zlib
|
||||||
[submodule "parallel-hashmap"]
|
[submodule "parallel-hashmap"]
|
||||||
path = native/jni/external/parallel-hashmap
|
path = native/src/external/parallel-hashmap
|
||||||
url = https://github.com/greg7mdp/parallel-hashmap.git
|
url = https://github.com/greg7mdp/parallel-hashmap.git
|
||||||
|
[submodule "zopfli"]
|
||||||
|
path = native/src/external/zopfli
|
||||||
|
url = https://github.com/google/zopfli.git
|
||||||
|
[submodule "cxx-rs"]
|
||||||
|
path = native/src/external/cxx-rs
|
||||||
|
url = https://github.com/topjohnwu/cxx.git
|
||||||
|
[submodule "lsplt"]
|
||||||
|
path = native/src/external/lsplt
|
||||||
|
url = https://github.com/LSPosed/LSPlt.git
|
||||||
|
[submodule "system_properties"]
|
||||||
|
path = native/src/external/system_properties
|
||||||
|
url = https://github.com/topjohnwu/system_properties.git
|
||||||
[submodule "termux-elf-cleaner"]
|
[submodule "termux-elf-cleaner"]
|
||||||
path = tools/termux-elf-cleaner
|
path = tools/termux-elf-cleaner
|
||||||
url = https://github.com/termux/termux-elf-cleaner.git
|
url = https://github.com/termux/termux-elf-cleaner.git
|
||||||
[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
|
## Introduction
|
||||||
|
|
||||||
Magisk is a suite of open source software for customizing Android, supporting devices higher than Android 5.0.<br>
|
Magisk is a suite of open source software for customizing Android, supporting devices higher than Android 6.0.<br>
|
||||||
Some highlight features:
|
Some highlight features:
|
||||||
|
|
||||||
- **MagiskSU**: Provide root access for applications
|
- **MagiskSU**: Provide root access for applications
|
||||||
@@ -18,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.
|
[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/v26.1)
|
||||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v24.1)
|
[](https://github.com/topjohnwu/Magisk/releases/tag/v26.2)
|
||||||
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-debug.apk)
|
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-release.apk)
|
||||||
|
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-debug.apk)
|
||||||
|
|
||||||
## Useful Links
|
## Useful Links
|
||||||
|
|
||||||
- [Installation Instruction](https://topjohnwu.github.io/Magisk/install.html)
|
- [Installation Instruction](https://topjohnwu.github.io/Magisk/install.html)
|
||||||
|
- [Building and Development](https://topjohnwu.github.io/Magisk/build.html)
|
||||||
- [Magisk Documentation](https://topjohnwu.github.io/Magisk/)
|
- [Magisk Documentation](https://topjohnwu.github.io/Magisk/)
|
||||||
- [Magisk Troubleshoot Wiki](https://www.didgeridoohan.com/magisk/HomePage) (by [@Didgeridoohan](https://github.com/Didgeridoohan))
|
|
||||||
|
|
||||||
## Bug Reports
|
## Bug Reports
|
||||||
|
|
||||||
**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 installation issues, upload both boot image and install logs.<br>
|
||||||
For Magisk issues, upload boot logcat or dmesg.<br>
|
For Magisk issues, upload boot logcat or dmesg.<br>
|
||||||
For Magisk app crashes, record and upload the logcat when the crash occurs.
|
For Magisk app crashes, record and upload the logcat when the crash occurs.
|
||||||
|
|
||||||
## Building and Development
|
|
||||||
|
|
||||||
- Magisk builds on any OS Android Studio supports. Install Android Studio and do the initial setups.
|
|
||||||
- Clone sources: `git clone --recurse-submodules https://github.com/topjohnwu/Magisk.git`
|
|
||||||
- Install Python 3.6+ \
|
|
||||||
(Windows only: select **'Add Python to PATH'** in installer, and run `pip install colorama` after install)
|
|
||||||
- Configure to use the JDK bundled in Android Studio:
|
|
||||||
- macOS: `export JAVA_HOME="/Applications/Android Studio.app/Contents/jre/Contents/Home"`
|
|
||||||
- Linux: `export PATH="/path/to/androidstudio/jre/bin:$PATH"`
|
|
||||||
- Windows: Add `C:\Path\To\Android Studio\jre\bin` to environment variable `PATH`
|
|
||||||
- Set environment variable `ANDROID_SDK_ROOT` to the Android SDK folder (can be found in Android Studio settings)
|
|
||||||
- Run `./build.py ndk` to let the script download and install NDK for you
|
|
||||||
- To start building, run `build.py` to see your options. \
|
|
||||||
For each action, use `-h` to access help (e.g. `./build.py all -h`)
|
|
||||||
- To start development, open the project with Android Studio. The IDE can be used for both app (Kotlin/Java) and native (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
|
## Translation Contributions
|
||||||
|
|
||||||
Default string resources for the Magisk app and its stub APK are located here:
|
Default string resources for the Magisk app and its stub APK are located here:
|
||||||
|
7
app/.gitignore
vendored
7
app/.gitignore
vendored
@@ -3,10 +3,9 @@
|
|||||||
/local.properties
|
/local.properties
|
||||||
.idea/
|
.idea/
|
||||||
/build
|
/build
|
||||||
app/release
|
|
||||||
*.hprof
|
*.hprof
|
||||||
.externalNativeBuild/
|
.externalNativeBuild/
|
||||||
*.apk
|
*.apk
|
||||||
src/main/assets
|
src/*/assets
|
||||||
src/main/jniLibs
|
src/*/jniLibs
|
||||||
src/main/resources
|
src/*/resources
|
||||||
|
@@ -19,12 +19,17 @@ kapt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
namespace = "com.topjohnwu.magisk"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.topjohnwu.magisk"
|
applicationId = "com.topjohnwu.magisk"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
versionName = Config.version
|
versionName = Config.version
|
||||||
versionCode = Config.versionCode
|
versionCode = Config.versionCode
|
||||||
ndk.abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
ndk {
|
||||||
|
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||||
|
debugSymbolLevel = "FULL"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -37,11 +42,13 @@ android {
|
|||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
dataBinding = true
|
dataBinding = true
|
||||||
|
aidl = true
|
||||||
}
|
}
|
||||||
|
|
||||||
packagingOptions {
|
packaging {
|
||||||
resources {
|
resources {
|
||||||
excludes += "/META-INF/*"
|
excludes += "/META-INF/*"
|
||||||
|
excludes += "/META-INF/versions/**"
|
||||||
excludes += "/org/bouncycastle/**"
|
excludes += "/org/bouncycastle/**"
|
||||||
excludes += "/kotlin/**"
|
excludes += "/kotlin/**"
|
||||||
excludes += "/kotlinx/**"
|
excludes += "/kotlinx/**"
|
||||||
@@ -50,14 +57,6 @@ android {
|
|||||||
excludes += "/*.bin"
|
excludes += "/*.bin"
|
||||||
excludes += "/*.json"
|
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:jtar:1.0.0")
|
||||||
implementation("com.github.topjohnwu:indeterminate-checkbox:1.0.7")
|
implementation("com.github.topjohnwu:indeterminate-checkbox:1.0.7")
|
||||||
implementation("com.github.topjohnwu:lz4-java:1.7.1")
|
implementation("com.github.topjohnwu:lz4-java:1.7.1")
|
||||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
implementation("com.jakewharton.timber:timber:5.0.1")
|
||||||
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
|
implementation("org.bouncycastle:bcpkix-jdk18on:1.76")
|
||||||
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0")
|
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.3.0")
|
||||||
implementation("dev.rikka.rikkax.insets:insets:1.1.1")
|
implementation("dev.rikka.rikkax.insets:insets:1.3.0")
|
||||||
implementation("dev.rikka.rikkax.recyclerview:recyclerview-ktx:1.3.1")
|
implementation("dev.rikka.rikkax.recyclerview:recyclerview-ktx:1.3.2")
|
||||||
implementation("io.noties.markwon:core:4.6.2")
|
implementation("io.noties.markwon:core:4.6.2")
|
||||||
|
|
||||||
val vBAdapt = "4.0.0"
|
val vLibsu = "5.2.1"
|
||||||
val bindingAdapter = "me.tatarka.bindingcollectionadapter2:bindingcollectionadapter"
|
|
||||||
implementation("${bindingAdapter}:${vBAdapt}")
|
|
||||||
implementation("${bindingAdapter}-recyclerview:${vBAdapt}")
|
|
||||||
|
|
||||||
val vLibsu = "4.0.0"
|
|
||||||
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
|
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
|
||||||
implementation("com.github.topjohnwu.libsu:io:${vLibsu}")
|
|
||||||
implementation("com.github.topjohnwu.libsu:service:${vLibsu}")
|
implementation("com.github.topjohnwu.libsu:service:${vLibsu}")
|
||||||
|
implementation("com.github.topjohnwu.libsu:nio:${vLibsu}")
|
||||||
|
|
||||||
val vRetrofit = "2.9.0"
|
val vRetrofit = "2.9.0"
|
||||||
implementation("com.squareup.retrofit2:retrofit:${vRetrofit}")
|
implementation("com.squareup.retrofit2:retrofit:${vRetrofit}")
|
||||||
implementation("com.squareup.retrofit2:converter-moshi:${vRetrofit}")
|
implementation("com.squareup.retrofit2:converter-moshi:${vRetrofit}")
|
||||||
implementation("com.squareup.retrofit2:converter-scalars:${vRetrofit}")
|
implementation("com.squareup.retrofit2:converter-scalars:${vRetrofit}")
|
||||||
|
|
||||||
val vOkHttp = "4.9.3"
|
val vOkHttp = "4.11.0"
|
||||||
implementation("com.squareup.okhttp3:okhttp:${vOkHttp}")
|
implementation("com.squareup.okhttp3:okhttp:${vOkHttp}")
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor:${vOkHttp}")
|
implementation("com.squareup.okhttp3:logging-interceptor:${vOkHttp}")
|
||||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:${vOkHttp}")
|
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:${vOkHttp}")
|
||||||
|
|
||||||
val vMoshi = "1.13.0"
|
val vMoshi = "1.15.0"
|
||||||
implementation("com.squareup.moshi:moshi:${vMoshi}")
|
implementation("com.squareup.moshi:moshi:${vMoshi}")
|
||||||
kapt("com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}")
|
kapt("com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}")
|
||||||
|
|
||||||
val vRoom = "2.4.1"
|
val vRoom = "2.6.0-beta01"
|
||||||
implementation("androidx.room:room-runtime:${vRoom}")
|
implementation("androidx.room:room-runtime:${vRoom}")
|
||||||
implementation("androidx.room:room-ktx:${vRoom}")
|
implementation("androidx.room:room-ktx:${vRoom}")
|
||||||
kapt("androidx.room:room-compiler:${vRoom}")
|
kapt("androidx.room:room-compiler:${vRoom}")
|
||||||
|
|
||||||
val vNav = "2.5.0-alpha01"
|
val vNav = "2.7.1"
|
||||||
implementation("androidx.navigation:navigation-fragment-ktx:${vNav}")
|
implementation("androidx.navigation:navigation-fragment-ktx:${vNav}")
|
||||||
implementation("androidx.navigation:navigation-ui-ktx:${vNav}")
|
implementation("androidx.navigation:navigation-ui-ktx:${vNav}")
|
||||||
|
|
||||||
implementation("androidx.biometric:biometric:1.1.0")
|
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.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
implementation("androidx.appcompat:appcompat:1.4.1")
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||||
implementation("androidx.preference:preference:1.2.0")
|
implementation("androidx.recyclerview:recyclerview:1.3.1")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
implementation("androidx.fragment:fragment-ktx:1.6.1")
|
||||||
implementation("androidx.fragment:fragment-ktx:1.4.1")
|
|
||||||
implementation("androidx.transition:transition:1.4.1")
|
implementation("androidx.transition:transition:1.4.1")
|
||||||
implementation("androidx.core:core-ktx:1.7.0")
|
implementation("androidx.core:core-ktx:1.10.1")
|
||||||
implementation("androidx.core:core-splashscreen:1.0.0-beta01")
|
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||||
implementation("com.google.android.material:material:1.5.0")
|
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
|
# Parcelable
|
||||||
-keepclassmembers class * implements android.os.Parcelable {
|
-keepclassmembers class * implements android.os.Parcelable {
|
||||||
public static final ** CREATOR;
|
public static final ** CREATOR;
|
||||||
@@ -26,12 +8,23 @@
|
|||||||
public static void check*(...);
|
public static void check*(...);
|
||||||
public static void throw*(...);
|
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
|
# Stub
|
||||||
-keep class com.topjohnwu.magisk.core.App { <init>(java.lang.Object); }
|
-keep class com.topjohnwu.magisk.core.App { <init>(java.lang.Object); }
|
||||||
-keepclassmembers class androidx.appcompat.app.AppCompatDelegateImpl {
|
-keepclassmembers class androidx.appcompat.app.AppCompatDelegateImpl {
|
||||||
boolean mActivityHandlesUiModeChecked;
|
boolean mActivityHandlesConfigFlagsChecked;
|
||||||
boolean mActivityHandlesUiMode;
|
int mActivityHandlesConfigFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
# main
|
||||||
|
-keep,allowoptimization public class com.topjohnwu.magisk.signing.SignBoot {
|
||||||
|
public static void main(java.lang.String[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
# Strip Timber verbose and debug logging
|
# Strip Timber verbose and debug logging
|
||||||
@@ -40,6 +33,17 @@
|
|||||||
public void d(**);
|
public void d(**);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# https://github.com/square/retrofit/issues/3751#issuecomment-1192043644
|
||||||
|
# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items).
|
||||||
|
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
|
||||||
|
-keep,allowobfuscation,allowshrinking class retrofit2.Response
|
||||||
|
|
||||||
|
# With R8 full mode generic signatures are stripped for classes that are not
|
||||||
|
# kept. Suspend functions are wrapped in continuations where the type argument
|
||||||
|
# is used.
|
||||||
|
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
|
||||||
|
|
||||||
|
|
||||||
# Excessive obfuscation
|
# Excessive obfuscation
|
||||||
-repackageclasses 'a'
|
-repackageclasses 'a'
|
||||||
-allowaccessmodification
|
-allowaccessmodification
|
||||||
|
@@ -5,11 +5,5 @@ plugins {
|
|||||||
setupCommon()
|
setupCommon()
|
||||||
|
|
||||||
android {
|
android {
|
||||||
defaultConfig {
|
namespace = "com.topjohnwu.shared"
|
||||||
consumerProguardFiles("proguard-rules.pro")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
api("io.michaelrocks:paranoid-core:0.3.7")
|
|
||||||
}
|
}
|
||||||
|
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"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.topjohnwu.shared"
|
|
||||||
android:installLocation="internalOnly">
|
android:installLocation="internalOnly">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS" />
|
<uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS" />
|
||||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="29" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="29"
|
android:maxSdkVersion="29"
|
||||||
|
@@ -2,9 +2,6 @@ package com.topjohnwu.magisk;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import io.michaelrocks.paranoid.Obfuscate;
|
|
||||||
|
|
||||||
@Obfuscate
|
|
||||||
public class ProviderInstaller {
|
public class ProviderInstaller {
|
||||||
|
|
||||||
public static boolean install(Context context) {
|
public static boolean install(Context context) {
|
||||||
|
@@ -1,20 +1,25 @@
|
|||||||
package com.topjohnwu.magisk;
|
package com.topjohnwu.magisk;
|
||||||
|
|
||||||
import static android.os.Build.VERSION.SDK_INT;
|
import static android.os.Build.VERSION.SDK_INT;
|
||||||
|
import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.ApplicationInfo;
|
import android.content.pm.ApplicationInfo;
|
||||||
import android.content.res.AssetManager;
|
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.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import io.michaelrocks.paranoid.Obfuscate;
|
|
||||||
|
|
||||||
@Obfuscate
|
|
||||||
public class StubApk {
|
public class StubApk {
|
||||||
private static File dynDir;
|
private static File dynDir;
|
||||||
private static Method addAssetPath;
|
private static Method addAssetPath;
|
||||||
@@ -22,7 +27,7 @@ public class StubApk {
|
|||||||
private static File getDynDir(ApplicationInfo info) {
|
private static File getDynDir(ApplicationInfo info) {
|
||||||
if (dynDir == null) {
|
if (dynDir == null) {
|
||||||
final String dataDir;
|
final String dataDir;
|
||||||
if (SDK_INT >= 24) {
|
if (SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
// Use device protected path to allow directBootAware
|
// Use device protected path to allow directBootAware
|
||||||
dataDir = info.deviceProtectedDataDir;
|
dataDir = info.deviceProtectedDataDir;
|
||||||
} else {
|
} else {
|
||||||
@@ -50,12 +55,33 @@ public class StubApk {
|
|||||||
return new File(getDynDir(info), "update.apk");
|
return new File(getDynDir(info), "update.apk");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void addAssetPath(AssetManager asset, String path) {
|
@TargetApi(Build.VERSION_CODES.R)
|
||||||
try {
|
private static ResourcesLoader getResourcesLoader(File path) throws IOException {
|
||||||
if (addAssetPath == null)
|
var loader = new ResourcesLoader();
|
||||||
addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
|
ResourcesProvider provider;
|
||||||
addAssetPath.invoke(asset, path);
|
if (path.isDirectory()) {
|
||||||
} catch (Exception ignored) {}
|
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) {
|
public static void restartProcess(Activity activity) {
|
||||||
|
@@ -25,9 +25,6 @@ import java.util.UUID;
|
|||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import io.michaelrocks.paranoid.Obfuscate;
|
|
||||||
|
|
||||||
@Obfuscate
|
|
||||||
public final class APKInstall {
|
public final class APKInstall {
|
||||||
|
|
||||||
public static void transfer(InputStream in, OutputStream out) throws IOException {
|
public static void transfer(InputStream in, OutputStream out) throws IOException {
|
||||||
@@ -39,6 +36,16 @@ public final class APKInstall {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void registerReceiver(
|
||||||
|
Context context, BroadcastReceiver receiver, IntentFilter filter) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
// noinspection InlinedApi
|
||||||
|
context.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED);
|
||||||
|
} else {
|
||||||
|
context.registerReceiver(receiver, filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static Session startSession(Context context) {
|
public static Session startSession(Context context) {
|
||||||
return startSession(context, null, null, null);
|
return startSession(context, null, null, null);
|
||||||
}
|
}
|
||||||
@@ -51,9 +58,9 @@ public final class APKInstall {
|
|||||||
// If pkg is not null, look for package added event
|
// If pkg is not null, look for package added event
|
||||||
var filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
|
var filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
|
||||||
filter.addDataScheme("package");
|
filter.addDataScheme("package");
|
||||||
context.registerReceiver(receiver, filter);
|
registerReceiver(context, receiver, filter);
|
||||||
}
|
}
|
||||||
context.registerReceiver(receiver, new IntentFilter(receiver.sessionId));
|
registerReceiver(context, receiver, new IntentFilter(receiver.sessionId));
|
||||||
return receiver;
|
return receiver;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,27 +101,25 @@ public final class APKInstall {
|
|||||||
} else if (sessionId.equals(intent.getAction())) {
|
} else if (sessionId.equals(intent.getAction())) {
|
||||||
int status = intent.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID);
|
int status = intent.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID);
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case STATUS_PENDING_USER_ACTION:
|
case STATUS_PENDING_USER_ACTION ->
|
||||||
userAction = intent.getParcelableExtra(Intent.EXTRA_INTENT);
|
userAction = intent.getParcelableExtra(Intent.EXTRA_INTENT);
|
||||||
break;
|
case STATUS_SUCCESS -> {
|
||||||
case STATUS_SUCCESS:
|
|
||||||
if (packageName == null) {
|
if (packageName == null) {
|
||||||
onSuccess(context);
|
onSuccess(context);
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
default:
|
default -> {
|
||||||
int id = intent.getIntExtra(EXTRA_SESSION_ID, 0);
|
int id = intent.getIntExtra(EXTRA_SESSION_ID, 0);
|
||||||
if (id > 0) {
|
var installer = context.getPackageManager().getPackageInstaller();
|
||||||
var installer = context.getPackageManager().getPackageInstaller();
|
try {
|
||||||
var info = installer.getSessionInfo(id);
|
installer.abandonSession(id);
|
||||||
if (info != null) {
|
} catch (SecurityException ignored) {
|
||||||
installer.abandonSession(info.getSessionId());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (onFailure != null) {
|
if (onFailure != null) {
|
||||||
onFailure.run();
|
onFailure.run();
|
||||||
}
|
}
|
||||||
context.getApplicationContext().unregisterReceiver(this);
|
context.getApplicationContext().unregisterReceiver(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
}
|
}
|
||||||
|
@@ -1,22 +1,23 @@
|
|||||||
package com.topjohnwu.magisk.utils;
|
package com.topjohnwu.magisk.utils;
|
||||||
|
|
||||||
|
import android.os.Process;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
|
|
||||||
import dalvik.system.DexClassLoader;
|
import dalvik.system.BaseDexClassLoader;
|
||||||
|
|
||||||
public class DynamicClassLoader extends DexClassLoader {
|
public class DynamicClassLoader extends BaseDexClassLoader {
|
||||||
|
|
||||||
private static final ClassLoader base = Object.class.getClassLoader();
|
|
||||||
|
|
||||||
public DynamicClassLoader(File apk) {
|
public DynamicClassLoader(File apk) {
|
||||||
super(apk.getPath(), apk.getParent(), null, base);
|
this(apk, getSystemClassLoader());
|
||||||
}
|
}
|
||||||
|
|
||||||
public DynamicClassLoader(File apk, ClassLoader parent) {
|
public DynamicClassLoader(File apk, ClassLoader parent) {
|
||||||
super(apk.getPath(), apk.getParent(), null, parent);
|
// Set optimizedDirectory to null for RootService to bypass DexFile's security checks
|
||||||
|
super(apk.getPath(), Process.myUid() == 0 ? null : apk.getParentFile(), null, parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -28,7 +29,7 @@ public class DynamicClassLoader extends DexClassLoader {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Then check boot classpath
|
// Then check boot classpath
|
||||||
return base.loadClass(name);
|
return getSystemClassLoader().loadClass(name);
|
||||||
} catch (ClassNotFoundException ignored) {
|
} catch (ClassNotFoundException ignored) {
|
||||||
try {
|
try {
|
||||||
// Next try current dex
|
// Next try current dex
|
||||||
@@ -46,7 +47,7 @@ public class DynamicClassLoader extends DexClassLoader {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public URL getResource(String name) {
|
public URL getResource(String name) {
|
||||||
URL resource = base.getResource(name);
|
URL resource = getSystemClassLoader().getResource(name);
|
||||||
if (resource != null)
|
if (resource != null)
|
||||||
return resource;
|
return resource;
|
||||||
resource = findResource(name);
|
resource = findResource(name);
|
||||||
@@ -58,7 +59,7 @@ public class DynamicClassLoader extends DexClassLoader {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Enumeration<URL> getResources(String name) throws IOException {
|
public Enumeration<URL> getResources(String name) throws IOException {
|
||||||
return new CompoundEnumeration<>(base.getResources(name),
|
return new CompoundEnumeration<>(getSystemClassLoader().getResources(name),
|
||||||
findResources(name), getParent().getResources(name));
|
findResources(name), getParent().getResources(name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,16 +1,22 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
package="com.topjohnwu.magisk">
|
|
||||||
|
|
||||||
<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
|
<application
|
||||||
android:name=".core.App"
|
android:name=".core.App"
|
||||||
android:extractNativeLibs="true"
|
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:multiArch="true"
|
android:multiArch="true"
|
||||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
|
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning"
|
||||||
|
tools:remove="android:appComponentFactory">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
@@ -29,7 +35,6 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".ui.surequest.SuRequestActivity"
|
android:name=".ui.surequest.SuRequestActivity"
|
||||||
android:directBootAware="true"
|
android:directBootAware="true"
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:taskAffinity=""
|
android:taskAffinity=""
|
||||||
tools:ignore="AppLinkUrlError">
|
tools:ignore="AppLinkUrlError">
|
||||||
@@ -41,7 +46,6 @@
|
|||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".core.Receiver"
|
android:name=".core.Receiver"
|
||||||
android:directBootAware="true"
|
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
||||||
@@ -58,7 +62,8 @@
|
|||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".core.download.DownloadService"
|
android:name=".core.download.DownloadService"
|
||||||
android:exported="false" />
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".core.JobService"
|
android:name=".core.JobService"
|
||||||
|
@@ -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.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.databinding.OnRebindCallback
|
import androidx.databinding.OnRebindCallback
|
||||||
import androidx.databinding.ViewDataBinding
|
import androidx.databinding.ViewDataBinding
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
import com.topjohnwu.magisk.ktx.startAnimations
|
|
||||||
|
|
||||||
abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHolder {
|
abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHolder {
|
||||||
|
|
||||||
@@ -20,11 +21,12 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
|||||||
protected abstract val layoutRes: Int
|
protected abstract val layoutRes: Int
|
||||||
|
|
||||||
private val navigation get() = activity?.navigation
|
private val navigation get() = activity?.navigation
|
||||||
|
open val snackbarView: View? get() = null
|
||||||
open val snackbarAnchorView: View? get() = null
|
open val snackbarAnchorView: View? get() = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
startObserveEvents()
|
startObserveLiveData()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
@@ -36,9 +38,17 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
|||||||
it.setVariable(BR.viewModel, viewModel)
|
it.setVariable(BR.viewModel, viewModel)
|
||||||
it.lifecycleOwner = viewLifecycleOwner
|
it.lifecycleOwner = viewLifecycleOwner
|
||||||
}
|
}
|
||||||
|
if (this is MenuProvider) {
|
||||||
|
activity?.addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.STARTED)
|
||||||
|
}
|
||||||
|
savedInstanceState?.let { viewModel.onRestoreState(it) }
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
viewModel.onSaveState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
activity?.supportActionBar?.subtitle = null
|
activity?.supportActionBar?.subtitle = null
|
||||||
@@ -70,7 +80,10 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
viewModel.requestRefresh()
|
viewModel.let {
|
||||||
|
if (it is AsyncLoadViewModel)
|
||||||
|
it.startLoading()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun onPreBind(binding: Binding) {
|
protected open fun onPreBind(binding: Binding) {
|
||||||
@@ -78,7 +91,6 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun NavDirections.navigate() {
|
fun NavDirections.navigate() {
|
||||||
navigation?.navigate(this)
|
navigation?.currentDestination?.getAction(actionId)?.let { navigation!!.navigate(this) }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,79 +1,32 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
package com.topjohnwu.magisk.arch
|
||||||
|
|
||||||
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
import android.Manifest.permission.*
|
||||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.annotation.CallSuper
|
import android.os.Bundle
|
||||||
import androidx.databinding.Bindable
|
|
||||||
import androidx.databinding.Observable
|
|
||||||
import androidx.databinding.PropertyChangeRegistry
|
import androidx.databinding.PropertyChangeRegistry
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
import com.topjohnwu.magisk.BR
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Info
|
|
||||||
import com.topjohnwu.magisk.databinding.ObservableHost
|
import com.topjohnwu.magisk.databinding.ObservableHost
|
||||||
import com.topjohnwu.magisk.databinding.set
|
|
||||||
import com.topjohnwu.magisk.events.BackPressEvent
|
import com.topjohnwu.magisk.events.BackPressEvent
|
||||||
|
import com.topjohnwu.magisk.events.DialogBuilder
|
||||||
|
import com.topjohnwu.magisk.events.DialogEvent
|
||||||
import com.topjohnwu.magisk.events.NavigationEvent
|
import com.topjohnwu.magisk.events.NavigationEvent
|
||||||
import com.topjohnwu.magisk.events.PermissionEvent
|
import com.topjohnwu.magisk.events.PermissionEvent
|
||||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
|
|
||||||
abstract class BaseViewModel(
|
abstract class BaseViewModel : ViewModel(), ObservableHost {
|
||||||
initialState: State = State.LOADING
|
|
||||||
) : ViewModel(), ObservableHost {
|
|
||||||
|
|
||||||
override var callbacks: PropertyChangeRegistry? = null
|
override var callbacks: PropertyChangeRegistry? = null
|
||||||
|
|
||||||
enum class State {
|
private val _viewEvents = MutableLiveData<ViewEvent>()
|
||||||
LOADED, LOADING, LOADING_FAILED
|
|
||||||
}
|
|
||||||
|
|
||||||
@get:Bindable
|
|
||||||
val loading get() = state == State.LOADING
|
|
||||||
@get:Bindable
|
|
||||||
val loaded get() = state == State.LOADED
|
|
||||||
@get:Bindable
|
|
||||||
val loadFailed get() = state == State.LOADING_FAILED
|
|
||||||
|
|
||||||
val isConnected get() = Info.isConnected
|
|
||||||
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
|
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
|
||||||
|
|
||||||
var state= initialState
|
open fun onSaveState(state: Bundle) {}
|
||||||
set(value) = set(value, field, { field = it }, BR.loading, BR.loaded, BR.loadFailed)
|
open fun onRestoreState(state: Bundle) {}
|
||||||
|
open fun onNetworkChanged(network: Boolean) {}
|
||||||
private val _viewEvents = MutableLiveData<ViewEvent>()
|
|
||||||
private var runningJob: Job? = null
|
|
||||||
private val refreshCallback = object : Observable.OnPropertyChangedCallback() {
|
|
||||||
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
|
||||||
requestRefresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
isConnected.addOnPropertyChangedCallback(refreshCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** This should probably never be called manually, it's called manually via delegate. */
|
|
||||||
@Synchronized
|
|
||||||
fun requestRefresh() {
|
|
||||||
if (runningJob?.isActive == true) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
runningJob = refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun refresh(): Job? = null
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
override fun onCleared() {
|
|
||||||
isConnected.removeOnPropertyChangedCallback(refreshCallback)
|
|
||||||
super.onCleared()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
||||||
PermissionEvent(permission, callback).publish()
|
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 back() = BackPressEvent().publish()
|
||||||
|
|
||||||
fun <Event : ViewEvent> Event.publish() {
|
fun ViewEvent.publish() {
|
||||||
_viewEvents.postValue(this)
|
_viewEvents.postValue(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <Event : ViewEventWithScope> Event.publish() {
|
fun DialogBuilder.show() {
|
||||||
scope = viewModelScope
|
DialogEvent(this).publish()
|
||||||
_viewEvents.postValue(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NavDirections.navigate(pop: Boolean = false) {
|
fun NavDirections.navigate(pop: Boolean = false) {
|
||||||
|
@@ -20,12 +20,14 @@ abstract class NavigationActivity<Binding : ViewDataBinding> : UIActivity<Bindin
|
|||||||
val navigation: NavController get() = navHostFragment.navController
|
val navigation: NavController get() = navHostFragment.navController
|
||||||
|
|
||||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||||
return currentFragment?.onKeyEvent(event) == true || super.dispatchKeyEvent(event)
|
return if (binded && currentFragment?.onKeyEvent(event) == true) true else super.dispatchKeyEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
if (currentFragment?.onBackPressed()?.not() == true) {
|
if (binded) {
|
||||||
super.onBackPressed()
|
if (currentFragment?.onBackPressed() == false) {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,13 +5,18 @@ import android.graphics.Color
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.content.res.use
|
import androidx.core.content.res.use
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.databinding.ViewDataBinding
|
import androidx.databinding.ViewDataBinding
|
||||||
|
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||||
|
import androidx.transition.AutoTransition
|
||||||
|
import androidx.transition.TransitionManager
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||||
import rikka.insets.WindowInsetsHelper
|
import rikka.insets.WindowInsetsHelper
|
||||||
@@ -22,12 +27,13 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
|||||||
protected lateinit var binding: Binding
|
protected lateinit var binding: Binding
|
||||||
protected abstract val layoutRes: Int
|
protected abstract val layoutRes: Int
|
||||||
|
|
||||||
|
protected val binded get() = ::binding.isInitialized
|
||||||
|
|
||||||
open val snackbarView get() = binding.root
|
open val snackbarView get() = binding.root
|
||||||
open val snackbarAnchorView: View? get() = null
|
open val snackbarAnchorView: View? get() = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val theme = Config.darkTheme
|
AppCompatDelegate.setDefaultNightMode(Config.darkTheme)
|
||||||
AppCompatDelegate.setDefaultNightMode(theme)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -36,7 +42,7 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
|||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
startObserveEvents()
|
startObserveLiveData()
|
||||||
|
|
||||||
// We need to set the window background explicitly since for whatever reason it's not
|
// We need to set the window background explicitly since for whatever reason it's not
|
||||||
// propagated upstream
|
// propagated upstream
|
||||||
@@ -84,7 +90,10 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
viewModel.requestRefresh()
|
viewModel.let {
|
||||||
|
if (it is AsyncLoadViewModel)
|
||||||
|
it.startLoading()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEventDispatched(event: ViewEvent) = when (event) {
|
override fun onEventDispatched(event: ViewEvent) = when (event) {
|
||||||
@@ -93,3 +102,14 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
|||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ViewGroup.startAnimations() {
|
||||||
|
val transition = AutoTransition()
|
||||||
|
.setInterpolator(FastOutSlowInInterpolator())
|
||||||
|
.setDuration(400)
|
||||||
|
.excludeTarget(R.id.main_toolbar, true)
|
||||||
|
TransitionManager.beginDelayedTransition(
|
||||||
|
this,
|
||||||
|
transition
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
package com.topjohnwu.magisk.arch
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for passing events from ViewModels to Activities/Fragments
|
* Class for passing events from ViewModels to Activities/Fragments
|
||||||
@@ -9,10 +8,6 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
*/
|
*/
|
||||||
abstract class ViewEvent
|
abstract class ViewEvent
|
||||||
|
|
||||||
abstract class ViewEventWithScope: ViewEvent() {
|
|
||||||
lateinit var scope: CoroutineScope
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContextExecutor {
|
interface ContextExecutor {
|
||||||
operator fun invoke(context: Context)
|
operator fun invoke(context: Context)
|
||||||
}
|
}
|
||||||
|
@@ -1,15 +1,24 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
package com.topjohnwu.magisk.arch
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
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
|
val viewModel: BaseViewModel
|
||||||
|
|
||||||
fun startObserveEvents() {
|
fun startObserveLiveData() {
|
||||||
viewModel.viewEvents.observe(this) {
|
viewModel.viewEvents.observe(this, this::onEventDispatched)
|
||||||
onEventDispatched(it)
|
Info.isConnected.observe(this, viewModel::onNetworkChanged)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,3 +26,24 @@ interface ViewModelHolder : LifecycleOwner {
|
|||||||
*/
|
*/
|
||||||
fun onEventDispatched(event: ViewEvent) {}
|
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
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.topjohnwu.magisk.StubApk
|
import com.topjohnwu.magisk.StubApk
|
||||||
import com.topjohnwu.magisk.core.utils.*
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
import com.topjohnwu.magisk.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.ui.surequest.SuRequestActivity
|
||||||
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
import com.topjohnwu.superuser.ipc.RootService
|
import com.topjohnwu.superuser.ipc.RootService
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
open class App() : Application() {
|
open class App() : Application() {
|
||||||
@@ -22,9 +28,9 @@ open class App() : Application() {
|
|||||||
constructor(o: Any) : this() {
|
constructor(o: Any) : this() {
|
||||||
val data = StubApk.Data(o)
|
val data = StubApk.Data(o)
|
||||||
// Add the root service name mapping
|
// 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
|
// Send back the actual root service class
|
||||||
data.rootService = RootRegistry::class.java
|
data.rootService = RootUtils::class.java
|
||||||
Info.stub = data
|
Info.stub = data
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,43 +44,44 @@ open class App() : Application() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(context: Context) {
|
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
|
// Get the actual ContextImpl
|
||||||
val app: Application
|
val app: Application
|
||||||
val base: Context
|
val base: Context
|
||||||
if (context is Application) {
|
if (context is Application) {
|
||||||
app = context
|
app = context
|
||||||
base = context.baseContext
|
base = context.baseContext
|
||||||
|
AppApkPath = StubApk.current(base).path
|
||||||
} else {
|
} else {
|
||||||
app = this
|
app = this
|
||||||
base = context
|
base = context
|
||||||
|
AppApkPath = base.packageResourcePath
|
||||||
}
|
}
|
||||||
super.attachBaseContext(base)
|
super.attachBaseContext(base)
|
||||||
ServiceLocator.context = 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()
|
refreshLocale()
|
||||||
AppApkPath = if (isRunningAsStub) {
|
resources.patch()
|
||||||
StubApk.current(base).path
|
Notifications.setup()
|
||||||
} else {
|
|
||||||
base.packageResourcePath
|
|
||||||
}
|
|
||||||
|
|
||||||
base.resources.patch()
|
|
||||||
app.registerActivityLifecycleCallbacks(ActivityTracker)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
RootRegistry.bindTask = RootService.bindOrTask(
|
ProcessLifecycle.init(this)
|
||||||
intent<RootRegistry>(),
|
|
||||||
UiThreadHandler.executor,
|
|
||||||
RootRegistry.Connection
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
@@ -86,20 +93,21 @@ open class App() : Application() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
|
||||||
object ActivityTracker : Application.ActivityLifecycleCallbacks {
|
object ActivityTracker : Application.ActivityLifecycleCallbacks {
|
||||||
|
|
||||||
|
val foreground: Activity? get() = ref.get()
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
var foreground: Activity? = null
|
private var ref = WeakReference<Activity>(null)
|
||||||
|
|
||||||
override fun onActivityResumed(activity: Activity) {
|
override fun onActivityResumed(activity: Activity) {
|
||||||
if (activity is SuRequestActivity) return
|
if (activity is SuRequestActivity) return
|
||||||
foreground = activity
|
ref = WeakReference(activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityPaused(activity: Activity) {
|
override fun onActivityPaused(activity: Activity) {
|
||||||
if (activity is SuRequestActivity) return
|
if (activity is SuRequestActivity) return
|
||||||
foreground = null
|
ref.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
||||||
|
@@ -1,34 +1,33 @@
|
|||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.util.Xml
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||||
|
import com.topjohnwu.magisk.core.repository.BoolDBPropertyNoWrite
|
||||||
|
import com.topjohnwu.magisk.core.repository.DBConfig
|
||||||
|
import com.topjohnwu.magisk.core.repository.PreferenceConfig
|
||||||
import com.topjohnwu.magisk.core.utils.refreshLocale
|
import com.topjohnwu.magisk.core.utils.refreshLocale
|
||||||
import com.topjohnwu.magisk.data.preference.PreferenceModel
|
|
||||||
import com.topjohnwu.magisk.data.repository.DBBoolSettingsNoWrite
|
|
||||||
import com.topjohnwu.magisk.data.repository.DBConfig
|
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
|
||||||
import com.topjohnwu.magisk.ui.theme.Theme
|
import com.topjohnwu.magisk.ui.theme.Theme
|
||||||
import org.xmlpull.v1.XmlPullParser
|
import kotlinx.coroutines.GlobalScope
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
object Config : PreferenceModel, DBConfig {
|
object Config : PreferenceConfig, DBConfig {
|
||||||
|
|
||||||
override val stringDB get() = ServiceLocator.stringDB
|
override val stringDB get() = ServiceLocator.stringDB
|
||||||
override val settingsDB get() = ServiceLocator.settingsDB
|
override val settingsDB get() = ServiceLocator.settingsDB
|
||||||
override val context get() = ServiceLocator.deContext
|
override val context get() = ServiceLocator.deContext
|
||||||
|
override val coroutineScope get() = GlobalScope
|
||||||
|
|
||||||
@get:SuppressLint("ApplySharedPref")
|
private val prefsFile = File("${context.filesDir.parent}/shared_prefs", "${fileName}.xml")
|
||||||
val prefsFile: File get() {
|
|
||||||
// Flush prefs to disk
|
@SuppressLint("ApplySharedPref")
|
||||||
prefs.edit().apply {
|
fun getPrefsFile(): File {
|
||||||
remove(Key.ASKED_HOME)
|
prefs.edit().remove(Key.ASKED_HOME).commit()
|
||||||
}.commit()
|
return prefsFile
|
||||||
return File("${context.filesDir.parent}/shared_prefs", "${fileName}.xml")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object Key {
|
object Key {
|
||||||
@@ -70,6 +69,7 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
const val BETA_CHANNEL = 1
|
const val BETA_CHANNEL = 1
|
||||||
const val CUSTOM_CHANNEL = 2
|
const val CUSTOM_CHANNEL = 2
|
||||||
const val CANARY_CHANNEL = 3
|
const val CANARY_CHANNEL = 3
|
||||||
|
const val DEBUG_CHANNEL = 4
|
||||||
|
|
||||||
// root access mode
|
// root access mode
|
||||||
const val ROOT_ACCESS_DISABLED = 0
|
const val ROOT_ACCESS_DISABLED = 0
|
||||||
@@ -106,13 +106,14 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
|
|
||||||
private val defaultChannel =
|
private val defaultChannel =
|
||||||
if (BuildConfig.DEBUG)
|
if (BuildConfig.DEBUG)
|
||||||
|
Value.DEBUG_CHANNEL
|
||||||
|
else if (Const.APP_IS_CANARY)
|
||||||
Value.CANARY_CHANNEL
|
Value.CANARY_CHANNEL
|
||||||
else
|
else
|
||||||
Value.DEFAULT_CHANNEL
|
Value.DEFAULT_CHANNEL
|
||||||
|
|
||||||
@JvmField var keepVerity = false
|
@JvmField var keepVerity = false
|
||||||
@JvmField var keepEnc = false
|
@JvmField var keepEnc = false
|
||||||
@JvmField var patchVbmeta = false
|
|
||||||
@JvmField var recovery = false
|
@JvmField var recovery = false
|
||||||
|
|
||||||
var bootId by preference(Key.BOOT_ID, "")
|
var bootId by preference(Key.BOOT_ID, "")
|
||||||
@@ -131,7 +132,15 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
var themeOrdinal by preference(Key.THEME_ORDINAL, Theme.Piplup.ordinal)
|
var themeOrdinal by preference(Key.THEME_ORDINAL, Theme.Piplup.ordinal)
|
||||||
var suReAuth by preference(Key.SU_REAUTH, false)
|
var suReAuth by preference(Key.SU_REAUTH, false)
|
||||||
var suTapjack by preference(Key.SU_TAPJACK, true)
|
var suTapjack by preference(Key.SU_TAPJACK, true)
|
||||||
var checkUpdate by preference(Key.CHECK_UPDATES, true)
|
private var checkUpdatePrefs by preference(Key.CHECK_UPDATES, true)
|
||||||
|
var checkUpdate
|
||||||
|
get() = checkUpdatePrefs
|
||||||
|
set(value) {
|
||||||
|
if (checkUpdatePrefs != value) {
|
||||||
|
checkUpdatePrefs = value
|
||||||
|
JobService.schedule(AppContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
var doh by preference(Key.DOH, false)
|
var doh by preference(Key.DOH, false)
|
||||||
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
|
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
|
||||||
|
|
||||||
@@ -149,7 +158,7 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
|
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
|
||||||
var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
|
var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
|
||||||
var zygisk by dbSettings(Key.ZYGISK, false)
|
var 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 suManager by dbStrings(Key.SU_MANAGER, "", true)
|
||||||
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
|
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
|
||||||
|
|
||||||
@@ -158,9 +167,8 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
fun load(pkg: String?) {
|
fun load(pkg: String?) {
|
||||||
// Only try to load prefs when fresh install and a previous package name is set
|
// Only try to load prefs when fresh install and a previous package name is set
|
||||||
if (pkg != null && prefs.all.isEmpty()) runCatching {
|
if (pkg != null && prefs.all.isEmpty()) runCatching {
|
||||||
context.contentResolver.openInputStream(Provider.PREFS_URI(pkg))?.use {
|
context.contentResolver.openInputStream(Provider.preferencesUri(pkg))?.writeTo(prefsFile)
|
||||||
prefs.edit { parsePrefs(it) }
|
return
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
@@ -169,57 +177,10 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
suBiometric = true
|
suBiometric = true
|
||||||
remove(SU_FINGERPRINT)
|
remove(SU_FINGERPRINT)
|
||||||
prefs.getString(Key.UPDATE_CHANNEL, null).also {
|
prefs.getString(Key.UPDATE_CHANNEL, null).also {
|
||||||
if (it == null)
|
if (it == null ||
|
||||||
|
it.toInt() > Value.DEBUG_CHANNEL ||
|
||||||
|
it.toInt() < Value.DEFAULT_CHANNEL) {
|
||||||
putString(Key.UPDATE_CHANNEL, defaultChannel.toString())
|
putString(Key.UPDATE_CHANNEL, defaultChannel.toString())
|
||||||
else if (it.toInt() > Value.CANARY_CHANNEL)
|
|
||||||
putString(Key.UPDATE_CHANNEL, Value.CANARY_CHANNEL.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun SharedPreferences.Editor.parsePrefs(input: InputStream) {
|
|
||||||
runCatching {
|
|
||||||
val parser = Xml.newPullParser()
|
|
||||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
|
|
||||||
parser.setInput(input, "UTF-8")
|
|
||||||
parser.nextTag()
|
|
||||||
parser.require(XmlPullParser.START_TAG, null, "map")
|
|
||||||
while (parser.next() != XmlPullParser.END_TAG) {
|
|
||||||
if (parser.eventType != XmlPullParser.START_TAG)
|
|
||||||
continue
|
|
||||||
val key: String = parser.getAttributeValue(null, "name")
|
|
||||||
fun value() = parser.getAttributeValue(null, "value")!!
|
|
||||||
when (parser.name) {
|
|
||||||
"string" -> {
|
|
||||||
parser.require(XmlPullParser.START_TAG, null, "string")
|
|
||||||
putString(key, parser.nextText())
|
|
||||||
parser.require(XmlPullParser.END_TAG, null, "string")
|
|
||||||
}
|
|
||||||
"boolean" -> {
|
|
||||||
parser.require(XmlPullParser.START_TAG, null, "boolean")
|
|
||||||
putBoolean(key, value().toBoolean())
|
|
||||||
parser.nextTag()
|
|
||||||
parser.require(XmlPullParser.END_TAG, null, "boolean")
|
|
||||||
}
|
|
||||||
"int" -> {
|
|
||||||
parser.require(XmlPullParser.START_TAG, null, "int")
|
|
||||||
putInt(key, value().toInt())
|
|
||||||
parser.nextTag()
|
|
||||||
parser.require(XmlPullParser.END_TAG, null, "int")
|
|
||||||
}
|
|
||||||
"long" -> {
|
|
||||||
parser.require(XmlPullParser.START_TAG, null, "long")
|
|
||||||
putLong(key, value().toLong())
|
|
||||||
parser.nextTag()
|
|
||||||
parser.require(XmlPullParser.END_TAG, null, "long")
|
|
||||||
}
|
|
||||||
"float" -> {
|
|
||||||
parser.require(XmlPullParser.START_TAG, null, "int")
|
|
||||||
putFloat(key, value().toFloat())
|
|
||||||
parser.nextTag()
|
|
||||||
parser.require(XmlPullParser.END_TAG, null, "int")
|
|
||||||
}
|
|
||||||
else -> parser.next()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,8 +15,7 @@ object Const {
|
|||||||
else Build.SUPPORTED_32_BIT_ABIS.firstOrNull()
|
else Build.SUPPORTED_32_BIT_ABIS.firstOrNull()
|
||||||
|
|
||||||
// Paths
|
// Paths
|
||||||
lateinit var MAGISKTMP: String
|
const val MAGISK_PATH = "/data/adb/modules"
|
||||||
val MAGISK_PATH get() = "$MAGISKTMP/modules"
|
|
||||||
const val TMPDIR = "/dev/tmp"
|
const val TMPDIR = "/dev/tmp"
|
||||||
const val MAGISK_LOG = "/cache/magisk.log"
|
const val MAGISK_LOG = "/cache/magisk.log"
|
||||||
|
|
||||||
@@ -25,11 +24,11 @@ object Const {
|
|||||||
val APP_IS_CANARY get() = Version.isCanary(BuildConfig.VERSION_CODE)
|
val APP_IS_CANARY get() = Version.isCanary(BuildConfig.VERSION_CODE)
|
||||||
|
|
||||||
object Version {
|
object Version {
|
||||||
const val MIN_VERSION = "v21.0"
|
const val MIN_VERSION = "v22.0"
|
||||||
const val MIN_VERCODE = 21000
|
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_24_0() = Info.env.versionCode >= 24000 || isCanary()
|
||||||
|
fun atLeast_25_0() = Info.env.versionCode >= 25000 || isCanary()
|
||||||
fun isCanary() = isCanary(Info.env.versionCode)
|
fun isCanary() = isCanary(Info.env.versionCode)
|
||||||
|
|
||||||
fun isCanary(ver: Int) = ver > 0 && ver % 100 != 0
|
fun isCanary(ver: Int) = ver > 0 && ver % 100 != 0
|
||||||
|
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
@@ -13,45 +12,49 @@ import android.content.res.Resources
|
|||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.StubApk
|
import com.topjohnwu.magisk.StubApk
|
||||||
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
|
import com.topjohnwu.magisk.core.ktx.unwrap
|
||||||
import com.topjohnwu.magisk.core.utils.syncLocale
|
import com.topjohnwu.magisk.core.utils.syncLocale
|
||||||
import com.topjohnwu.magisk.di.AppContext
|
|
||||||
|
|
||||||
lateinit var AppApkPath: String
|
lateinit var AppApkPath: String
|
||||||
|
|
||||||
fun AssetManager.addAssetPath(path: String) = StubApk.addAssetPath(this, path)
|
fun Resources.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.patch(): Resources {
|
fun Resources.patch(): Resources {
|
||||||
syncLocale()
|
|
||||||
if (isRunningAsStub)
|
if (isRunningAsStub)
|
||||||
assets.addAssetPath(AppApkPath)
|
addAssetPath(AppApkPath)
|
||||||
|
syncLocale()
|
||||||
return this
|
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 {
|
fun createNewResources(): Resources {
|
||||||
val asset = AssetManager::class.java.newInstance()
|
val asset = AssetManager::class.java.newInstance()
|
||||||
asset.addAssetPath(AppApkPath)
|
|
||||||
val config = Configuration(AppContext.resources.configuration)
|
val config = Configuration(AppContext.resources.configuration)
|
||||||
val metrics = DisplayMetrics()
|
val metrics = DisplayMetrics()
|
||||||
metrics.setTo(AppContext.resources.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) =
|
fun Class<*>.cmp(pkg: String) =
|
||||||
ComponentName(pkg, Info.stub?.classToComponent?.get(name) ?: name)
|
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))
|
inline fun <reified T> Context.intent() = Intent().setComponent(T::class.java.cmp(packageName))
|
||||||
|
|
||||||
// Keep a reference to these resources to prevent it from
|
// Keep a reference to these resources to prevent it from
|
||||||
|
@@ -1,16 +1,14 @@
|
|||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import android.os.Build
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.databinding.ObservableBoolean
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.topjohnwu.magisk.StubApk
|
import com.topjohnwu.magisk.StubApk
|
||||||
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
|
import com.topjohnwu.magisk.core.ktx.getProperty
|
||||||
import com.topjohnwu.magisk.core.model.UpdateInfo
|
import com.topjohnwu.magisk.core.model.UpdateInfo
|
||||||
import com.topjohnwu.magisk.core.utils.net.NetworkObserver
|
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
import com.topjohnwu.magisk.core.utils.NetworkObserver
|
||||||
import com.topjohnwu.magisk.di.AppContext
|
|
||||||
import com.topjohnwu.magisk.ktx.getProperty
|
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import com.topjohnwu.superuser.ShellUtils.fastCmd
|
import com.topjohnwu.superuser.ShellUtils.fastCmd
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
|
||||||
|
|
||||||
val isRunningAsStub get() = Info.stub != null
|
val isRunningAsStub get() = Info.stub != null
|
||||||
|
|
||||||
@@ -29,42 +27,54 @@ object Info {
|
|||||||
// Device state
|
// Device state
|
||||||
@JvmStatic val env by lazy { loadState() }
|
@JvmStatic val env by lazy { loadState() }
|
||||||
@JvmField var isSAR = false
|
@JvmField var isSAR = false
|
||||||
|
var legacySAR = false
|
||||||
var isAB = false
|
var isAB = false
|
||||||
@JvmField val isZygiskEnabled = System.getenv("ZYGISK_ENABLED") == "1"
|
@JvmField val isZygiskEnabled = System.getenv("ZYGISK_ENABLED") == "1"
|
||||||
@JvmStatic val isFDE get() = crypto == "block"
|
@JvmStatic val isFDE get() = crypto == "block"
|
||||||
@JvmField var ramdisk = false
|
@JvmField var ramdisk = false
|
||||||
@JvmField var vbmeta = false
|
var patchBootVbmeta = false
|
||||||
var crypto = ""
|
var crypto = ""
|
||||||
var noDataExec = false
|
var noDataExec = false
|
||||||
|
var isRooted = false
|
||||||
|
|
||||||
@JvmField var hasGMS = true
|
@JvmField var hasGMS = true
|
||||||
val isSamsung = Build.MANUFACTURER.equals("samsung", ignoreCase = true)
|
|
||||||
@JvmField val isEmulator =
|
@JvmField val isEmulator =
|
||||||
getProperty("ro.kernel.qemu", "0") == "1" ||
|
getProperty("ro.kernel.qemu", "0") == "1" ||
|
||||||
getProperty("ro.boot.qemu", "0") == "1"
|
getProperty("ro.boot.qemu", "0") == "1"
|
||||||
|
|
||||||
val isConnected by lazy {
|
val isConnected: LiveData<Boolean> by lazy {
|
||||||
ObservableBoolean(false).also { field ->
|
MutableLiveData(false).also { field ->
|
||||||
NetworkObserver.observe(AppContext) {
|
NetworkObserver.observe(AppContext) {
|
||||||
UiThreadHandler.run { field.set(it) }
|
remote = EMPTY_REMOTE
|
||||||
|
field.postValue(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadState() = Env(
|
val showSuperUser: Boolean get() {
|
||||||
fastCmd("magisk -v").split(":".toRegex())[0],
|
return env.isActive && (Const.USER_ID == 0
|
||||||
runCatching { fastCmd("magisk -V").toInt() }.getOrDefault(-1)
|
|| 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(
|
class Env(
|
||||||
val versionString: String = "",
|
val versionString: String = "",
|
||||||
|
val isDebug: Boolean = false,
|
||||||
code: Int = -1
|
code: Int = -1
|
||||||
) {
|
) {
|
||||||
val versionCode = when {
|
val versionCode = when {
|
||||||
code < Const.Version.MIN_VERCODE -> -1
|
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 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 androidx.core.content.getSystemService
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.core.base.BaseJobService
|
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 com.topjohnwu.magisk.view.Notifications
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -33,7 +33,7 @@ class JobService : BaseJobService() {
|
|||||||
svc.fetchUpdate()?.let {
|
svc.fetchUpdate()?.let {
|
||||||
Info.remote = it
|
Info.remote = it
|
||||||
if (Info.env.isActive && BuildConfig.VERSION_CODE < it.magisk.versionCode)
|
if (Info.env.isActive && BuildConfig.VERSION_CODE < it.magisk.versionCode)
|
||||||
Notifications.updateAvailable(this)
|
Notifications.updateAvailable()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,22 +1,13 @@
|
|||||||
package com.topjohnwu.magisk.core
|
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.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
||||||
|
import com.topjohnwu.magisk.core.base.BaseProvider
|
||||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class Provider : ContentProvider() {
|
class Provider : BaseProvider() {
|
||||||
|
|
||||||
override fun attachInfo(context: Context, info: ProviderInfo) {
|
|
||||||
super.attachInfo(context.wrap(), info)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
|
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
|
||||||
SuCallbackHandler.run(context!!, method, extras)
|
SuCallbackHandler.run(context!!, method, extras)
|
||||||
@@ -25,24 +16,13 @@ class Provider : ContentProvider() {
|
|||||||
|
|
||||||
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
|
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
|
||||||
return when (uri.encodedPath ?: return null) {
|
return when (uri.encodedPath ?: return null) {
|
||||||
"/apk_file" -> ParcelFileDescriptor.open(File(context!!.packageCodePath), MODE_READ_ONLY)
|
"/prefs_file" -> ParcelFileDescriptor.open(Config.getPrefsFile(), MODE_READ_ONLY)
|
||||||
"/prefs_file" -> ParcelFileDescriptor.open(Config.prefsFile, MODE_READ_ONLY)
|
|
||||||
else -> super.openFile(uri, mode)
|
else -> super.openFile(uri, mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun APK_URI(pkg: String) =
|
fun preferencesUri(pkg: String): Uri =
|
||||||
Uri.Builder().scheme("content").authority("$pkg.provider").path("apk_file").build()
|
|
||||||
|
|
||||||
fun PREFS_URI(pkg: String) =
|
|
||||||
Uri.Builder().scheme("content").authority("$pkg.provider").path("prefs_file").build()
|
Uri.Builder().scheme("content").authority("$pkg.provider").path("prefs_file").build()
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.ContextWrapper
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.topjohnwu.magisk.core.base.BaseReceiver
|
import com.topjohnwu.magisk.core.base.BaseReceiver
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import com.topjohnwu.magisk.view.Shortcuts
|
import com.topjohnwu.magisk.view.Shortcuts
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
@@ -26,8 +26,9 @@ open class Receiver : BaseReceiver() {
|
|||||||
return if (uid == -1) null else uid
|
return if (uid == -1) null else uid
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReceive(context: ContextWrapper, intent: Intent?) {
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
intent ?: return
|
intent ?: return
|
||||||
|
super.onReceive(context, intent)
|
||||||
|
|
||||||
fun rmPolicy(uid: Int) = GlobalScope.launch {
|
fun rmPolicy(uid: Int) = GlobalScope.launch {
|
||||||
policyDB.delete(uid)
|
policyDB.delete(uid)
|
||||||
@@ -50,7 +51,7 @@ open class Receiver : BaseReceiver() {
|
|||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
val installer = context.packageManager.getInstallerPackageName(context.packageName)
|
val installer = context.packageManager.getInstallerPackageName(context.packageName)
|
||||||
if (installer == context.packageName) {
|
if (installer == context.packageName) {
|
||||||
Notifications.updateDone(context)
|
Notifications.updateDone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,25 +1,33 @@
|
|||||||
package com.topjohnwu.magisk.core.base
|
package com.topjohnwu.magisk.core.base
|
||||||
|
|
||||||
|
import android.Manifest.permission.POST_NOTIFICATIONS
|
||||||
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
||||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
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.GetContent
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
|
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
|
||||||
import androidx.annotation.WorkerThread
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
|
import com.topjohnwu.magisk.core.ktx.reflectField
|
||||||
|
import com.topjohnwu.magisk.core.ktx.toast
|
||||||
import com.topjohnwu.magisk.core.utils.RequestInstall
|
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.core.wrap
|
||||||
import com.topjohnwu.magisk.ktx.reflectField
|
|
||||||
import java.util.concurrent.CountDownLatch
|
interface ContentResultCallback: ActivityResultCallback<Uri>, Parcelable {
|
||||||
import java.util.concurrent.TimeUnit
|
fun onActivityLaunch() {}
|
||||||
|
// Make the result type explicitly non-null
|
||||||
|
override fun onActivityResult(result: Uri)
|
||||||
|
}
|
||||||
|
|
||||||
abstract class BaseActivity : AppCompatActivity() {
|
abstract class BaseActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@@ -28,26 +36,27 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
permissionCallback?.invoke(it)
|
permissionCallback?.invoke(it)
|
||||||
permissionCallback = null
|
permissionCallback = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var installCallback: ((Boolean) -> Unit)? = null
|
||||||
private val requestInstall = registerForActivityResult(RequestInstall()) {
|
private val requestInstall = registerForActivityResult(RequestInstall()) {
|
||||||
permissionCallback?.invoke(it)
|
installCallback?.invoke(it)
|
||||||
permissionCallback = null
|
installCallback = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private var contentCallback: ((Uri) -> Unit)? = null
|
private var contentCallback: ContentResultCallback? = null
|
||||||
private val getContent = registerForActivityResult(GetContent()) {
|
private val getContent = registerForActivityResult(GetContent()) {
|
||||||
if (it != null) contentCallback?.invoke(it)
|
if (it != null) contentCallback?.onActivityResult(it)
|
||||||
contentCallback = null
|
contentCallback = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private var uninstallLatch = CountDownLatch(1)
|
private val mReferrerField by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
private val uninstallPkg = registerForActivityResult(UninstallPackage()) {
|
Activity::class.java.reflectField("mReferrer")
|
||||||
uninstallLatch.countDown()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun applyOverrideConfiguration(config: Configuration?) {
|
val realCallingPackage: String? get() {
|
||||||
// Force applying our preferred local
|
callingPackage?.let { return it }
|
||||||
config?.setLocale(currentLocale)
|
mReferrerField.get(this)?.let { return it as String }
|
||||||
super.applyOverrideConfiguration(config)
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
override fun attachBaseContext(base: Context) {
|
||||||
@@ -59,36 +68,50 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
// Overwrite private members to avoid nasty "false" stack traces being logged
|
// Overwrite private members to avoid nasty "false" stack traces being logged
|
||||||
val delegate = delegate
|
val delegate = delegate
|
||||||
val clz = delegate.javaClass
|
val clz = delegate.javaClass
|
||||||
clz.reflectField("mActivityHandlesUiModeChecked").set(delegate, true)
|
clz.reflectField("mActivityHandlesConfigFlagsChecked").set(delegate, true)
|
||||||
clz.reflectField("mActivityHandlesUiMode").set(delegate, false)
|
clz.reflectField("mActivityHandlesConfigFlags").set(delegate, 0)
|
||||||
}
|
}
|
||||||
|
contentCallback = savedInstanceState?.getParcelable(CONTENT_CALLBACK_KEY)
|
||||||
super.onCreate(savedInstanceState)
|
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) {
|
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
||||||
if (permission == WRITE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= 30) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
|
||||||
// We do not need external rw on 30+
|
permission == WRITE_EXTERNAL_STORAGE) {
|
||||||
|
// We do not need external rw on R+
|
||||||
|
callback(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU &&
|
||||||
|
permission == POST_NOTIFICATIONS) {
|
||||||
|
// All apps have notification permissions before T
|
||||||
callback(true)
|
callback(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
permissionCallback = callback
|
|
||||||
if (permission == REQUEST_INSTALL_PACKAGES) {
|
if (permission == REQUEST_INSTALL_PACKAGES) {
|
||||||
|
installCallback = callback
|
||||||
requestInstall.launch(Unit)
|
requestInstall.launch(Unit)
|
||||||
} else {
|
} else {
|
||||||
|
permissionCallback = callback
|
||||||
requestPermission.launch(permission)
|
requestPermission.launch(permission)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getContent(type: String, callback: (Uri) -> Unit) {
|
fun getContent(type: String, callback: ContentResultCallback) {
|
||||||
contentCallback = callback
|
contentCallback = callback
|
||||||
getContent.launch(type)
|
try {
|
||||||
}
|
getContent.launch(type)
|
||||||
|
callback.onActivityLaunch()
|
||||||
@WorkerThread
|
} catch (e: ActivityNotFoundException) {
|
||||||
fun uninstallAndWait(pkg: String) {
|
toast(R.string.app_not_found, Toast.LENGTH_SHORT)
|
||||||
uninstallLatch = CountDownLatch(1)
|
}
|
||||||
uninstallPkg.launch(pkg)
|
|
||||||
uninstallLatch.await(3, TimeUnit.SECONDS)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun recreate() {
|
override fun recreate() {
|
||||||
@@ -100,4 +123,8 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
startActivity(Intent(intent).setFlags(0))
|
startActivity(Intent(intent).setFlags(0))
|
||||||
finish()
|
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.app.job.JobService
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.topjohnwu.magisk.core.wrap
|
import com.topjohnwu.magisk.core.patch
|
||||||
|
|
||||||
abstract class BaseJobService : JobService() {
|
abstract class BaseJobService : JobService() {
|
||||||
override fun attachBaseContext(base: Context) {
|
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.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.topjohnwu.magisk.core.wrap
|
import androidx.annotation.CallSuper
|
||||||
|
import com.topjohnwu.magisk.core.patch
|
||||||
|
|
||||||
abstract class BaseReceiver : BroadcastReceiver() {
|
abstract class BaseReceiver : BroadcastReceiver() {
|
||||||
|
@CallSuper
|
||||||
final override fun onReceive(context: Context, intent: Intent?) {
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
onReceive(context.wrap() as ContextWrapper, intent)
|
context.patch()
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun onReceive(context: ContextWrapper, intent: Intent?)
|
|
||||||
}
|
}
|
||||||
|
@@ -2,10 +2,13 @@ package com.topjohnwu.magisk.core.base
|
|||||||
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.topjohnwu.magisk.core.wrap
|
import android.content.Intent
|
||||||
|
import android.os.IBinder
|
||||||
|
import com.topjohnwu.magisk.core.patch
|
||||||
|
|
||||||
abstract class BaseService : Service() {
|
open class BaseService : Service() {
|
||||||
override fun attachBaseContext(base: Context) {
|
override fun attachBaseContext(base: Context) {
|
||||||
super.attachBaseContext(base.wrap())
|
super.attachBaseContext(base.patch())
|
||||||
}
|
}
|
||||||
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
}
|
}
|
||||||
|
@@ -1,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.BranchInfo
|
||||||
import com.topjohnwu.magisk.core.model.ModuleJson
|
import com.topjohnwu.magisk.core.model.ModuleJson
|
||||||
@@ -12,15 +12,12 @@ private const val FILE = "file"
|
|||||||
|
|
||||||
interface GithubPageServices {
|
interface GithubPageServices {
|
||||||
|
|
||||||
@GET("{$FILE}")
|
@GET
|
||||||
suspend fun fetchUpdateJSON(@Path(FILE) file: String): UpdateInfo
|
suspend fun fetchUpdateJSON(@Url file: String): UpdateInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RawServices {
|
interface RawServices {
|
||||||
|
|
||||||
@GET
|
|
||||||
suspend fun fetchCustomUpdate(@Url url: String): UpdateInfo
|
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Streaming
|
@Streaming
|
||||||
suspend fun fetchFile(@Url url: String): ResponseBody
|
suspend fun fetchFile(@Url url: String): ResponseBody
|
@@ -1,15 +1,27 @@
|
|||||||
package com.topjohnwu.magisk.data.database
|
package com.topjohnwu.magisk.core.data
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
|
import androidx.room.migration.Migration
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
import com.topjohnwu.magisk.core.model.su.SuLog
|
import com.topjohnwu.magisk.core.model.su.SuLog
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@Database(version = 1, entities = [SuLog::class], exportSchema = false)
|
@Database(version = 2, entities = [SuLog::class], exportSchema = false)
|
||||||
abstract class SuLogDatabase : RoomDatabase() {
|
abstract class SuLogDatabase : RoomDatabase() {
|
||||||
|
|
||||||
abstract fun suLogDao(): SuLogDao
|
abstract fun suLogDao(): SuLogDao
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val MIGRATION_1_2 = object : Migration(1, 2) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) = with(database) {
|
||||||
|
execSQL("ALTER TABLE logs ADD COLUMN target INTEGER NOT NULL DEFAULT -1")
|
||||||
|
execSQL("ALTER TABLE logs ADD COLUMN context TEXT NOT NULL DEFAULT ''")
|
||||||
|
execSQL("ALTER TABLE logs ADD COLUMN gids TEXT NOT NULL DEFAULT ''")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Dao
|
@Dao
|
@@ -0,0 +1,47 @@
|
|||||||
|
package com.topjohnwu.magisk.core.data.magiskdb
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.core.ktx.await
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
open class MagiskDB {
|
||||||
|
|
||||||
|
suspend fun <R> exec(
|
||||||
|
query: String,
|
||||||
|
mapper: suspend (Map<String, String>) -> R
|
||||||
|
): List<R> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val out = Shell.cmd("magisk --sqlite '$query'").await().out
|
||||||
|
out.map { line ->
|
||||||
|
line.split("\\|".toRegex())
|
||||||
|
.map { it.split("=", limit = 2) }
|
||||||
|
.filter { it.size == 2 }
|
||||||
|
.associate { it[0] to it[1] }
|
||||||
|
.let { mapper(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend inline fun exec(query: String) {
|
||||||
|
exec(query) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Map<String, Any>.toQuery(): String {
|
||||||
|
val keys = this.keys.joinToString(",")
|
||||||
|
val values = this.values.joinToString(",") {
|
||||||
|
when (it) {
|
||||||
|
is Boolean -> if (it) "1" else "0"
|
||||||
|
is Number -> it.toString()
|
||||||
|
else -> "\"$it\""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "($keys) VALUES($values)"
|
||||||
|
}
|
||||||
|
|
||||||
|
object Table {
|
||||||
|
const val POLICY = "policies"
|
||||||
|
const val SETTINGS = "settings"
|
||||||
|
const val STRINGS = "strings"
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,53 @@
|
|||||||
|
package com.topjohnwu.magisk.core.data.magiskdb
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.core.Const
|
||||||
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
|
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class PolicyDao : MagiskDB() {
|
||||||
|
|
||||||
|
suspend fun deleteOutdated() {
|
||||||
|
val nowSeconds = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())
|
||||||
|
val query = "DELETE FROM ${Table.POLICY} WHERE " +
|
||||||
|
"(until > 0 AND until < $nowSeconds) OR until < 0"
|
||||||
|
exec(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun delete(uid: Int) {
|
||||||
|
val query = "DELETE FROM ${Table.POLICY} WHERE uid == $uid"
|
||||||
|
exec(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetch(uid: Int): SuPolicy? {
|
||||||
|
val query = "SELECT * FROM ${Table.POLICY} WHERE uid == $uid LIMIT = 1"
|
||||||
|
return exec(query, ::toPolicy).firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun update(policy: SuPolicy) {
|
||||||
|
val map = policy.toMap()
|
||||||
|
if (!Const.Version.atLeast_25_0()) {
|
||||||
|
// Put in package_name for old database
|
||||||
|
map["package_name"] = AppContext.packageManager.getNameForUid(policy.uid)!!
|
||||||
|
}
|
||||||
|
val query = "REPLACE INTO ${Table.POLICY} ${map.toQuery()}"
|
||||||
|
exec(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchAll(): List<SuPolicy> {
|
||||||
|
val query = "SELECT * FROM ${Table.POLICY} WHERE uid/100000 == ${Const.USER_ID}"
|
||||||
|
return exec(query, ::toPolicy).filterNotNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toPolicy(map: Map<String, String>): SuPolicy? {
|
||||||
|
val uid = map["uid"]?.toInt() ?: return null
|
||||||
|
val policy = SuPolicy(uid)
|
||||||
|
|
||||||
|
map["policy"]?.toInt()?.let { policy.policy = it }
|
||||||
|
map["until"]?.toLong()?.let { policy.until = it }
|
||||||
|
map["logging"]?.toInt()?.let { policy.logging = it != 0 }
|
||||||
|
map["notification"]?.toInt()?.let { policy.notification = it != 0 }
|
||||||
|
return policy
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,20 @@
|
|||||||
|
package com.topjohnwu.magisk.core.data.magiskdb
|
||||||
|
|
||||||
|
class SettingsDao : MagiskDB() {
|
||||||
|
|
||||||
|
suspend fun delete(key: String) {
|
||||||
|
val query = "DELETE FROM ${Table.SETTINGS} WHERE key == \"$key\""
|
||||||
|
exec(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun put(key: String, value: Int) {
|
||||||
|
val kv = mapOf("key" to key, "value" to value)
|
||||||
|
val query = "REPLACE INTO ${Table.SETTINGS} ${kv.toQuery()}"
|
||||||
|
exec(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetch(key: String, default: Int = -1): Int {
|
||||||
|
val query = "SELECT value FROM ${Table.SETTINGS} WHERE key == \"$key\" LIMIT 1"
|
||||||
|
return exec(query) { it["value"]?.toInt() }.firstOrNull() ?: default
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,20 @@
|
|||||||
|
package com.topjohnwu.magisk.core.data.magiskdb
|
||||||
|
|
||||||
|
class StringDao : MagiskDB() {
|
||||||
|
|
||||||
|
suspend fun delete(key: String) {
|
||||||
|
val query = "DELETE FROM ${Table.STRINGS} WHERE key == \"$key\""
|
||||||
|
exec(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun put(key: String, value: String) {
|
||||||
|
val kv = mapOf("key" to key, "value" to value)
|
||||||
|
val query = "REPLACE INTO ${Table.STRINGS} ${kv.toQuery()}"
|
||||||
|
exec(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetch(key: String, default: String = ""): String {
|
||||||
|
val query = "SELECT value FROM ${Table.STRINGS} WHERE key == \"$key\" LIMIT 1"
|
||||||
|
return exec(query) { it["value"] }.firstOrNull() ?: default
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
package com.topjohnwu.magisk.di
|
package com.topjohnwu.magisk.core.di
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.squareup.moshi.Moshi
|
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.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.lifecycle.ViewModelStoreOwner
|
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
import com.topjohnwu.magisk.core.data.SuLogDatabase
|
||||||
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
|
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
||||||
import com.topjohnwu.magisk.core.magiskdb.StringDao
|
import com.topjohnwu.magisk.core.data.magiskdb.SettingsDao
|
||||||
import com.topjohnwu.magisk.data.database.SuLogDatabase
|
import com.topjohnwu.magisk.core.data.magiskdb.StringDao
|
||||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
import com.topjohnwu.magisk.core.ktx.deviceProtectedContext
|
||||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
import com.topjohnwu.magisk.core.repository.LogRepository
|
||||||
import com.topjohnwu.magisk.ktx.deviceProtectedContext
|
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||||
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
import com.topjohnwu.magisk.core.utils.BiometricHelper
|
||||||
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 io.noties.markwon.Markwon
|
import io.noties.markwon.Markwon
|
||||||
import io.noties.markwon.utils.NoCopySpannableFactory
|
import io.noties.markwon.utils.NoCopySpannableFactory
|
||||||
|
|
||||||
@@ -31,6 +24,7 @@ object ServiceLocator {
|
|||||||
lateinit var context: Context
|
lateinit var context: Context
|
||||||
val deContext by lazy { context.deviceProtectedContext }
|
val deContext by lazy { context.deviceProtectedContext }
|
||||||
val timeoutPrefs by lazy { deContext.getSharedPreferences("su_timeout", 0) }
|
val timeoutPrefs by lazy { deContext.getSharedPreferences("su_timeout", 0) }
|
||||||
|
val biometrics by lazy { BiometricHelper(context) }
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
val policyDB = PolicyDao()
|
val policyDB = PolicyDao()
|
||||||
@@ -47,32 +41,13 @@ object ServiceLocator {
|
|||||||
NetworkService(
|
NetworkService(
|
||||||
createApiService(retrofit, Const.Url.GITHUB_PAGE_URL),
|
createApiService(retrofit, Const.Url.GITHUB_PAGE_URL),
|
||||||
createApiService(retrofit, Const.Url.GITHUB_RAW_URL),
|
createApiService(retrofit, Const.Url.GITHUB_RAW_URL),
|
||||||
createApiService(retrofit, Const.Url.GITHUB_API_URL)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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) =
|
private fun createSuLogDatabase(context: Context) =
|
||||||
Room.databaseBuilder(context, SuLogDatabase::class.java, "sulogs.db")
|
Room.databaseBuilder(context, SuLogDatabase::class.java, "sulogs.db")
|
||||||
|
.addMigrations(SuLogDatabase.MIGRATION_1_2)
|
||||||
.fallbackToDestructiveMigration()
|
.fallbackToDestructiveMigration()
|
||||||
.build()
|
.build()
|
||||||
|
|
@@ -1,5 +1,6 @@
|
|||||||
package com.topjohnwu.magisk.core.download
|
package com.topjohnwu.magisk.core.download
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.PendingIntent.*
|
import android.app.PendingIntent.*
|
||||||
@@ -11,25 +12,28 @@ import androidx.core.net.toFile
|
|||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.StubApk
|
import com.topjohnwu.magisk.StubApk
|
||||||
import com.topjohnwu.magisk.core.*
|
import com.topjohnwu.magisk.core.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.tasks.HideAPK
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||||
import com.topjohnwu.magisk.ktx.copyAndClose
|
|
||||||
import com.topjohnwu.magisk.ktx.forEach
|
|
||||||
import com.topjohnwu.magisk.ktx.withStreams
|
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
|
||||||
import com.topjohnwu.magisk.utils.APKInstall
|
import com.topjohnwu.magisk.utils.APKInstall
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
import java.util.Properties
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipFile
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
@@ -43,14 +47,12 @@ class DownloadService : NotificationService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
|
||||||
job.cancel()
|
job.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun download(subject: Subject) {
|
private fun download(subject: Subject) {
|
||||||
update(subject.notifyId)
|
notifyUpdate(subject.notifyId)
|
||||||
val coroutineScope = CoroutineScope(job + Dispatchers.IO)
|
CoroutineScope(job + Dispatchers.IO).launch {
|
||||||
coroutineScope.launch {
|
|
||||||
try {
|
try {
|
||||||
val stream = service.fetchFile(subject.url).toProgressStream(subject)
|
val stream = service.fetchFile(subject.url).toProgressStream(subject)
|
||||||
when (subject) {
|
when (subject) {
|
||||||
@@ -59,7 +61,7 @@ class DownloadService : NotificationService() {
|
|||||||
}
|
}
|
||||||
val activity = ActivityTracker.foreground
|
val activity = ActivityTracker.foreground
|
||||||
if (activity != null && subject.autoLaunch) {
|
if (activity != null && subject.autoLaunch) {
|
||||||
remove(subject.notifyId)
|
notifyRemove(subject.notifyId)
|
||||||
subject.pendingIntent(activity)?.send()
|
subject.pendingIntent(activity)?.send()
|
||||||
} else {
|
} else {
|
||||||
notifyFinish(subject)
|
notifyFinish(subject)
|
||||||
@@ -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) {
|
fun writeTee(output: OutputStream) {
|
||||||
val uri = MediaStoreUtils.getFile("${subject.title}.apk").uri
|
val uri = MediaStoreUtils.getFile("${subject.title}.apk").uri
|
||||||
val external = uri.outputStream()
|
val external = uri.outputStream()
|
||||||
stream.copyAndClose(TeeOutputStream(external, output))
|
stream.copyAndClose(TeeOutputStream(external, output))
|
||||||
}
|
}
|
||||||
@@ -87,35 +89,34 @@ class DownloadService : NotificationService() {
|
|||||||
// Download full APK to stub update path
|
// Download full APK to stub update path
|
||||||
writeTee(updateApk.outputStream())
|
writeTee(updateApk.outputStream())
|
||||||
|
|
||||||
if (Info.stub!!.version < subject.stub.versionCode) {
|
val zf = ZipFile(updateApk)
|
||||||
|
val prop = Properties()
|
||||||
|
prop.load(ByteArrayInputStream(zf.comment.toByteArray()))
|
||||||
|
val stubVersion = prop.getProperty("stubVersion").toIntOrNull() ?: -1
|
||||||
|
if (Info.stub!!.version < stubVersion) {
|
||||||
// Also upgrade stub
|
// Also upgrade stub
|
||||||
update(subject.notifyId) {
|
notifyUpdate(subject.notifyId) {
|
||||||
it.setProgress(0, 0, true)
|
it.setProgress(0, 0, true)
|
||||||
.setContentTitle(getString(R.string.hide_app_title))
|
.setContentTitle(getString(R.string.hide_app_title))
|
||||||
.setContentText("")
|
.setContentText("")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download
|
// Extract stub
|
||||||
val apk = subject.file.toFile()
|
val apk = subject.file.toFile()
|
||||||
service.fetchFile(subject.stub.link).byteStream().writeTo(apk)
|
zf.getInputStream(zf.getEntry("assets/stub.apk")).writeTo(apk)
|
||||||
|
zf.close()
|
||||||
|
|
||||||
// Patch and install
|
// Patch and install
|
||||||
val session = APKInstall.startSession(this)
|
subject.intent = HideAPK.upgrade(this, apk)
|
||||||
session.openStream(this).use {
|
?: throw IOException("HideAPK patch error")
|
||||||
val label = applicationInfo.nonLocalizedLabel
|
|
||||||
if (!HideAPK.patch(this, apk, it, packageName, label)) {
|
|
||||||
throw IOException("HideAPK patch error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
apk.delete()
|
apk.delete()
|
||||||
subject.intent = session.waitIntent()
|
|
||||||
} else {
|
} else {
|
||||||
ActivityTracker.foreground?.let {
|
ActivityTracker.foreground?.let {
|
||||||
// Relaunch the process if we are foreground
|
// Relaunch the process if we are foreground
|
||||||
StubApk.restartProcess(it)
|
StubApk.restartProcess(it)
|
||||||
} ?: run {
|
} ?: run {
|
||||||
// Or else kill the current process after posting notification
|
// Or else kill the current process after posting notification
|
||||||
subject.intent = Notifications.selfLaunchIntent(this)
|
subject.intent = selfLaunchIntent()
|
||||||
subject.postDownload = { Runtime.getRuntime().exit(0) }
|
subject.postDownload = { Runtime.getRuntime().exit(0) }
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -196,19 +197,23 @@ class DownloadService : NotificationService() {
|
|||||||
fun getPendingIntent(context: Context, subject: Subject): PendingIntent {
|
fun getPendingIntent(context: Context, subject: Subject): PendingIntent {
|
||||||
val flag = FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT or FLAG_ONE_SHOT
|
val flag = FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT or FLAG_ONE_SHOT
|
||||||
val intent = intent(context, subject)
|
val intent = intent(context, subject)
|
||||||
return if (Build.VERSION.SDK_INT >= 26) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
getForegroundService(context, REQUEST_CODE, intent, flag)
|
getForegroundService(context, REQUEST_CODE, intent, flag)
|
||||||
} else {
|
} else {
|
||||||
getService(context, REQUEST_CODE, intent, flag)
|
getService(context, REQUEST_CODE, intent, flag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start(context: Context, subject: Subject) {
|
@SuppressLint("InlinedApi")
|
||||||
val app = context.applicationContext
|
fun start(activity: BaseActivity, subject: Subject) {
|
||||||
if (Build.VERSION.SDK_INT >= 26) {
|
activity.withPermission(Manifest.permission.POST_NOTIFICATIONS) {
|
||||||
app.startForegroundService(intent(app, subject))
|
// Always download regardless of notification permission status
|
||||||
} else {
|
val app = activity.applicationContext
|
||||||
app.startService(intent(app, subject))
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
app.startForegroundService(intent(app, subject))
|
||||||
|
} else {
|
||||||
|
app.startService(intent(app, subject))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,13 +2,13 @@ package com.topjohnwu.magisk.core.download
|
|||||||
|
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.IBinder
|
import android.os.Build
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.base.BaseService
|
import com.topjohnwu.magisk.core.base.BaseService
|
||||||
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.core.ktx.synchronized
|
||||||
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
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 com.topjohnwu.magisk.view.Notifications
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@@ -20,7 +20,7 @@ open class NotificationService : BaseService() {
|
|||||||
|
|
||||||
protected val service get() = ServiceLocator.networkService
|
protected val service get() = ServiceLocator.networkService
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? = null
|
private var attachedNotificationId = 0
|
||||||
|
|
||||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
super.onTaskRemoved(rootIntent)
|
super.onTaskRemoved(rootIntent)
|
||||||
@@ -33,11 +33,11 @@ open class NotificationService : BaseService() {
|
|||||||
val total = max.toFloat() / 1048576
|
val total = max.toFloat() / 1048576
|
||||||
val id = subject.notifyId
|
val id = subject.notifyId
|
||||||
|
|
||||||
update(id) { it.setContentTitle(subject.title) }
|
notifyUpdate(id) { it.setContentTitle(subject.title) }
|
||||||
|
|
||||||
return ProgressInputStream(byteStream()) {
|
return ProgressInputStream(byteStream()) {
|
||||||
val progress = it.toFloat() / 1048576
|
val progress = it.toFloat() / 1048576
|
||||||
update(id) { notification ->
|
notifyUpdate(id) { notification ->
|
||||||
if (max > 0) {
|
if (max > 0) {
|
||||||
broadcast(progress / total, subject)
|
broadcast(progress / total, subject)
|
||||||
notification
|
notification
|
||||||
@@ -52,7 +52,7 @@ open class NotificationService : BaseService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
|
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
|
||||||
val notification = remove(id)?.also(editor) ?: return -1
|
val notification = notifyRemove(id)?.also(editor) ?: return -1
|
||||||
val newId = Notifications.nextId()
|
val newId = Notifications.nextId()
|
||||||
Notifications.mgr.notify(newId, notification.build())
|
Notifications.mgr.notify(newId, notification.build())
|
||||||
return newId
|
return newId
|
||||||
@@ -76,29 +76,44 @@ open class NotificationService : BaseService() {
|
|||||||
subject.pendingIntent(this)?.let { intent -> it.setContentIntent(intent) }
|
subject.pendingIntent(this)?.let { intent -> it.setContentIntent(intent) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun create() = Notifications.progress(this, "")
|
private fun attachNotification(id: Int, notification: Notification) {
|
||||||
|
attachedNotificationId = id
|
||||||
|
startForeground(id, notification)
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateForeground() {
|
private fun maybeDetachNotification(id: Int) : Boolean {
|
||||||
|
if (attachedNotificationId != id) return false
|
||||||
if (hasNotifications) {
|
if (hasNotifications) {
|
||||||
val (id, notification) = notifications.entries.first()
|
val (anotherId, notification) = notifications.entries.first()
|
||||||
startForeground(id, notification.build())
|
// Attaching a new notification will remove the current showing one
|
||||||
} else {
|
attachNotification(anotherId, notification.build())
|
||||||
stopForeground(false)
|
return true
|
||||||
}
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
stopForeground(true)
|
||||||
|
}
|
||||||
|
attachedNotificationId = 0
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun update(id: Int, editor: (Notification.Builder) -> Unit = {}) {
|
protected fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit = {}) {
|
||||||
|
fun create() = Notifications.startProgress("")
|
||||||
|
|
||||||
val wasEmpty = !hasNotifications
|
val wasEmpty = !hasNotifications
|
||||||
val notification = notifications.getOrPut(id, ::create).also(editor)
|
val notification = notifications.getOrPut(id, ::create).also(editor).build()
|
||||||
if (wasEmpty)
|
if (wasEmpty)
|
||||||
updateForeground()
|
attachNotification(id, notification)
|
||||||
else
|
else
|
||||||
Notifications.mgr.notify(id, notification.build())
|
Notifications.mgr.notify(id, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun remove(id: Int): Notification.Builder? {
|
protected fun notifyRemove(id: Int): Notification.Builder? {
|
||||||
val n = notifications.remove(id)?.also { updateForeground() }
|
val n = notifications.remove(id)
|
||||||
Notifications.mgr.cancel(id)
|
if (n == null || !maybeDetachNotification(id))
|
||||||
|
Notifications.mgr.cancel(id)
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -8,12 +8,11 @@ import android.net.Uri
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
|
import com.topjohnwu.magisk.core.ktx.cachedFile
|
||||||
import com.topjohnwu.magisk.core.model.MagiskJson
|
import com.topjohnwu.magisk.core.model.MagiskJson
|
||||||
import com.topjohnwu.magisk.core.model.StubJson
|
|
||||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||||
import com.topjohnwu.magisk.di.AppContext
|
|
||||||
import com.topjohnwu.magisk.ktx.cachedFile
|
|
||||||
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
@@ -59,7 +58,6 @@ sealed class Subject : Parcelable {
|
|||||||
@Parcelize
|
@Parcelize
|
||||||
class App(
|
class App(
|
||||||
private val json: MagiskJson = Info.remote.magisk,
|
private val json: MagiskJson = Info.remote.magisk,
|
||||||
val stub: StubJson = Info.remote.stub,
|
|
||||||
override val notifyId: Int = Notifications.nextId()
|
override val notifyId: Int = Notifications.nextId()
|
||||||
) : Subject() {
|
) : Subject() {
|
||||||
override val title: String get() = "Magisk-${json.version}(${json.versionCode})"
|
override val title: String get() = "Magisk-${json.version}(${json.versionCode})"
|
||||||
|
152
app/src/main/java/com/topjohnwu/magisk/core/ktx/XAndroid.kt
Normal file
152
app/src/main/java/com/topjohnwu/magisk/core/ktx/XAndroid.kt
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
package com.topjohnwu.magisk.core.ktx
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.*
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.drawable.AdaptiveIconDrawable
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.graphics.drawable.LayerDrawable
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Build.VERSION.SDK_INT
|
||||||
|
import android.os.Process
|
||||||
|
import android.view.View
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||||
|
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||||
|
import com.topjohnwu.magisk.utils.APKInstall
|
||||||
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.String
|
||||||
|
|
||||||
|
fun Context.rawResource(id: Int) = resources.openRawResource(id)
|
||||||
|
|
||||||
|
fun Context.getBitmap(id: Int): Bitmap {
|
||||||
|
var drawable = AppCompatResources.getDrawable(this, id)!!
|
||||||
|
if (drawable is BitmapDrawable)
|
||||||
|
return drawable.bitmap
|
||||||
|
if (SDK_INT >= Build.VERSION_CODES.O && drawable is AdaptiveIconDrawable) {
|
||||||
|
drawable = LayerDrawable(arrayOf(drawable.background, drawable.foreground))
|
||||||
|
}
|
||||||
|
val bitmap = Bitmap.createBitmap(
|
||||||
|
drawable.intrinsicWidth, drawable.intrinsicHeight,
|
||||||
|
Bitmap.Config.ARGB_8888
|
||||||
|
)
|
||||||
|
val canvas = Canvas(bitmap)
|
||||||
|
drawable.setBounds(0, 0, canvas.width, canvas.height)
|
||||||
|
drawable.draw(canvas)
|
||||||
|
return bitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
val Context.deviceProtectedContext: Context get() =
|
||||||
|
if (SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
createDeviceProtectedStorageContext()
|
||||||
|
} else { this }
|
||||||
|
|
||||||
|
fun Context.cachedFile(name: String) = File(cacheDir, name)
|
||||||
|
|
||||||
|
fun ApplicationInfo.getLabel(pm: PackageManager): String {
|
||||||
|
runCatching {
|
||||||
|
if (labelRes > 0) {
|
||||||
|
val res = pm.getResourcesForApplication(this)
|
||||||
|
val config = Configuration()
|
||||||
|
config.setLocale(currentLocale)
|
||||||
|
res.updateConfiguration(config, res.displayMetrics)
|
||||||
|
return res.getString(labelRes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadLabel(pm).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.unwrap(): Context {
|
||||||
|
var context = this
|
||||||
|
while (context is ContextWrapper)
|
||||||
|
context = context.baseContext
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Activity.hideKeyboard() {
|
||||||
|
val view = currentFocus ?: return
|
||||||
|
getSystemService<InputMethodManager>()
|
||||||
|
?.hideSoftInputFromWindow(view.windowToken, 0)
|
||||||
|
view.clearFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
val View.activity: Activity get() {
|
||||||
|
var context = context
|
||||||
|
while(true) {
|
||||||
|
if (context !is ContextWrapper)
|
||||||
|
error("View is not attached to activity")
|
||||||
|
if (context is Activity)
|
||||||
|
return context
|
||||||
|
context = context.baseContext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("PrivateApi")
|
||||||
|
fun getProperty(key: String, def: String): String {
|
||||||
|
runCatching {
|
||||||
|
val clazz = Class.forName("android.os.SystemProperties")
|
||||||
|
val get = clazz.getMethod("get", String::class.java, String::class.java)
|
||||||
|
return get.invoke(clazz, key, def) as String
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
@Throws(PackageManager.NameNotFoundException::class)
|
||||||
|
fun PackageManager.getPackageInfo(uid: Int, pid: Int): PackageInfo? {
|
||||||
|
val flag = PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||||
|
val pkgs = getPackagesForUid(uid) ?: throw PackageManager.NameNotFoundException()
|
||||||
|
if (pkgs.size > 1) {
|
||||||
|
if (pid <= 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// Try to find package name from PID
|
||||||
|
val proc = RootUtils.obj?.getAppProcess(pid)
|
||||||
|
if (proc == null) {
|
||||||
|
if (uid == Process.SHELL_UID) {
|
||||||
|
// It is possible that some apps installed are sharing UID with shell.
|
||||||
|
// We will not be able to find a package from the active process list,
|
||||||
|
// because the client is forked from ADB shell, not any app process.
|
||||||
|
return getPackageInfo("com.android.shell", flag)
|
||||||
|
}
|
||||||
|
} else if (uid == proc.uid) {
|
||||||
|
return getPackageInfo(proc.pkgList[0], flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (pkgs.size == 1) {
|
||||||
|
return getPackageInfo(pkgs[0], flag)
|
||||||
|
}
|
||||||
|
throw PackageManager.NameNotFoundException()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.registerRuntimeReceiver(receiver: BroadcastReceiver, filter: IntentFilter) {
|
||||||
|
APKInstall.registerReceiver(this, receiver, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.selfLaunchIntent(): Intent {
|
||||||
|
val pm = packageManager
|
||||||
|
val intent = pm.getLaunchIntentForPackage(packageName)!!
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.toast(msg: CharSequence, duration: Int) {
|
||||||
|
UiThreadHandler.run { Toast.makeText(this, msg, duration).show() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.toast(resId: Int, duration: Int) {
|
||||||
|
UiThreadHandler.run { Toast.makeText(this, resId, duration).show() }
|
||||||
|
}
|
@@ -1,13 +1,17 @@
|
|||||||
package com.topjohnwu.magisk.ktx
|
package com.topjohnwu.magisk.core.ktx
|
||||||
|
|
||||||
import androidx.collection.SparseArrayCompat
|
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.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
|
import java.text.DateFormat
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.Collections
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
@@ -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 <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 =
|
fun Class<*>.reflectField(name: String): Field =
|
||||||
getDeclaredField(name).apply { isAccessible = true }
|
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)
|
@JsonClass(generateAdapter = true)
|
||||||
data class UpdateInfo(
|
data class UpdateInfo(
|
||||||
val magisk: MagiskJson = MagiskJson(),
|
val magisk: MagiskJson = MagiskJson(),
|
||||||
val stub: StubJson = StubJson()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@@ -19,13 +18,6 @@ data class MagiskJson(
|
|||||||
val note: String = ""
|
val note: String = ""
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class StubJson(
|
|
||||||
val versionCode: Int = -1,
|
|
||||||
val link: String = ""
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class ModuleJson(
|
data class ModuleJson(
|
||||||
val version: String,
|
val version: String,
|
||||||
|
@@ -2,9 +2,9 @@ package com.topjohnwu.magisk.core.model.module
|
|||||||
|
|
||||||
import com.squareup.moshi.JsonDataException
|
import com.squareup.moshi.JsonDataException
|
||||||
import com.topjohnwu.magisk.core.Const
|
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.Shell
|
||||||
import com.topjohnwu.superuser.io.SuFile
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@@ -26,13 +26,12 @@ data class LocalModule(
|
|||||||
var outdated = false
|
var outdated = false
|
||||||
|
|
||||||
private var updateUrl: String = ""
|
private var updateUrl: String = ""
|
||||||
private val removeFile = SuFile(path, "remove")
|
private val removeFile = RootUtils.fs.getFile(path, "remove")
|
||||||
private val disableFile = SuFile(path, "disable")
|
private val disableFile = RootUtils.fs.getFile(path, "disable")
|
||||||
private val updateFile = SuFile(path, "update")
|
private val updateFile = RootUtils.fs.getFile(path, "update")
|
||||||
private val ruleFile = SuFile(path, "sepolicy.rule")
|
private val riruFolder = RootUtils.fs.getFile(path, "riru")
|
||||||
private val riruFolder = SuFile(path, "riru")
|
private val zygiskFolder = RootUtils.fs.getFile(path, "zygisk")
|
||||||
private val zygiskFolder = SuFile(path, "zygisk")
|
private val unloaded = RootUtils.fs.getFile(zygiskFolder, "unloaded")
|
||||||
private val unloaded = SuFile(zygiskFolder, "unloaded")
|
|
||||||
|
|
||||||
val updated: Boolean get() = updateFile.exists()
|
val updated: Boolean get() = updateFile.exists()
|
||||||
val isRiru: Boolean get() = (id == "riru-core") || riruFolder.exists()
|
val isRiru: Boolean get() = (id == "riru-core") || riruFolder.exists()
|
||||||
@@ -42,19 +41,12 @@ data class LocalModule(
|
|||||||
var enable: Boolean
|
var enable: Boolean
|
||||||
get() = !disableFile.exists()
|
get() = !disableFile.exists()
|
||||||
set(enable) {
|
set(enable) {
|
||||||
val dir = "$PERSIST/$id"
|
|
||||||
if (enable) {
|
if (enable) {
|
||||||
disableFile.delete()
|
disableFile.delete()
|
||||||
if (Const.Version.atLeast_21_2())
|
Shell.cmd("copy_preinit_files").submit()
|
||||||
Shell.cmd("copy_sepolicy_rules").submit()
|
|
||||||
else
|
|
||||||
Shell.cmd("mkdir -p $dir", "cp -af $ruleFile $dir").submit()
|
|
||||||
} else {
|
} else {
|
||||||
!disableFile.createNewFile()
|
!disableFile.createNewFile()
|
||||||
if (Const.Version.atLeast_21_2())
|
Shell.cmd("copy_preinit_files").submit()
|
||||||
Shell.cmd("copy_sepolicy_rules").submit()
|
|
||||||
else
|
|
||||||
Shell.cmd("rm -rf $dir").submit()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,16 +56,10 @@ data class LocalModule(
|
|||||||
if (remove) {
|
if (remove) {
|
||||||
if (updateFile.exists()) return
|
if (updateFile.exists()) return
|
||||||
removeFile.createNewFile()
|
removeFile.createNewFile()
|
||||||
if (Const.Version.atLeast_21_2())
|
Shell.cmd("copy_preinit_files").submit()
|
||||||
Shell.cmd("copy_sepolicy_rules").submit()
|
|
||||||
else
|
|
||||||
Shell.cmd("rm -rf $PERSIST/$id").submit()
|
|
||||||
} else {
|
} else {
|
||||||
removeFile.delete()
|
removeFile.delete()
|
||||||
if (Const.Version.atLeast_21_2())
|
Shell.cmd("copy_preinit_files").submit()
|
||||||
Shell.cmd("copy_sepolicy_rules").submit()
|
|
||||||
else
|
|
||||||
Shell.cmd("cp -af $ruleFile $PERSIST/$id").submit()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,13 +122,13 @@ data class LocalModule(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private val PERSIST get() = "${Const.MAGISKTMP}/mirror/persist/magisk"
|
fun loaded() = RootUtils.fs.getFile(Const.MAGISK_PATH).exists()
|
||||||
|
|
||||||
suspend fun installed() = withContext(Dispatchers.IO) {
|
suspend fun installed() = withContext(Dispatchers.IO) {
|
||||||
SuFile(Const.MAGISK_PATH)
|
RootUtils.fs.getFile(Const.MAGISK_PATH)
|
||||||
.listFiles()
|
.listFiles()
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
.filter { !it.isFile }
|
.filter { !it.isFile && !it.isHidden }
|
||||||
.map { LocalModule("${Const.MAGISK_PATH}/${it.name}") }
|
.map { LocalModule("${Const.MAGISK_PATH}/${it.name}") }
|
||||||
.sortedBy { it.name.lowercase(Locale.ROOT) }
|
.sortedBy { it.name.lowercase(Locale.ROOT) }
|
||||||
}
|
}
|
||||||
|
@@ -1,19 +1,73 @@
|
|||||||
package com.topjohnwu.magisk.core.model.su
|
package com.topjohnwu.magisk.core.model.su
|
||||||
|
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import com.topjohnwu.magisk.ktx.now
|
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||||
|
|
||||||
@Entity(tableName = "logs")
|
@Entity(tableName = "logs")
|
||||||
data class SuLog(
|
class SuLog(
|
||||||
val fromUid: Int,
|
val fromUid: Int,
|
||||||
val toUid: Int,
|
val toUid: Int,
|
||||||
val fromPid: Int,
|
val fromPid: Int,
|
||||||
val packageName: String,
|
val packageName: String,
|
||||||
val appName: String,
|
val appName: String,
|
||||||
val command: String,
|
val command: String,
|
||||||
val action: Boolean,
|
val action: Int,
|
||||||
val time: Long = now
|
val target: Int,
|
||||||
|
val context: String,
|
||||||
|
val gids: String,
|
||||||
|
val time: Long = System.currentTimeMillis()
|
||||||
) {
|
) {
|
||||||
@PrimaryKey(autoGenerate = true) var id: Int = 0
|
@PrimaryKey(autoGenerate = true) var id: Int = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
package com.topjohnwu.magisk.core.model.su
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
class SuPolicy(val uid: Int) {
|
||||||
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
|
|
||||||
) {
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val INTERACTIVE = 0
|
const val INTERACTIVE = 0
|
||||||
const val DENY = 1
|
const val DENY = 1
|
||||||
const val ALLOW = 2
|
const val ALLOW = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toLog(toUid: Int, fromPid: Int, command: String) = SuLog(
|
var policy: Int = INTERACTIVE
|
||||||
uid, toUid, fromPid, packageName, appName,
|
var until: Long = -1L
|
||||||
command, policy == ALLOW)
|
var logging: Boolean = true
|
||||||
|
var notification: Boolean = true
|
||||||
|
|
||||||
fun toMap() = mapOf(
|
fun toMap(): MutableMap<String, Any> = mutableMapOf(
|
||||||
"uid" to uid,
|
"uid" to uid,
|
||||||
"package_name" to packageName,
|
|
||||||
"policy" to policy,
|
"policy" to policy,
|
||||||
"until" to until,
|
"until" to until,
|
||||||
"logging" to logging,
|
"logging" to logging,
|
||||||
"notification" to notification
|
"notification" to notification
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(PackageManager.NameNotFoundException::class)
|
|
||||||
fun Map<String, String>.toPolicy(pm: PackageManager): SuPolicy {
|
|
||||||
val uid = get("uid")?.toIntOrNull() ?: -1
|
|
||||||
val packageName = get("package_name").orEmpty()
|
|
||||||
val info = pm.getApplicationInfo(packageName, PackageManager.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.data.magiskdb.SettingsDao
|
||||||
import com.topjohnwu.magisk.core.magiskdb.StringDao
|
import com.topjohnwu.magisk.core.data.magiskdb.StringDao
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlin.properties.ReadWriteProperty
|
import kotlin.properties.ReadWriteProperty
|
||||||
@@ -11,26 +11,27 @@ import kotlin.reflect.KProperty
|
|||||||
interface DBConfig {
|
interface DBConfig {
|
||||||
val settingsDB: SettingsDao
|
val settingsDB: SettingsDao
|
||||||
val stringDB: StringDao
|
val stringDB: StringDao
|
||||||
|
val coroutineScope: CoroutineScope
|
||||||
|
|
||||||
fun dbSettings(
|
fun dbSettings(
|
||||||
name: String,
|
name: String,
|
||||||
default: Int
|
default: Int
|
||||||
) = DBSettingsValue(name, default)
|
) = IntDBProperty(name, default)
|
||||||
|
|
||||||
fun dbSettings(
|
fun dbSettings(
|
||||||
name: String,
|
name: String,
|
||||||
default: Boolean
|
default: Boolean
|
||||||
) = DBBoolSettings(name, default)
|
) = BoolDBProperty(name, default)
|
||||||
|
|
||||||
fun dbStrings(
|
fun dbStrings(
|
||||||
name: String,
|
name: String,
|
||||||
default: String,
|
default: String,
|
||||||
sync: Boolean = false
|
sync: Boolean = false
|
||||||
) = DBStringsValue(name, default, sync)
|
) = StringDBProperty(name, default, sync)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class DBSettingsValue(
|
class IntDBProperty(
|
||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: Int
|
private val default: Int
|
||||||
) : ReadWriteProperty<DBConfig, Int> {
|
) : ReadWriteProperty<DBConfig, Int> {
|
||||||
@@ -48,18 +49,18 @@ class DBSettingsValue(
|
|||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
this.value = value
|
this.value = value
|
||||||
}
|
}
|
||||||
GlobalScope.launch {
|
thisRef.coroutineScope.launch {
|
||||||
thisRef.settingsDB.put(name, value)
|
thisRef.settingsDB.put(name, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open class DBBoolSettings(
|
open class BoolDBProperty(
|
||||||
name: String,
|
name: String,
|
||||||
default: Boolean
|
default: Boolean
|
||||||
) : ReadWriteProperty<DBConfig, Boolean> {
|
) : ReadWriteProperty<DBConfig, Boolean> {
|
||||||
|
|
||||||
val base = DBSettingsValue(name, if (default) 1 else 0)
|
val base = IntDBProperty(name, if (default) 1 else 0)
|
||||||
|
|
||||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Boolean =
|
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Boolean =
|
||||||
base.getValue(thisRef, property) != 0
|
base.getValue(thisRef, property) != 0
|
||||||
@@ -68,10 +69,10 @@ open class DBBoolSettings(
|
|||||||
base.setValue(thisRef, property, if (value) 1 else 0)
|
base.setValue(thisRef, property, if (value) 1 else 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
class DBBoolSettingsNoWrite(
|
class BoolDBPropertyNoWrite(
|
||||||
name: String,
|
name: String,
|
||||||
default: Boolean
|
default: Boolean
|
||||||
) : DBBoolSettings(name, default) {
|
) : BoolDBProperty(name, default) {
|
||||||
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Boolean) {
|
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Boolean) {
|
||||||
synchronized(base) {
|
synchronized(base) {
|
||||||
base.value = if (value) 1 else 0
|
base.value = if (value) 1 else 0
|
||||||
@@ -79,7 +80,7 @@ class DBBoolSettingsNoWrite(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DBStringsValue(
|
class StringDBProperty(
|
||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: String,
|
private val default: String,
|
||||||
private val sync: Boolean
|
private val sync: Boolean
|
||||||
@@ -106,7 +107,7 @@ class DBStringsValue(
|
|||||||
thisRef.stringDB.delete(name)
|
thisRef.stringDB.delete(name)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
GlobalScope.launch {
|
thisRef.coroutineScope.launch {
|
||||||
thisRef.stringDB.delete(name)
|
thisRef.stringDB.delete(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +117,7 @@ class DBStringsValue(
|
|||||||
thisRef.stringDB.put(name, value)
|
thisRef.stringDB.put(name, value)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
GlobalScope.launch {
|
thisRef.coroutineScope.launch {
|
||||||
thisRef.stringDB.put(name, value)
|
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.Const
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
import com.topjohnwu.magisk.core.data.SuLogDao
|
||||||
|
import com.topjohnwu.magisk.core.ktx.await
|
||||||
import com.topjohnwu.magisk.core.model.su.SuLog
|
import com.topjohnwu.magisk.core.model.su.SuLog
|
||||||
import com.topjohnwu.magisk.data.database.SuLogDao
|
|
||||||
import com.topjohnwu.magisk.ktx.await
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
|
|
||||||
|
|
@@ -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
|
||||||
import com.topjohnwu.magisk.core.Config.Value.BETA_CHANNEL
|
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.CANARY_CHANNEL
|
||||||
import com.topjohnwu.magisk.core.Config.Value.CUSTOM_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.DEFAULT_CHANNEL
|
||||||
import com.topjohnwu.magisk.core.Config.Value.STABLE_CHANNEL
|
import com.topjohnwu.magisk.core.Config.Value.STABLE_CHANNEL
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.data.network.GithubApiServices
|
import com.topjohnwu.magisk.core.data.GithubPageServices
|
||||||
import com.topjohnwu.magisk.data.network.GithubPageServices
|
import com.topjohnwu.magisk.core.data.RawServices
|
||||||
import com.topjohnwu.magisk.data.network.RawServices
|
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class NetworkService(
|
class NetworkService(
|
||||||
private val pages: GithubPageServices,
|
private val pages: GithubPageServices,
|
||||||
private val raw: RawServices,
|
private val raw: RawServices
|
||||||
private val api: GithubApiServices
|
|
||||||
) {
|
) {
|
||||||
suspend fun fetchUpdate() = safe {
|
suspend fun fetchUpdate() = safe {
|
||||||
var info = when (Config.updateChannel) {
|
var info = when (Config.updateChannel) {
|
||||||
DEFAULT_CHANNEL, STABLE_CHANNEL -> fetchStableUpdate()
|
DEFAULT_CHANNEL, STABLE_CHANNEL -> fetchStableUpdate()
|
||||||
BETA_CHANNEL -> fetchBetaUpdate()
|
BETA_CHANNEL -> fetchBetaUpdate()
|
||||||
CANARY_CHANNEL -> fetchCanaryUpdate()
|
CANARY_CHANNEL -> fetchCanaryUpdate()
|
||||||
|
DEBUG_CHANNEL -> fetchDebugUpdate()
|
||||||
CUSTOM_CHANNEL -> fetchCustomUpdate(Config.customChannelUrl)
|
CUSTOM_CHANNEL -> fetchCustomUpdate(Config.customChannelUrl)
|
||||||
else -> throw IllegalArgumentException()
|
else -> throw IllegalArgumentException()
|
||||||
}
|
}
|
||||||
if (info.magisk.versionCode < Info.env.versionCode &&
|
if (info.magisk.versionCode < Info.env.versionCode &&
|
||||||
Config.updateChannel == DEFAULT_CHANNEL
|
Config.updateChannel == DEFAULT_CHANNEL) {
|
||||||
) {
|
|
||||||
Config.updateChannel = BETA_CHANNEL
|
Config.updateChannel = BETA_CHANNEL
|
||||||
info = fetchBetaUpdate()
|
info = fetchBetaUpdate()
|
||||||
}
|
}
|
||||||
@@ -40,11 +39,15 @@ class NetworkService(
|
|||||||
private suspend fun fetchStableUpdate() = pages.fetchUpdateJSON("stable.json")
|
private suspend fun fetchStableUpdate() = pages.fetchUpdateJSON("stable.json")
|
||||||
private suspend fun fetchBetaUpdate() = pages.fetchUpdateJSON("beta.json")
|
private suspend fun fetchBetaUpdate() = pages.fetchUpdateJSON("beta.json")
|
||||||
private suspend fun fetchCanaryUpdate() = pages.fetchUpdateJSON("canary.json")
|
private suspend fun fetchCanaryUpdate() = pages.fetchUpdateJSON("canary.json")
|
||||||
private suspend fun 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? {
|
private inline fun <T> safe(factory: () -> T): T? {
|
||||||
return try {
|
return try {
|
||||||
factory()
|
if (Info.isConnected.value == true)
|
||||||
|
factory()
|
||||||
|
else
|
||||||
|
null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
null
|
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 android.content.SharedPreferences
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import kotlin.properties.ReadWriteProperty
|
import kotlin.properties.ReadWriteProperty
|
||||||
import kotlin.reflect.KProperty
|
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: Boolean) = putBoolean(name, value)
|
||||||
fun SharedPreferences.Editor.put(name: String, value: Float) = putFloat(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 name: String,
|
||||||
private val default: Boolean,
|
private val default: Boolean,
|
||||||
private val commit: Boolean
|
private val commit: Boolean
|
||||||
) : Property(), ReadWriteProperty<PreferenceModel, Boolean> {
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Boolean> {
|
||||||
|
|
||||||
override operator fun getValue(
|
override operator fun getValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>
|
property: KProperty<*>
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val prefName = name.ifBlank { property.name }
|
val prefName = name.ifBlank { property.name }
|
||||||
@@ -38,7 +100,7 @@ class BooleanProperty(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override operator fun setValue(
|
override operator fun setValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>,
|
property: KProperty<*>,
|
||||||
value: Boolean
|
value: Boolean
|
||||||
) {
|
) {
|
||||||
@@ -51,10 +113,10 @@ class FloatProperty(
|
|||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: Float,
|
private val default: Float,
|
||||||
private val commit: Boolean
|
private val commit: Boolean
|
||||||
) : Property(), ReadWriteProperty<PreferenceModel, Float> {
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Float> {
|
||||||
|
|
||||||
override operator fun getValue(
|
override operator fun getValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>
|
property: KProperty<*>
|
||||||
): Float {
|
): Float {
|
||||||
val prefName = name.ifBlank { property.name }
|
val prefName = name.ifBlank { property.name }
|
||||||
@@ -62,7 +124,7 @@ class FloatProperty(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override operator fun setValue(
|
override operator fun setValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>,
|
property: KProperty<*>,
|
||||||
value: Float
|
value: Float
|
||||||
) {
|
) {
|
||||||
@@ -75,10 +137,10 @@ class IntProperty(
|
|||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: Int,
|
private val default: Int,
|
||||||
private val commit: Boolean
|
private val commit: Boolean
|
||||||
) : Property(), ReadWriteProperty<PreferenceModel, Int> {
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Int> {
|
||||||
|
|
||||||
override operator fun getValue(
|
override operator fun getValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>
|
property: KProperty<*>
|
||||||
): Int {
|
): Int {
|
||||||
val prefName = name.ifBlank { property.name }
|
val prefName = name.ifBlank { property.name }
|
||||||
@@ -86,7 +148,7 @@ class IntProperty(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override operator fun setValue(
|
override operator fun setValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>,
|
property: KProperty<*>,
|
||||||
value: Int
|
value: Int
|
||||||
) {
|
) {
|
||||||
@@ -99,10 +161,10 @@ class LongProperty(
|
|||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: Long,
|
private val default: Long,
|
||||||
private val commit: Boolean
|
private val commit: Boolean
|
||||||
) : Property(), ReadWriteProperty<PreferenceModel, Long> {
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Long> {
|
||||||
|
|
||||||
override operator fun getValue(
|
override operator fun getValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>
|
property: KProperty<*>
|
||||||
): Long {
|
): Long {
|
||||||
val prefName = name.ifBlank { property.name }
|
val prefName = name.ifBlank { property.name }
|
||||||
@@ -110,7 +172,7 @@ class LongProperty(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override operator fun setValue(
|
override operator fun setValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>,
|
property: KProperty<*>,
|
||||||
value: Long
|
value: Long
|
||||||
) {
|
) {
|
||||||
@@ -123,10 +185,10 @@ class StringProperty(
|
|||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: String,
|
private val default: String,
|
||||||
private val commit: Boolean
|
private val commit: Boolean
|
||||||
) : Property(), ReadWriteProperty<PreferenceModel, String> {
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, String> {
|
||||||
|
|
||||||
override operator fun getValue(
|
override operator fun getValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>
|
property: KProperty<*>
|
||||||
): String {
|
): String {
|
||||||
val prefName = name.ifBlank { property.name }
|
val prefName = name.ifBlank { property.name }
|
||||||
@@ -134,7 +196,7 @@ class StringProperty(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override operator fun setValue(
|
override operator fun setValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>,
|
property: KProperty<*>,
|
||||||
value: String
|
value: String
|
||||||
) {
|
) {
|
||||||
@@ -147,10 +209,10 @@ class StringSetProperty(
|
|||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: Set<String>,
|
private val default: Set<String>,
|
||||||
private val commit: Boolean
|
private val commit: Boolean
|
||||||
) : Property(), ReadWriteProperty<PreferenceModel, Set<String>> {
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Set<String>> {
|
||||||
|
|
||||||
override operator fun getValue(
|
override operator fun getValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>
|
property: KProperty<*>
|
||||||
): Set<String> {
|
): Set<String> {
|
||||||
val prefName = name.ifBlank { property.name }
|
val prefName = name.ifBlank { property.name }
|
||||||
@@ -158,7 +220,7 @@ class StringSetProperty(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override operator fun setValue(
|
override operator fun setValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>,
|
property: KProperty<*>,
|
||||||
value: Set<String>
|
value: Set<String>
|
||||||
) {
|
) {
|
@@ -6,13 +6,13 @@ import android.widget.Toast
|
|||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||||
|
import com.topjohnwu.magisk.core.ktx.getPackageInfo
|
||||||
|
import com.topjohnwu.magisk.core.ktx.toast
|
||||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||||
import com.topjohnwu.magisk.core.model.su.toPolicy
|
import com.topjohnwu.magisk.core.model.su.createSuLog
|
||||||
import com.topjohnwu.magisk.core.model.su.toUidPolicy
|
import kotlinx.coroutines.runBlocking
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
object SuCallbackHandler {
|
object SuCallbackHandler {
|
||||||
@@ -53,59 +53,50 @@ object SuCallbackHandler {
|
|||||||
private fun handleLogging(context: Context, data: Bundle) {
|
private fun handleLogging(context: Context, data: Bundle) {
|
||||||
val fromUid = data.getIntComp("from.uid", -1)
|
val fromUid = data.getIntComp("from.uid", -1)
|
||||||
val notify = data.getBoolean("notify", true)
|
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 pm = context.packageManager
|
||||||
|
|
||||||
val policy = runCatching {
|
val log = runCatching {
|
||||||
fromUid.toPolicy(pm, allow)
|
pm.getPackageInfo(fromUid, pid)?.let {
|
||||||
}.getOrElse {
|
pm.createSuLog(it, toUid, pid, command, policy, target, seContext, gids)
|
||||||
GlobalScope.launch { ServiceLocator.policyDB.delete(fromUid) }
|
}
|
||||||
fromUid.toUidPolicy(pm, allow)
|
}.getOrNull() ?: createSuLog(fromUid, toUid, pid, command, policy, target, seContext, gids)
|
||||||
}
|
|
||||||
|
|
||||||
if (notify)
|
if (notify)
|
||||||
notify(context, policy)
|
notify(context, log.action == SuPolicy.ALLOW, log.appName)
|
||||||
|
|
||||||
val toUid = data.getIntComp("to.uid", -1)
|
runBlocking { ServiceLocator.logRepo.insert(log) }
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleNotify(context: Context, data: Bundle) {
|
private fun handleNotify(context: Context, data: Bundle) {
|
||||||
val fromUid = data.getIntComp("from.uid", -1)
|
val uid = data.getIntComp("from.uid", -1)
|
||||||
val allow = data.getIntComp("policy", SuPolicy.ALLOW)
|
val pid = data.getIntComp("pid", -1)
|
||||||
|
val policy = data.getIntComp("policy", SuPolicy.ALLOW)
|
||||||
|
|
||||||
val pm = context.packageManager
|
val pm = context.packageManager
|
||||||
|
|
||||||
|
val appName = runCatching {
|
||||||
|
pm.getPackageInfo(uid, pid)?.applicationInfo?.getLabel(pm)
|
||||||
|
}.getOrNull() ?: "[UID] $uid"
|
||||||
|
|
||||||
val policy = runCatching {
|
notify(context, policy == SuPolicy.ALLOW, appName)
|
||||||
fromUid.toPolicy(pm, allow)
|
|
||||||
}.getOrElse {
|
|
||||||
GlobalScope.launch { ServiceLocator.policyDB.delete(fromUid) }
|
|
||||||
fromUid.toUidPolicy(pm, allow)
|
|
||||||
}
|
|
||||||
notify(context, policy)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notify(context: Context, policy: SuPolicy) {
|
private fun notify(context: Context, granted: Boolean, appName: String) {
|
||||||
if (Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
|
if (Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
|
||||||
val resId = if (policy.policy == SuPolicy.ALLOW)
|
val resId = if (granted)
|
||||||
R.string.su_allow_toast
|
R.string.su_allow_toast
|
||||||
else
|
else
|
||||||
R.string.su_deny_toast
|
R.string.su_deny_toast
|
||||||
|
|
||||||
Utils.toast(context.getString(resId, policy.appName), Toast.LENGTH_SHORT)
|
context.toast(context.getString(resId, appName), Toast.LENGTH_SHORT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,32 +1,31 @@
|
|||||||
package com.topjohnwu.magisk.core.su
|
package com.topjohnwu.magisk.core.su
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
||||||
|
import com.topjohnwu.magisk.core.ktx.getPackageInfo
|
||||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||||
import com.topjohnwu.magisk.core.model.su.toPolicy
|
|
||||||
import com.topjohnwu.magisk.ktx.now
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.Closeable
|
|
||||||
import java.io.DataOutputStream
|
import java.io.DataOutputStream
|
||||||
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class SuRequestHandler(
|
class SuRequestHandler(
|
||||||
private val pm: PackageManager,
|
val pm: PackageManager,
|
||||||
private val policyDB: PolicyDao
|
private val policyDB: PolicyDao
|
||||||
) : Closeable {
|
) {
|
||||||
|
|
||||||
private lateinit var output: DataOutputStream
|
private lateinit var output: File
|
||||||
lateinit var policy: SuPolicy
|
private lateinit var policy: SuPolicy
|
||||||
|
lateinit var pkgInfo: PackageInfo
|
||||||
private set
|
private set
|
||||||
|
|
||||||
// Return true to indicate undetermined policy, require user interaction
|
// Return true to indicate undetermined policy, require user interaction
|
||||||
@@ -35,8 +34,8 @@ class SuRequestHandler(
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
// Never allow com.topjohnwu.magisk (could be malware)
|
// Never allow com.topjohnwu.magisk (could be malware)
|
||||||
if (policy.packageName == BuildConfig.APPLICATION_ID) {
|
if (pkgInfo.packageName == BuildConfig.APPLICATION_ID) {
|
||||||
Shell.cmd("(pm uninstall ${BuildConfig.APPLICATION_ID})& >/dev/null 2>&1").exec()
|
Shell.cmd("(pm uninstall ${BuildConfig.APPLICATION_ID} >/dev/null 2>&1)&").exec()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,52 +53,50 @@ class SuRequestHandler(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
private suspend fun init(intent: Intent): Boolean {
|
||||||
override fun close() {
|
val uid = intent.getIntExtra("uid", -1)
|
||||||
if (::output.isInitialized)
|
val pid = intent.getIntExtra("pid", -1)
|
||||||
output.close()
|
val fifo = intent.getStringExtra("fifo")
|
||||||
}
|
if (uid <= 0 || pid <= 0 || fifo == null) {
|
||||||
|
return false
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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)
|
val until = if (time > 0)
|
||||||
TimeUnit.MILLISECONDS.toSeconds(now) + TimeUnit.MINUTES.toSeconds(time.toLong())
|
TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) +
|
||||||
|
TimeUnit.MINUTES.toSeconds(time.toLong())
|
||||||
else
|
else
|
||||||
time.toLong()
|
time.toLong()
|
||||||
|
|
||||||
policy.policy = action
|
policy.policy = action
|
||||||
policy.until = until
|
policy.until = until
|
||||||
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
output.writeInt(policy.policy)
|
DataOutputStream(FileOutputStream(output)).use {
|
||||||
output.flush()
|
it.writeInt(policy.policy)
|
||||||
|
it.flush()
|
||||||
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
} finally {
|
}
|
||||||
runCatching { close() }
|
if (until >= 0) {
|
||||||
if (until >= 0)
|
policyDB.update(policy)
|
||||||
policyDB.update(policy)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,11 +3,11 @@ package com.topjohnwu.magisk.core.tasks
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
|
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||||
import com.topjohnwu.magisk.core.utils.unzip
|
import com.topjohnwu.magisk.core.utils.unzip
|
||||||
import com.topjohnwu.magisk.di.AppContext
|
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@@ -4,30 +4,32 @@ import android.app.Activity
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.StubApk
|
import com.topjohnwu.magisk.StubApk
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.Info
|
|
||||||
import com.topjohnwu.magisk.core.Provider
|
import com.topjohnwu.magisk.core.Provider
|
||||||
|
import com.topjohnwu.magisk.core.ktx.await
|
||||||
|
import com.topjohnwu.magisk.core.ktx.toast
|
||||||
|
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||||
import com.topjohnwu.magisk.core.utils.AXML
|
import com.topjohnwu.magisk.core.utils.AXML
|
||||||
import com.topjohnwu.magisk.core.utils.Keygen
|
import com.topjohnwu.magisk.core.utils.Keygen
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
|
||||||
import com.topjohnwu.magisk.signing.JarMap
|
import com.topjohnwu.magisk.signing.JarMap
|
||||||
import com.topjohnwu.magisk.signing.SignApk
|
import com.topjohnwu.magisk.signing.SignApk
|
||||||
import com.topjohnwu.magisk.utils.APKInstall
|
import com.topjohnwu.magisk.utils.APKInstall
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Runnable
|
import kotlinx.coroutines.Runnable
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
|
import kotlin.random.asKotlinRandom
|
||||||
|
|
||||||
object HideAPK {
|
object HideAPK {
|
||||||
|
|
||||||
@@ -37,8 +39,7 @@ object HideAPK {
|
|||||||
|
|
||||||
// Some arbitrary limit
|
// Some arbitrary limit
|
||||||
const val MAX_LABEL_LENGTH = 32
|
const val MAX_LABEL_LENGTH = 32
|
||||||
|
const val PLACEHOLDER = "COMPONENT_PLACEHOLDER"
|
||||||
private val svc get() = ServiceLocator.networkService
|
|
||||||
|
|
||||||
private fun genPackageName(): String {
|
private fun genPackageName(): String {
|
||||||
val random = SecureRandom()
|
val random = SecureRandom()
|
||||||
@@ -63,24 +64,91 @@ object HideAPK {
|
|||||||
return builder.toString()
|
return builder.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun patch(
|
private fun classNameGenerator() = sequence {
|
||||||
|
val c1 = mutableListOf<String>()
|
||||||
|
val c2 = mutableListOf<String>()
|
||||||
|
val c3 = mutableListOf<String>()
|
||||||
|
val random = SecureRandom()
|
||||||
|
val kRandom = random.asKotlinRandom()
|
||||||
|
|
||||||
|
fun <T> chain(vararg iters: Iterable<T>) = sequence {
|
||||||
|
iters.forEach { it.forEach { v -> yield(v) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
for (a in chain('a'..'z', 'A'..'Z')) {
|
||||||
|
if (a != 'a' && a != 'A') {
|
||||||
|
c1.add("$a")
|
||||||
|
}
|
||||||
|
for (b in chain('a'..'z', 'A'..'Z', '0'..'9')) {
|
||||||
|
c2.add("$a$b")
|
||||||
|
for (c in chain('a'..'z', 'A'..'Z', '0'..'9')) {
|
||||||
|
c3.add("$a$b$c")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c1.shuffle(random)
|
||||||
|
c2.shuffle(random)
|
||||||
|
c3.shuffle(random)
|
||||||
|
|
||||||
|
fun notJavaKeyword(name: String) = when (name) {
|
||||||
|
"do", "if", "for", "int", "new", "try" -> false
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<String>.process() = asSequence().filter(::notJavaKeyword)
|
||||||
|
|
||||||
|
val names = mutableListOf<String>()
|
||||||
|
names.addAll(c1)
|
||||||
|
names.addAll(c2.process().take(30))
|
||||||
|
names.addAll(c3.process().take(30))
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val seg = 2 + random.nextInt(4)
|
||||||
|
val cls = StringBuilder()
|
||||||
|
for (i in 0 until seg) {
|
||||||
|
cls.append(names.random(kRandom))
|
||||||
|
if (i != seg - 1)
|
||||||
|
cls.append('.')
|
||||||
|
}
|
||||||
|
// Old Android does not support capitalized package names
|
||||||
|
// Check Android 7.0.0 PackageParser#buildClassName
|
||||||
|
cls[0] = cls[0].lowercaseChar()
|
||||||
|
yield(cls.toString())
|
||||||
|
}
|
||||||
|
}.distinct().iterator()
|
||||||
|
|
||||||
|
private fun patch(
|
||||||
context: Context,
|
context: Context,
|
||||||
apk: File, out: OutputStream,
|
apk: File, out: OutputStream,
|
||||||
pkg: String, label: CharSequence
|
pkg: String, label: CharSequence
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val info = context.packageManager.getPackageArchiveInfo(apk.path, 0) ?: return false
|
val info = context.packageManager.getPackageArchiveInfo(apk.path, 0) ?: return false
|
||||||
val name = info.applicationInfo.nonLocalizedLabel.toString()
|
val origLabel = info.applicationInfo.nonLocalizedLabel.toString()
|
||||||
try {
|
try {
|
||||||
JarMap.open(apk, true).use { jar ->
|
JarMap.open(apk, true).use { jar ->
|
||||||
val je = jar.getJarEntry(ANDROID_MANIFEST)
|
val je = jar.getJarEntry(ANDROID_MANIFEST)
|
||||||
val xml = AXML(jar.getRawData(je))
|
val xml = AXML(jar.getRawData(je))
|
||||||
|
val generator = classNameGenerator()
|
||||||
|
|
||||||
if (!xml.findAndPatch(APPLICATION_ID to pkg, name to label.toString()))
|
if (!xml.patchStrings {
|
||||||
|
for (i in it.indices) {
|
||||||
|
val s = it[i]
|
||||||
|
if (s.contains(APPLICATION_ID)) {
|
||||||
|
it[i] = s.replace(APPLICATION_ID, pkg)
|
||||||
|
} else if (s.contains(PLACEHOLDER)) {
|
||||||
|
it[i] = generator.next()
|
||||||
|
} else if (s == origLabel) {
|
||||||
|
it[i] = label.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
return false
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Write apk changes
|
// Write apk changes
|
||||||
jar.getOutputStream(je).use { it.write(xml.bytes) }
|
jar.getOutputStream(je).use { it.write(xml.bytes) }
|
||||||
val keys = Keygen(context)
|
val keys = Keygen()
|
||||||
SignApk.sign(keys.cert, keys.key, jar, out)
|
SignApk.sign(keys.cert, keys.key, jar, out)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -92,41 +160,42 @@ object HideAPK {
|
|||||||
|
|
||||||
private fun launchApp(activity: Activity, pkg: String) {
|
private fun launchApp(activity: Activity, pkg: String) {
|
||||||
val intent = activity.packageManager.getLaunchIntentForPackage(pkg) ?: return
|
val intent = activity.packageManager.getLaunchIntentForPackage(pkg) ?: return
|
||||||
Config.suManager = if (pkg == APPLICATION_ID) "" else pkg
|
|
||||||
val self = activity.packageName
|
val self = activity.packageName
|
||||||
val flag = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
val flag = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
activity.grantUriPermission(pkg, Provider.APK_URI(self), flag)
|
activity.grantUriPermission(pkg, Provider.preferencesUri(self), flag)
|
||||||
activity.grantUriPermission(pkg, Provider.PREFS_URI(self), flag)
|
|
||||||
intent.putExtra(Const.Key.PREV_PKG, self)
|
intent.putExtra(Const.Key.PREV_PKG, self)
|
||||||
activity.startActivity(intent)
|
activity.startActivity(intent)
|
||||||
activity.finish()
|
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")
|
val stub = File(activity.cacheDir, "stub.apk")
|
||||||
try {
|
try {
|
||||||
svc.fetchFile(Info.remote.stub.link).byteStream().writeTo(stub)
|
activity.assets.open("stub.apk").writeTo(stub)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
stub.createNewFile()
|
return false
|
||||||
val cmd = "\$MAGISKBIN/magiskinit -x manager ${stub.path}"
|
|
||||||
if (!Shell.cmd(cmd).exec().isSuccess)
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a new random package name and signature
|
// Generate a new random package name and signature
|
||||||
|
val repack = File(activity.cacheDir, "patched.apk")
|
||||||
val pkg = genPackageName()
|
val pkg = genPackageName()
|
||||||
Config.keyStoreRaw = ""
|
Config.keyStoreRaw = ""
|
||||||
|
|
||||||
|
if (!patch(activity, stub, FileOutputStream(repack), pkg, label))
|
||||||
|
return false
|
||||||
|
|
||||||
// Install and auto launch app
|
// Install and auto launch app
|
||||||
val session = APKInstall.startSession(activity, pkg, onFailure) {
|
val session = APKInstall.startSession(activity, pkg, onFailure) {
|
||||||
launchApp(activity, pkg)
|
launchApp(activity, pkg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Config.suManager = pkg
|
||||||
|
val cmd = "adb_pm_install $repack $pkg"
|
||||||
|
if (Shell.cmd(cmd).exec().isSuccess) return true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val success = session.openStream(activity).use {
|
session.install(activity, repack)
|
||||||
patch(activity, stub, it, pkg, label)
|
|
||||||
}
|
|
||||||
if (!success) return false
|
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
return false
|
return false
|
||||||
@@ -145,7 +214,7 @@ object HideAPK {
|
|||||||
}
|
}
|
||||||
val onFailure = Runnable {
|
val onFailure = Runnable {
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
Utils.toast(R.string.failure, Toast.LENGTH_LONG)
|
activity.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||||
}
|
}
|
||||||
val success = withContext(Dispatchers.IO) {
|
val success = withContext(Dispatchers.IO) {
|
||||||
patchAndHide(activity, label, onFailure)
|
patchAndHide(activity, label, onFailure)
|
||||||
@@ -163,13 +232,16 @@ object HideAPK {
|
|||||||
}
|
}
|
||||||
val onFailure = Runnable {
|
val onFailure = Runnable {
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
Utils.toast(R.string.failure, Toast.LENGTH_LONG)
|
activity.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||||
}
|
}
|
||||||
val apk = StubApk.current(activity)
|
val apk = StubApk.current(activity)
|
||||||
val session = APKInstall.startSession(activity, APPLICATION_ID, onFailure) {
|
val session = APKInstall.startSession(activity, APPLICATION_ID, onFailure) {
|
||||||
launchApp(activity, APPLICATION_ID)
|
launchApp(activity, APPLICATION_ID)
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
|
Config.suManager = ""
|
||||||
|
val cmd = "adb_pm_install $apk $APPLICATION_ID"
|
||||||
|
if (Shell.cmd(cmd).await().isSuccess) return
|
||||||
val success = withContext(Dispatchers.IO) {
|
val success = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
session.install(activity, apk)
|
session.install(activity, apk)
|
||||||
@@ -182,4 +254,17 @@ object HideAPK {
|
|||||||
}
|
}
|
||||||
if (!success) onFailure.run()
|
if (!success) onFailure.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
fun upgrade(context: Context, apk: File): Intent? {
|
||||||
|
val label = context.applicationInfo.nonLocalizedLabel
|
||||||
|
val pkg = context.packageName
|
||||||
|
val session = APKInstall.startSession(context)
|
||||||
|
session.openStream(context).use {
|
||||||
|
if (!patch(context, apk, it, pkg, label)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return session.waitIntent()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,30 +1,36 @@
|
|||||||
package com.topjohnwu.magisk.core.tasks
|
package com.topjohnwu.magisk.core.tasks
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.system.ErrnoException
|
||||||
import android.system.Os
|
import android.system.Os
|
||||||
|
import android.system.OsConstants
|
||||||
|
import android.system.OsConstants.O_WRONLY
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.core.os.postDelayed
|
import androidx.core.os.postDelayed
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.StubApk
|
import com.topjohnwu.magisk.StubApk
|
||||||
import com.topjohnwu.magisk.core.*
|
import com.topjohnwu.magisk.core.AppApkPath
|
||||||
|
import com.topjohnwu.magisk.core.Config
|
||||||
|
import com.topjohnwu.magisk.core.Const
|
||||||
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
|
import com.topjohnwu.magisk.core.ktx.copyAndClose
|
||||||
|
import com.topjohnwu.magisk.core.ktx.reboot
|
||||||
|
import com.topjohnwu.magisk.core.ktx.toast
|
||||||
|
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||||
import com.topjohnwu.magisk.ktx.reboot
|
|
||||||
import com.topjohnwu.magisk.ktx.withStreams
|
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
|
||||||
import com.topjohnwu.magisk.signing.SignBoot
|
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.ShellUtils
|
import com.topjohnwu.superuser.ShellUtils
|
||||||
import com.topjohnwu.superuser.internal.NOPList
|
import com.topjohnwu.superuser.internal.NOPList
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
import com.topjohnwu.superuser.io.SuFile
|
import com.topjohnwu.superuser.nio.ExtendedFile
|
||||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
import com.topjohnwu.superuser.nio.FileSystemManager
|
||||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import net.jpountz.lz4.LZ4FrameInputStream
|
import net.jpountz.lz4.LZ4FrameInputStream
|
||||||
@@ -37,29 +43,34 @@ import java.io.*
|
|||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
abstract class MagiskInstallImpl protected constructor(
|
abstract class MagiskInstallImpl protected constructor(
|
||||||
protected val console: MutableList<String> = NOPList.getInstance(),
|
protected val console: MutableList<String> = NOPList.getInstance(),
|
||||||
private val logs: MutableList<String> = NOPList.getInstance()
|
private val logs: MutableList<String> = NOPList.getInstance()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
protected var installDir = File("xxx")
|
protected lateinit var installDir: ExtendedFile
|
||||||
private lateinit var srcBoot: File
|
private lateinit var srcBoot: ExtendedFile
|
||||||
|
|
||||||
private val shell = Shell.getShell()
|
private val shell = Shell.getShell()
|
||||||
private val service get() = ServiceLocator.networkService
|
private val service get() = ServiceLocator.networkService
|
||||||
protected val context get() = ServiceLocator.deContext
|
protected val context get() = ServiceLocator.deContext
|
||||||
private val useRootDir = shell.isRoot && Info.noDataExec
|
private val useRootDir = shell.isRoot && Info.noDataExec
|
||||||
|
|
||||||
|
private val rootFS get() = RootUtils.fs
|
||||||
|
private val localFS get() = FileSystemManager.getLocal()
|
||||||
|
|
||||||
private fun findImage(): Boolean {
|
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()) {
|
if (bootPath.isEmpty()) {
|
||||||
console.add("! Unable to detect target image")
|
console.add("! Unable to detect target image")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
srcBoot = SuFile(bootPath)
|
srcBoot = rootFS.getFile(bootPath)
|
||||||
console.add("- Target image: $bootPath")
|
console.add("- Target image: $bootPath")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -77,7 +88,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
console.add("! Unable to detect target image")
|
console.add("! Unable to detect target image")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
srcBoot = SuFile(bootPath)
|
srcBoot = rootFS.getFile(bootPath)
|
||||||
console.add("- Target image: $bootPath")
|
console.add("- Target image: $bootPath")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -86,7 +97,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
console.add("- Device platform: ${Const.CPU_ABI}")
|
console.add("- Device platform: ${Const.CPU_ABI}")
|
||||||
console.add("- Installing: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
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.deleteRecursively()
|
||||||
installDir.mkdirs()
|
installDir.mkdirs()
|
||||||
|
|
||||||
@@ -107,7 +118,9 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
val name = n.substring(3, n.length - 3)
|
val name = n.substring(3, n.length - 3)
|
||||||
val dest = File(installDir, name)
|
val dest = File(installDir, name)
|
||||||
zf.getInputStream(it).writeTo(dest)
|
zf.getInputStream(it).writeTo(dest)
|
||||||
|
dest.setExecutable(true)
|
||||||
}
|
}
|
||||||
|
zf.close()
|
||||||
} else {
|
} else {
|
||||||
val info = context.applicationInfo
|
val info = context.applicationInfo
|
||||||
var libs = File(info.nativeLibraryDir).listFiles { _, name ->
|
var libs = File(info.nativeLibraryDir).listFiles { _, name ->
|
||||||
@@ -115,7 +128,8 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
} ?: emptyArray()
|
} ?: emptyArray()
|
||||||
|
|
||||||
// Also symlink magisk32 on non 64-bit only 64-bit devices
|
// Also symlink magisk32 on non 64-bit only 64-bit devices
|
||||||
val lib32 = info.javaClass.getDeclaredField("secondaryNativeLibraryDir").get(info) as String?
|
val lib32 = info.javaClass.getDeclaredField("secondaryNativeLibraryDir")
|
||||||
|
.get(info) as String?
|
||||||
if (lib32 != null) {
|
if (lib32 != null) {
|
||||||
libs += File(lib32, "libmagisk32.so")
|
libs += File(lib32, "libmagisk32.so")
|
||||||
}
|
}
|
||||||
@@ -127,7 +141,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract scripts
|
// Extract scripts
|
||||||
for (script in listOf("util_functions.sh", "boot_patch.sh", "addon.d.sh")) {
|
for (script in listOf("util_functions.sh", "boot_patch.sh", "addon.d.sh", "stub.apk")) {
|
||||||
val dest = File(installDir, script)
|
val dest = File(installDir, script)
|
||||||
context.assets.open(script).writeTo(dest)
|
context.assets.open(script).writeTo(dest)
|
||||||
}
|
}
|
||||||
@@ -146,7 +160,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
|
|
||||||
if (useRootDir) {
|
if (useRootDir) {
|
||||||
// Move everything to tmpfs to workaround Samsung bullshit
|
// Move everything to tmpfs to workaround Samsung bullshit
|
||||||
SuFile(Const.TMPDIR).also {
|
rootFS.getFile(Const.TMPDIR).also {
|
||||||
arrayOf(
|
arrayOf(
|
||||||
"rm -rf $it",
|
"rm -rf $it",
|
||||||
"mkdir -p $it",
|
"mkdir -p $it",
|
||||||
@@ -160,105 +174,215 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optimization for SuFile I/O streams to skip an internal trial and error
|
private fun InputStream.copyAndCloseOut(out: OutputStream) = out.use { copyTo(it) }
|
||||||
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 newTarEntry(name: String, size: Long): TarEntry {
|
private fun newTarEntry(name: String, size: Long): TarEntry {
|
||||||
console.add("-- Writing: $name")
|
console.add("-- Writing: $name")
|
||||||
return TarEntry(TarHeader.createHeader(name, size, 0, false, 420 /* 0644 */))
|
return TarEntry(TarHeader.createHeader(name, size, 0, false, 420 /* 0644 */))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class LZ4InputStream(s: InputStream) : LZ4FrameInputStream(s) {
|
||||||
|
// Workaround bug in LZ4FrameInputStream
|
||||||
|
override fun available() = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private class NoBootException : IOException()
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun processTar(input: InputStream, output: OutputStream): OutputStream {
|
private fun processTar(tarIn: TarInputStream, tarOut: TarOutputStream): ExtendedFile {
|
||||||
console.add("- Processing tar file")
|
console.add("- Processing tar file")
|
||||||
val tarOut = TarOutputStream(output)
|
lateinit var entry: TarEntry
|
||||||
TarInputStream(input).use { tarIn ->
|
|
||||||
lateinit var entry: TarEntry
|
|
||||||
|
|
||||||
fun decompressedStream(): InputStream {
|
fun decompressedStream(): InputStream {
|
||||||
val src = if (entry.name.endsWith(".lz4")) LZ4FrameInputStream(tarIn) else tarIn
|
return if (entry.name.endsWith(".lz4")) LZ4InputStream(tarIn) else tarIn
|
||||||
return object : FilterInputStream(src) {
|
}
|
||||||
override fun available() = 0 /* Workaround bug in LZ4FrameInputStream */
|
|
||||||
override fun close() { /* Never close src stream */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (tarIn.nextEntry?.let { entry = it } != null) {
|
while (tarIn.nextEntry?.let { entry = it } != null) {
|
||||||
if (entry.name.startsWith("boot.img") ||
|
if (entry.name.startsWith("boot.img") ||
|
||||||
(Config.recovery && entry.name.contains("recovery.img"))) {
|
entry.name.startsWith("init_boot.img") ||
|
||||||
val name = entry.name.replace(".lz4", "")
|
(Config.recovery && entry.name.contains("recovery.img"))) {
|
||||||
console.add("-- Extracting: $name")
|
val name = entry.name.replace(".lz4", "")
|
||||||
|
console.add("-- Extracting: $name")
|
||||||
|
|
||||||
val extract = installDirFile(name)
|
val extract = installDir.getChildFile(name)
|
||||||
decompressedStream().cleanPump(SuFileOutputStream.open(extract))
|
decompressedStream().copyAndCloseOut(extract.newOutputStream())
|
||||||
} else if (entry.name.contains("vbmeta.img")) {
|
} else if (entry.name.contains("vbmeta.img")) {
|
||||||
val rawData = decompressedStream().readBytes()
|
val rawData = decompressedStream().readBytes()
|
||||||
// Valid vbmeta.img should be at least 256 bytes
|
// Valid vbmeta.img should be at least 256 bytes
|
||||||
if (rawData.size < 256)
|
if (rawData.size < 256)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
// Patch flags to AVB_VBMETA_IMAGE_FLAGS_HASHTREE_DISABLED |
|
// Patch flags to AVB_VBMETA_IMAGE_FLAGS_HASHTREE_DISABLED |
|
||||||
// AVB_VBMETA_IMAGE_FLAGS_VERIFICATION_DISABLED
|
// AVB_VBMETA_IMAGE_FLAGS_VERIFICATION_DISABLED
|
||||||
console.add("-- Patching: vbmeta.img")
|
console.add("-- Patching: vbmeta.img")
|
||||||
ByteBuffer.wrap(rawData).putInt(120, 3)
|
ByteBuffer.wrap(rawData).putInt(120, 3)
|
||||||
tarOut.putNextEntry(newTarEntry("vbmeta.img", rawData.size.toLong()))
|
tarOut.putNextEntry(newTarEntry("vbmeta.img", rawData.size.toLong()))
|
||||||
tarOut.write(rawData)
|
tarOut.write(rawData)
|
||||||
} else {
|
// vbmeta partition exist, disable boot vbmeta patch
|
||||||
console.add("-- Copying: ${entry.name}")
|
Info.patchBootVbmeta = false
|
||||||
tarOut.putNextEntry(entry)
|
} else if (entry.name.contains("userdata.img")) {
|
||||||
tarIn.copyTo(tarOut, bufferSize = 1024 * 1024)
|
continue
|
||||||
}
|
} else {
|
||||||
|
console.add("-- Copying: ${entry.name}")
|
||||||
|
tarOut.putNextEntry(entry)
|
||||||
|
tarIn.copyTo(tarOut, bufferSize = 1024 * 1024)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val boot = installDirFile("boot.img")
|
|
||||||
val recovery = installDirFile("recovery.img")
|
val boot = installDir.getChildFile("boot.img")
|
||||||
if (Config.recovery && recovery.exists() && boot.exists()) {
|
val initBoot = installDir.getChildFile("init_boot.img")
|
||||||
// Install to recovery
|
val recovery = installDir.getChildFile("recovery.img")
|
||||||
srcBoot = recovery
|
|
||||||
// Repack boot image to prevent auto restore
|
fun ExtendedFile.copyToTar() {
|
||||||
arrayOf(
|
newInputStream().use {
|
||||||
"cd $installDir",
|
tarOut.putNextEntry(newTarEntry(name, length()))
|
||||||
"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()))
|
|
||||||
it.copyTo(tarOut)
|
it.copyTo(tarOut)
|
||||||
}
|
}
|
||||||
boot.delete()
|
delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch priority: recovery > init_boot > boot
|
||||||
|
return when {
|
||||||
|
recovery.exists() -> {
|
||||||
|
if (boot.exists()) {
|
||||||
|
// Repack boot image to prevent auto restore
|
||||||
|
arrayOf(
|
||||||
|
"cd $installDir",
|
||||||
|
"chmod -R 755 .",
|
||||||
|
"./magiskboot unpack boot.img",
|
||||||
|
"./magiskboot repack boot.img",
|
||||||
|
"cat new-boot.img > boot.img",
|
||||||
|
"./magiskboot cleanup",
|
||||||
|
"rm -f new-boot.img",
|
||||||
|
"cd /").sh()
|
||||||
|
boot.copyToTar()
|
||||||
|
}
|
||||||
|
recovery
|
||||||
|
}
|
||||||
|
initBoot.exists() -> {
|
||||||
|
if (boot.exists())
|
||||||
|
boot.copyToTar()
|
||||||
|
initBoot
|
||||||
|
}
|
||||||
|
boot.exists() -> boot
|
||||||
|
else -> throw NoBootException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun processZip(zipIn: ZipInputStream): ExtendedFile {
|
||||||
|
console.add("- Processing zip file")
|
||||||
|
val boot = installDir.getChildFile("boot.img")
|
||||||
|
val initBoot = installDir.getChildFile("init_boot.img")
|
||||||
|
lateinit var entry: ZipEntry
|
||||||
|
while (zipIn.nextEntry?.also { entry = it } != null) {
|
||||||
|
if (entry.isDirectory) continue
|
||||||
|
when (entry.name.substringAfterLast('/')) {
|
||||||
|
"payload.bin" -> {
|
||||||
|
try {
|
||||||
|
return processPayload(zipIn)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
// No boot image in payload.bin, continue to find boot images
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"init_boot.img" -> {
|
||||||
|
console.add("- Extracting init_boot.img")
|
||||||
|
zipIn.copyAndCloseOut(initBoot.newOutputStream())
|
||||||
|
return initBoot
|
||||||
|
}
|
||||||
|
"boot.img" -> {
|
||||||
|
console.add("- Extracting boot.img")
|
||||||
|
zipIn.copyAndCloseOut(boot.newOutputStream())
|
||||||
|
// Don't return here since there might be an init_boot.img
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (boot.exists()) {
|
||||||
|
return boot
|
||||||
} else {
|
} else {
|
||||||
if (!boot.exists()) {
|
throw NoBootException()
|
||||||
console.add("! No boot image found")
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun processPayload(input: InputStream): ExtendedFile {
|
||||||
|
var fifo: File? = null
|
||||||
|
try {
|
||||||
|
console.add("- Processing payload.bin")
|
||||||
|
fifo = File.createTempFile("payload-fifo-", null, installDir)
|
||||||
|
fifo.delete()
|
||||||
|
Os.mkfifo(fifo.path, 420 /* 0644 */)
|
||||||
|
|
||||||
|
// Enqueue the shell command first, or the subsequent FIFO open will block
|
||||||
|
val future = arrayOf(
|
||||||
|
"cd $installDir",
|
||||||
|
"./magiskboot extract $fifo",
|
||||||
|
"cd /"
|
||||||
|
).eq()
|
||||||
|
|
||||||
|
val fd = Os.open(fifo.path, O_WRONLY, 0)
|
||||||
|
try {
|
||||||
|
val bufSize = 1024 * 1024
|
||||||
|
val buf = ByteBuffer.allocate(bufSize)
|
||||||
|
buf.position(input.read(buf.array()).coerceAtLeast(0)).flip()
|
||||||
|
while (buf.hasRemaining()) {
|
||||||
|
try {
|
||||||
|
Os.write(fd, buf)
|
||||||
|
} catch (e: ErrnoException) {
|
||||||
|
if (e.errno != OsConstants.EPIPE)
|
||||||
|
throw e
|
||||||
|
// If SIGPIPE, then the other side is closed, we're done
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (!buf.hasRemaining()) {
|
||||||
|
buf.limit(bufSize)
|
||||||
|
buf.position(input.read(buf.array()).coerceAtLeast(0)).flip()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Os.close(fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
val success = try { future.get().isSuccess } catch (e: Exception) { false }
|
||||||
|
if (!success) {
|
||||||
|
console.add("! Error while extracting payload.bin")
|
||||||
throw IOException()
|
throw IOException()
|
||||||
}
|
}
|
||||||
srcBoot = boot
|
val boot = installDir.getChildFile("boot.img")
|
||||||
|
val initBoot = installDir.getChildFile("init_boot.img")
|
||||||
|
return when {
|
||||||
|
initBoot.exists() -> {
|
||||||
|
console.add("-- Extract init_boot.img")
|
||||||
|
initBoot
|
||||||
|
}
|
||||||
|
boot.exists() -> {
|
||||||
|
console.add("-- Extract boot.img")
|
||||||
|
boot
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw NoBootException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: ErrnoException) {
|
||||||
|
throw IOException(e)
|
||||||
|
} finally {
|
||||||
|
fifo?.delete()
|
||||||
}
|
}
|
||||||
return tarOut
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleFile(uri: Uri): Boolean {
|
private fun handleFile(uri: Uri): Boolean {
|
||||||
val outStream: OutputStream
|
val outStream: OutputStream
|
||||||
var outFile: MediaStoreUtils.UriFile? = null
|
val outFile: MediaStoreUtils.UriFile
|
||||||
|
|
||||||
// Process input file
|
// Process input file
|
||||||
try {
|
try {
|
||||||
uri.inputStream().buffered().use { src ->
|
uri.inputStream().buffered().use { src ->
|
||||||
src.mark(500)
|
src.mark(500)
|
||||||
val magic = ByteArray(5)
|
val magic = ByteArray(4)
|
||||||
if (src.skip(257) != 257L || src.read(magic) != magic.size) {
|
val tarMagic = ByteArray(5)
|
||||||
|
if (src.read(magic) != magic.size || src.skip(253) != 253L ||
|
||||||
|
src.read(tarMagic) != tarMagic.size
|
||||||
|
) {
|
||||||
console.add("! Invalid input file")
|
console.add("! Invalid input file")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -274,40 +398,69 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
toString()
|
toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
outStream = if (magic.contentEquals("ustar".toByteArray())) {
|
srcBoot = if (tarMagic.contentEquals("ustar".toByteArray())) {
|
||||||
// tar file
|
// tar file
|
||||||
outFile = MediaStoreUtils.getFile("$filename.tar", true)
|
outFile = MediaStoreUtils.getFile("$filename.tar", true)
|
||||||
processTar(src, outFile!!.uri.outputStream())
|
outStream = TarOutputStream(outFile.uri.outputStream())
|
||||||
|
|
||||||
|
try {
|
||||||
|
processTar(TarInputStream(src), outStream)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
outStream.close()
|
||||||
|
outFile.delete()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// raw image
|
// raw image
|
||||||
srcBoot = installDirFile("boot.img")
|
|
||||||
console.add("- Copying image to cache")
|
|
||||||
src.cleanPump(SuFileOutputStream.open(srcBoot))
|
|
||||||
outFile = MediaStoreUtils.getFile("$filename.img", true)
|
outFile = MediaStoreUtils.getFile("$filename.img", true)
|
||||||
outFile!!.uri.outputStream()
|
outStream = outFile.uri.outputStream()
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (magic.contentEquals("CrAU".toByteArray())) {
|
||||||
|
processPayload(src)
|
||||||
|
} else if (magic.contentEquals("PK\u0003\u0004".toByteArray())) {
|
||||||
|
processZip(ZipInputStream(src))
|
||||||
|
} else {
|
||||||
|
console.add("- Copying image to cache")
|
||||||
|
installDir.getChildFile("boot.img").also {
|
||||||
|
src.copyAndCloseOut(it.newOutputStream())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
outStream.close()
|
||||||
|
outFile.delete()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
|
if (e is NoBootException)
|
||||||
|
console.add("! No boot image found")
|
||||||
console.add("! Process error")
|
console.add("! Process error")
|
||||||
outFile?.delete()
|
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch file
|
// Patch file
|
||||||
if (!patchBoot()) {
|
if (!patchBoot()) {
|
||||||
outFile!!.delete()
|
outFile.delete()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output file
|
// Output file
|
||||||
try {
|
try {
|
||||||
val newBoot = installDirFile("new-boot.img")
|
val newBoot = installDir.getChildFile("new-boot.img")
|
||||||
if (outStream is TarOutputStream) {
|
if (outStream is TarOutputStream) {
|
||||||
val name = if (srcBoot.path.contains("recovery")) "recovery.img" else "boot.img"
|
val name = with(srcBoot.path) {
|
||||||
|
when {
|
||||||
|
contains("recovery") -> "recovery.img"
|
||||||
|
contains("init_boot") -> "init_boot.img"
|
||||||
|
else -> "boot.img"
|
||||||
|
}
|
||||||
|
}
|
||||||
outStream.putNextEntry(newTarEntry(name, newBoot.length()))
|
outStream.putNextEntry(newTarEntry(name, newBoot.length()))
|
||||||
}
|
}
|
||||||
SuFileInputStream.open(newBoot).cleanPump(outStream)
|
newBoot.newInputStream().copyAndClose(outStream)
|
||||||
newBoot.delete()
|
newBoot.delete()
|
||||||
|
|
||||||
console.add("")
|
console.add("")
|
||||||
@@ -317,7 +470,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
console.add("****************************")
|
console.add("****************************")
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
console.add("! Failed to output to $outFile")
|
console.add("! Failed to output to $outFile")
|
||||||
outFile!!.delete()
|
outFile.delete()
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -330,23 +483,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun patchBoot(): Boolean {
|
private fun patchBoot(): Boolean {
|
||||||
var isSigned = false
|
val newBoot = installDir.getChildFile("new-boot.img")
|
||||||
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")
|
|
||||||
if (!useRootDir) {
|
if (!useRootDir) {
|
||||||
// Create output files before hand
|
// Create output files before hand
|
||||||
newBoot.createNewFile()
|
newBoot.createNewFile()
|
||||||
@@ -357,33 +494,15 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
"cd $installDir",
|
"cd $installDir",
|
||||||
"KEEPFORCEENCRYPT=${Config.keepEnc} " +
|
"KEEPFORCEENCRYPT=${Config.keepEnc} " +
|
||||||
"KEEPVERITY=${Config.keepVerity} " +
|
"KEEPVERITY=${Config.keepVerity} " +
|
||||||
"PATCHVBMETAFLAG=${Config.patchVbmeta} " +
|
"PATCHVBMETAFLAG=${Info.patchBootVbmeta} " +
|
||||||
"RECOVERYMODE=${Config.recovery} " +
|
"RECOVERYMODE=${Config.recovery} " +
|
||||||
|
"LEGACYSAR=${Info.legacySAR} " +
|
||||||
"sh boot_patch.sh $srcBoot")
|
"sh boot_patch.sh $srcBoot")
|
||||||
|
val isSuccess = cmds.sh().isSuccess
|
||||||
|
|
||||||
if (!cmds.sh().isSuccess)
|
shell.newJob().add("./magiskboot cleanup", "cd /").exec()
|
||||||
return false
|
|
||||||
|
|
||||||
val job = shell.newJob().add("./magiskboot cleanup", "cd /")
|
return isSuccess
|
||||||
|
|
||||||
if (isSigned) {
|
|
||||||
console.add("- Signing boot image with verity keys")
|
|
||||||
val signed = File.createTempFile("signed", ".img", context.cacheDir)
|
|
||||||
try {
|
|
||||||
val src = 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun flashBoot() = "direct_install $installDir $srcBoot".sh().isSuccess
|
private fun flashBoot() = "direct_install $installDir $srcBoot".sh().isSuccess
|
||||||
@@ -405,16 +524,17 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Array<String>.eq() = shell.newJob().add(*this).to(console, logs).enqueue()
|
||||||
private fun String.sh() = shell.newJob().add(this).to(console, logs).exec()
|
private fun String.sh() = shell.newJob().add(this).to(console, logs).exec()
|
||||||
private fun Array<String>.sh() = shell.newJob().add(*this).to(console, logs).exec()
|
private fun Array<String>.sh() = shell.newJob().add(*this).to(console, logs).exec()
|
||||||
private fun String.fsh() = ShellUtils.fastCmd(shell, this)
|
private fun String.fsh() = ShellUtils.fastCmd(shell, this)
|
||||||
private fun Array<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 fun direct() = findImage() && extractFiles() && patchBoot() && flashBoot()
|
||||||
|
|
||||||
protected suspend fun secondSlot() =
|
protected fun secondSlot() =
|
||||||
findSecondary() && extractFiles() && patchBoot() && flashBoot() && postOTA()
|
findSecondary() && extractFiles() && patchBoot() && flashBoot() && postOTA()
|
||||||
|
|
||||||
protected fun fixEnv() = extractFiles() && "fix_env $installDir".sh().isSuccess
|
protected fun fixEnv() = extractFiles() && "fix_env $installDir".sh().isSuccess
|
||||||
@@ -425,20 +545,15 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
protected abstract suspend fun operations(): Boolean
|
protected abstract suspend fun operations(): Boolean
|
||||||
|
|
||||||
open suspend fun exec(): Boolean {
|
open suspend fun exec(): Boolean {
|
||||||
synchronized(Companion) {
|
if (haveActiveSession.getAndSet(true))
|
||||||
if (haveActiveSession)
|
return false
|
||||||
return false
|
|
||||||
haveActiveSession = true
|
|
||||||
}
|
|
||||||
val result = withContext(Dispatchers.IO) { operations() }
|
val result = withContext(Dispatchers.IO) { operations() }
|
||||||
synchronized(Companion) {
|
haveActiveSession.set(false)
|
||||||
haveActiveSession = false
|
|
||||||
}
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var haveActiveSession = false
|
private var haveActiveSession = AtomicBoolean(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,7 +578,7 @@ abstract class MagiskInstaller(
|
|||||||
console: MutableList<String>,
|
console: MutableList<String>,
|
||||||
logs: MutableList<String>
|
logs: MutableList<String>
|
||||||
) : MagiskInstaller(console, logs) {
|
) : MagiskInstaller(console, logs) {
|
||||||
override suspend fun operations() = doPatchFile(uri)
|
override suspend fun operations() = patchFile(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
class SecondSlot(
|
class SecondSlot(
|
||||||
@@ -510,7 +625,7 @@ abstract class MagiskInstaller(
|
|||||||
override suspend fun exec(): Boolean {
|
override suspend fun exec(): Boolean {
|
||||||
val success = super.exec()
|
val success = super.exec()
|
||||||
callback()
|
callback()
|
||||||
Utils.toast(
|
context.toast(
|
||||||
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
|
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
|
||||||
Toast.LENGTH_LONG
|
Toast.LENGTH_LONG
|
||||||
)
|
)
|
||||||
|
@@ -4,7 +4,6 @@ import java.io.ByteArrayOutputStream
|
|||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder.LITTLE_ENDIAN
|
import java.nio.ByteOrder.LITTLE_ENDIAN
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class AXML(b: ByteArray) {
|
class AXML(b: ByteArray) {
|
||||||
|
|
||||||
@@ -30,7 +29,7 @@ class AXML(b: ByteArray) {
|
|||||||
* Followed by an array of uint32_t with size = number of strings
|
* Followed by an array of uint32_t with size = number of strings
|
||||||
* Each entry points to an offset into the string data
|
* Each entry points to an offset into the string data
|
||||||
*/
|
*/
|
||||||
fun findAndPatch(vararg patterns: Pair<String, String>): Boolean {
|
fun patchStrings(patchFn: (Array<String>) -> Unit): Boolean {
|
||||||
val buffer = ByteBuffer.wrap(bytes).order(LITTLE_ENDIAN)
|
val buffer = ByteBuffer.wrap(bytes).order(LITTLE_ENDIAN)
|
||||||
|
|
||||||
fun findStringPool(): Int {
|
fun findStringPool(): Int {
|
||||||
@@ -43,7 +42,6 @@ class AXML(b: ByteArray) {
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
var patch = false
|
|
||||||
val start = findStringPool()
|
val start = findStringPool()
|
||||||
if (start < 0)
|
if (start < 0)
|
||||||
return false
|
return false
|
||||||
@@ -58,34 +56,26 @@ class AXML(b: ByteArray) {
|
|||||||
val dataOff = start + intBuf.get()
|
val dataOff = start + intBuf.get()
|
||||||
intBuf.get()
|
intBuf.get()
|
||||||
|
|
||||||
val strings = ArrayList<String>(count)
|
val strList = ArrayList<String>(count)
|
||||||
// Read and patch all strings
|
// Collect all strings in the pool
|
||||||
loop@ for (i in 0 until count) {
|
for (i in 0 until count) {
|
||||||
val off = dataOff + intBuf.get()
|
val off = dataOff + intBuf.get()
|
||||||
val len = buffer.getShort(off)
|
val len = buffer.getShort(off)
|
||||||
val str = String(bytes, off + 2, len * 2, UTF_16LE)
|
strList.add(String(bytes, off + 2, len * 2, UTF_16LE))
|
||||||
for ((from, to) in patterns) {
|
|
||||||
if (str.contains(from)) {
|
|
||||||
strings.add(str.replace(from, to))
|
|
||||||
patch = true
|
|
||||||
continue@loop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
strings.add(str)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!patch)
|
val strArr = strList.toTypedArray()
|
||||||
return false
|
patchFn(strArr)
|
||||||
|
|
||||||
// Write everything before string data, will patch values later
|
// Write everything before string data, will patch values later
|
||||||
val baos = RawByteStream()
|
val baos = RawByteStream()
|
||||||
baos.write(bytes, 0, dataOff)
|
baos.write(bytes, 0, dataOff)
|
||||||
|
|
||||||
// Write string data
|
// Write string data
|
||||||
val strList = IntArray(count)
|
val offList = IntArray(count)
|
||||||
for (i in 0 until count) {
|
for (i in 0 until count) {
|
||||||
strList[i] = baos.size() - dataOff
|
offList[i] = baos.size() - dataOff
|
||||||
val str = strings[i]
|
val str = strArr[i]
|
||||||
baos.write(str.length.toShortBytes())
|
baos.write(str.length.toShortBytes())
|
||||||
baos.write(str.toByteArray(UTF_16LE))
|
baos.write(str.toByteArray(UTF_16LE))
|
||||||
// Null terminate
|
// Null terminate
|
||||||
@@ -104,7 +94,7 @@ class AXML(b: ByteArray) {
|
|||||||
// Patch index table
|
// Patch index table
|
||||||
newBuffer.position(start + STRING_INDICES_OFF)
|
newBuffer.position(start + STRING_INDICES_OFF)
|
||||||
val newIntBuf = newBuffer.asIntBuffer()
|
val newIntBuf = newBuffer.asIntBuffer()
|
||||||
strList.forEach { newIntBuf.put(it) }
|
offList.forEach { newIntBuf.put(it) }
|
||||||
|
|
||||||
// Write the rest of the chunks
|
// Write the rest of the chunks
|
||||||
val nextOff = start + size
|
val nextOff = start + size
|
||||||
|
@@ -1,30 +1,24 @@
|
|||||||
package com.topjohnwu.magisk.core.utils
|
package com.topjohnwu.magisk.core.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
|
import androidx.biometric.BiometricManager.Authenticators
|
||||||
import androidx.biometric.BiometricPrompt
|
import androidx.biometric.BiometricPrompt
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import 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
|
BiometricManager.BIOMETRIC_SUCCESS -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
val isEnabled: Boolean get() {
|
val isEnabled get() = isSupported && Config.suBiometric
|
||||||
val enabled = Config.suBiometric
|
|
||||||
if (enabled && !isSupported) {
|
|
||||||
Config.suBiometric = false
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
fun authenticate(
|
fun authenticate(
|
||||||
activity: FragmentActivity,
|
activity: FragmentActivity,
|
||||||
@@ -48,7 +42,7 @@ object BiometricHelper {
|
|||||||
)
|
)
|
||||||
val info = BiometricPrompt.PromptInfo.Builder()
|
val info = BiometricPrompt.PromptInfo.Builder()
|
||||||
.setConfirmationRequired(true)
|
.setConfirmationRequired(true)
|
||||||
.setDeviceCredentialAllowed(false)
|
.setAllowedAuthenticators(Authenticators.BIOMETRIC_WEAK)
|
||||||
.setTitle(activity.getString(R.string.authenticate))
|
.setTitle(activity.getString(R.string.authenticate))
|
||||||
.setNegativeButtonText(activity.getString(android.R.string.cancel))
|
.setNegativeButtonText(activity.getString(android.R.string.cancel))
|
||||||
.build()
|
.build()
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
package com.topjohnwu.magisk.core.utils
|
package com.topjohnwu.magisk.core.utils
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Runnable
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.util.concurrent.AbstractExecutorService
|
import java.util.concurrent.AbstractExecutorService
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
@@ -1,26 +1,22 @@
|
|||||||
package com.topjohnwu.magisk.core.utils
|
package com.topjohnwu.magisk.core.utils
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Base64OutputStream
|
import android.util.Base64OutputStream
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.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.x500.X500Name
|
||||||
|
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||||
|
import org.bouncycastle.cert.X509v3CertificateBuilder
|
||||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
|
|
||||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.security.KeyPairGenerator
|
import java.security.KeyPairGenerator
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.security.PrivateKey
|
import java.security.PrivateKey
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import java.util.*
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.Random
|
||||||
import java.util.zip.GZIPInputStream
|
import java.util.zip.GZIPInputStream
|
||||||
import java.util.zip.GZIPOutputStream
|
import java.util.zip.GZIPOutputStream
|
||||||
|
|
||||||
@@ -29,13 +25,11 @@ private interface CertKeyProvider {
|
|||||||
val key: PrivateKey
|
val key: PrivateKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
class Keygen : CertKeyProvider {
|
||||||
class Keygen(context: Context) : CertKeyProvider {
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ALIAS = "magisk"
|
private const val ALIAS = "magisk"
|
||||||
private val PASSWORD get() = "magisk".toCharArray()
|
private val PASSWORD get() = "magisk".toCharArray()
|
||||||
private const val TESTKEY_CERT = "61ed377e85d386a8dfee6b864bd85b0bfaa5af81"
|
|
||||||
private const val DNAME = "C=US,ST=California,L=Mountain View,O=Google Inc.,OU=Android,CN=Android"
|
private const val DNAME = "C=US,ST=California,L=Mountain View,O=Google Inc.,OU=Android,CN=Android"
|
||||||
private const val BASE64_FLAG = Base64.NO_PADDING or Base64.NO_WRAP
|
private const val BASE64_FLAG = Base64.NO_PADDING or Base64.NO_WRAP
|
||||||
}
|
}
|
||||||
@@ -43,49 +37,9 @@ class Keygen(context: Context) : CertKeyProvider {
|
|||||||
private val start = Calendar.getInstance().apply { add(Calendar.MONTH, -3) }
|
private val start = Calendar.getInstance().apply { add(Calendar.MONTH, -3) }
|
||||||
private val end = (start.clone() as Calendar).apply { add(Calendar.YEAR, 30) }
|
private val end = (start.clone() as Calendar).apply { add(Calendar.YEAR, 30) }
|
||||||
|
|
||||||
override val cert get() = provider.cert
|
private val ks = init()
|
||||||
override val key get() = provider.key
|
override val cert = ks.getCertificate(ALIAS) as X509Certificate
|
||||||
|
override val key = ks.getKey(ALIAS, PASSWORD) as PrivateKey
|
||||||
private val provider: CertKeyProvider
|
|
||||||
|
|
||||||
inner class KeyStoreProvider :
|
|
||||||
CertKeyProvider {
|
|
||||||
private val ks by lazy { init() }
|
|
||||||
override val cert by lazy { ks.getCertificate(ALIAS) as X509Certificate }
|
|
||||||
override val key by lazy { ks.getKey(
|
|
||||||
ALIAS,
|
|
||||||
PASSWORD
|
|
||||||
) as PrivateKey }
|
|
||||||
}
|
|
||||||
|
|
||||||
class TestProvider : CertKeyProvider {
|
|
||||||
override val cert by lazy {
|
|
||||||
readCertificate(ByteArrayInputStream(KeyData.testCert()))
|
|
||||||
}
|
|
||||||
override val key by lazy {
|
|
||||||
readPrivateKey(ByteArrayInputStream(KeyData.testKey()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
val pm = context.packageManager
|
|
||||||
val info = pm.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES)
|
|
||||||
val sig = info.signatures[0]
|
|
||||||
val digest = MessageDigest.getInstance("SHA1")
|
|
||||||
val chksum = digest.digest(sig.toByteArray())
|
|
||||||
|
|
||||||
val sb = StringBuilder()
|
|
||||||
for (b in chksum) {
|
|
||||||
sb.append("%02x".format(0xFF and b.toInt()))
|
|
||||||
}
|
|
||||||
|
|
||||||
provider = if (sb.toString() == TESTKEY_CERT) {
|
|
||||||
// The app was signed by the test key, continue to use it (legacy mode)
|
|
||||||
TestProvider()
|
|
||||||
} else {
|
|
||||||
KeyStoreProvider()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun init(): KeyStore {
|
private fun init(): KeyStore {
|
||||||
val raw = Config.keyStoreRaw
|
val raw = Config.keyStoreRaw
|
||||||
@@ -93,12 +47,8 @@ class Keygen(context: Context) : CertKeyProvider {
|
|||||||
if (raw.isEmpty()) {
|
if (raw.isEmpty()) {
|
||||||
ks.load(null)
|
ks.load(null)
|
||||||
} else {
|
} else {
|
||||||
GZIPInputStream(Base64.decode(raw,
|
GZIPInputStream(Base64.decode(raw, BASE64_FLAG).inputStream()).use {
|
||||||
BASE64_FLAG
|
ks.load(it, PASSWORD)
|
||||||
).inputStream()).use {
|
|
||||||
ks.load(it,
|
|
||||||
PASSWORD
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,22 +59,19 @@ class Keygen(context: Context) : CertKeyProvider {
|
|||||||
// Generate new private key and certificate
|
// Generate new private key and certificate
|
||||||
val kp = KeyPairGenerator.getInstance("RSA").apply { initialize(4096) }.genKeyPair()
|
val kp = KeyPairGenerator.getInstance("RSA").apply { initialize(4096) }.genKeyPair()
|
||||||
val dname = X500Name(DNAME)
|
val dname = X500Name(DNAME)
|
||||||
val builder = JcaX509v3CertificateBuilder(dname, BigInteger(160, Random()),
|
val builder = X509v3CertificateBuilder(
|
||||||
start.time, end.time, dname, kp.public)
|
dname, BigInteger(160, Random()),
|
||||||
|
start.time, end.time, Locale.ROOT, dname,
|
||||||
|
SubjectPublicKeyInfo.getInstance(kp.public.encoded)
|
||||||
|
)
|
||||||
val signer = JcaContentSignerBuilder("SHA1WithRSA").build(kp.private)
|
val signer = JcaContentSignerBuilder("SHA1WithRSA").build(kp.private)
|
||||||
val cert = JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
val cert = JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
||||||
|
|
||||||
// Store them into keystore
|
// Store them into keystore
|
||||||
ks.setKeyEntry(
|
ks.setKeyEntry(ALIAS, kp.private, PASSWORD, arrayOf(cert))
|
||||||
ALIAS, kp.private,
|
|
||||||
PASSWORD, arrayOf(cert))
|
|
||||||
val bytes = ByteArrayOutputStream()
|
val bytes = ByteArrayOutputStream()
|
||||||
GZIPOutputStream(Base64OutputStream(bytes,
|
GZIPOutputStream(Base64OutputStream(bytes, BASE64_FLAG)).use {
|
||||||
BASE64_FLAG
|
ks.store(it, PASSWORD)
|
||||||
)).use {
|
|
||||||
ks.store(it,
|
|
||||||
PASSWORD
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Config.keyStoreRaw = bytes.toString("UTF-8")
|
Config.keyStoreRaw = bytes.toString("UTF-8")
|
||||||
|
|
||||||
|
@@ -6,13 +6,13 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.core.ActivityTracker
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.createNewResources
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
var currentLocale: Locale = Locale.getDefault()
|
var currentLocale: Locale = Locale.getDefault()
|
||||||
|
|
||||||
@@ -28,6 +28,11 @@ withContext(Dispatchers.Default) {
|
|||||||
// Create a completely new resource to prevent cross talk over active configs
|
// Create a completely new resource to prevent cross talk over active configs
|
||||||
val res = createNewResources()
|
val res = createNewResources()
|
||||||
|
|
||||||
|
fun changeLocale(locale: Locale) {
|
||||||
|
res.configuration.setLocale(locale)
|
||||||
|
res.updateConfiguration(res.configuration, res.displayMetrics)
|
||||||
|
}
|
||||||
|
|
||||||
val locales = ArrayList<String>().apply {
|
val locales = ArrayList<String>().apply {
|
||||||
// Add default locale
|
// Add default locale
|
||||||
add("en")
|
add("en")
|
||||||
@@ -41,13 +46,13 @@ withContext(Dispatchers.Default) {
|
|||||||
}.map {
|
}.map {
|
||||||
Locale.forLanguageTag(it)
|
Locale.forLanguageTag(it)
|
||||||
}.distinctBy {
|
}.distinctBy {
|
||||||
res.setLocale(it)
|
changeLocale(it)
|
||||||
res.getString(compareId)
|
res.getString(compareId)
|
||||||
}.sortedWith { a, b ->
|
}.sortedWith { a, b ->
|
||||||
a.getDisplayName(a).compareTo(b.getDisplayName(b), true)
|
a.getDisplayName(a).compareTo(b.getDisplayName(b), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.setLocale(defaultLocale)
|
changeLocale(defaultLocale)
|
||||||
val defName = res.getString(R.string.system_default)
|
val defName = res.getString(R.string.system_default)
|
||||||
|
|
||||||
val names = ArrayList<String>(locales.size + 1)
|
val names = ArrayList<String>(locales.size + 1)
|
||||||
@@ -71,11 +76,6 @@ fun Resources.setConfig(config: Configuration) {
|
|||||||
|
|
||||||
fun Resources.syncLocale() = setConfig(configuration)
|
fun Resources.syncLocale() = setConfig(configuration)
|
||||||
|
|
||||||
fun Resources.setLocale(locale: Locale) {
|
|
||||||
configuration.setLocale(locale)
|
|
||||||
updateConfiguration(configuration, displayMetrics)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refreshLocale() {
|
fun refreshLocale() {
|
||||||
val localeConfig = Config.locale
|
val localeConfig = Config.locale
|
||||||
currentLocale = when {
|
currentLocale = when {
|
||||||
@@ -84,4 +84,5 @@ fun refreshLocale() {
|
|||||||
}
|
}
|
||||||
Locale.setDefault(currentLocale)
|
Locale.setDefault(currentLocale)
|
||||||
AppContext.resources.syncLocale()
|
AppContext.resources.syncLocale()
|
||||||
|
ActivityTracker.foreground?.recreate()
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,7 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.di.AppContext
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -87,7 +87,7 @@ object MediaStoreUtils {
|
|||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun getFile(displayName: String, skipQuery: Boolean = false): UriFile {
|
fun getFile(displayName: String, skipQuery: Boolean = false): UriFile {
|
||||||
if (Build.VERSION.SDK_INT < 30) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||||
// Fallback to file based I/O pre Android 11
|
// Fallback to file based I/O pre Android 11
|
||||||
val parent = File(Environment.getExternalStorageDirectory(), relativePath)
|
val parent = File(Environment.getExternalStorageDirectory(), relativePath)
|
||||||
parent.mkdirs()
|
parent.mkdirs()
|
||||||
@@ -102,6 +102,8 @@ object MediaStoreUtils {
|
|||||||
|
|
||||||
fun Uri.outputStream() = cr.openOutputStream(this, "rwt") ?: throw FileNotFoundException()
|
fun Uri.outputStream() = cr.openOutputStream(this, "rwt") ?: throw FileNotFoundException()
|
||||||
|
|
||||||
|
fun Uri.fileDescriptor(mode: String) = cr.openFileDescriptor(this, mode) ?: throw FileNotFoundException()
|
||||||
|
|
||||||
val Uri.displayName: String get() {
|
val Uri.displayName: String get() {
|
||||||
if (scheme == "file") {
|
if (scheme == "file") {
|
||||||
// Simple uri wrapper over file, directly get file name
|
// Simple uri wrapper over file, directly get file name
|
||||||
|
@@ -0,0 +1,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,
|
context: Context,
|
||||||
input: Unit
|
input: Unit
|
||||||
): SynchronousResult<Boolean>? {
|
): SynchronousResult<Boolean>? {
|
||||||
if (Build.VERSION.SDK_INT < 26)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
|
||||||
return SynchronousResult(true)
|
return SynchronousResult(true)
|
||||||
if (context.packageManager.canRequestPackageInstalls())
|
if (context.packageManager.canRequestPackageInstalls())
|
||||||
return SynchronousResult(true)
|
return SynchronousResult(true)
|
||||||
|
@@ -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.Const
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
import com.topjohnwu.magisk.ktx.cachedFile
|
import com.topjohnwu.magisk.core.ktx.cachedFile
|
||||||
import com.topjohnwu.magisk.ktx.deviceProtectedContext
|
import com.topjohnwu.magisk.core.ktx.deviceProtectedContext
|
||||||
import com.topjohnwu.magisk.ktx.rawResource
|
import com.topjohnwu.magisk.core.ktx.rawResource
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.ShellUtils
|
import com.topjohnwu.superuser.ShellUtils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -19,8 +19,9 @@ import java.util.jar.JarFile
|
|||||||
class ShellInit : Shell.Initializer() {
|
class ShellInit : Shell.Initializer() {
|
||||||
override fun onInit(context: Context, shell: Shell): Boolean {
|
override fun onInit(context: Context, shell: Shell): Boolean {
|
||||||
if (shell.isRoot) {
|
if (shell.isRoot) {
|
||||||
RootRegistry.bindTask?.let { shell.execTask(it) }
|
Info.isRooted = true
|
||||||
RootRegistry.bindTask = null
|
RootUtils.bindTask?.let { shell.execTask(it) }
|
||||||
|
RootUtils.bindTask = null
|
||||||
}
|
}
|
||||||
shell.newJob().apply {
|
shell.newJob().apply {
|
||||||
add("export ASH_STANDALONE=1")
|
add("export ASH_STANDALONE=1")
|
||||||
@@ -40,7 +41,7 @@ class ShellInit : Shell.Initializer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (shell.isRoot) {
|
if (shell.isRoot) {
|
||||||
add("export MAGISKTMP=\$(magisk --path)/.magisk")
|
add("export MAGISKTMP=\$(magisk --path)")
|
||||||
// Test if we can properly execute stuff in /data
|
// Test if we can properly execute stuff in /data
|
||||||
Info.noDataExec = !shell.newJob().add("$localBB sh -c \"$localBB true\"").exec().isSuccess
|
Info.noDataExec = !shell.newJob().add("$localBB sh -c \"$localBB true\"").exec().isSuccess
|
||||||
}
|
}
|
||||||
@@ -48,12 +49,12 @@ class ShellInit : Shell.Initializer() {
|
|||||||
if (Info.noDataExec) {
|
if (Info.noDataExec) {
|
||||||
// Copy it out of /data to workaround Samsung bullshit
|
// Copy it out of /data to workaround Samsung bullshit
|
||||||
add(
|
add(
|
||||||
"if [ -x \$MAGISKTMP/busybox/busybox ]; then",
|
"if [ -x \$MAGISKTMP/.magisk/busybox/busybox ]; then",
|
||||||
" cp -af $localBB \$MAGISKTMP/busybox/busybox",
|
" cp -af $localBB \$MAGISKTMP/.magisk/busybox/busybox",
|
||||||
" exec \$MAGISKTMP/busybox/busybox sh",
|
" exec \$MAGISKTMP/.magisk/busybox/busybox sh",
|
||||||
"else",
|
"else",
|
||||||
" cp -af $localBB /dev/.busybox",
|
" cp -af $localBB /dev/busybox",
|
||||||
" exec /dev/.busybox sh",
|
" exec /dev/busybox sh",
|
||||||
"fi"
|
"fi"
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -72,18 +73,17 @@ class ShellInit : Shell.Initializer() {
|
|||||||
fun getVar(name: String) = fastCmd("echo \$$name")
|
fun getVar(name: String) = fastCmd("echo \$$name")
|
||||||
fun getBool(name: String) = getVar(name).toBoolean()
|
fun getBool(name: String) = getVar(name).toBoolean()
|
||||||
|
|
||||||
Const.MAGISKTMP = getVar("MAGISKTMP")
|
Info.isSAR = getBool("SYSTEM_AS_ROOT")
|
||||||
Info.isSAR = getBool("SYSTEM_ROOT")
|
|
||||||
Info.ramdisk = getBool("RAMDISKEXIST")
|
Info.ramdisk = getBool("RAMDISKEXIST")
|
||||||
Info.vbmeta = getBool("VBMETAEXIST")
|
|
||||||
Info.isAB = getBool("ISAB")
|
Info.isAB = getBool("ISAB")
|
||||||
Info.crypto = getVar("CRYPTOTYPE")
|
Info.crypto = getVar("CRYPTOTYPE")
|
||||||
|
Info.patchBootVbmeta = getBool("PATCHVBMETAFLAG")
|
||||||
|
Info.legacySAR = getBool("LEGACYSAR")
|
||||||
|
|
||||||
// Default presets
|
// Default presets
|
||||||
Config.recovery = getBool("RECOVERYMODE")
|
Config.recovery = getBool("RECOVERYMODE")
|
||||||
Config.keepVerity = getBool("KEEPVERITY")
|
Config.keepVerity = getBool("KEEPVERITY")
|
||||||
Config.keepEnc = getBool("KEEPFORCEENCRYPT")
|
Config.keepEnc = getBool("KEEPFORCEENCRYPT")
|
||||||
Config.patchVbmeta = getBool("PATCHVBMETAFLAG")
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@@ -1,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
|
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.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@@ -31,12 +29,12 @@ fun InputStream.unzip(folder: File, path: String, junkPath: Boolean) {
|
|||||||
else
|
else
|
||||||
entry.name
|
entry.name
|
||||||
|
|
||||||
var dest = File(folder, name)
|
val dest = File(folder, name)
|
||||||
if (!dest.parentFile!!.exists() && !dest.parentFile!!.mkdirs()) {
|
dest.parentFile!!.let {
|
||||||
dest = SuFile(folder, name)
|
if (!it.exists())
|
||||||
dest.parentFile!!.mkdirs()
|
it.mkdirs()
|
||||||
}
|
}
|
||||||
SuFileOutputStream.open(dest).use { out -> zin.copyTo(out) }
|
dest.outputStream().use { out -> zin.copyTo(out) }
|
||||||
}
|
}
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
throw IOException(e)
|
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.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Button
|
import android.widget.*
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.ProgressBar
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.cardview.widget.CardView
|
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.chip.Chip
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.topjohnwu.magisk.R
|
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.superuser.internal.UiThreadHandler
|
||||||
import com.topjohnwu.widget.IndeterminateCheckBox
|
import com.topjohnwu.widget.IndeterminateCheckBox
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -289,3 +287,13 @@ fun TextView.setTextColorAttr(attr: Int) {
|
|||||||
context.theme.resolveAttribute(attr, tv, true)
|
context.theme.resolveAttribute(attr, tv, true)
|
||||||
setTextColor(tv.data)
|
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
|
package com.topjohnwu.magisk.databinding
|
||||||
|
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.databinding.ListChangeRegistry
|
import androidx.databinding.ListChangeRegistry
|
||||||
import androidx.databinding.ObservableList
|
import androidx.databinding.ObservableList
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListUpdateCallback
|
import androidx.recyclerview.widget.ListUpdateCallback
|
||||||
import java.util.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlin.collections.ArrayList
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.util.AbstractList
|
||||||
|
|
||||||
/**
|
// Only expose the immutable List types
|
||||||
* @param callback The callback that controls the behavior of the DiffObservableList.
|
interface DiffList<T : DiffItem<*>> : List<T> {
|
||||||
* @param detectMoves True if DiffUtil should try to detect moved items, false otherwise.
|
fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult
|
||||||
*/
|
|
||||||
open class DiffObservableList<T>(
|
|
||||||
private val callback: Callback<T>,
|
|
||||||
private val detectMoves: Boolean = true
|
|
||||||
) : AbstractList<T>(), ObservableList<T> {
|
|
||||||
|
|
||||||
protected var list: MutableList<T> = ArrayList()
|
@MainThread
|
||||||
|
fun update(newItems: List<T>, diffResult: DiffUtil.DiffResult)
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
suspend fun update(newItems: List<T>)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilterList<T : DiffItem<*>> : List<T> {
|
||||||
|
fun filter(filter: (T) -> Boolean)
|
||||||
|
|
||||||
|
@MainThread
|
||||||
|
fun set(newItems: List<T>)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T : DiffItem<*>> diffList(): DiffList<T> = DiffObservableList()
|
||||||
|
|
||||||
|
fun <T : DiffItem<*>> filterList(scope: CoroutineScope): FilterList<T> =
|
||||||
|
FilterableDiffObservableList(scope)
|
||||||
|
|
||||||
|
private open class DiffObservableList<T : DiffItem<*>>
|
||||||
|
: AbstractList<T>(), ObservableList<T>, DiffList<T>, ListUpdateCallback {
|
||||||
|
|
||||||
|
protected var list: List<T> = emptyList()
|
||||||
private val listeners = ListChangeRegistry()
|
private val listeners = ListChangeRegistry()
|
||||||
protected val listCallback = ObservableListUpdateCallback()
|
|
||||||
|
|
||||||
override val size: Int get() = list.size
|
override val size: Int get() = list.size
|
||||||
|
|
||||||
/**
|
override fun get(index: Int) = list[index]
|
||||||
* Calculates the list of update operations that can convert this list into the given one.
|
|
||||||
*
|
override fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult {
|
||||||
* @param newItems The items that this list will be set to.
|
return doCalculateDiff(list, newItems)
|
||||||
* @return A DiffResult that contains the information about the edit sequence to covert this
|
|
||||||
* list into the given one.
|
|
||||||
*/
|
|
||||||
fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult {
|
|
||||||
val frozenList = ArrayList(list)
|
|
||||||
return doCalculateDiff(frozenList, newItems)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun doCalculateDiff(oldItems: List<T>, newItems: List<T>): DiffUtil.DiffResult {
|
protected fun doCalculateDiff(oldItems: List<T>, newItems: List<T>): DiffUtil.DiffResult {
|
||||||
@@ -41,47 +56,34 @@ open class DiffObservableList<T>(
|
|||||||
|
|
||||||
override fun getNewListSize() = newItems.size
|
override fun getNewListSize() = newItems.size
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
val oldItem = oldItems[oldItemPosition]
|
val oldItem = oldItems[oldItemPosition]
|
||||||
val newItem = newItems[newItemPosition]
|
val newItem = newItems[newItemPosition]
|
||||||
return callback.areItemsTheSame(oldItem, newItem)
|
return (oldItem as DiffItem<Any>).itemSameAs(newItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
val oldItem = oldItems[oldItemPosition]
|
val oldItem = oldItems[oldItemPosition]
|
||||||
val newItem = newItems[newItemPosition]
|
val newItem = newItems[newItemPosition]
|
||||||
return callback.areContentsTheSame(oldItem, newItem)
|
return (oldItem as DiffItem<Any>).contentSameAs(newItem)
|
||||||
}
|
}
|
||||||
}, detectMoves)
|
}, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the contents of this list to the given one using the DiffResults to dispatch change
|
|
||||||
* notifications.
|
|
||||||
*
|
|
||||||
* @param newItems The items to set this list to.
|
|
||||||
* @param diffResult The diff results to dispatch change notifications.
|
|
||||||
*/
|
|
||||||
@MainThread
|
@MainThread
|
||||||
fun update(newItems: List<T>, diffResult: DiffUtil.DiffResult) {
|
override fun update(newItems: List<T>, diffResult: DiffUtil.DiffResult) {
|
||||||
list = newItems.toMutableList()
|
list = ArrayList(newItems)
|
||||||
diffResult.dispatchUpdatesTo(listCallback)
|
diffResult.dispatchUpdatesTo(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@WorkerThread
|
||||||
* Sets this list to the given items. This is a convenience method for calling [ ][.calculateDiff] followed by [.update].
|
override suspend fun update(newItems: List<T>) {
|
||||||
*
|
val diffResult = calculateDiff(newItems)
|
||||||
*
|
withContext(Dispatchers.Main) {
|
||||||
* **Warning!** If the lists are large this operation may be too slow for the main thread. In
|
update(newItems, diffResult)
|
||||||
* that case, you should call [.calculateDiff] on a background thread and then
|
}
|
||||||
* [.update] on the main thread.
|
|
||||||
*
|
|
||||||
* @param newItems The items to set this list to.
|
|
||||||
*/
|
|
||||||
@MainThread
|
|
||||||
fun update(newItems: List<T>) {
|
|
||||||
val diffResult = doCalculateDiff(list, newItems)
|
|
||||||
update(newItems, diffResult)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addOnListChangedCallback(listener: ObservableList.OnListChangedCallback<out ObservableList<T>>) {
|
override fun addOnListChangedCallback(listener: ObservableList.OnListChangedCallback<out ObservableList<T>>) {
|
||||||
@@ -92,113 +94,63 @@ open class DiffObservableList<T>(
|
|||||||
listeners.remove(listener)
|
listeners.remove(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun get(index: Int) = list[index]
|
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||||
|
listeners.notifyChanged(this, position, count)
|
||||||
override fun add(index: Int, element: T) {
|
|
||||||
list.add(index, element)
|
|
||||||
notifyAdd(index, 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addAll(elements: Collection<T>) = addAll(size, elements)
|
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||||
|
listeners.notifyMoved(this, fromPosition, toPosition, 1)
|
||||||
override fun addAll(index: Int, elements: Collection<T>): Boolean {
|
|
||||||
val added = list.addAll(index, elements)
|
|
||||||
if (added) {
|
|
||||||
notifyAdd(index, elements.size)
|
|
||||||
}
|
|
||||||
return added
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clear() {
|
override fun onInserted(position: Int, count: Int) {
|
||||||
val oldSize = size
|
modCount += 1
|
||||||
list.clear()
|
listeners.notifyInserted(this, position, count)
|
||||||
if (oldSize != 0) {
|
|
||||||
notifyRemove(0, oldSize)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun remove(element: T): Boolean {
|
override fun onRemoved(position: Int, count: Int) {
|
||||||
val index = indexOf(element)
|
modCount += 1
|
||||||
return if (index >= 0) {
|
listeners.notifyRemoved(this, position, count)
|
||||||
removeAt(index)
|
}
|
||||||
true
|
}
|
||||||
} else {
|
|
||||||
false
|
private class FilterableDiffObservableList<T : DiffItem<*>>(
|
||||||
}
|
private val scope: CoroutineScope
|
||||||
}
|
) : DiffObservableList<T>(), FilterList<T> {
|
||||||
|
|
||||||
override fun removeAt(index: Int): T {
|
private var sublist: List<T> = emptyList()
|
||||||
val element = list.removeAt(index)
|
private var job: Job? = null
|
||||||
notifyRemove(index, 1)
|
private var lastFilter: ((T) -> Boolean)? = null
|
||||||
return element
|
|
||||||
}
|
// ---
|
||||||
|
|
||||||
override fun set(index: Int, element: T): T {
|
override fun filter(filter: (T) -> Boolean) {
|
||||||
val old = list.set(index, element)
|
lastFilter = filter
|
||||||
listeners.notifyChanged(this, index, 1)
|
job?.cancel()
|
||||||
return old
|
job = scope.launch(Dispatchers.Default) {
|
||||||
}
|
val oldList = sublist
|
||||||
|
val newList = list.filter(filter)
|
||||||
private fun notifyAdd(start: Int, count: Int) {
|
val diff = doCalculateDiff(oldList, newList)
|
||||||
listeners.notifyInserted(this, start, count)
|
withContext(Dispatchers.Main) {
|
||||||
}
|
sublist = newList
|
||||||
|
diff.dispatchUpdatesTo(this@FilterableDiffObservableList)
|
||||||
private fun notifyRemove(start: Int, count: Int) {
|
}
|
||||||
listeners.notifyRemoved(this, start, count)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ---
|
||||||
* A Callback class used by DiffUtil while calculating the diff between two lists.
|
|
||||||
*/
|
override fun get(index: Int): T {
|
||||||
interface Callback<T> {
|
return sublist[index]
|
||||||
/**
|
}
|
||||||
* Called by the DiffUtil to decide whether two object represent the same Item.
|
|
||||||
*
|
override val size: Int
|
||||||
*
|
get() = sublist.size
|
||||||
* For example, if your items have unique ids, this method should check their id equality.
|
|
||||||
*
|
@MainThread
|
||||||
* @param oldItem The old item.
|
override fun set(newItems: List<T>) {
|
||||||
* @param newItem The new item.
|
onRemoved(0, sublist.size)
|
||||||
* @return True if the two items represent the same object or false if they are different.
|
list = newItems
|
||||||
*/
|
sublist = emptyList()
|
||||||
fun areItemsTheSame(oldItem: T, newItem: T): Boolean
|
lastFilter?.let { filter(it) }
|
||||||
|
|
||||||
/**
|
|
||||||
* Called by the DiffUtil when it wants to check whether two items have the same data.
|
|
||||||
* DiffUtil uses this information to detect if the contents of an item has changed.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* DiffUtil uses this method to check equality instead of [Object.equals] so
|
|
||||||
* that you can change its behavior depending on your UI.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* This method is called only if [.areItemsTheSame] returns `true` for
|
|
||||||
* these items.
|
|
||||||
*
|
|
||||||
* @param oldItem The old item.
|
|
||||||
* @param newItem The new item which replaces the old item.
|
|
||||||
* @return True if the contents of the items are the same or false if they are different.
|
|
||||||
*/
|
|
||||||
fun areContentsTheSame(oldItem: T, newItem: T): Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class ObservableListUpdateCallback : ListUpdateCallback {
|
|
||||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
|
||||||
listeners.notifyChanged(this@DiffObservableList, position, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
|
||||||
listeners.notifyMoved(this@DiffObservableList, fromPosition, toPosition, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInserted(position: Int, count: Int) {
|
|
||||||
modCount += 1
|
|
||||||
listeners.notifyInserted(this@DiffObservableList, position, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRemoved(position: Int, count: Int) {
|
|
||||||
modCount += 1
|
|
||||||
listeners.notifyRemoved(this@DiffObservableList, position, count)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,85 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.databinding
|
|
||||||
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.HandlerThread
|
|
||||||
import android.os.Looper
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class FilterableDiffObservableList<T>(
|
|
||||||
callback: Callback<T>
|
|
||||||
) : DiffObservableList<T>(callback) {
|
|
||||||
|
|
||||||
var filter: ((T) -> Boolean)? = null
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
queueUpdate()
|
|
||||||
}
|
|
||||||
@Volatile
|
|
||||||
private var sublist: MutableList<T> = super.list
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
private val ui by lazy { Handler(Looper.getMainLooper()) }
|
|
||||||
private val handler = Handler(HandlerThread("List${hashCode()}").apply { start() }.looper)
|
|
||||||
private val updater = Runnable {
|
|
||||||
val filter = filter ?: { true }
|
|
||||||
val newList = super.list.filter(filter)
|
|
||||||
val diff = synchronized(this) { doCalculateDiff(sublist, newList) }
|
|
||||||
ui.post {
|
|
||||||
sublist = Collections.synchronizedList(newList)
|
|
||||||
diff.dispatchUpdatesTo(listCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun queueUpdate() {
|
|
||||||
handler.removeCallbacks(updater)
|
|
||||||
handler.post(updater)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasFilter() = filter != null
|
|
||||||
|
|
||||||
fun filter(switch: (T) -> Boolean) {
|
|
||||||
filter = switch
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reset() {
|
|
||||||
filter = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
override fun get(index: Int): T {
|
|
||||||
return sublist.get(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun add(element: T): Boolean {
|
|
||||||
return sublist.add(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun add(index: Int, element: T) {
|
|
||||||
sublist.add(index, element)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addAll(elements: Collection<T>): Boolean {
|
|
||||||
return sublist.addAll(elements)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addAll(index: Int, elements: Collection<T>): Boolean {
|
|
||||||
return sublist.addAll(index, elements)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun remove(element: T): Boolean {
|
|
||||||
return sublist.remove(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun removeAt(index: Int): T {
|
|
||||||
return sublist.removeAt(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun set(index: Int, element: T): T {
|
|
||||||
return sublist.set(index, element)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val size: Int
|
|
||||||
get() = sublist.size
|
|
||||||
}
|
|
@@ -1,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
|
package com.topjohnwu.magisk.databinding
|
||||||
|
|
||||||
import androidx.annotation.CallSuper
|
|
||||||
import androidx.databinding.PropertyChangeRegistry
|
import androidx.databinding.PropertyChangeRegistry
|
||||||
import androidx.databinding.ViewDataBinding
|
import androidx.databinding.ViewDataBinding
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.topjohnwu.magisk.BR
|
|
||||||
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
|
||||||
|
|
||||||
abstract class RvItem {
|
abstract class RvItem {
|
||||||
|
|
||||||
abstract val layoutRes: Int
|
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 {
|
abstract class ObservableRvItem : RvItem(), ObservableHost {
|
||||||
override var callbacks: PropertyChangeRegistry? = null
|
override var callbacks: PropertyChangeRegistry? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
interface ItemWrapper<E> {
|
||||||
* This item addresses issues where enclosing recycler has to be invalidated or generally
|
val item: E
|
||||||
* 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 ViewAwareItem {
|
||||||
|
fun onBind(binding: ViewDataBinding, recyclerView: RecyclerView)
|
||||||
interface LenientRvItem {
|
}
|
||||||
fun onBindingBound(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 android.app.Activity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.arch.UIActivity
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
|
import com.topjohnwu.magisk.events.DialogBuilder
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
|
|
||||||
class DarkThemeDialog : DialogEvent() {
|
class DarkThemeDialog : DialogBuilder {
|
||||||
|
|
||||||
override fun build(dialog: MagiskDialog) {
|
override fun build(dialog: MagiskDialog) {
|
||||||
val activity = dialog.ownerActivity!!
|
val activity = dialog.ownerActivity!!
|
||||||
@@ -33,6 +35,6 @@ class DarkThemeDialog : DialogEvent() {
|
|||||||
|
|
||||||
private fun selectTheme(mode: Int, activity: Activity) {
|
private fun selectTheme(mode: Int, activity: Activity) {
|
||||||
Config.darkTheme = mode
|
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 androidx.lifecycle.lifecycleScope
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
@@ -6,11 +6,12 @@ import com.topjohnwu.magisk.R
|
|||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||||
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
||||||
|
import com.topjohnwu.magisk.events.DialogBuilder
|
||||||
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class EnvFixDialog(private val vm: HomeViewModel) : DialogEvent() {
|
class EnvFixDialog(private val vm: HomeViewModel, private val code: Int) : DialogBuilder {
|
||||||
|
|
||||||
override fun build(dialog: MagiskDialog) {
|
override fun build(dialog: MagiskDialog) {
|
||||||
dialog.apply {
|
dialog.apply {
|
||||||
@@ -38,8 +39,10 @@ class EnvFixDialog(private val vm: HomeViewModel) : DialogEvent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Info.env.versionCode != BuildConfig.VERSION_CODE ||
|
if (code == 2 || // No rules block, module policy not loaded
|
||||||
|
Info.env.versionCode != BuildConfig.VERSION_CODE ||
|
||||||
Info.env.versionString != BuildConfig.VERSION_NAME) {
|
Info.env.versionString != BuildConfig.VERSION_NAME) {
|
||||||
|
dialog.setMessage(R.string.env_full_fix_msg)
|
||||||
dialog.setButton(MagiskDialog.ButtonType.POSITIVE) {
|
dialog.setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||||
text = android.R.string.ok
|
text = android.R.string.ok
|
||||||
onClick {
|
onClick {
|
@@ -0,0 +1,33 @@
|
|||||||
|
package com.topjohnwu.magisk.dialog
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.topjohnwu.magisk.MainDirections
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.core.Const
|
||||||
|
import com.topjohnwu.magisk.events.DialogBuilder
|
||||||
|
import com.topjohnwu.magisk.ui.module.ModuleViewModel
|
||||||
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
|
|
||||||
|
class LocalModuleInstallDialog(
|
||||||
|
private val viewModel: ModuleViewModel,
|
||||||
|
private val uri: Uri,
|
||||||
|
private val displayName: String
|
||||||
|
) : DialogBuilder {
|
||||||
|
override fun build(dialog: MagiskDialog) {
|
||||||
|
dialog.apply {
|
||||||
|
setTitle(R.string.confirm_install_title)
|
||||||
|
setMessage(context.getString(R.string.confirm_install, displayName))
|
||||||
|
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||||
|
text = android.R.string.ok
|
||||||
|
onClick {
|
||||||
|
viewModel.apply {
|
||||||
|
MainDirections.actionFlashFragment(Const.Value.FLASH_ZIP, uri).navigate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||||
|
text = android.R.string.cancel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,11 +1,11 @@
|
|||||||
package com.topjohnwu.magisk.events.dialog
|
package com.topjohnwu.magisk.dialog
|
||||||
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
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.DownloadService
|
||||||
import com.topjohnwu.magisk.core.download.Subject
|
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 com.topjohnwu.magisk.view.MagiskDialog
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ class ManagerInstallDialog : MarkDownDialog() {
|
|||||||
setCancelable(true)
|
setCancelable(true)
|
||||||
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||||
text = R.string.install
|
text = R.string.install
|
||||||
onClick { DownloadService.start(context, Subject.App()) }
|
onClick { DownloadService.start(activity, Subject.App()) }
|
||||||
}
|
}
|
||||||
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||||
text = android.R.string.cancel
|
text = android.R.string.cancel
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user