You've already forked Magisk
mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-09-06 06:36:58 +00:00
Compare commits
1656 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0c9384233 | ||
|
|
2488668b06 | ||
|
|
52a98cbd51 | ||
|
|
1840c4c486 | ||
|
|
34080f3958 | ||
|
|
e9b76b6aa5 | ||
|
|
b7799b53d9 | ||
|
|
1e206515c7 | ||
|
|
6bb313184d | ||
|
|
2763992434 | ||
|
|
18fe0e6442 | ||
|
|
a70c73bffd | ||
|
|
b4ae3493a6 | ||
|
|
1a16004b20 | ||
|
|
56707b8119 | ||
|
|
c3f9533ddc | ||
|
|
3b3abd63cc | ||
|
|
411d3ed4e9 | ||
|
|
f29cc26103 | ||
|
|
1cd595a598 | ||
|
|
22e023b58d | ||
|
|
7be958e35d | ||
|
|
69b66ef637 | ||
|
|
daf8653c38 | ||
|
|
e2545e57cf | ||
|
|
7cb0909c70 | ||
|
|
cc5ff36165 | ||
|
|
18b1ef6c29 | ||
|
|
7fe012347a | ||
|
|
5c165c9bb0 | ||
|
|
6c3519923d | ||
|
|
9ea859810d | ||
|
|
8dae7b5451 | ||
|
|
f827755aaf | ||
|
|
637a8af234 | ||
|
|
b0fc580860 | ||
|
|
9279f30e89 | ||
|
|
b505819ca2 | ||
|
|
39d1d23909 | ||
|
|
69529ac59c | ||
|
|
a18a440236 | ||
|
|
aa7846c1c0 | ||
|
|
24ba4ab95b | ||
|
|
762b70ba9d | ||
|
|
41b77e4f25 | ||
|
|
2087e47300 | ||
|
|
46ce765860 | ||
|
|
5117dc1a31 | ||
|
|
620fd7d124 | ||
|
|
3e991dc003 | ||
|
|
15cab86152 | ||
|
|
aa785b5845 | ||
|
|
97731a519a | ||
|
|
b696dae808 | ||
|
|
732a8260c2 | ||
|
|
4ff60ef9a9 | ||
|
|
23b1b69110 | ||
|
|
3a4fe53f27 | ||
|
|
e48afff5e8 | ||
|
|
3f4f4598e8 | ||
|
|
3921e9cb1b | ||
|
|
0b987dd0b0 | ||
|
|
1620e15f99 | ||
|
|
b089511e91 | ||
|
|
958788c1aa | ||
|
|
b5a8a27296 | ||
|
|
98123775ad | ||
|
|
c7133974be | ||
|
|
04324a7ebe | ||
|
|
f54daa3469 | ||
|
|
07c22ccd39 | ||
|
|
e893c13cf1 | ||
|
|
dba5020e4f | ||
|
|
87e036a190 | ||
|
|
3dd94672b0 | ||
|
|
004b193f69 | ||
|
|
4417997749 | ||
|
|
2eef542054 | ||
|
|
a07d4080b6 | ||
|
|
b9d0a3b3d4 | ||
|
|
76405bd984 | ||
|
|
4e2b88b3d0 | ||
|
|
7048aa1014 | ||
|
|
1c2fcd14b5 | ||
|
|
84e1bd7bc3 | ||
|
|
362eea741f | ||
|
|
4de93cfd4b | ||
|
|
03cee0b8d4 | ||
|
|
54ecc001f4 | ||
|
|
5c325d9466 | ||
|
|
0e851cdcf8 | ||
|
|
af054e4e31 | ||
|
|
33fb4653f0 | ||
|
|
d9f0aed571 | ||
|
|
98813c24fb | ||
|
|
3cc81bb3fd | ||
|
|
366dd52419 | ||
|
|
fe6b658c02 | ||
|
|
3cf66d1c57 | ||
|
|
382568bd3c | ||
|
|
d130aa02a1 | ||
|
|
1a1646795f | ||
|
|
d52ea1b068 | ||
|
|
e14f7b6908 | ||
|
|
4709a32641 | ||
|
|
71b7f52663 | ||
|
|
981ccabbef | ||
|
|
9e07eb592c | ||
|
|
9555380818 | ||
|
|
f80d5d858e | ||
|
|
a1ce6f5f12 | ||
|
|
1aade8f8a8 | ||
|
|
b9213b7043 | ||
|
|
4af72324f4 | ||
|
|
b6ea5b8984 | ||
|
|
c279e08c88 | ||
|
|
2717feac21 | ||
|
|
8adf27859d | ||
|
|
307cf87215 | ||
|
|
ca31412c05 | ||
|
|
f59fbd5dca | ||
|
|
2285f5e888 | ||
|
|
da36e5bcd5 | ||
|
|
4ed9f57fdc | ||
|
|
ea7be6162f | ||
|
|
3726eb6032 | ||
|
|
6e918ffd68 | ||
|
|
4772868d6a | ||
|
|
78df677a42 | ||
|
|
85a4b249b3 | ||
|
|
d06e9a0b51 | ||
|
|
5eb774a2ad | ||
|
|
cbbbbab483 | ||
|
|
e5641d5bdb | ||
|
|
a721206c6f | ||
|
|
c7a27481f9 | ||
|
|
594c304634 | ||
|
|
d0ec387c28 | ||
|
|
7dbfba76bf | ||
|
|
2a4aa95a6f | ||
|
|
5520f0fbf7 | ||
|
|
a1a87c9956 | ||
|
|
2c53356bfd | ||
|
|
85d9756f62 | ||
|
|
79586ece4c | ||
|
|
6851d11a8e | ||
|
|
996a857096 | ||
|
|
d7158131e4 | ||
|
|
3d3082bc82 | ||
|
|
744ebca206 | ||
|
|
92077ebe53 | ||
|
|
78ca682bc5 | ||
|
|
af01a36296 | ||
|
|
97ed1b16d0 | ||
|
|
508a001753 | ||
|
|
c1909d520b | ||
|
|
9b1e173373 | ||
|
|
4ba365565f | ||
|
|
ae34659b26 | ||
|
|
79a85f5937 | ||
|
|
b249832571 | ||
|
|
577b5912af | ||
|
|
9e8c68af12 | ||
|
|
03418ddcbf | ||
|
|
220a1c84ce | ||
|
|
9a4458ffac | ||
|
|
7a9e6d2ad2 | ||
|
|
9656cf2f86 | ||
|
|
584bad5314 | ||
|
|
459088024f | ||
|
|
d740bbe058 | ||
|
|
6ecc04a4df | ||
|
|
15a7e9af57 | ||
|
|
0329f00129 | ||
|
|
cd8a2edefb | ||
|
|
4318ab5cd2 | ||
|
|
3517e6d752 | ||
|
|
67845f9c21 | ||
|
|
f562710438 | ||
|
|
e836909c50 | ||
|
|
7769ba5f54 | ||
|
|
7fe9db90a1 | ||
|
|
8f7d6dfb77 | ||
|
|
2839978cc1 | ||
|
|
e73f87b758 | ||
|
|
bd0409fd15 | ||
|
|
babdfe80cb | ||
|
|
636223b289 | ||
|
|
aa0a2f77cf | ||
|
|
e38f35eab2 | ||
|
|
cb39514705 | ||
|
|
78a444d601 | ||
|
|
37b81ad1f6 | ||
|
|
7871c2f595 | ||
|
|
57d83635c6 | ||
|
|
76fbf4634a | ||
|
|
7ce4bd3330 | ||
|
|
ad0e6511e1 | ||
|
|
a4a734458b | ||
|
|
f989756b93 | ||
|
|
5763a3d908 | ||
|
|
1b745ae1a0 | ||
|
|
b6d50bea2c | ||
|
|
831a398bf1 | ||
|
|
a848783b97 | ||
|
|
4d876f0145 | ||
|
|
bdfedea4e0 | ||
|
|
ea0e3a09ef | ||
|
|
dadae20960 | ||
|
|
4ed34cd648 | ||
|
|
0d38c94c9c | ||
|
|
2a2a452bd4 | ||
|
|
13c2695e98 | ||
|
|
3ff60ed49f | ||
|
|
bbb1786ec3 | ||
|
|
4bfd2dac54 | ||
|
|
857c12372a | ||
|
|
33f5154269 | ||
|
|
ed37ddd570 | ||
|
|
cd5384f13e | ||
|
|
11b2ddbad8 | ||
|
|
cf9957ce4d | ||
|
|
44643ad7b3 | ||
|
|
1e53a5555e | ||
|
|
616adc22e1 | ||
|
|
916e373edb | ||
|
|
021ae15395 | ||
|
|
52cf72002a | ||
|
|
68874bf571 | ||
|
|
a468fd946d | ||
|
|
e327565434 | ||
|
|
c3b4678f6e | ||
|
|
978216eade | ||
|
|
44cfe94e4d | ||
|
|
f9e82c9e8a | ||
|
|
25b4b107d3 | ||
|
|
db651fa9ec | ||
|
|
23ad611566 | ||
|
|
095d821240 | ||
|
|
e23f23a8b7 | ||
|
|
48f829b76e | ||
|
|
0b82fe197c | ||
|
|
af99c1b843 | ||
|
|
c6646efe68 | ||
|
|
66a7ef5615 | ||
|
|
9474750bdf | ||
|
|
e86db0bd61 | ||
|
|
a29fc11798 | ||
|
|
a66a3b7438 | ||
|
|
44029875a6 | ||
|
|
ccf21b0992 | ||
|
|
4e14dab60a | ||
|
|
6e299018a4 | ||
|
|
555a54ec53 | ||
|
|
1565bf5442 | ||
|
|
14b830027b | ||
|
|
38325e708e | ||
|
|
646260ad6d | ||
|
|
d1d26f4481 | ||
|
|
357d913f18 | ||
|
|
71b0c8b42b | ||
|
|
cdc66c1ac8 | ||
|
|
e9af773901 | ||
|
|
eadf6e8b96 | ||
|
|
87bec70d9f | ||
|
|
3668b28f62 | ||
|
|
933e4bd163 | ||
|
|
e3ab9e9a1e | ||
|
|
58ad2c1416 | ||
|
|
c5291ad33b | ||
|
|
77d8445bfd | ||
|
|
f8395a7dc6 | ||
|
|
727c70005e | ||
|
|
38ab6858f0 | ||
|
|
a54114f149 | ||
|
|
7a4a5c8992 | ||
|
|
928a16d8cc | ||
|
|
3f7f6e619a | ||
|
|
c2f96975ce | ||
|
|
8bd4760b00 | ||
|
|
4f4aeb893d | ||
|
|
fed4f1b50f | ||
|
|
e11087cd1a | ||
|
|
e6eb51551c | ||
|
|
c5c608f0d3 | ||
|
|
4737c5117a | ||
|
|
9806b38d8e | ||
|
|
6bfe34e5a8 | ||
|
|
34dd9eb7d6 | ||
|
|
2d8beabbd4 | ||
|
|
4d9b7e7114 | ||
|
|
40aab13601 | ||
|
|
4c0f72f68f | ||
|
|
dd565a11ea | ||
|
|
1735a713cb | ||
|
|
52ba6d11bc | ||
|
|
7357a35f8d | ||
|
|
aeb7fd7cb3 | ||
|
|
1b4a6850b8 | ||
|
|
07b45f39df | ||
|
|
1d0b873950 | ||
|
|
d449f49d73 | ||
|
|
e8787b5cfd | ||
|
|
d17ed2b979 | ||
|
|
b496923cbb | ||
|
|
759d196aad | ||
|
|
a7ab8216ce | ||
|
|
b9e89a1a2d | ||
|
|
c7c9fb9576 | ||
|
|
8b095de04d | ||
|
|
468325b51a | ||
|
|
e5058bfb8b | ||
|
|
d4b9ef736d | ||
|
|
00d3cb0908 | ||
|
|
d35072d4e6 | ||
|
|
1a964e78dd | ||
|
|
4264ae49c0 | ||
|
|
f08712cd0a | ||
|
|
3906fe75dc | ||
|
|
2497e548c9 | ||
|
|
e4635684e9 | ||
|
|
9b61bdfc9a | ||
|
|
6066b5cf86 | ||
|
|
5cdf95a4d0 | ||
|
|
910a36fdc1 | ||
|
|
8331206acb | ||
|
|
8423dc8d63 | ||
|
|
6077c989a7 | ||
|
|
c97d1044fa | ||
|
|
f42c089b26 | ||
|
|
1f8c063dc6 | ||
|
|
4874520d65 | ||
|
|
5e53639969 | ||
|
|
83ab0ca6cd | ||
|
|
70fd03d5fc | ||
|
|
2e52875b50 | ||
|
|
fd9b990ad7 | ||
|
|
69978a9442 | ||
|
|
d155da52ce | ||
|
|
9c5b131913 | ||
|
|
9d740cec1a | ||
|
|
c2978eb9c3 | ||
|
|
38abad1e44 | ||
|
|
b4863eb51b | ||
|
|
3817167ba1 | ||
|
|
d1a35dd2ba | ||
|
|
26116ac414 | ||
|
|
0b26882fce | ||
|
|
a2495fb5fb | ||
|
|
0beb3bf16a | ||
|
|
b68658e974 | ||
|
|
3ae7344747 | ||
|
|
4eb71830b3 | ||
|
|
9183a0a6ea | ||
|
|
bb64ba0ef6 | ||
|
|
d89a568897 | ||
|
|
9fd1f41e8b | ||
|
|
c1ab348673 | ||
|
|
00247c7901 | ||
|
|
3c75f474c6 | ||
|
|
db1f5b0397 | ||
|
|
db277c3e55 | ||
|
|
b9c93c66f6 | ||
|
|
a250e2b56c | ||
|
|
cd96454886 | ||
|
|
741b679306 | ||
|
|
90013e486d | ||
|
|
4e2ecdb920 | ||
|
|
6e5df1f06b | ||
|
|
9469e79e3c | ||
|
|
db78c20161 | ||
|
|
1699da1754 | ||
|
|
754e690274 | ||
|
|
6f74ed6ceb | ||
|
|
71205bc530 | ||
|
|
10e236abdf | ||
|
|
2248af00f3 | ||
|
|
7e61716277 | ||
|
|
50edb8d072 | ||
|
|
515f81944c | ||
|
|
46d4708386 | ||
|
|
aabc36f86b | ||
|
|
e0d5d90267 | ||
|
|
482a5b991b | ||
|
|
20124fe410 | ||
|
|
f8dcec116a | ||
|
|
343a339aae | ||
|
|
42606efe56 | ||
|
|
cae58c8790 | ||
|
|
3a39dd4049 | ||
|
|
89ff3c6572 | ||
|
|
7bf9c74216 | ||
|
|
e2f3753551 | ||
|
|
cacf873645 | ||
|
|
11e1e7ee36 | ||
|
|
87801b6f23 | ||
|
|
7ce4789e17 | ||
|
|
9dc6d9afce | ||
|
|
d6a5354bff | ||
|
|
07af37475b | ||
|
|
1b9c273b10 | ||
|
|
262c52db56 | ||
|
|
eb777296d4 | ||
|
|
fc70a384d3 | ||
|
|
34b2f525a3 | ||
|
|
569e9ad937 | ||
|
|
c495b3d183 | ||
|
|
8b16bfbb54 | ||
|
|
b2f1fd9966 | ||
|
|
317153c53a | ||
|
|
fa60daf9b5 | ||
|
|
aadb2d825c | ||
|
|
0e7fe537e3 | ||
|
|
409de3ac44 | ||
|
|
759055eaa5 | ||
|
|
9016e6727d | ||
|
|
a3381da7ed | ||
|
|
351e094440 | ||
|
|
2106751ea4 | ||
|
|
7ae3cd1c43 | ||
|
|
edfd4dcddf | ||
|
|
fb89cf1367 | ||
|
|
b7b345cf8a | ||
|
|
0be487e47e | ||
|
|
5471147422 | ||
|
|
6305159c5e | ||
|
|
2ed092c9db | ||
|
|
5c6a7ffa6f | ||
|
|
9ab7550970 | ||
|
|
47e7a0a434 | ||
|
|
4cc5e9f986 | ||
|
|
6a2ae89846 | ||
|
|
3c93539e02 | ||
|
|
05e5ac2ad2 | ||
|
|
10b1782732 | ||
|
|
e029994ef8 | ||
|
|
9679874874 | ||
|
|
8186f253e8 | ||
|
|
d4fe8632ec | ||
|
|
d7776f6597 | ||
|
|
3219d945f5 | ||
|
|
8a73a16029 | ||
|
|
ce90f9b60d | ||
|
|
bdf54d562f | ||
|
|
e744cc8ea6 | ||
|
|
babcf36495 | ||
|
|
e4094c0caa | ||
|
|
2e51fe20a1 | ||
|
|
c29636c452 | ||
|
|
22017a5543 | ||
|
|
50e2f33d1c | ||
|
|
5e6eb8dd01 | ||
|
|
18acb97dfe | ||
|
|
bf2f823b8c | ||
|
|
d0c4226997 | ||
|
|
4ea8bd0229 | ||
|
|
ee0d58a9b8 | ||
|
|
bf04fa134b | ||
|
|
297662cafb | ||
|
|
f464a9b269 | ||
|
|
d19fcd5e21 | ||
|
|
c0981174a8 | ||
|
|
0b5f973b31 | ||
|
|
4159b3871c | ||
|
|
580c993c0b | ||
|
|
0cc29350a0 | ||
|
|
490a784993 | ||
|
|
9c774f96db | ||
|
|
99afe7ac07 | ||
|
|
b3f05fd925 | ||
|
|
683cfee88b | ||
|
|
3bcaf0ed5b | ||
|
|
edb76503d3 | ||
|
|
484038638f | ||
|
|
8dfb30fefe | ||
|
|
2a252d13b8 | ||
|
|
afa364cfc3 | ||
|
|
dfa36fb25d | ||
|
|
c8492b0c58 | ||
|
|
083ef803fe | ||
|
|
351f0269ae | ||
|
|
a29ae15ff7 | ||
|
|
34dded3b25 | ||
|
|
975b1a5e36 | ||
|
|
e11508f84d | ||
|
|
0772f6dcaf | ||
|
|
d3fe3a711a | ||
|
|
756d8356ca | ||
|
|
42003b4006 | ||
|
|
dc65a2b884 | ||
|
|
071ae79fa8 | ||
|
|
c11ccbae2d | ||
|
|
6ef86d8d20 | ||
|
|
985249c3d0 | ||
|
|
622e09862a | ||
|
|
7505599ea0 | ||
|
|
575c417403 | ||
|
|
9f7a3db8be | ||
|
|
029422679c | ||
|
|
05d6d2b51b | ||
|
|
4cff0384f7 | ||
|
|
68db366696 | ||
|
|
358538717c | ||
|
|
24603b3cef | ||
|
|
4eb9240806 | ||
|
|
0469f0b5ae | ||
|
|
0b8577d02b | ||
|
|
97135879a1 | ||
|
|
fef41f68c0 | ||
|
|
0ac19e3a4e | ||
|
|
2793d209a4 | ||
|
|
71e9c044e6 | ||
|
|
42e5f5150a | ||
|
|
90545057e9 | ||
|
|
cffd024e9e | ||
|
|
8c858592c4 | ||
|
|
4f1a1879e5 | ||
|
|
e88eed9a8d | ||
|
|
9581ae8245 | ||
|
|
4202b7a9dc | ||
|
|
b4c398542a | ||
|
|
081148b2d7 | ||
|
|
a32c4561ed | ||
|
|
cc79a96fa3 | ||
|
|
ff340ce3d8 | ||
|
|
134508193d | ||
|
|
c2b74aa83e | ||
|
|
3358eab991 | ||
|
|
a609e0aad4 | ||
|
|
f97866a961 | ||
|
|
e1987c42c4 | ||
|
|
18566715e1 | ||
|
|
79f0f3230c | ||
|
|
63a89d9f04 | ||
|
|
f639f39e79 | ||
|
|
b4099fc5f9 | ||
|
|
ff2513e276 | ||
|
|
f24d52436b | ||
|
|
9de6e8846b | ||
|
|
01a1213463 | ||
|
|
f0fbd9214a | ||
|
|
c4f37c550f | ||
|
|
448384af06 | ||
|
|
3f840f53a0 | ||
|
|
d8718d8ac8 | ||
|
|
2fb46a11dc | ||
|
|
9a11412719 | ||
|
|
98874be171 | ||
|
|
704f91545e | ||
|
|
efb3239cbd | ||
|
|
7e7ddeb9e2 | ||
|
|
9e8218089b | ||
|
|
3f660a3963 | ||
|
|
daeb6711b0 | ||
|
|
4e1aec28a0 | ||
|
|
5512917ec1 | ||
|
|
cd1edc5d56 | ||
|
|
4f52587586 | ||
|
|
d7ee4ef5f5 | ||
|
|
31f88e0f05 | ||
|
|
9f1740cc4f | ||
|
|
f2c15c7701 | ||
|
|
e67d0678f9 | ||
|
|
b1faa5eed4 | ||
|
|
7f1f0b9048 | ||
|
|
183e5f2ecc | ||
|
|
14efe4939a | ||
|
|
3dc7d77ea9 | ||
|
|
0f07bbb3e5 | ||
|
|
dd5a3416bf | ||
|
|
2fb49ad780 | ||
|
|
92f0e53fee | ||
|
|
876132694d | ||
|
|
1257ba41c6 | ||
|
|
2cc71ac7ed | ||
|
|
753808a4ce | ||
|
|
32cd694ad5 | ||
|
|
f008420891 | ||
|
|
fa8900be65 | ||
|
|
69c2f407d6 | ||
|
|
ffcd093db1 | ||
|
|
8dbf93750f | ||
|
|
e266a81167 | ||
|
|
e841aab9e7 | ||
|
|
49f259065d | ||
|
|
b10379e700 | ||
|
|
810d27a618 | ||
|
|
9b60c005c7 | ||
|
|
cc6ca0bda2 | ||
|
|
4512232637 | ||
|
|
2c092ffdef | ||
|
|
66406227d6 | ||
|
|
a11d25bb44 | ||
|
|
2e58d902b7 | ||
|
|
237794b05c | ||
|
|
563a587882 | ||
|
|
24505cd111 | ||
|
|
0c681cdab4 | ||
|
|
13ef3058c6 | ||
|
|
50b159b43d | ||
|
|
8c6c328730 | ||
|
|
c9812ddf08 | ||
|
|
2ef0449c2c | ||
|
|
5edc750c47 | ||
|
|
2f0e396d7f | ||
|
|
000a163beb | ||
|
|
80dd37ee31 | ||
|
|
e0b5645064 | ||
|
|
e51aacb0b7 | ||
|
|
2d6af94aa0 | ||
|
|
7cfce9ff7a | ||
|
|
7f088d6241 | ||
|
|
d11038f3de | ||
|
|
6df42a4be7 | ||
|
|
7fd111b91f | ||
|
|
dd7dc2ec5a | ||
|
|
86c586d882 | ||
|
|
66ac6f72fc | ||
|
|
f21f448099 | ||
|
|
548d70f30c | ||
|
|
39e714c6d8 | ||
|
|
9968af0785 | ||
|
|
be7586137c | ||
|
|
7999b66c3c | ||
|
|
c82a46c1ee | ||
|
|
666ab1941f | ||
|
|
71e37345b4 | ||
|
|
e7c82f20e3 | ||
|
|
afa771a980 | ||
|
|
0d1de98cca | ||
|
|
02bf7dca01 | ||
|
|
8cc76b1d86 | ||
|
|
77a275cbcd | ||
|
|
3956cbe2d2 | ||
|
|
945de8d9a0 | ||
|
|
6dabd3bb2d | ||
|
|
4c80808997 | ||
|
|
5a39f7cdde | ||
|
|
5d400fbe90 | ||
|
|
e36596470c | ||
|
|
668e549208 | ||
|
|
256ff31d11 | ||
|
|
2414d5d7f5 | ||
|
|
b7fc15d399 | ||
|
|
c09b4dabc4 | ||
|
|
a4aa4a91a3 | ||
|
|
8f0ea5925a | ||
|
|
936ad1aa20 | ||
|
|
d021bca6ef | ||
|
|
55ed6109c1 | ||
|
|
f6d765bf81 | ||
|
|
88e8f2bf83 | ||
|
|
c849759682 | ||
|
|
605eae21bc | ||
|
|
93eb277a88 | ||
|
|
8edf556c9e | ||
|
|
7fcb63230f | ||
|
|
12093a3dad | ||
|
|
ebb0ec6c42 | ||
|
|
188546515c | ||
|
|
c8990b0f68 | ||
|
|
7dced4b9d9 | ||
|
|
3145e67feb | ||
|
|
e9348d9b6a | ||
|
|
1a1b346c05 | ||
|
|
920d059837 | ||
|
|
bef5c3bd1b | ||
|
|
97037f7d03 | ||
|
|
a7392ed3d7 | ||
|
|
3eb1a7e384 | ||
|
|
1ecdc78c2f | ||
|
|
d279dba37e | ||
|
|
a4f97fa151 | ||
|
|
ff7ac582f0 | ||
|
|
d2c2456fbe | ||
|
|
e9f562a8b7 | ||
|
|
084e0a73dc | ||
|
|
10f991b8d0 | ||
|
|
79620c97d1 | ||
|
|
ffec9a4ddd | ||
|
|
9b18960bbd | ||
|
|
a009fdbdc3 | ||
|
|
c1fc3f373c | ||
|
|
f4cf5dc0cd | ||
|
|
355341f0ab | ||
|
|
7f65f7d3ca | ||
|
|
9fa096c6f4 | ||
|
|
70415a396a | ||
|
|
c921964938 | ||
|
|
3bf47a6838 | ||
|
|
d3d28f0623 | ||
|
|
f880b57544 | ||
|
|
32b7a26fa6 | ||
|
|
32fc34f922 | ||
|
|
b82a393692 | ||
|
|
3c7e792167 | ||
|
|
0ad66875ab | ||
|
|
1191ac2671 | ||
|
|
928b3425e3 | ||
|
|
0726a00e3b | ||
|
|
5a88984d34 | ||
|
|
18de60f68c | ||
|
|
1893359142 | ||
|
|
f5e5ab2436 | ||
|
|
ff5ea1a70d | ||
|
|
54ee63a409 | ||
|
|
f095606b50 | ||
|
|
e8f31c78d7 | ||
|
|
b34c477d5e | ||
|
|
28611304f7 | ||
|
|
76af9e6e1f | ||
|
|
7b3b965ed7 | ||
|
|
567b905ef1 | ||
|
|
a94268329c | ||
|
|
a11a18686a | ||
|
|
c58e3a99ee | ||
|
|
b166663e89 | ||
|
|
ac13ac14f6 | ||
|
|
06531f6d06 | ||
|
|
f6274d94f6 | ||
|
|
2b303a7e23 | ||
|
|
2bb074a5ad | ||
|
|
3b2db56243 | ||
|
|
45483fde74 | ||
|
|
d742cfa48f | ||
|
|
95353ce9eb | ||
|
|
ab2cc72814 | ||
|
|
5c54a2c008 | ||
|
|
2fe3082518 | ||
|
|
5a889d28c8 | ||
|
|
45e7c1c030 | ||
|
|
c6dcff0ae7 | ||
|
|
b791dc5e1a | ||
|
|
46db281006 | ||
|
|
636479b15b | ||
|
|
dcbb4eabb5 | ||
|
|
068cedaa84 | ||
|
|
02dd962601 | ||
|
|
256d715648 | ||
|
|
cbe97cdfde | ||
|
|
407dfc7547 | ||
|
|
a8e4e077ec | ||
|
|
3d06ba1878 | ||
|
|
8a23d1da58 | ||
|
|
d3eb61e0e4 | ||
|
|
7cdf2d244d | ||
|
|
c59a41a607 | ||
|
|
e0410b6f10 | ||
|
|
8eac6c0b48 | ||
|
|
bf8b74e996 | ||
|
|
691e41e22e | ||
|
|
15e91d42ee | ||
|
|
5e8e94fd0f | ||
|
|
5313a46aa2 | ||
|
|
761a8dde65 | ||
|
|
a73acfb9c2 | ||
|
|
fbe17dde03 | ||
|
|
a01a3404fe | ||
|
|
454e5dfc5d | ||
|
|
47545b45b8 | ||
|
|
7c9908d953 | ||
|
|
5f4cd50cc4 | ||
|
|
b0fba6ce5b | ||
|
|
1f5992f2c2 | ||
|
|
abfd3c3e5d | ||
|
|
97da7f9691 | ||
|
|
2752083d29 | ||
|
|
c826318da4 | ||
|
|
6582a4abd9 | ||
|
|
a699dab5b3 | ||
|
|
21c8ad5b9e | ||
|
|
195d885887 | ||
|
|
519bd2f30f | ||
|
|
20ef724fad | ||
|
|
f443cbaa2b | ||
|
|
dbf45da8ab | ||
|
|
6b67902d53 | ||
|
|
0ad0ef485c | ||
|
|
7dfe3e53d5 | ||
|
|
5be3bd1e64 | ||
|
|
bc0c1980db | ||
|
|
2997258fd0 | ||
|
|
11600fc116 | ||
|
|
a8640f52ef | ||
|
|
0f4e44c38f | ||
|
|
053f4d481d | ||
|
|
f466c27da9 | ||
|
|
bfe6bc3095 | ||
|
|
ff8f3e766e | ||
|
|
6635ea3e29 | ||
|
|
591788c0df | ||
|
|
571b8986a4 | ||
|
|
bb7a74e4b4 | ||
|
|
76ddfeb93a | ||
|
|
c38b826abf | ||
|
|
21d7db0959 | ||
|
|
d7b51d2807 | ||
|
|
a7af8b5722 | ||
|
|
9c93fe6003 | ||
|
|
21505a7470 | ||
|
|
ba6e6cc15a | ||
|
|
fd7bf2bc3a | ||
|
|
b2cd24ed1b | ||
|
|
66cf2c984a | ||
|
|
de1b2b19b0 | ||
|
|
e31583485d | ||
|
|
490e51c1d7 | ||
|
|
1df2a04713 | ||
|
|
42804d5314 | ||
|
|
558710bbdd | ||
|
|
f4926cb822 | ||
|
|
1e77e0862a | ||
|
|
8c696cb8ca | ||
|
|
62ef8ade8f | ||
|
|
3d88dd3123 | ||
|
|
880b348ce6 | ||
|
|
31fe3a1cd8 | ||
|
|
19182ffddf | ||
|
|
afcc60066e | ||
|
|
d3ade06421 | ||
|
|
f1a3ef9590 | ||
|
|
d1d73f11a5 | ||
|
|
05697372f8 | ||
|
|
0c1f68816e | ||
|
|
92546e8a74 | ||
|
|
a4faa3f392 | ||
|
|
df191cd2b5 | ||
|
|
baa19f0ccf | ||
|
|
5a49bd3ac9 | ||
|
|
b37d7e0500 | ||
|
|
f4ed6274a4 | ||
|
|
56eb1a1cf9 | ||
|
|
a7c156a9e3 | ||
|
|
d81ca77231 | ||
|
|
bf013f6ebb | ||
|
|
dd8116e285 | ||
|
|
b5d80a88d1 | ||
|
|
7f4f95cf83 | ||
|
|
87c2f6ad14 | ||
|
|
ad47dba064 | ||
|
|
41b701846f | ||
|
|
5c42830328 | ||
|
|
69617309f8 | ||
|
|
48e2d6a8da | ||
|
|
b4120cddfb | ||
|
|
54e3f1998a | ||
|
|
edcf9f1b0c | ||
|
|
de3747d65e | ||
|
|
b76a3614da | ||
|
|
94cc64c51b | ||
|
|
0f71edee96 | ||
|
|
e097c097fe | ||
|
|
1443a5b175 | ||
|
|
2d82ad93dd | ||
|
|
384c257a74 | ||
|
|
49dfa2c3a0 | ||
|
|
7bd3e768db | ||
|
|
65224ed22b | ||
|
|
0a28dfe1e2 | ||
|
|
1c8ebfacb0 | ||
|
|
5d6d241791 | ||
|
|
4f116d15b9 | ||
|
|
228570640e | ||
|
|
65a79610aa | ||
|
|
24984ea4f2 | ||
|
|
048b2af0fc | ||
|
|
449989ddd9 | ||
|
|
01ebe5724a | ||
|
|
95fb230b8c | ||
|
|
632971af15 | ||
|
|
5787aa1078 | ||
|
|
d8b9265484 | ||
|
|
9ea3169ca9 | ||
|
|
aebf2672cd | ||
|
|
68ac409bfd | ||
|
|
fef44bd24f | ||
|
|
e4a7617dde | ||
|
|
4dfb193d10 | ||
|
|
c248d94995 | ||
|
|
d4ac458d17 | ||
|
|
93e443c4ad | ||
|
|
4b3988cef9 | ||
|
|
4eb5ee17b4 | ||
|
|
e1b63d7dec | ||
|
|
4b5651bd6f | ||
|
|
50515d9128 | ||
|
|
28b5faab0c | ||
|
|
82a01c22d3 | ||
|
|
be9b0c2e8f | ||
|
|
b6affe06a5 | ||
|
|
1e05f8c646 | ||
|
|
7e9d4512b6 | ||
|
|
5fa127c415 | ||
|
|
ac26681fe7 | ||
|
|
3c62636133 | ||
|
|
ca874fa12c | ||
|
|
c3508bbb99 | ||
|
|
6935033db5 | ||
|
|
421277d730 | ||
|
|
56988944b5 | ||
|
|
528601d25a | ||
|
|
ddd153c00d | ||
|
|
b8c1588284 | ||
|
|
4dac9e40bd | ||
|
|
def1811d48 | ||
|
|
c53e507713 | ||
|
|
e0ea777249 | ||
|
|
4c1962f3c7 | ||
|
|
258e89c964 | ||
|
|
3d3bfb42e5 | ||
|
|
6dbd8baa7e | ||
|
|
e660fabc57 | ||
|
|
2115bcd8b0 | ||
|
|
1bdd6e1a9d | ||
|
|
98deec232b | ||
|
|
022c217cfe | ||
|
|
81f57949ed | ||
|
|
fca5eb083f | ||
|
|
a3695cc66b | ||
|
|
6723d20616 | ||
|
|
627ec91687 | ||
|
|
9126cf0c73 | ||
|
|
16322ab30c | ||
|
|
5682917356 | ||
|
|
c91ccc8b4e | ||
|
|
63f670fc36 | ||
|
|
e20b07fa24 | ||
|
|
472656517f | ||
|
|
d232cba02d | ||
|
|
e49d29a914 | ||
|
|
3aa1a68cdc | ||
|
|
f94452083f | ||
|
|
ce1ee5cb9d | ||
|
|
48df6b8485 | ||
|
|
ae23ae2d37 | ||
|
|
e34e04af04 | ||
|
|
ff3f377911 | ||
|
|
18065826b9 | ||
|
|
84e19ceef0 | ||
|
|
59161efd08 | ||
|
|
6663fd3526 | ||
|
|
2c44e1bb93 | ||
|
|
e3f6399473 | ||
|
|
89c2c21774 | ||
|
|
2954eb4bdc | ||
|
|
e08de91666 | ||
|
|
a170acb9d7 | ||
|
|
6a086bb222 | ||
|
|
b2f152e641 | ||
|
|
6c5b261804 | ||
|
|
8bd0c44e83 | ||
|
|
34c36984e9 | ||
|
|
8bd6aca0dd | ||
|
|
983b74be77 | ||
|
|
a3eafdd2c6 | ||
|
|
ea75a09f95 | ||
|
|
4c747c4148 | ||
|
|
49abfcafed | ||
|
|
50710c72ad | ||
|
|
2e299b3814 | ||
|
|
43d11d877d | ||
|
|
d7e7df3bd9 | ||
|
|
8d8ba11221 | ||
|
|
2536a18c00 | ||
|
|
11728b2b15 | ||
|
|
627501b9ba | ||
|
|
3599384b38 | ||
|
|
4b307cad2c | ||
|
|
7496d51580 | ||
|
|
4194ac894c | ||
|
|
ffb5d9ea9c | ||
|
|
770b28ca30 | ||
|
|
62e464f706 | ||
|
|
8d0dc37ec0 | ||
|
|
fe41df87bb | ||
|
|
8276a0775d | ||
|
|
abfb3bb3bb | ||
|
|
e184eb4a23 | ||
|
|
d0fc372ecd | ||
|
|
6f54c57647 | ||
|
|
e8ae103d5f | ||
|
|
b0198dab6c | ||
|
|
b75ec09998 | ||
|
|
c8ac6c07b0 | ||
|
|
27814e3015 | ||
|
|
f59309a445 | ||
|
|
b0292d7319 | ||
|
|
7f18616cc0 | ||
|
|
2fef98a5af | ||
|
|
36765caedc | ||
|
|
f7aed10ea2 | ||
|
|
410bbb8285 | ||
|
|
f56ea52932 | ||
|
|
cb4361b7b7 | ||
|
|
ecd332c573 | ||
|
|
a0fe78a728 | ||
|
|
49cc9c529e | ||
|
|
7635b2c33f | ||
|
|
50c26d33ab | ||
|
|
f642fb3b99 | ||
|
|
e68dd866a3 | ||
|
|
73d36fdff0 | ||
|
|
5561cd3c77 | ||
|
|
32a9acb913 | ||
|
|
f7f23c6e77 | ||
|
|
3d4edbd9dc | ||
|
|
bdf385f374 | ||
|
|
9f78c3e64b | ||
|
|
f370052815 | ||
|
|
9df4b10067 | ||
|
|
d20517483e | ||
|
|
713ce4719b | ||
|
|
f3d39e7515 | ||
|
|
61783ffc82 | ||
|
|
05c4ad01d5 | ||
|
|
12647dcf30 | ||
|
|
da38f59e62 | ||
|
|
cf4ef54dc5 | ||
|
|
12e9873514 | ||
|
|
f7c0e407ca | ||
|
|
82c7662cdf | ||
|
|
4f0bced53e | ||
|
|
f1b6c9f4aa | ||
|
|
0ab31ab0df | ||
|
|
46e8f0779f | ||
|
|
3fb72a4d20 | ||
|
|
db20f65d7c | ||
|
|
63cfe7b47b | ||
|
|
db590091b3 | ||
|
|
7b25e74418 | ||
|
|
82f303e1c6 | ||
|
|
c038683b54 | ||
|
|
3a37ed6b60 | ||
|
|
706a492218 | ||
|
|
c0be5383de | ||
|
|
3b8ce85092 | ||
|
|
b6298f8602 | ||
|
|
abfec57972 | ||
|
|
470fc97d1f | ||
|
|
8d59caf635 | ||
|
|
acf25aa4d3 | ||
|
|
16de4674ec | ||
|
|
65b0ea792e | ||
|
|
fc6b02f607 | ||
|
|
136d8c39d9 | ||
|
|
24a8b41182 | ||
|
|
810cf4dee8 | ||
|
|
9bf835e810 | ||
|
|
eca37bce38 | ||
|
|
3ee6a2baf2 | ||
|
|
69fa7f238d | ||
|
|
de2306bd12 | ||
|
|
714feeb9a7 | ||
|
|
ca99808fd2 | ||
|
|
f8f8c28fec | ||
|
|
f497867ba5 | ||
|
|
383192784d | ||
|
|
605189bc6e | ||
|
|
c0a2e3674c | ||
|
|
76f0602684 | ||
|
|
477ff12cde | ||
|
|
9c09ad3b62 | ||
|
|
a967afc629 | ||
|
|
dcc1fd3ee4 | ||
|
|
933f020b3c | ||
|
|
f5c02be5bf | ||
|
|
68fbdd474c | ||
|
|
2cbc048352 | ||
|
|
e990ffd4a0 | ||
|
|
743c7c9326 | ||
|
|
067248da75 | ||
|
|
f5c982355a | ||
|
|
f98c68a280 | ||
|
|
773bf0c6bc | ||
|
|
080ab6032c | ||
|
|
350144df29 | ||
|
|
9ac0f11d9a | ||
|
|
8079d456ab | ||
|
|
acf166cf9d | ||
|
|
439d497a13 | ||
|
|
0580932610 | ||
|
|
85399f609c | ||
|
|
4bcfee397b | ||
|
|
34bcb1dd26 | ||
|
|
117d1ed080 | ||
|
|
f324252681 | ||
|
|
0dad06cdfe | ||
|
|
9396288ca2 | ||
|
|
f89f08833e | ||
|
|
79e8962854 | ||
|
|
34e5a7cd24 | ||
|
|
7343c195b7 | ||
|
|
0af041b54e | ||
|
|
92a8a3e91f | ||
|
|
f41575d8b0 | ||
|
|
d93c4a5103 | ||
|
|
6fe9b69aad | ||
|
|
5d162f81c4 | ||
|
|
4771c2810b | ||
|
|
0cd99712fa | ||
|
|
b591af7803 | ||
|
|
171d68ca72 | ||
|
|
bade4f2c6a | ||
|
|
5754782a4e | ||
|
|
decdd54c19 | ||
|
|
ffe47300a1 | ||
|
|
6f9c3c4ff3 | ||
|
|
9b3efffba9 | ||
|
|
003fea52b1 | ||
|
|
2b17c77195 | ||
|
|
c252a50fd7 | ||
|
|
cf8f042a20 | ||
|
|
844bc2d808 | ||
|
|
27f7fa7153 | ||
|
|
b325aa4555 | ||
|
|
c2c3bf0ba4 | ||
|
|
0d977b54f7 | ||
|
|
20860da4b4 | ||
|
|
3ea10b7cf9 | ||
|
|
1ec33863bc | ||
|
|
a260e99090 | ||
|
|
25efdd3d6f | ||
|
|
00a1e18959 | ||
|
|
c59f8adc4a | ||
|
|
1eb83ad812 | ||
|
|
7717f0a6b0 | ||
|
|
5e1fba3603 | ||
|
|
66cc9bc545 | ||
|
|
12aa5838d9 | ||
|
|
4f73534837 | ||
|
|
c4d145835c | ||
|
|
f822ca5b23 | ||
|
|
8aaa45c62a | ||
|
|
2f4f257070 | ||
|
|
97c1e181c5 | ||
|
|
ea80cddd57 | ||
|
|
09a294c219 | ||
|
|
408399eae0 | ||
|
|
391852a102 | ||
|
|
5b37de8fe5 | ||
|
|
7df23ceb74 | ||
|
|
6099f3b015 | ||
|
|
a5cc31783c | ||
|
|
6b34ec3ab9 | ||
|
|
5c333dec33 | ||
|
|
775d095b3c | ||
|
|
7679b5d516 | ||
|
|
7702094053 | ||
|
|
3798d50457 | ||
|
|
95e1e57407 | ||
|
|
93ba4cca68 | ||
|
|
fe4981da21 | ||
|
|
e4f94c4c52 | ||
|
|
708fe514f8 | ||
|
|
11c882380f | ||
|
|
fb93af665d | ||
|
|
0db405f2cc | ||
|
|
fb8000b58b | ||
|
|
1b9d8e068a | ||
|
|
038f73a5f7 | ||
|
|
649b49ff45 | ||
|
|
1418bc454d | ||
|
|
29cc372bfa | ||
|
|
69b00d3782 | ||
|
|
a328e2bf3c | ||
|
|
4c1ea0e421 | ||
|
|
7e01f9c95e | ||
|
|
8b28baabd7 | ||
|
|
f49966d86e | ||
|
|
f4ac7c8e7c | ||
|
|
2b65e1ffc2 | ||
|
|
c81a3fa286 | ||
|
|
44f005077d | ||
|
|
013b6e68ec | ||
|
|
95c964673d | ||
|
|
94ec11db58 | ||
|
|
c4e8dda37c | ||
|
|
e136fb3a4f | ||
|
|
01b985eded | ||
|
|
1f0a35f073 | ||
|
|
2b9b019093 | ||
|
|
10186a9e3d | ||
|
|
89d8fea7d2 | ||
|
|
f623b98858 | ||
|
|
632cee1613 | ||
|
|
c0f2164bc5 | ||
|
|
f6e4a27fdd | ||
|
|
257ceb99f7 | ||
|
|
706d53065b | ||
|
|
0f95a7babe | ||
|
|
7cb2806878 | ||
|
|
9c0e18975c | ||
|
|
3da318b48e | ||
|
|
dfe1f2c108 | ||
|
|
f42a87b51a | ||
|
|
ab25857176 | ||
|
|
7da36079c1 | ||
|
|
2bef967af1 | ||
|
|
7e4194418a | ||
|
|
aa02057895 | ||
|
|
fb8dc07599 | ||
|
|
66e30a7723 | ||
|
|
0298ab99c4 | ||
|
|
d11358671e | ||
|
|
8b5cb4c7b0 | ||
|
|
aad52ae743 | ||
|
|
8ddab84745 | ||
|
|
6865652125 | ||
|
|
ed4d0867e8 | ||
|
|
1c71e02454 | ||
|
|
f332e87cab | ||
|
|
023dbc6cb5 | ||
|
|
4dd3f55407 | ||
|
|
7b9a71c9af | ||
|
|
901d22cdfa | ||
|
|
93e1266ee7 | ||
|
|
0a4e7eea41 | ||
|
|
e3801d6965 | ||
|
|
336f1687c1 | ||
|
|
d4e2f2df6e | ||
|
|
f152b4c26e | ||
|
|
bd935b0553 | ||
|
|
a9b3b7a359 | ||
|
|
7a007b342a | ||
|
|
0783f3d5b6 | ||
|
|
afe3c2bc1b | ||
|
|
82f8948fd4 | ||
|
|
b9cdc755d1 | ||
|
|
a6f81c66e5 | ||
|
|
1ff45ac5f5 | ||
|
|
48bde7375f | ||
|
|
0601fa3b3d | ||
|
|
d0d3c8dbfd | ||
|
|
8057de1973 | ||
|
|
43c1105d62 | ||
|
|
6adf516b30 | ||
|
|
bf80b08b5f | ||
|
|
3e0b1df46d | ||
|
|
84811c80b6 | ||
|
|
45e0df9c57 | ||
|
|
bc51ce7c7b | ||
|
|
b693d13b93 | ||
|
|
39982d57ef | ||
|
|
15e27e54fb | ||
|
|
851404205b | ||
|
|
117ae71025 | ||
|
|
027ec70262 | ||
|
|
55fdee4d65 | ||
|
|
0d42f937dd | ||
|
|
ac8372dd26 | ||
|
|
5e56a6bbee | ||
|
|
3c6c409df0 | ||
|
|
d05408c89f | ||
|
|
ee0ec3fbfa | ||
|
|
122a73e086 | ||
|
|
29a9b18c4c | ||
|
|
1561272109 | ||
|
|
3e61ab0d25 | ||
|
|
a49dc6ccb7 | ||
|
|
60f3d62f00 | ||
|
|
e613855a4f | ||
|
|
22662d7e03 | ||
|
|
6e7e5be1a2 | ||
|
|
8b2ab778c9 | ||
|
|
35f3766ecf | ||
|
|
995304dabb | ||
|
|
803982a271 | ||
|
|
9164bf22c2 | ||
|
|
911a576893 | ||
|
|
79ee85c0f9 | ||
|
|
483dbcdc40 | ||
|
|
a1096b5bf0 | ||
|
|
5ac0e64edb | ||
|
|
60b2624607 | ||
|
|
d2e2847b03 | ||
|
|
b9669f54f7 | ||
|
|
8c7bd77d33 | ||
|
|
ba1ce16b8b | ||
|
|
68090943f4 | ||
|
|
a4fb1297b0 | ||
|
|
860a05abf2 | ||
|
|
8bb2f356c0 | ||
|
|
4950020635 | ||
|
|
0a6140c6eb | ||
|
|
bba2ac8817 | ||
|
|
331b1f542f | ||
|
|
ccb55205e6 | ||
|
|
9cc91b30b3 | ||
|
|
e836caf31e | ||
|
|
beaa1e5be2 | ||
|
|
ea545bae26 | ||
|
|
1c9ec2df45 | ||
|
|
b76c80e2ce | ||
|
|
236990f4a3 | ||
|
|
1ed32df20d | ||
|
|
8476eb9f4b | ||
|
|
735af7843b | ||
|
|
ded73e958b | ||
|
|
6dcb84d4f4 | ||
|
|
501bc9f438 | ||
|
|
f88e812b63 | ||
|
|
be6386c410 | ||
|
|
2af4fd17c4 | ||
|
|
f870418bd0 | ||
|
|
00659e4795 | ||
|
|
cdda10207e | ||
|
|
701700279f | ||
|
|
a9d804724a | ||
|
|
e033a9ab47 | ||
|
|
059e5fb8aa | ||
|
|
a78f255928 | ||
|
|
1d10e69288 | ||
|
|
63590d379c | ||
|
|
5f63e88984 | ||
|
|
75584e2b19 | ||
|
|
1426ee2ebd | ||
|
|
b6643b7bfc | ||
|
|
721dfdf553 | ||
|
|
2963747d14 | ||
|
|
e7350d5041 | ||
|
|
f37e8f4ca8 | ||
|
|
594c2accc0 | ||
|
|
7acfac6a91 | ||
|
|
0646f48e14 | ||
|
|
37565fd067 | ||
|
|
26b2e7dc5d | ||
|
|
c3313623e4 | ||
|
|
2089223690 | ||
|
|
52e1b84d41 | ||
|
|
8794141b7f | ||
|
|
f6126dd20e | ||
|
|
18acfda99b | ||
|
|
bec5edca84 | ||
|
|
6fb20b3ee5 | ||
|
|
eaf4d8064b | ||
|
|
2a5f5b1bba | ||
|
|
c538a77937 | ||
|
|
aa9e7b1ed1 | ||
|
|
a3066eddab | ||
|
|
d1729fa787 | ||
|
|
93961dde2c | ||
|
|
1024e68eb6 | ||
|
|
6ae2c9387d | ||
|
|
fba83e2330 | ||
|
|
f1295cb7d6 | ||
|
|
dc61dfbde6 | ||
|
|
21466426da | ||
|
|
3f0136362b | ||
|
|
e92d77bbec | ||
|
|
07bd36c94b | ||
|
|
b1dbbdef12 | ||
|
|
3e479726ec | ||
|
|
4cc41eccb3 | ||
|
|
8f08ae59ac | ||
|
|
e8d4e492d6 | ||
|
|
b8090a8e18 | ||
|
|
c609a01e55 | ||
|
|
c97fb385cd | ||
|
|
da6c57750e | ||
|
|
6951d926f7 | ||
|
|
6623195bd5 | ||
|
|
ec31bb9a82 | ||
|
|
8618cc383a | ||
|
|
4b01e3a3c7 | ||
|
|
7f748c23c1 | ||
|
|
963d248cc7 | ||
|
|
657056e636 | ||
|
|
9d5efea66e | ||
|
|
658d74e026 | ||
|
|
5113f6d375 | ||
|
|
96405c26d0 | ||
|
|
4ea5f34bf3 | ||
|
|
dbd13a2019 | ||
|
|
06773235da | ||
|
|
e57556a8af | ||
|
|
b54b78c29d | ||
|
|
317336f771 | ||
|
|
b4e52f6135 | ||
|
|
f2ca042915 | ||
|
|
1060dd2906 | ||
|
|
2e0f7a82fa | ||
|
|
5798536559 | ||
|
|
ab9a83c82f | ||
|
|
c87fdbea0f | ||
|
|
ec8fffe61c | ||
|
|
61d52991f1 | ||
|
|
9100186dce | ||
|
|
d2bc2cfcf8 | ||
|
|
5a71998b4e | ||
|
|
42278f12ff | ||
|
|
f5593e051c | ||
|
|
a27e30cf54 | ||
|
|
79140c7636 | ||
|
|
1f4c595cd3 | ||
|
|
b5b62e03af | ||
|
|
67e2a4720e | ||
|
|
f5c2d72429 | ||
|
|
2f5331ab48 | ||
|
|
7f8257152f | ||
|
|
0cd80f2556 | ||
|
|
1717387876 | ||
|
|
109363ebf6 | ||
|
|
716c4fa386 | ||
|
|
9a09b4eb20 | ||
|
|
95a5b57265 | ||
|
|
13fbf397d1 | ||
|
|
20be99ec8a | ||
|
|
04c53c3578 | ||
|
|
51bc27a869 | ||
|
|
71b083794c | ||
|
|
b100d0c503 | ||
|
|
76061296c9 | ||
|
|
bb303d2da1 | ||
|
|
c91c070343 | ||
|
|
aec06a6f61 | ||
|
|
e8ba671fc2 | ||
|
|
1860e5d133 | ||
|
|
f2cb3c38fe | ||
|
|
9a28dd4f6e | ||
|
|
d2acd59ea8 | ||
|
|
79dfdb29e7 | ||
|
|
eb21c8b42e | ||
|
|
541bb53553 | ||
|
|
fe8997efae | ||
|
|
23455c722c | ||
|
|
5ce29c30d2 | ||
|
|
70d67728fd | ||
|
|
e546884b08 | ||
|
|
b36e6d987d | ||
|
|
53c3dd5e8b | ||
|
|
da723b207a | ||
|
|
e050f77198 | ||
|
|
540b4b7ea9 | ||
|
|
bbef22daf7 | ||
|
|
9ed110c91b | ||
|
|
a30d510eb1 | ||
|
|
ef98eaed8f | ||
|
|
2a257f327c | ||
|
|
4060c2107c | ||
|
|
cd23d27048 | ||
|
|
18b86e4fd2 | ||
|
|
5f2e22a259 | ||
|
|
4e97b18977 | ||
|
|
f9bde347bc | ||
|
|
947a7d6a2f | ||
|
|
872ab2e99b | ||
|
|
90b8813bb7 | ||
|
|
88d0f63294 | ||
|
|
79fa0d3a90 | ||
|
|
8e61080a4a | ||
|
|
3f9a64417b | ||
|
|
eb959379e8 | ||
|
|
41a644afb9 | ||
|
|
6b42db943d | ||
|
|
1c325459eb | ||
|
|
6d88d8ad95 | ||
|
|
246997f273 | ||
|
|
b6144ae582 | ||
|
|
afe17c73b4 | ||
|
|
b51b884fc7 | ||
|
|
d3e4b29e62 | ||
|
|
24059e7403 | ||
|
|
107a2a6682 | ||
|
|
56b4ab6672 | ||
|
|
4662454938 | ||
|
|
db4f78d463 | ||
|
|
880de21596 | ||
|
|
622dd84c9e | ||
|
|
f983bfc883 | ||
|
|
45cdb3fdb0 | ||
|
|
9a707236b8 | ||
|
|
e9e6ad3bb0 | ||
|
|
ab78a81d15 | ||
|
|
18340099b7 | ||
|
|
a013696a41 | ||
|
|
8a2a6d9232 | ||
|
|
12aa6d86e4 | ||
|
|
7d08969d28 | ||
|
|
dda4aa8488 | ||
|
|
cdaef3d801 | ||
|
|
9159166128 | ||
|
|
dc0882e043 | ||
|
|
c811f015ef | ||
|
|
d8f0b66fe1 | ||
|
|
dc3d57deba | ||
|
|
d089698475 | ||
|
|
8ed2dd6687 | ||
|
|
50305ca1fe | ||
|
|
3e91567636 | ||
|
|
0b4dd63d36 | ||
|
|
38d0f85deb | ||
|
|
c5b452f369 | ||
|
|
6ce9225f52 | ||
|
|
13a8820603 | ||
|
|
503997a09a | ||
|
|
17efdff134 | ||
|
|
984f32f994 | ||
|
|
eee7f097e3 | ||
|
|
086059ec30 | ||
|
|
7ff22c68c7 | ||
|
|
1232113772 | ||
|
|
039d4936cb | ||
|
|
784dd80965 | ||
|
|
1ffe9bd83b | ||
|
|
0c28b23224 | ||
|
|
ec1af9dc1e | ||
|
|
ff4cea229a | ||
|
|
3f81f9371f | ||
|
|
60e89a7d22 | ||
|
|
c50daa5c9e | ||
|
|
58d00ab863 | ||
|
|
ce916459c5 | ||
|
|
4094d560ab | ||
|
|
4dbf7eb04b | ||
|
|
a39577c44d | ||
|
|
125ee46685 | ||
|
|
ce84f1762c | ||
|
|
a687d1347b | ||
|
|
6d9db20614 | ||
|
|
c62dfc1bcc | ||
|
|
aabe2696fe | ||
|
|
ae0d605310 | ||
|
|
2a694596b5 | ||
|
|
ff0a76606e | ||
|
|
dead74801d | ||
|
|
ab207a1bb3 | ||
|
|
f152e8c33d | ||
|
|
797ba4fbf4 | ||
|
|
a848f10bba | ||
|
|
552ec1eb35 | ||
|
|
1385d2a4f4 | ||
|
|
3b5c9abf7a | ||
|
|
e0fa032bd3 | ||
|
|
7b69650fcd | ||
|
|
08a8df489f | ||
|
|
9f35a8a520 | ||
|
|
0df891b336 | ||
|
|
385853a290 | ||
|
|
fa3ef8a1c1 | ||
|
|
c93ada03c7 | ||
|
|
0064b01ae0 | ||
|
|
1469b82aa2 | ||
|
|
2d5cf8a6fe | ||
|
|
290959f74c | ||
|
|
4d9f58ee72 | ||
|
|
9241246de6 | ||
|
|
58a5d52b78 | ||
|
|
2906178ac3 | ||
|
|
e0afbb647b | ||
|
|
50be50cf6a | ||
|
|
77a9d3a5bc | ||
|
|
f9c7a4c933 | ||
|
|
2b759b84b0 | ||
|
|
1e45c63ea5 | ||
|
|
b14a260827 | ||
|
|
ade1597e03 | ||
|
|
2739d3cb67 | ||
|
|
dc5e78e142 | ||
|
|
e9759a5868 | ||
|
|
e7ab802498 | ||
|
|
42672c2e27 | ||
|
|
e65d61d313 | ||
|
|
076da5c7c4 | ||
|
|
9deaf2507c | ||
|
|
5c114c67de | ||
|
|
d904cb0441 | ||
|
|
bd1dd9d863 | ||
|
|
afebe734b8 | ||
|
|
e21a78164e | ||
|
|
1e0f96d0fd | ||
|
|
bf650332d8 | ||
|
|
f32e0af830 | ||
|
|
4c94f90e5d | ||
|
|
ffb4224640 | ||
|
|
89fff4830b | ||
|
|
16e4c67992 | ||
|
|
cf47214ee4 | ||
|
|
0feab753fb | ||
|
|
d0b6318b90 | ||
|
|
966e23b846 | ||
|
|
5b8a1fc2a7 | ||
|
|
02ea3ca525 | ||
|
|
0632b146b8 | ||
|
|
1b0b180761 | ||
|
|
0d11f73a1d | ||
|
|
533cb8eb58 | ||
|
|
8ac1181e9a | ||
|
|
5ca1892eb0 | ||
|
|
e32db6a0e8 | ||
|
|
82fff615d6 | ||
|
|
24a8f0808d | ||
|
|
4a7c3c06bc | ||
|
|
da93bbc1fe | ||
|
|
fa2dbe981e | ||
|
|
ce6cceae8b | ||
|
|
7b26e8b818 | ||
|
|
2da5fcb00b | ||
|
|
a079966f97 | ||
|
|
468796c23d | ||
|
|
5833aadef5 | ||
|
|
eb261c8026 | ||
|
|
a4c48847d1 | ||
|
|
43288be091 | ||
|
|
1ad7a6fe93 | ||
|
|
4e0a3f5e72 | ||
|
|
d7c33f647d | ||
|
|
9087207dc0 | ||
|
|
2760f37e6b | ||
|
|
3fa3426032 | ||
|
|
2e4dc91b96 | ||
|
|
aaaaa3d044 | ||
|
|
1edc4449d5 | ||
|
|
f3cd4da026 | ||
|
|
872c55207c | ||
|
|
339ca6d666 | ||
|
|
4aeac3b8f4 | ||
|
|
d625beb7f3 | ||
|
|
735b65c50c | ||
|
|
efb1eab327 | ||
|
|
49d4785da0 | ||
|
|
28e65ce383 | ||
|
|
c3b6a48373 | ||
|
|
a42ebd429b | ||
|
|
8f89010752 | ||
|
|
105a18f719 | ||
|
|
eb04ca4c4a | ||
|
|
6092d7ca88 | ||
|
|
66cad101c0 | ||
|
|
0a14f43f9c | ||
|
|
311c1f0dfd | ||
|
|
0499588107 | ||
|
|
d4d837a562 | ||
|
|
fbcbb20178 | ||
|
|
0914700fc6 | ||
|
|
eeced2fb5b | ||
|
|
6509e3d4f5 | ||
|
|
317052604b | ||
|
|
5538f7168c | ||
|
|
dcb9e4cd93 | ||
|
|
d9382f59bf | ||
|
|
403a0c770a | ||
|
|
f0f1cdc501 | ||
|
|
4e272b70ef | ||
|
|
8dc62a0232 | ||
|
|
9225b47568 | ||
|
|
d462873e74 | ||
|
|
fc19b50290 | ||
|
|
333fe6da0e | ||
|
|
75fcda9f81 | ||
|
|
44ba2a9903 | ||
|
|
2fceb1ad96 | ||
|
|
bacb5fa462 | ||
|
|
67f8dc494e | ||
|
|
3e4caabecb | ||
|
|
dcd5183b24 | ||
|
|
d80c6b42a6 |
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ""
|
||||||
|
labels: ""
|
||||||
|
assignees: ""
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
## READ BEFORE OPENING ISSUES
|
||||||
|
|
||||||
|
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 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 a crash of Magisk app, dump the full `logcat` **when the crash happens**.
|
||||||
|
|
||||||
|
If you experience other issues related to Magisk, upload `magisk.log`, and preferably also include a boot `logcat` (start dumping `logcat` when the device boots up)
|
||||||
|
|
||||||
|
**DO NOT** open issues regarding root detection.
|
||||||
|
|
||||||
|
**DO NOT** ask for instructions.
|
||||||
|
|
||||||
|
**DO NOT** report issues if you have any modules installed.
|
||||||
|
|
||||||
|
Without following the rules above, your issue will be closed without explanation.
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
Device:
|
||||||
|
Android version:
|
||||||
|
Magisk version name:
|
||||||
|
Magisk version code:
|
||||||
6
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
6
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: XDA Community Support
|
||||||
|
url: https://forum.xda-developers.com/f/magisk.5903/
|
||||||
|
about: Please ask and answer questions here.
|
||||||
|
|
||||||
108
.github/workflows/build.yml
vendored
Normal file
108
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
name: Magisk Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
paths:
|
||||||
|
- 'app/**'
|
||||||
|
- 'native/**'
|
||||||
|
- 'stub/**'
|
||||||
|
- 'buildSrc/**'
|
||||||
|
- 'build.py'
|
||||||
|
- 'gradle.properties'
|
||||||
|
- '.github/workflows/build.yml'
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build on ${{ matrix.os }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||||
|
env:
|
||||||
|
NDK_CCACHE: ccache
|
||||||
|
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||||
|
CCACHE_COMPILERCHECK: "%compiler% -dumpmachine; %compiler% -dumpversion"
|
||||||
|
RUSTC_WRAPPER: sccache
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: 'recursive'
|
||||||
|
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: Set up ccache
|
||||||
|
uses: hendrikmuhs/ccache-action@v1.2
|
||||||
|
with:
|
||||||
|
key: ${{ runner.os }}-${{ github.sha }}
|
||||||
|
restore-keys: ${{ runner.os }}
|
||||||
|
|
||||||
|
- name: Set up sccache
|
||||||
|
uses: hendrikmuhs/ccache-action@v1.2
|
||||||
|
with:
|
||||||
|
variant: sccache
|
||||||
|
key: ${{ runner.os }}-${{ github.sha }}
|
||||||
|
restore-keys: ${{ runner.os }}
|
||||||
|
|
||||||
|
- name: Cache Gradle dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
!~/.gradle/caches/build-cache-*
|
||||||
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
|
||||||
|
restore-keys: ${{ runner.os }}-gradle-
|
||||||
|
|
||||||
|
- name: Cache build cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches/build-cache-*
|
||||||
|
key: ${{ runner.os }}-build-cache-${{ github.sha }}
|
||||||
|
restore-keys: ${{ runner.os }}-build-cache-
|
||||||
|
|
||||||
|
- name: Set up NDK
|
||||||
|
run: python build.py -v ndk
|
||||||
|
|
||||||
|
- name: Build release
|
||||||
|
run: |
|
||||||
|
python build.py -vr all
|
||||||
|
|
||||||
|
- name: Build debug
|
||||||
|
run: |
|
||||||
|
python build.py -v all
|
||||||
|
|
||||||
|
- name: Stop gradle daemon
|
||||||
|
run: ./gradlew --stop
|
||||||
|
|
||||||
|
# Only upload artifacts built on Linux
|
||||||
|
- name: Upload build artifact
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ github.sha }}
|
||||||
|
path: out
|
||||||
|
|
||||||
|
- name: Upload mapping and native debug symbols
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ github.sha }}-symbols
|
||||||
|
path: app/build/outputs
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,7 @@ out
|
|||||||
*.apk
|
*.apk
|
||||||
/config.prop
|
/config.prop
|
||||||
/update.sh
|
/update.sh
|
||||||
|
/dict.txt
|
||||||
|
|
||||||
# Built binaries
|
# Built binaries
|
||||||
native/out
|
native/out
|
||||||
|
|||||||
38
.gitmodules
vendored
38
.gitmodules
vendored
@@ -1,30 +1,48 @@
|
|||||||
[submodule "selinux"]
|
[submodule "selinux"]
|
||||||
path = native/jni/external/selinux
|
path = native/src/external/selinux
|
||||||
url = https://github.com/topjohnwu/selinux.git
|
url = https://github.com/topjohnwu/selinux.git
|
||||||
[submodule "busybox"]
|
[submodule "busybox"]
|
||||||
path = native/jni/external/busybox
|
path = native/src/external/busybox
|
||||||
url = https://github.com/topjohnwu/ndk-busybox.git
|
url = https://github.com/topjohnwu/ndk-busybox.git
|
||||||
[submodule "dtc"]
|
[submodule "dtc"]
|
||||||
path = native/jni/external/dtc
|
path = native/src/external/dtc
|
||||||
url = https://github.com/dgibson/dtc
|
url = https://github.com/dgibson/dtc.git
|
||||||
[submodule "lz4"]
|
[submodule "lz4"]
|
||||||
path = native/jni/external/lz4
|
path = native/src/external/lz4
|
||||||
url = https://github.com/lz4/lz4.git
|
url = https://github.com/lz4/lz4.git
|
||||||
[submodule "bzip2"]
|
[submodule "bzip2"]
|
||||||
path = native/jni/external/bzip2
|
path = native/src/external/bzip2
|
||||||
url = https://github.com/nemequ/bzip2.git
|
url = https://github.com/nemequ/bzip2.git
|
||||||
[submodule "xz"]
|
[submodule "xz"]
|
||||||
path = native/jni/external/xz
|
path = native/src/external/xz
|
||||||
url = https://github.com/xz-mirror/xz.git
|
url = https://github.com/xz-mirror/xz.git
|
||||||
[submodule "nanopb"]
|
[submodule "nanopb"]
|
||||||
path = native/jni/external/nanopb
|
path = native/src/external/nanopb
|
||||||
url = https://github.com/nanopb/nanopb.git
|
url = https://github.com/nanopb/nanopb.git
|
||||||
[submodule "mincrypt"]
|
[submodule "mincrypt"]
|
||||||
path = native/jni/external/mincrypt
|
path = native/src/external/mincrypt
|
||||||
url = https://github.com/topjohnwu/mincrypt.git
|
url = https://github.com/topjohnwu/mincrypt.git
|
||||||
[submodule "pcre"]
|
[submodule "pcre"]
|
||||||
path = native/jni/external/pcre
|
path = native/src/external/pcre
|
||||||
url = https://android.googlesource.com/platform/external/pcre
|
url = https://android.googlesource.com/platform/external/pcre
|
||||||
|
[submodule "libcxx"]
|
||||||
|
path = native/src/external/libcxx
|
||||||
|
url = https://github.com/topjohnwu/libcxx.git
|
||||||
|
[submodule "zlib"]
|
||||||
|
path = native/src/external/zlib
|
||||||
|
url = https://android.googlesource.com/platform/external/zlib
|
||||||
|
[submodule "parallel-hashmap"]
|
||||||
|
path = native/src/external/parallel-hashmap
|
||||||
|
url = https://github.com/greg7mdp/parallel-hashmap.git
|
||||||
|
[submodule "zopfli"]
|
||||||
|
path = native/src/external/zopfli
|
||||||
|
url = https://github.com/google/zopfli.git
|
||||||
|
[submodule "cxx-rs"]
|
||||||
|
path = native/src/external/cxx-rs
|
||||||
|
url = https://github.com/topjohnwu/cxx.git
|
||||||
|
[submodule "lsplt"]
|
||||||
|
path = native/src/external/lsplt
|
||||||
|
url = https://github.com/LSPosed/LSPlt.git
|
||||||
[submodule "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
|
||||||
|
|||||||
72
README.MD
72
README.MD
@@ -1,72 +1,68 @@
|
|||||||

|

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

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

|
|
||||||
|
#### This is not an officially supported Google product
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
Magisk is a suite of open source tools for customizing Android, supporting devices higher than Android 4.2. It covers fundamental parts of Android customization: root, boot scripts, SELinux patches, AVB2.0 / dm-verity / forceencrypt removals etc.
|
Magisk is a suite of open source software for customizing Android, supporting devices higher than Android 6.0.<br>
|
||||||
|
Some highlight features:
|
||||||
|
|
||||||
Here are some feature highlights:
|
- **MagiskSU**: Provide root access for applications
|
||||||
|
|
||||||
- **MagiskSU**: Provide root access to your device
|
|
||||||
- **Magisk Modules**: Modify read-only partitions by installing modules
|
- **Magisk Modules**: Modify read-only partitions by installing modules
|
||||||
- **MagiskHide**: Hide Magisk from root detections / system integrity checks
|
- **MagiskBoot**: The most complete tool for unpacking and repacking Android boot images
|
||||||
|
- **Zygisk**: Run code in every Android applications' processes
|
||||||
|
|
||||||
## Downloads
|
## Downloads
|
||||||
|
|
||||||
[](https://github.com/topjohnwu/Magisk/releases/download/manager-v7.5.1/MagiskManager-v7.5.1.apk)
|
[Github](https://github.com/topjohnwu/Magisk/) is the only source where you can get official Magisk information and downloads.
|
||||||
[](https://raw.githubusercontent.com/topjohnwu/magisk_files/canary/app-debug.apk)
|
|
||||||
<br>
|
[](https://github.com/topjohnwu/Magisk/releases/tag/v25.2)
|
||||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v20.4)
|
[](https://github.com/topjohnwu/Magisk/releases/tag/v26.0)
|
||||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v20.4)
|
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-release.apk)
|
||||||
|
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-debug.apk)
|
||||||
|
|
||||||
## Useful Links
|
## Useful Links
|
||||||
|
|
||||||
- [Installation Instruction](https://topjohnwu.github.io/Magisk/install.html)
|
- [Installation Instruction](https://topjohnwu.github.io/Magisk/install.html)
|
||||||
- [Frequently Asked Questions](https://topjohnwu.github.io/Magisk/faq.html)
|
- [Magisk Documentation](https://topjohnwu.github.io/Magisk/)
|
||||||
- [Full Official Docs](https://topjohnwu.github.io/Magisk/)
|
|
||||||
- [Magisk Troubleshoot Wiki](https://www.didgeridoohan.com/magisk/HomePage) (by [@Didgeridoohan](https://github.com/Didgeridoohan))
|
|
||||||
|
|
||||||
## Android Version Support
|
|
||||||
|
|
||||||
- Android 4.2+: MagiskSU and Magisk Modules Only
|
|
||||||
- Android 4.4+: All core features available
|
|
||||||
- Android 6.0+: Guaranteed MagiskHide support
|
|
||||||
- Android 7.0+: Full MagiskHide protection
|
|
||||||
- Android 9.0+: Magisk Manager stealth mode
|
|
||||||
|
|
||||||
## Bug Reports
|
## Bug Reports
|
||||||
|
|
||||||
Canary Channels are cutting edge builds for those adventurous. To access canary builds, install the Canary Magisk Manager, switch to the Canary Channel in settings and upgrade.
|
**Only bug reports from Debug builds will be accepted.**
|
||||||
|
|
||||||
**Only bug reports from Canary builds will be accepted.**
|
|
||||||
|
|
||||||
For installation issues, upload both boot image and install logs.<br>
|
For installation issues, upload both boot image and install logs.<br>
|
||||||
For Magisk issues, upload boot logcat or dmesg.<br>
|
For Magisk issues, upload boot logcat or dmesg.<br>
|
||||||
For Magisk Manager crashes, record and upload the logcat when the crash occurs.
|
For Magisk app crashes, record and upload the logcat when the crash occurs.
|
||||||
|
|
||||||
## Building and Development
|
## Building and Development
|
||||||
|
|
||||||
- Magisk builds on any OS Android Studio supports. Install Android Studio and do the initial setups.
|
- 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`
|
- Clone sources: `git clone --recurse-submodules https://github.com/topjohnwu/Magisk.git`
|
||||||
- Install Python 3.6+ \
|
- Install Python 3.8+ \
|
||||||
(Windows only: select **'Add Python to PATH'** in installer, and run `pip install colorama` after install)
|
(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:
|
- Configure to use the JDK bundled in Android Studio:
|
||||||
- macOS: `export JAVA_HOME="/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home"`
|
- macOS: `export JAVA_HOME="/Applications/Android Studio.app/Contents/jre/Contents/Home"`
|
||||||
- Linux: `export PATH="/path/to/androidstudio/jre/bin:$PATH"`
|
- Linux: `export PATH="/path/to/androidstudio/jre/bin:$PATH"`
|
||||||
- Windows: Add `C:\Path\To\Android Studio\jre\bin` to environment variable `PATH`
|
- Windows: Add `C:\Path\To\Android Studio\jre\bin` to environment variable `PATH`
|
||||||
- Set environment variable `ANDROID_HOME` to the Android SDK folder (can be found in Android Studio settings)
|
- 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
|
- Run `./build.py ndk` to let the script download and install NDK for you
|
||||||
- Set configurations in `config.prop`. A sample `config.prop.sample` is provided.
|
|
||||||
- To start building, run `build.py` to see your options. \
|
- To start building, run `build.py` to see your options. \
|
||||||
For each action, use `-h` to access help (e.g. `./build.py all -h`)
|
For each action, use `-h` to access help (e.g. `./build.py all -h`)
|
||||||
- To start development, open the project in Android Studio. Both app (Kotlin/Java) and native (C++/C) source code can be properly developed using the IDE, but *always* use `build.py` for building.
|
- To start development, open the project with Android Studio. The IDE can be used for both app (Kotlin/Java) and native sources.
|
||||||
- `build.py` builds in debug mode by default. If you want release builds (with `-r, --release`), you need a Java Keystore to sign APKs and zips. For more information, check [Google's Documentation](https://developer.android.com/studio/publish/app-signing.html#generate-key).
|
- Optionally, set custom configs with `config.prop`. A sample `config.prop.sample` is provided.
|
||||||
|
|
||||||
|
## Signing and Distribution
|
||||||
|
|
||||||
|
- The certificate of the key used to sign the final Magisk APK product is also directly embedded into some executables. In release builds, Magisk's root daemon will enforce this certificate check and reject and forcefully uninstall any non-matching Magisk apps to protect users from malicious and unverified Magisk APKs.
|
||||||
|
- To do any development on Magisk itself, switch to an **official debug build and reinstall Magisk** to bypass the signature check.
|
||||||
|
- To distribute your own Magisk builds signed with your own keys, set your signing configs in `config.prop`.
|
||||||
|
- Check [Google's Documentation](https://developer.android.com/studio/publish/app-signing.html#generate-key) for more details on generating your own key.
|
||||||
|
|
||||||
## Translation Contributions
|
## Translation Contributions
|
||||||
|
|
||||||
Default string resources for Magisk Manager and its stub APK are located here:
|
Default string resources for the Magisk app and its stub APK are located here:
|
||||||
|
|
||||||
- `app/src/main/res/values/strings.xml`
|
- `app/src/main/res/values/strings.xml`
|
||||||
- `stub/src/main/res/values/strings.xml`
|
- `stub/src/main/res/values/strings.xml`
|
||||||
|
|||||||
6
app/.gitignore
vendored
6
app/.gitignore
vendored
@@ -3,9 +3,9 @@
|
|||||||
/local.properties
|
/local.properties
|
||||||
.idea/
|
.idea/
|
||||||
/build
|
/build
|
||||||
app/release
|
|
||||||
*.hprof
|
*.hprof
|
||||||
.externalNativeBuild/
|
.externalNativeBuild/
|
||||||
public.certificate.x509.pem
|
|
||||||
private.key.pk8
|
|
||||||
*.apk
|
*.apk
|
||||||
|
src/*/assets
|
||||||
|
src/*/jniLibs
|
||||||
|
src/*/resources
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
kotlin("android")
|
kotlin("android")
|
||||||
kotlin("android.extensions")
|
kotlin("plugin.parcelize")
|
||||||
kotlin("kapt")
|
kotlin("kapt")
|
||||||
id("androidx.navigation.safeargs.kotlin")
|
id("androidx.navigation.safeargs.kotlin")
|
||||||
}
|
}
|
||||||
@@ -13,135 +13,109 @@ kapt {
|
|||||||
javacOptions {
|
javacOptions {
|
||||||
option("-Xmaxerrs", 1000)
|
option("-Xmaxerrs", 1000)
|
||||||
}
|
}
|
||||||
|
arguments {
|
||||||
|
arg("room.incremental", "true")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
namespace = "com.topjohnwu.magisk"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.topjohnwu.magisk"
|
applicationId = "com.topjohnwu.magisk"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
multiDexEnabled = true
|
versionName = Config.version
|
||||||
versionName = Config["appVersion"]
|
versionCode = Config.versionCode
|
||||||
versionCode = Config["appVersionCode"]?.toInt()
|
ndk {
|
||||||
buildConfigField("int", "LATEST_MAGISK", Config["versionCode"] ?: "Integer.MAX_VALUE")
|
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||||
|
debugSymbolLevel = "FULL"
|
||||||
javaCompileOptions.annotationProcessorOptions.arguments(
|
}
|
||||||
mapOf("room.incremental" to "true")
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
getByName("release") {
|
release {
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
isShrinkResources = true
|
isShrinkResources = true
|
||||||
proguardFiles(
|
proguardFiles("proguard-rules.pro")
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
||||||
"proguard-rules.pro"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
dataBinding = true
|
dataBinding = true
|
||||||
|
aidl = true
|
||||||
}
|
}
|
||||||
|
|
||||||
dependenciesInfo {
|
packaging {
|
||||||
includeInApk = false
|
resources {
|
||||||
includeInBundle = false
|
excludes += "/META-INF/*"
|
||||||
}
|
excludes += "/META-INF/versions/**"
|
||||||
|
excludes += "/org/bouncycastle/**"
|
||||||
packagingOptions {
|
excludes += "/kotlin/**"
|
||||||
exclude("/META-INF/**")
|
excludes += "/kotlinx/**"
|
||||||
exclude("/androidsupportmultidexversion.txt")
|
excludes += "/okhttp3/**"
|
||||||
exclude("/org/bouncycastle/**")
|
excludes += "/*.txt"
|
||||||
exclude("/kotlin/**")
|
excludes += "/*.bin"
|
||||||
exclude("/kotlinx/**")
|
excludes += "/*.json"
|
||||||
exclude("/okhttp3/**")
|
}
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "1.8"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
androidExtensions {
|
setupApp()
|
||||||
isExperimental = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val copyUtils = tasks.register("copyUtils", Copy::class) {
|
configurations.all {
|
||||||
from(rootProject.file("scripts/util_functions.sh"))
|
exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk7")
|
||||||
into("src/main/res/raw")
|
exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks["preBuild"]?.dependsOn(copyUtils)
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
|
|
||||||
implementation(kotlin("stdlib"))
|
|
||||||
implementation(project(":app:shared"))
|
implementation(project(":app:shared"))
|
||||||
implementation(project(":app:signing"))
|
|
||||||
|
|
||||||
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.jakewharton.timber:timber:4.7.1")
|
implementation("com.github.topjohnwu:lz4-java:1.7.1")
|
||||||
|
implementation("com.jakewharton.timber:timber:5.0.1")
|
||||||
|
implementation("org.bouncycastle:bcpkix-jdk18on:1.72")
|
||||||
|
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.3.0")
|
||||||
|
implementation("dev.rikka.rikkax.insets:insets:1.3.0")
|
||||||
|
implementation("dev.rikka.rikkax.recyclerview:recyclerview-ktx:1.3.1")
|
||||||
|
implementation("io.noties.markwon:core:4.6.2")
|
||||||
|
|
||||||
val vBAdapt = "4.0.0"
|
val vLibsu = "5.0.5"
|
||||||
val bindingAdapter = "me.tatarka.bindingcollectionadapter2:bindingcollectionadapter"
|
|
||||||
implementation("${bindingAdapter}:${vBAdapt}")
|
|
||||||
implementation("${bindingAdapter}-recyclerview:${vBAdapt}")
|
|
||||||
|
|
||||||
val vMarkwon = "4.6.0"
|
|
||||||
implementation("io.noties.markwon:core:${vMarkwon}")
|
|
||||||
implementation("io.noties.markwon:html:${vMarkwon}")
|
|
||||||
implementation("io.noties.markwon:image:${vMarkwon}")
|
|
||||||
implementation("com.caverock:androidsvg:1.4")
|
|
||||||
|
|
||||||
val vLibsu = "3.0.2"
|
|
||||||
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
|
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
|
||||||
implementation("com.github.topjohnwu.libsu:io:${vLibsu}")
|
implementation("com.github.topjohnwu.libsu:service:${vLibsu}")
|
||||||
|
implementation("com.github.topjohnwu.libsu:nio:${vLibsu}")
|
||||||
val vKoin = "2.1.6"
|
|
||||||
implementation("org.koin:koin-core:${vKoin}")
|
|
||||||
implementation("org.koin:koin-android:${vKoin}")
|
|
||||||
implementation("org.koin:koin-androidx-viewmodel:${vKoin}")
|
|
||||||
|
|
||||||
val vRetrofit = "2.9.0"
|
val vRetrofit = "2.9.0"
|
||||||
implementation("com.squareup.retrofit2:retrofit:${vRetrofit}")
|
implementation("com.squareup.retrofit2:retrofit:${vRetrofit}")
|
||||||
implementation("com.squareup.retrofit2:converter-moshi:${vRetrofit}")
|
implementation("com.squareup.retrofit2:converter-moshi:${vRetrofit}")
|
||||||
implementation("com.squareup.retrofit2:converter-scalars:${vRetrofit}")
|
implementation("com.squareup.retrofit2:converter-scalars:${vRetrofit}")
|
||||||
|
|
||||||
val vOkHttp = "3.12.12"
|
val vOkHttp = "4.10.0"
|
||||||
implementation("com.squareup.okhttp3:okhttp") {
|
implementation("com.squareup.okhttp3:okhttp:${vOkHttp}")
|
||||||
version {
|
|
||||||
strictly(vOkHttp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor:${vOkHttp}")
|
implementation("com.squareup.okhttp3:logging-interceptor:${vOkHttp}")
|
||||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:${vOkHttp}")
|
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:${vOkHttp}")
|
||||||
|
|
||||||
val vMoshi = "1.10.0"
|
val vMoshi = "1.14.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.2.5"
|
val vRoom = "2.5.1"
|
||||||
implementation("androidx.room:room-runtime:${vRoom}")
|
implementation("androidx.room:room-runtime:${vRoom}")
|
||||||
implementation("androidx.room:room-ktx:${vRoom}")
|
implementation("androidx.room:room-ktx:${vRoom}")
|
||||||
kapt("androidx.room:room-compiler:${vRoom}")
|
kapt("androidx.room:room-compiler:${vRoom}")
|
||||||
|
|
||||||
val vNav: String by rootProject.extra
|
val vNav = "2.5.3"
|
||||||
implementation("androidx.navigation:navigation-fragment-ktx:${vNav}")
|
implementation("androidx.navigation:navigation-fragment-ktx:${vNav}")
|
||||||
implementation("androidx.navigation:navigation-ui-ktx:${vNav}")
|
implementation("androidx.navigation:navigation-ui-ktx:${vNav}")
|
||||||
|
|
||||||
implementation("androidx.biometric:biometric:1.0.1")
|
implementation("androidx.biometric:biometric:1.1.0")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.0.1")
|
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
implementation("androidx.browser:browser:1.2.0")
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||||
implementation("androidx.preference:preference:1.1.1")
|
implementation("androidx.recyclerview:recyclerview:1.3.0")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.1.0")
|
implementation("androidx.fragment:fragment-ktx:1.5.6")
|
||||||
implementation("androidx.fragment:fragment-ktx:1.2.5")
|
implementation("androidx.transition:transition:1.4.1")
|
||||||
implementation("androidx.work:work-runtime-ktx:2.4.0")
|
implementation("androidx.core:core-ktx:1.9.0")
|
||||||
implementation("androidx.transition:transition:1.3.1")
|
implementation("androidx.core:core-splashscreen:1.0.0")
|
||||||
implementation("androidx.multidex:multidex:2.0.1")
|
implementation("com.google.android.material:material:1.8.0")
|
||||||
implementation("androidx.core:core-ktx:1.3.1")
|
|
||||||
implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.0.0")
|
|
||||||
implementation("com.google.android.material:material:1.2.1")
|
|
||||||
}
|
}
|
||||||
|
|||||||
88
app/proguard-rules.pro
vendored
88
app/proguard-rules.pro
vendored
@@ -1,57 +1,63 @@
|
|||||||
# Add project specific ProGuard rules here.
|
# Parcelable
|
||||||
# By default, the flags in this file are appended to flags specified
|
-keepclassmembers class * implements android.os.Parcelable {
|
||||||
# in /Users/topjohnwu/Library/Android/sdk/tools/proguard/proguard-android.txt
|
public static final ** CREATOR;
|
||||||
# You can edit the include path and order by changing the proguardFiles
|
}
|
||||||
# directive in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# Add any project specific keep options here:
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Kotlin
|
# Kotlin
|
||||||
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
||||||
public static void checkExpressionValueIsNotNull(...);
|
public static void check*(...);
|
||||||
public static void checkNotNullExpressionValue(...);
|
public static void throw*(...);
|
||||||
public static void checkReturnedValueIsNotNull(...);
|
}
|
||||||
public static void checkFieldIsNotNull(...);
|
-assumenosideeffects class java.util.Objects {
|
||||||
public static void checkParameterIsNotNull(...);
|
public static ** requireNonNull(...);
|
||||||
|
}
|
||||||
|
-assumenosideeffects public class kotlin.coroutines.jvm.internal.DebugMetadataKt {
|
||||||
|
private static ** getDebugMetadataAnnotation(...) return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Stubs
|
# Stub
|
||||||
-keep class a.* { *; }
|
-keep class com.topjohnwu.magisk.core.App { <init>(java.lang.Object); }
|
||||||
|
-keepclassmembers class androidx.appcompat.app.AppCompatDelegateImpl {
|
||||||
# Snet
|
boolean mActivityHandlesConfigFlagsChecked;
|
||||||
-keepclassmembers class com.topjohnwu.magisk.ui.safetynet.SafetyNetHelper { *; }
|
int mActivityHandlesConfigFlags;
|
||||||
-keep,allowobfuscation interface com.topjohnwu.magisk.ui.safetynet.SafetyNetHelper$Callback
|
|
||||||
-keepclassmembers class * implements com.topjohnwu.magisk.ui.safetynet.SafetyNetHelper$Callback {
|
|
||||||
void onResponse(org.json.JSONObject);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Fragments
|
# main
|
||||||
# TODO: Remove when AGP 4.1 release
|
-keep,allowoptimization public class com.topjohnwu.magisk.signing.SignBoot {
|
||||||
# https://issuetracker.google.com/issues/142601969
|
public static void main(java.lang.String[]);
|
||||||
-keep,allowobfuscation class * extends androidx.fragment.app.Fragment
|
}
|
||||||
-keepnames class androidx.navigation.fragment.NavHostFragment
|
|
||||||
|
|
||||||
# Strip Timber verbose and debug logging
|
# Strip Timber verbose and debug logging
|
||||||
-assumenosideeffects class timber.log.Timber.Tree {
|
-assumenosideeffects class timber.log.Timber$Tree {
|
||||||
public void v(**);
|
public void v(**);
|
||||||
public void d(**);
|
public void d(**);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# https://github.com/square/retrofit/issues/3751#issuecomment-1192043644
|
||||||
|
# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items).
|
||||||
|
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
|
||||||
|
-keep,allowobfuscation,allowshrinking class retrofit2.Response
|
||||||
|
|
||||||
|
# With R8 full mode generic signatures are stripped for classes that are not
|
||||||
|
# kept. Suspend functions are wrapped in continuations where the type argument
|
||||||
|
# is used.
|
||||||
|
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
|
||||||
|
|
||||||
|
|
||||||
# Excessive obfuscation
|
# Excessive obfuscation
|
||||||
-repackageclasses
|
-repackageclasses 'a'
|
||||||
-allowaccessmodification
|
-allowaccessmodification
|
||||||
|
|
||||||
# QOL
|
-obfuscationdictionary ../dict.txt
|
||||||
-dontnote **
|
-classobfuscationdictionary ../dict.txt
|
||||||
-dontwarn com.caverock.androidsvg.**
|
-packageobfuscationdictionary ../dict.txt
|
||||||
-dontwarn ru.noties.markwon.**
|
|
||||||
|
-dontwarn org.bouncycastle.jsse.BCSSLParameters
|
||||||
|
-dontwarn org.bouncycastle.jsse.BCSSLSocket
|
||||||
|
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
|
||||||
|
-dontwarn org.commonmark.ext.gfm.strikethrough.Strikethrough
|
||||||
|
-dontwarn org.conscrypt.Conscrypt*
|
||||||
|
-dontwarn org.conscrypt.ConscryptHostnameVerifier
|
||||||
|
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
|
||||||
|
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
|
||||||
|
-dontwarn org.openjsse.net.ssl.OpenJSSE
|
||||||
|
|||||||
@@ -2,13 +2,8 @@ plugins {
|
|||||||
id("com.android.library")
|
id("com.android.library")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
setupCommon()
|
||||||
defaultConfig {
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
|
||||||
consumerProguardFiles("proguard-rules.pro")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
android {
|
||||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
|
namespace = "com.topjohnwu.shared"
|
||||||
}
|
}
|
||||||
|
|||||||
25
app/shared/proguard-rules.pro
vendored
25
app/shared/proguard-rules.pro
vendored
@@ -1,25 +0,0 @@
|
|||||||
# Add project specific ProGuard rules here.
|
|
||||||
# You can control the set of applied configuration files using the
|
|
||||||
# proguardFiles setting in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
|
||||||
# debugging stack traces.
|
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
|
||||||
# hide the original source file name.
|
|
||||||
#-renamesourcefileattribute SourceFile
|
|
||||||
|
|
||||||
-keepclassmembers class * extends javax.net.ssl.SSLSocketFactory {
|
|
||||||
** delegate;
|
|
||||||
}
|
|
||||||
8
app/shared/src/debug/AndroidManifest.xml
Normal file
8
app/shared/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
|
tools:ignore="UnusedAttribute" />
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -1,23 +1,30 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.topjohnwu.shared">
|
android:installLocation="internalOnly">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
<uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS" />
|
||||||
|
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="29" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="29"
|
||||||
|
tools:ignore="ScopedStorage" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="Magisk Manager"
|
android:allowBackup="false"
|
||||||
android:installLocation="internalOnly"
|
android:label="Magisk"
|
||||||
android:usesCleartextTraffic="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||||
tools:ignore="UnusedAttribute">
|
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name="a.p"
|
|
||||||
android:authorities="${applicationId}.provider"
|
|
||||||
android:exported="false"
|
|
||||||
android:grantUriPermissions="true">
|
|
||||||
</provider>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
package a;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.FileProvider;
|
|
||||||
|
|
||||||
public class p extends FileProvider {
|
|
||||||
/* Stub */
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
package com.topjohnwu.magisk;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.AssetManager;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static android.os.Build.VERSION.SDK_INT;
|
|
||||||
|
|
||||||
public class DynAPK {
|
|
||||||
|
|
||||||
// Indices of the object array
|
|
||||||
private static final int STUB_VERSION_ENTRY = 0;
|
|
||||||
private static final int CLASS_COMPONENT_MAP = 1;
|
|
||||||
|
|
||||||
private static File dynDir;
|
|
||||||
private static Method addAssetPath;
|
|
||||||
|
|
||||||
private static File getDynDir(Context c) {
|
|
||||||
if (dynDir == null) {
|
|
||||||
if (SDK_INT >= 24) {
|
|
||||||
// Use protected context to allow directBootAware
|
|
||||||
c = c.createDeviceProtectedStorageContext();
|
|
||||||
}
|
|
||||||
dynDir = new File(c.getFilesDir().getParent(), "dyn");
|
|
||||||
dynDir.mkdir();
|
|
||||||
}
|
|
||||||
return dynDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static File current(Context c) {
|
|
||||||
return new File(getDynDir(c), "current.apk");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static File update(Context c) {
|
|
||||||
return new File(getDynDir(c), "update.apk");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Data load(Object o) {
|
|
||||||
Object[] arr = (Object[]) o;
|
|
||||||
Data data = new Data();
|
|
||||||
data.version = (int) arr[STUB_VERSION_ENTRY];
|
|
||||||
data.classToComponent = (Map<String, String>) arr[CLASS_COMPONENT_MAP];
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Object pack(Data data) {
|
|
||||||
Object[] arr = new Object[2];
|
|
||||||
arr[STUB_VERSION_ENTRY] = data.version;
|
|
||||||
arr[CLASS_COMPONENT_MAP] = data.classToComponent;
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void addAssetPath(AssetManager asset, String path) {
|
|
||||||
try {
|
|
||||||
if (addAssetPath == null)
|
|
||||||
addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
|
|
||||||
addAssetPath.invoke(asset, path);
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Data {
|
|
||||||
public int version;
|
|
||||||
public Map<String, String> classToComponent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,351 +0,0 @@
|
|||||||
package com.topjohnwu.magisk;
|
|
||||||
|
|
||||||
import android.content.ContentProvider;
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.pm.ProviderInfo;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.MatrixCursor;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Environment;
|
|
||||||
import android.os.ParcelFileDescriptor;
|
|
||||||
import android.provider.OpenableColumns;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.webkit.MimeTypeMap;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Modified from androidx.core.content.FileProvider
|
|
||||||
*/
|
|
||||||
public class FileProvider extends ContentProvider {
|
|
||||||
private static final String[] COLUMNS = {
|
|
||||||
OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE };
|
|
||||||
|
|
||||||
private static final File DEVICE_ROOT = new File("/");
|
|
||||||
|
|
||||||
private static HashMap<String, PathStrategy> sCache = new HashMap<>();
|
|
||||||
|
|
||||||
private PathStrategy mStrategy;
|
|
||||||
|
|
||||||
public static ProviderCallHandler callHandler;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreate() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void attachInfo(Context context, ProviderInfo info) {
|
|
||||||
super.attachInfo(context, info);
|
|
||||||
|
|
||||||
|
|
||||||
if (info.exported) {
|
|
||||||
throw new SecurityException("Provider must not be exported");
|
|
||||||
}
|
|
||||||
if (!info.grantUriPermissions) {
|
|
||||||
throw new SecurityException("Provider must grant uri permissions");
|
|
||||||
}
|
|
||||||
|
|
||||||
mStrategy = getPathStrategy(context, info.authority);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static Uri getUriForFile(Context context, String authority,
|
|
||||||
File file) {
|
|
||||||
final PathStrategy strategy = getPathStrategy(context, authority);
|
|
||||||
return strategy.getUriForFile(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Cursor query(Uri uri, String[] projection, String selection,
|
|
||||||
String[] selectionArgs,
|
|
||||||
String sortOrder) {
|
|
||||||
|
|
||||||
final File file = mStrategy.getFileForUri(uri);
|
|
||||||
|
|
||||||
if (projection == null) {
|
|
||||||
projection = COLUMNS;
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] cols = new String[projection.length];
|
|
||||||
Object[] values = new Object[projection.length];
|
|
||||||
int i = 0;
|
|
||||||
for (String col : projection) {
|
|
||||||
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
|
|
||||||
cols[i] = OpenableColumns.DISPLAY_NAME;
|
|
||||||
values[i++] = file.getName();
|
|
||||||
} else if (OpenableColumns.SIZE.equals(col)) {
|
|
||||||
cols[i] = OpenableColumns.SIZE;
|
|
||||||
values[i++] = file.length();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cols = copyOf(cols, i);
|
|
||||||
values = copyOf(values, i);
|
|
||||||
|
|
||||||
final MatrixCursor cursor = new MatrixCursor(cols, 1);
|
|
||||||
cursor.addRow(values);
|
|
||||||
return cursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getType(Uri uri) {
|
|
||||||
|
|
||||||
final File file = mStrategy.getFileForUri(uri);
|
|
||||||
|
|
||||||
final int lastDot = file.getName().lastIndexOf('.');
|
|
||||||
if (lastDot >= 0) {
|
|
||||||
final String extension = file.getName().substring(lastDot + 1);
|
|
||||||
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
|
||||||
if (mime != null) {
|
|
||||||
return mime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "application/octet-stream";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Uri insert(Uri uri, ContentValues values) {
|
|
||||||
throw new UnsupportedOperationException("No external inserts");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int update(Uri uri, ContentValues values, String selection,
|
|
||||||
String[] selectionArgs) {
|
|
||||||
throw new UnsupportedOperationException("No external updates");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int delete(Uri uri, String selection,
|
|
||||||
String[] selectionArgs) {
|
|
||||||
|
|
||||||
final File file = mStrategy.getFileForUri(uri);
|
|
||||||
return file.delete() ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Bundle call(String method, String arg, Bundle extras) {
|
|
||||||
if (callHandler != null)
|
|
||||||
return callHandler.call(getContext(), method, arg, extras);
|
|
||||||
return Bundle.EMPTY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ParcelFileDescriptor openFile(Uri uri, String mode)
|
|
||||||
throws FileNotFoundException {
|
|
||||||
|
|
||||||
final File file = mStrategy.getFileForUri(uri);
|
|
||||||
final int fileMode = modeToMode(mode);
|
|
||||||
return ParcelFileDescriptor.open(file, fileMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static PathStrategy getPathStrategy(Context context, String authority) {
|
|
||||||
PathStrategy strat;
|
|
||||||
synchronized (sCache) {
|
|
||||||
strat = sCache.get(authority);
|
|
||||||
if (strat == null) {
|
|
||||||
strat = createPathStrategy(context, authority);
|
|
||||||
sCache.put(authority, strat);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strat;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static PathStrategy createPathStrategy(Context context, String authority) {
|
|
||||||
final SimplePathStrategy strat = new SimplePathStrategy(authority);
|
|
||||||
|
|
||||||
strat.addRoot("root_files", buildPath(DEVICE_ROOT, "."));
|
|
||||||
strat.addRoot("internal_files", buildPath(context.getFilesDir(), "."));
|
|
||||||
strat.addRoot("cache_files", buildPath(context.getCacheDir(), "."));
|
|
||||||
strat.addRoot("external_files", buildPath(Environment.getExternalStorageDirectory(), "."));
|
|
||||||
{
|
|
||||||
File[] externalFilesDirs = getExternalFilesDirs(context, null);
|
|
||||||
if (externalFilesDirs.length > 0) {
|
|
||||||
strat.addRoot("external_file_files", buildPath(externalFilesDirs[0], "."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
{
|
|
||||||
File[] externalCacheDirs = getExternalCacheDirs(context);
|
|
||||||
if (externalCacheDirs.length > 0) {
|
|
||||||
strat.addRoot("external_cache_files", buildPath(externalCacheDirs[0], "."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
File[] externalMediaDirs = context.getExternalMediaDirs();
|
|
||||||
if (externalMediaDirs.length > 0) {
|
|
||||||
strat.addRoot("external_media_files", buildPath(externalMediaDirs[0], "."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return strat;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PathStrategy {
|
|
||||||
|
|
||||||
Uri getUriForFile(File file);
|
|
||||||
|
|
||||||
File getFileForUri(Uri uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
static class SimplePathStrategy implements PathStrategy {
|
|
||||||
private final String mAuthority;
|
|
||||||
private final HashMap<String, File> mRoots = new HashMap<>();
|
|
||||||
|
|
||||||
SimplePathStrategy(String authority) {
|
|
||||||
mAuthority = authority;
|
|
||||||
}
|
|
||||||
|
|
||||||
void addRoot(String name, File root) {
|
|
||||||
if (TextUtils.isEmpty(name)) {
|
|
||||||
throw new IllegalArgumentException("Name must not be empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
root = root.getCanonicalFile();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Failed to resolve canonical path for " + root, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
mRoots.put(name, root);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Uri getUriForFile(File file) {
|
|
||||||
String path;
|
|
||||||
try {
|
|
||||||
path = file.getCanonicalPath();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Map.Entry<String, File> mostSpecific = null;
|
|
||||||
for (Map.Entry<String, File> root : mRoots.entrySet()) {
|
|
||||||
final String rootPath = root.getValue().getPath();
|
|
||||||
if (path.startsWith(rootPath) && (mostSpecific == null
|
|
||||||
|| rootPath.length() > mostSpecific.getValue().getPath().length())) {
|
|
||||||
mostSpecific = root;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mostSpecific == null) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Failed to find configured root that contains " + path);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
final String rootPath = mostSpecific.getValue().getPath();
|
|
||||||
if (rootPath.endsWith("/")) {
|
|
||||||
path = path.substring(rootPath.length());
|
|
||||||
} else {
|
|
||||||
path = path.substring(rootPath.length() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
|
|
||||||
return new Uri.Builder().scheme("content")
|
|
||||||
.authority(mAuthority).encodedPath(path).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public File getFileForUri(Uri uri) {
|
|
||||||
String path = uri.getEncodedPath();
|
|
||||||
|
|
||||||
final int splitIndex = path.indexOf('/', 1);
|
|
||||||
final String tag = Uri.decode(path.substring(1, splitIndex));
|
|
||||||
path = Uri.decode(path.substring(splitIndex + 1));
|
|
||||||
|
|
||||||
final File root = mRoots.get(tag);
|
|
||||||
if (root == null) {
|
|
||||||
throw new IllegalArgumentException("Unable to find configured root for " + uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
File file = new File(root, path);
|
|
||||||
try {
|
|
||||||
file = file.getCanonicalFile();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!file.getPath().startsWith(root.getPath())) {
|
|
||||||
throw new SecurityException("Resolved path jumped beyond configured root");
|
|
||||||
}
|
|
||||||
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static int modeToMode(String mode) {
|
|
||||||
int modeBits;
|
|
||||||
if ("r".equals(mode)) {
|
|
||||||
modeBits = ParcelFileDescriptor.MODE_READ_ONLY;
|
|
||||||
} else if ("w".equals(mode) || "wt".equals(mode)) {
|
|
||||||
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
|
|
||||||
| ParcelFileDescriptor.MODE_CREATE
|
|
||||||
| ParcelFileDescriptor.MODE_TRUNCATE;
|
|
||||||
} else if ("wa".equals(mode)) {
|
|
||||||
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
|
|
||||||
| ParcelFileDescriptor.MODE_CREATE
|
|
||||||
| ParcelFileDescriptor.MODE_APPEND;
|
|
||||||
} else if ("rw".equals(mode)) {
|
|
||||||
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
|
|
||||||
| ParcelFileDescriptor.MODE_CREATE;
|
|
||||||
} else if ("rwt".equals(mode)) {
|
|
||||||
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
|
|
||||||
| ParcelFileDescriptor.MODE_CREATE
|
|
||||||
| ParcelFileDescriptor.MODE_TRUNCATE;
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("Invalid mode: " + mode);
|
|
||||||
}
|
|
||||||
return modeBits;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static File buildPath(File base, String... segments) {
|
|
||||||
File cur = base;
|
|
||||||
for (String segment : segments) {
|
|
||||||
if (segment != null) {
|
|
||||||
cur = new File(cur, segment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cur;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String[] copyOf(String[] original, int newLength) {
|
|
||||||
final String[] result = new String[newLength];
|
|
||||||
System.arraycopy(original, 0, result, 0, newLength);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Object[] copyOf(Object[] original, int newLength) {
|
|
||||||
final Object[] result = new Object[newLength];
|
|
||||||
System.arraycopy(original, 0, result, 0, newLength);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static File[] getExternalFilesDirs(Context context, String type) {
|
|
||||||
if (Build.VERSION.SDK_INT >= 19) {
|
|
||||||
return context.getExternalFilesDirs(type);
|
|
||||||
} else {
|
|
||||||
return new File[] { context.getExternalFilesDir(type) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static File[] getExternalCacheDirs(Context context) {
|
|
||||||
if (Build.VERSION.SDK_INT >= 19) {
|
|
||||||
return context.getExternalCacheDirs();
|
|
||||||
} else {
|
|
||||||
return new File[] { context.getExternalCacheDir() };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package com.topjohnwu.magisk;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
public interface ProviderCallHandler {
|
|
||||||
Bundle call(Context context, String method, String arg, Bundle extras);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.topjohnwu.magisk;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
public class ProviderInstaller {
|
||||||
|
|
||||||
|
public static boolean install(Context context) {
|
||||||
|
try {
|
||||||
|
// Try installing new SSL provider from Google Play Service
|
||||||
|
Context gms = context.createPackageContext("com.google.android.gms",
|
||||||
|
Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
|
||||||
|
gms.getClassLoader()
|
||||||
|
.loadClass("com.google.android.gms.common.security.ProviderInstallerImpl")
|
||||||
|
.getMethod("insertProvider", Context.class)
|
||||||
|
.invoke(null, gms);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
120
app/shared/src/main/java/com/topjohnwu/magisk/StubApk.java
Normal file
120
app/shared/src/main/java/com/topjohnwu/magisk/StubApk.java
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package com.topjohnwu.magisk;
|
||||||
|
|
||||||
|
import static android.os.Build.VERSION.SDK_INT;
|
||||||
|
import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
|
import android.content.res.AssetManager;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.content.res.loader.ResourcesLoader;
|
||||||
|
import android.content.res.loader.ResourcesProvider;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class StubApk {
|
||||||
|
private static File dynDir;
|
||||||
|
private static Method addAssetPath;
|
||||||
|
|
||||||
|
private static File getDynDir(ApplicationInfo info) {
|
||||||
|
if (dynDir == null) {
|
||||||
|
final String dataDir;
|
||||||
|
if (SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
// Use device protected path to allow directBootAware
|
||||||
|
dataDir = info.deviceProtectedDataDir;
|
||||||
|
} else {
|
||||||
|
dataDir = info.dataDir;
|
||||||
|
}
|
||||||
|
dynDir = new File(dataDir, "dyn");
|
||||||
|
dynDir.mkdirs();
|
||||||
|
}
|
||||||
|
return dynDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File current(Context c) {
|
||||||
|
return new File(getDynDir(c.getApplicationInfo()), "current.apk");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File current(ApplicationInfo info) {
|
||||||
|
return new File(getDynDir(info), "current.apk");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File update(Context c) {
|
||||||
|
return new File(getDynDir(c.getApplicationInfo()), "update.apk");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File update(ApplicationInfo info) {
|
||||||
|
return new File(getDynDir(info), "update.apk");
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.R)
|
||||||
|
private static ResourcesLoader getResourcesLoader(File path) throws IOException {
|
||||||
|
var loader = new ResourcesLoader();
|
||||||
|
ResourcesProvider provider;
|
||||||
|
if (path.isDirectory()) {
|
||||||
|
provider = ResourcesProvider.loadFromDirectory(path.getPath(), null);
|
||||||
|
} else {
|
||||||
|
var fd = ParcelFileDescriptor.open(path, MODE_READ_ONLY);
|
||||||
|
provider = ResourcesProvider.loadFromApk(fd);
|
||||||
|
}
|
||||||
|
loader.addProvider(provider);
|
||||||
|
return loader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void addAssetPath(Resources res, String path) {
|
||||||
|
if (SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
try {
|
||||||
|
res.addLoaders(getResourcesLoader(new File(path)));
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
} else {
|
||||||
|
AssetManager asset = res.getAssets();
|
||||||
|
try {
|
||||||
|
if (addAssetPath == null)
|
||||||
|
addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
|
||||||
|
addAssetPath.invoke(asset, path);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void restartProcess(Activity activity) {
|
||||||
|
Intent intent = activity.getPackageManager()
|
||||||
|
.getLaunchIntentForPackage(activity.getPackageName());
|
||||||
|
activity.finishAffinity();
|
||||||
|
activity.startActivity(intent);
|
||||||
|
Runtime.getRuntime().exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Data {
|
||||||
|
// Indices of the object array
|
||||||
|
private static final int STUB_VERSION = 0;
|
||||||
|
private static final int CLASS_COMPONENT_MAP = 1;
|
||||||
|
private static final int ROOT_SERVICE = 2;
|
||||||
|
private static final int ARR_SIZE = 3;
|
||||||
|
|
||||||
|
private final Object[] arr;
|
||||||
|
|
||||||
|
public Data() { arr = new Object[ARR_SIZE]; }
|
||||||
|
public Data(Object o) { arr = (Object[]) o; }
|
||||||
|
public Object getObject() { return arr; }
|
||||||
|
|
||||||
|
public int getVersion() { return (int) arr[STUB_VERSION]; }
|
||||||
|
public void setVersion(int version) { arr[STUB_VERSION] = version; }
|
||||||
|
public Map<String, String> getClassToComponent() {
|
||||||
|
// noinspection unchecked
|
||||||
|
return (Map<String, String>) arr[CLASS_COMPONENT_MAP];
|
||||||
|
}
|
||||||
|
public void setClassToComponent(Map<String, String> map) {
|
||||||
|
arr[CLASS_COMPONENT_MAP] = map;
|
||||||
|
}
|
||||||
|
public Class<?> getRootService() { return (Class<?>) arr[ROOT_SERVICE]; }
|
||||||
|
public void setRootService(Class<?> service) { arr[ROOT_SERVICE] = service; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.net;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.Socket;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javax.net.ssl.HttpsURLConnection;
|
|
||||||
import javax.net.ssl.SSLSocket;
|
|
||||||
import javax.net.ssl.SSLSocketFactory;
|
|
||||||
|
|
||||||
public class NoSSLv3SocketFactory extends SSLSocketFactory {
|
|
||||||
|
|
||||||
private final static SSLSocketFactory delegate = HttpsURLConnection.getDefaultSSLSocketFactory();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String[] getDefaultCipherSuites() {
|
|
||||||
return delegate.getDefaultCipherSuites();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String[] getSupportedCipherSuites() {
|
|
||||||
return delegate.getSupportedCipherSuites();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Socket createSafeSocket(Socket socket) {
|
|
||||||
if (socket instanceof SSLSocket)
|
|
||||||
return new SSLSocketWrapper((SSLSocket) socket) {
|
|
||||||
@Override
|
|
||||||
public void setEnabledProtocols(String[] protocols) {
|
|
||||||
List<String> proto = new ArrayList<>(Arrays.asList(getSupportedProtocols()));
|
|
||||||
proto.remove("SSLv3");
|
|
||||||
super.setEnabledProtocols(proto.toArray(new String[0]));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return socket;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
|
|
||||||
return createSafeSocket(delegate.createSocket(s, host, port, autoClose));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Socket createSocket() throws IOException {
|
|
||||||
return createSafeSocket(delegate.createSocket());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Socket createSocket(String host, int port) throws IOException {
|
|
||||||
return createSafeSocket(delegate.createSocket(host, port));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
|
|
||||||
return createSafeSocket(delegate.createSocket(host, port, localHost, localPort));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Socket createSocket(InetAddress host, int port) throws IOException {
|
|
||||||
return createSafeSocket(delegate.createSocket(host, port));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
|
|
||||||
return createSafeSocket(delegate.createSocket(address, port, localAddress, localPort));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,333 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.net;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.SocketAddress;
|
|
||||||
import java.net.SocketException;
|
|
||||||
import java.nio.channels.SocketChannel;
|
|
||||||
|
|
||||||
import javax.net.ssl.HandshakeCompletedListener;
|
|
||||||
import javax.net.ssl.SSLParameters;
|
|
||||||
import javax.net.ssl.SSLSession;
|
|
||||||
import javax.net.ssl.SSLSocket;
|
|
||||||
|
|
||||||
class SSLSocketWrapper extends SSLSocket {
|
|
||||||
|
|
||||||
private SSLSocket mBase;
|
|
||||||
|
|
||||||
SSLSocketWrapper(SSLSocket socket) {
|
|
||||||
mBase = socket;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String[] getSupportedCipherSuites() {
|
|
||||||
return mBase.getSupportedCipherSuites();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String[] getEnabledCipherSuites() {
|
|
||||||
return mBase.getEnabledCipherSuites();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setEnabledCipherSuites(String[] suites) {
|
|
||||||
mBase.setEnabledCipherSuites(suites);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String[] getSupportedProtocols() {
|
|
||||||
return mBase.getSupportedProtocols();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String[] getEnabledProtocols() {
|
|
||||||
return mBase.getEnabledProtocols();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setEnabledProtocols(String[] protocols) {
|
|
||||||
mBase.setEnabledProtocols(protocols);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SSLSession getSession() {
|
|
||||||
return mBase.getSession();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SSLSession getHandshakeSession() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addHandshakeCompletedListener(HandshakeCompletedListener listener) {
|
|
||||||
mBase.addHandshakeCompletedListener(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void removeHandshakeCompletedListener(HandshakeCompletedListener listener) {
|
|
||||||
mBase.removeHandshakeCompletedListener(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void startHandshake() throws IOException {
|
|
||||||
mBase.startHandshake();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setUseClientMode(boolean mode) {
|
|
||||||
mBase.setUseClientMode(mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean getUseClientMode() {
|
|
||||||
return mBase.getUseClientMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setNeedClientAuth(boolean need) {
|
|
||||||
mBase.setNeedClientAuth(need);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean getNeedClientAuth() {
|
|
||||||
return mBase.getNeedClientAuth();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setWantClientAuth(boolean want) {
|
|
||||||
mBase.setWantClientAuth(want);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean getWantClientAuth() {
|
|
||||||
return mBase.getWantClientAuth();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setEnableSessionCreation(boolean flag) {
|
|
||||||
mBase.setEnableSessionCreation(flag);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean getEnableSessionCreation() {
|
|
||||||
return mBase.getEnableSessionCreation();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SSLParameters getSSLParameters() {
|
|
||||||
return mBase.getSSLParameters();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setSSLParameters(SSLParameters params) {
|
|
||||||
mBase.setSSLParameters(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return mBase.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void connect(SocketAddress endpoint) throws IOException {
|
|
||||||
mBase.connect(endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void connect(SocketAddress endpoint, int timeout) throws IOException {
|
|
||||||
mBase.connect(endpoint, timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void bind(SocketAddress bindpoint) throws IOException {
|
|
||||||
mBase.bind(bindpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InetAddress getInetAddress() {
|
|
||||||
return mBase.getInetAddress();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InetAddress getLocalAddress() {
|
|
||||||
return mBase.getLocalAddress();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getPort() {
|
|
||||||
return mBase.getPort();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getLocalPort() {
|
|
||||||
return mBase.getLocalPort();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SocketAddress getRemoteSocketAddress() {
|
|
||||||
return mBase.getRemoteSocketAddress();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SocketAddress getLocalSocketAddress() {
|
|
||||||
return mBase.getLocalSocketAddress();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SocketChannel getChannel() {
|
|
||||||
return mBase.getChannel();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InputStream getInputStream() throws IOException {
|
|
||||||
return mBase.getInputStream();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public OutputStream getOutputStream() throws IOException {
|
|
||||||
return mBase.getOutputStream();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setTcpNoDelay(boolean on) throws SocketException {
|
|
||||||
mBase.setTcpNoDelay(on);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean getTcpNoDelay() throws SocketException {
|
|
||||||
return mBase.getTcpNoDelay();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setSoLinger(boolean on, int linger) throws SocketException {
|
|
||||||
mBase.setSoLinger(on, linger);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getSoLinger() throws SocketException {
|
|
||||||
return mBase.getSoLinger();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendUrgentData(int data) throws IOException {
|
|
||||||
mBase.sendUrgentData(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setOOBInline(boolean on) throws SocketException {
|
|
||||||
mBase.setOOBInline(on);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean getOOBInline() throws SocketException {
|
|
||||||
return mBase.getOOBInline();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setSoTimeout(int timeout) throws SocketException {
|
|
||||||
mBase.setSoTimeout(timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getSoTimeout() throws SocketException {
|
|
||||||
return mBase.getSoTimeout();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setSendBufferSize(int size) throws SocketException {
|
|
||||||
mBase.setSendBufferSize(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getSendBufferSize() throws SocketException {
|
|
||||||
return mBase.getSendBufferSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setReceiveBufferSize(int size) throws SocketException {
|
|
||||||
mBase.setReceiveBufferSize(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getReceiveBufferSize() throws SocketException {
|
|
||||||
return mBase.getReceiveBufferSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setKeepAlive(boolean on) throws SocketException {
|
|
||||||
mBase.setKeepAlive(on);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean getKeepAlive() throws SocketException {
|
|
||||||
return mBase.getKeepAlive();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setTrafficClass(int tc) throws SocketException {
|
|
||||||
mBase.setTrafficClass(tc);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getTrafficClass() throws SocketException {
|
|
||||||
return mBase.getTrafficClass();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setReuseAddress(boolean on) throws SocketException {
|
|
||||||
mBase.setReuseAddress(on);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean getReuseAddress() throws SocketException {
|
|
||||||
return mBase.getReuseAddress();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() throws IOException {
|
|
||||||
mBase.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void shutdownInput() throws IOException {
|
|
||||||
mBase.shutdownInput();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void shutdownOutput() throws IOException {
|
|
||||||
mBase.shutdownOutput();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isConnected() {
|
|
||||||
return mBase.isConnected();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isBound() {
|
|
||||||
return mBase.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isClosed() {
|
|
||||||
return mBase.isClosed();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isInputShutdown() {
|
|
||||||
return mBase.isInputShutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isOutputShutdown() {
|
|
||||||
return mBase.isOutputShutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setPerformancePreferences(int connectionTime, int latency, int bandwidth) {
|
|
||||||
mBase.setPerformancePreferences(connectionTime, latency, bandwidth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +1,179 @@
|
|||||||
package com.topjohnwu.magisk.utils;
|
package com.topjohnwu.magisk.utils;
|
||||||
|
|
||||||
|
import static android.content.pm.PackageInstaller.EXTRA_SESSION_ID;
|
||||||
|
import static android.content.pm.PackageInstaller.EXTRA_STATUS;
|
||||||
|
import static android.content.pm.PackageInstaller.STATUS_FAILURE_INVALID;
|
||||||
|
import static android.content.pm.PackageInstaller.STATUS_PENDING_USER_ACTION;
|
||||||
|
import static android.content.pm.PackageInstaller.STATUS_SUCCESS;
|
||||||
|
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.content.pm.PackageInstaller.SessionParams;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
import com.topjohnwu.magisk.FileProvider;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FilterOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class APKInstall {
|
public final class APKInstall {
|
||||||
public static void install(Context c, File apk) {
|
|
||||||
c.startActivity(installIntent(c, apk));
|
public static void transfer(InputStream in, OutputStream out) throws IOException {
|
||||||
|
int size = 8192;
|
||||||
|
var buffer = new byte[size];
|
||||||
|
int read;
|
||||||
|
while ((read = in.read(buffer, 0, size)) >= 0) {
|
||||||
|
out.write(buffer, 0, read);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Intent installIntent(Context c, File apk) {
|
public static void registerReceiver(
|
||||||
Intent install = new Intent(Intent.ACTION_INSTALL_PACKAGE);
|
Context context, BroadcastReceiver receiver, IntentFilter filter) {
|
||||||
install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
// noinspection InlinedApi
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
context.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED);
|
||||||
install.setData(FileProvider.getUriForFile(c, c.getPackageName() + ".provider", apk));
|
|
||||||
} else {
|
} else {
|
||||||
apk.setReadable(true, false);
|
context.registerReceiver(receiver, filter);
|
||||||
install.setData(Uri.fromFile(apk));
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Session startSession(Context context) {
|
||||||
|
return startSession(context, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Session startSession(Context context, String pkg,
|
||||||
|
Runnable onFailure, Runnable onSuccess) {
|
||||||
|
var receiver = new InstallReceiver(pkg, onSuccess, onFailure);
|
||||||
|
context = context.getApplicationContext();
|
||||||
|
if (pkg != null) {
|
||||||
|
// If pkg is not null, look for package added event
|
||||||
|
var filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
|
||||||
|
filter.addDataScheme("package");
|
||||||
|
registerReceiver(context, receiver, filter);
|
||||||
|
}
|
||||||
|
registerReceiver(context, receiver, new IntentFilter(receiver.sessionId));
|
||||||
|
return receiver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Session {
|
||||||
|
// @WorkerThread
|
||||||
|
OutputStream openStream(Context context) throws IOException;
|
||||||
|
// @WorkerThread
|
||||||
|
void install(Context context, File apk) throws IOException;
|
||||||
|
// @WorkerThread @Nullable
|
||||||
|
Intent waitIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class InstallReceiver extends BroadcastReceiver implements Session {
|
||||||
|
private final String packageName;
|
||||||
|
private final Runnable onSuccess;
|
||||||
|
private final Runnable onFailure;
|
||||||
|
private final CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
private Intent userAction = null;
|
||||||
|
|
||||||
|
final String sessionId = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
private InstallReceiver(String packageName, Runnable onSuccess, Runnable onFailure) {
|
||||||
|
this.packageName = packageName;
|
||||||
|
this.onSuccess = onSuccess;
|
||||||
|
this.onFailure = onFailure;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) {
|
||||||
|
Uri data = intent.getData();
|
||||||
|
if (data == null)
|
||||||
|
return;
|
||||||
|
String pkg = data.getSchemeSpecificPart();
|
||||||
|
if (pkg.equals(packageName)) {
|
||||||
|
onSuccess(context);
|
||||||
|
}
|
||||||
|
} else if (sessionId.equals(intent.getAction())) {
|
||||||
|
int status = intent.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID);
|
||||||
|
switch (status) {
|
||||||
|
case STATUS_PENDING_USER_ACTION ->
|
||||||
|
userAction = intent.getParcelableExtra(Intent.EXTRA_INTENT);
|
||||||
|
case STATUS_SUCCESS -> {
|
||||||
|
if (packageName == null) {
|
||||||
|
onSuccess(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
int id = intent.getIntExtra(EXTRA_SESSION_ID, 0);
|
||||||
|
var installer = context.getPackageManager().getPackageInstaller();
|
||||||
|
try {
|
||||||
|
installer.abandonSession(id);
|
||||||
|
} catch (SecurityException ignored) {
|
||||||
|
}
|
||||||
|
if (onFailure != null) {
|
||||||
|
onFailure.run();
|
||||||
|
}
|
||||||
|
context.getApplicationContext().unregisterReceiver(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onSuccess(Context context) {
|
||||||
|
if (onSuccess != null)
|
||||||
|
onSuccess.run();
|
||||||
|
context.getApplicationContext().unregisterReceiver(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Intent waitIntent() {
|
||||||
|
try {
|
||||||
|
// noinspection ResultOfMethodCallIgnored
|
||||||
|
latch.await(5, TimeUnit.SECONDS);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
return userAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OutputStream openStream(Context context) throws IOException {
|
||||||
|
// noinspection InlinedApi
|
||||||
|
var flag = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE;
|
||||||
|
var intent = new Intent(sessionId).setPackage(context.getPackageName());
|
||||||
|
var pending = PendingIntent.getBroadcast(context, 0, intent, flag);
|
||||||
|
|
||||||
|
var installer = context.getPackageManager().getPackageInstaller();
|
||||||
|
var params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
params.setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED);
|
||||||
|
}
|
||||||
|
var session = installer.openSession(installer.createSession(params));
|
||||||
|
var out = session.openWrite(sessionId, 0, -1);
|
||||||
|
return new FilterOutputStream(out) {
|
||||||
|
@Override
|
||||||
|
public void write(byte[] b, int off, int len) throws IOException {
|
||||||
|
out.write(b, off, len);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
super.close();
|
||||||
|
session.commit(pending.getIntentSender());
|
||||||
|
session.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void install(Context context, File apk) throws IOException {
|
||||||
|
try (var src = new FileInputStream(apk);
|
||||||
|
var out = openStream(context)) {
|
||||||
|
transfer(src, out);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return install;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,35 @@
|
|||||||
package com.topjohnwu.magisk.utils;
|
package com.topjohnwu.magisk.utils;
|
||||||
|
|
||||||
|
import android.os.Process;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
|
|
||||||
import dalvik.system.DexClassLoader;
|
import dalvik.system.BaseDexClassLoader;
|
||||||
|
|
||||||
public class DynamicClassLoader extends DexClassLoader {
|
public class DynamicClassLoader extends BaseDexClassLoader {
|
||||||
|
|
||||||
private ClassLoader base = Object.class.getClassLoader();
|
public DynamicClassLoader(File apk) {
|
||||||
|
this(apk, getSystemClassLoader());
|
||||||
|
}
|
||||||
|
|
||||||
public DynamicClassLoader(File apk, ClassLoader parent) {
|
public DynamicClassLoader(File apk, ClassLoader parent) {
|
||||||
super(apk.getPath(), apk.getParent(), null, parent);
|
// Set optimizedDirectory to null for RootService to bypass DexFile's security checks
|
||||||
|
super(apk.getPath(), Process.myUid() == 0 ? null : apk.getParentFile(), null, parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||||
// First check if already loaded
|
// First check if already loaded
|
||||||
Class cls = findLoadedClass(name);
|
Class<?> cls = findLoadedClass(name);
|
||||||
if (cls != null)
|
if (cls != null)
|
||||||
return cls;
|
return cls;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Then check boot classpath
|
// Then check boot classpath
|
||||||
return base.loadClass(name);
|
return getSystemClassLoader().loadClass(name);
|
||||||
} catch (ClassNotFoundException ignored) {
|
} catch (ClassNotFoundException ignored) {
|
||||||
try {
|
try {
|
||||||
// Next try current dex
|
// Next try current dex
|
||||||
@@ -42,7 +47,7 @@ public class DynamicClassLoader extends DexClassLoader {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public URL getResource(String name) {
|
public URL getResource(String name) {
|
||||||
URL resource = base.getResource(name);
|
URL resource = getSystemClassLoader().getResource(name);
|
||||||
if (resource != null)
|
if (resource != null)
|
||||||
return resource;
|
return resource;
|
||||||
resource = findResource(name);
|
resource = findResource(name);
|
||||||
@@ -54,7 +59,7 @@ public class DynamicClassLoader extends DexClassLoader {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Enumeration<URL> getResources(String name) throws IOException {
|
public Enumeration<URL> getResources(String name) throws IOException {
|
||||||
return new CompoundEnumeration<>(base.getResources(name),
|
return new CompoundEnumeration<>(getSystemClassLoader().getResources(name),
|
||||||
findResources(name), getParent().getResources(name));
|
findResources(name), getParent().getResources(name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
app/signing/.gitignore
vendored
1
app/signing/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
/build
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
id("java-library")
|
|
||||||
id("java")
|
|
||||||
id("com.github.johnrengelman.shadow") version "6.0.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
java {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
|
|
||||||
val jar by tasks.getting(Jar::class) {
|
|
||||||
manifest {
|
|
||||||
attributes["Main-Class"] = "com.topjohnwu.signing.ZipSigner"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val shadowJar by tasks.getting(ShadowJar::class) {
|
|
||||||
archiveBaseName.set("zipsigner")
|
|
||||||
archiveClassifier.set(null as String?)
|
|
||||||
archiveVersion.set("4.0")
|
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
jcenter()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
|
|
||||||
|
|
||||||
api("org.bouncycastle:bcprov-jdk15on:1.66")
|
|
||||||
api("org.bouncycastle:bcpkix-jdk15on:1.66")
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package com.topjohnwu.signing;
|
|
||||||
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
public class BootSigner {
|
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
|
||||||
if (args.length > 0 && "-verify".equals(args[0])) {
|
|
||||||
String certPath = "";
|
|
||||||
if (args.length >= 2) {
|
|
||||||
/* args[1] is the path to a public key certificate */
|
|
||||||
certPath = args[1];
|
|
||||||
}
|
|
||||||
boolean signed = SignBoot.verifySignature(System.in,
|
|
||||||
certPath.isEmpty() ? null : new FileInputStream(certPath));
|
|
||||||
System.exit(signed ? 0 : 1);
|
|
||||||
} else if (args.length > 0 && "-sign".equals(args[0])) {
|
|
||||||
InputStream cert = null;
|
|
||||||
InputStream key = null;
|
|
||||||
String name = "/boot";
|
|
||||||
|
|
||||||
if (args.length >= 3) {
|
|
||||||
cert = new FileInputStream(args[1]);
|
|
||||||
key = new FileInputStream(args[2]);
|
|
||||||
}
|
|
||||||
if (args.length == 2) {
|
|
||||||
name = args[1];
|
|
||||||
} else if (args.length >= 4) {
|
|
||||||
name = args[3];
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean success = SignBoot.doSignature(name, System.in, System.out, cert, key);
|
|
||||||
System.exit(success ? 0 : 1);
|
|
||||||
} else {
|
|
||||||
System.err.println(
|
|
||||||
"BootSigner <actions> [args]\n" +
|
|
||||||
"Input from stdin, outputs to stdout\n" +
|
|
||||||
"\n" +
|
|
||||||
"Actions:\n" +
|
|
||||||
" -verify [x509.pem]\n" +
|
|
||||||
" verify image, cert is optional\n" +
|
|
||||||
" -sign [x509.pem] [pk8] [name]\n" +
|
|
||||||
" sign image, name, cert and key pair are optional\n" +
|
|
||||||
" name should be /boot (default) or /recovery\n"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
package com.topjohnwu.signing;
|
|
||||||
|
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
|
||||||
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.security.KeyStore;
|
|
||||||
import java.security.KeyStoreException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.PrivateKey;
|
|
||||||
import java.security.Security;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
|
|
||||||
public class ZipSigner {
|
|
||||||
|
|
||||||
private static void usage() {
|
|
||||||
System.err.println("ZipSigner usage:");
|
|
||||||
System.err.println(" zipsigner.jar input.jar output.jar");
|
|
||||||
System.err.println(" sign jar with AOSP test keys");
|
|
||||||
System.err.println(" zipsigner.jar x509.pem pk8 input.jar output.jar");
|
|
||||||
System.err.println(" sign jar with certificate / private key pair");
|
|
||||||
System.err.println(" zipsigner.jar keyStore keyStorePass alias keyPass input.jar output.jar");
|
|
||||||
System.err.println(" sign jar with Java KeyStore");
|
|
||||||
System.exit(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void sign(JarMap input, FileOutputStream output) throws Exception {
|
|
||||||
sign(SignApk.class.getResourceAsStream("/keys/testkey.x509.pem"),
|
|
||||||
SignApk.class.getResourceAsStream("/keys/testkey.pk8"), input, output);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void sign(InputStream certIs, InputStream keyIs,
|
|
||||||
JarMap input, FileOutputStream output) throws Exception {
|
|
||||||
X509Certificate cert = CryptoUtils.readCertificate(certIs);
|
|
||||||
PrivateKey key = CryptoUtils.readPrivateKey(keyIs);
|
|
||||||
SignApk.sign(cert, key, input, output);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void sign(String keyStore, String keyStorePass, String alias, String keyPass,
|
|
||||||
JarMap in, FileOutputStream out) throws Exception {
|
|
||||||
KeyStore ks;
|
|
||||||
try {
|
|
||||||
ks = KeyStore.getInstance("JKS");
|
|
||||||
try (InputStream is = new FileInputStream(keyStore)) {
|
|
||||||
ks.load(is, keyStorePass.toCharArray());
|
|
||||||
}
|
|
||||||
} catch (KeyStoreException|IOException|CertificateException|NoSuchAlgorithmException e) {
|
|
||||||
ks = KeyStore.getInstance("PKCS12");
|
|
||||||
try (InputStream is = new FileInputStream(keyStore)) {
|
|
||||||
ks.load(is, keyStorePass.toCharArray());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
X509Certificate cert = (X509Certificate) ks.getCertificate(alias);
|
|
||||||
PrivateKey key = (PrivateKey) ks.getKey(alias, keyPass.toCharArray());
|
|
||||||
SignApk.sign(cert, key, in, out);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
|
||||||
if (args.length != 2 && args.length != 4 && args.length != 6)
|
|
||||||
usage();
|
|
||||||
|
|
||||||
Security.insertProviderAt(new BouncyCastleProvider(), 1);
|
|
||||||
|
|
||||||
try (JarMap in = JarMap.open(args[args.length - 2], false);
|
|
||||||
FileOutputStream out = new FileOutputStream(args[args.length - 1])) {
|
|
||||||
if (args.length == 2) {
|
|
||||||
sign(in, out);
|
|
||||||
} else if (args.length == 4) {
|
|
||||||
try (InputStream cert = new FileInputStream(args[0]);
|
|
||||||
InputStream key = new FileInputStream(args[1])) {
|
|
||||||
sign(cert, key, in, out);
|
|
||||||
}
|
|
||||||
} else if (args.length == 6) {
|
|
||||||
sign(args[0], args[1], args[2], args[3], in, out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
@@ -1,27 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIEqDCCA5CgAwIBAgIJAJNurL4H8gHfMA0GCSqGSIb3DQEBBQUAMIGUMQswCQYD
|
|
||||||
VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g
|
|
||||||
VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE
|
|
||||||
AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe
|
|
||||||
Fw0wODAyMjkwMTMzNDZaFw0zNTA3MTcwMTMzNDZaMIGUMQswCQYDVQQGEwJVUzET
|
|
||||||
MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G
|
|
||||||
A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p
|
|
||||||
ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI
|
|
||||||
hvcNAQEBBQADggENADCCAQgCggEBANaTGQTexgskse3HYuDZ2CU+Ps1s6x3i/waM
|
|
||||||
qOi8qM1r03hupwqnbOYOuw+ZNVn/2T53qUPn6D1LZLjk/qLT5lbx4meoG7+yMLV4
|
|
||||||
wgRDvkxyGLhG9SEVhvA4oU6Jwr44f46+z4/Kw9oe4zDJ6pPQp8PcSvNQIg1QCAcy
|
|
||||||
4ICXF+5qBTNZ5qaU7Cyz8oSgpGbIepTYOzEJOmc3Li9kEsBubULxWBjf/gOBzAzU
|
|
||||||
RNps3cO4JFgZSAGzJWQTT7/emMkod0jb9WdqVA2BVMi7yge54kdVMxHEa5r3b97s
|
|
||||||
zI5p58ii0I54JiCUP5lyfTwE/nKZHZnfm644oLIXf6MdW2r+6R8CAQOjgfwwgfkw
|
|
||||||
HQYDVR0OBBYEFEhZAFY9JyxGrhGGBaR0GawJyowRMIHJBgNVHSMEgcEwgb6AFEhZ
|
|
||||||
AFY9JyxGrhGGBaR0GawJyowRoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE
|
|
||||||
CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH
|
|
||||||
QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG
|
|
||||||
CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJAJNurL4H8gHfMAwGA1Ud
|
|
||||||
EwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAHqvlozrUMRBBVEY0NqrrwFbinZa
|
|
||||||
J6cVosK0TyIUFf/azgMJWr+kLfcHCHJsIGnlw27drgQAvilFLAhLwn62oX6snb4Y
|
|
||||||
LCBOsVMR9FXYJLZW2+TcIkCRLXWG/oiVHQGo/rWuWkJgU134NDEFJCJGjDbiLCpe
|
|
||||||
+ZTWHdcwauTJ9pUbo8EvHRkU3cYfGmLaLfgn9gP+pWA7LFQNvXwBnDa6sppCccEX
|
|
||||||
31I828XzgXpJ4O+mDL1/dBd+ek8ZPUP0IgdyZm5MTYPhvVqGCHzzTy3sIeJFymwr
|
|
||||||
sBbmg2OAUNLEMO6nwmocSdN2ClirfxqCzJOLSDE4QyS9BAH6EhY6UFcOaE0=
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
@@ -1,24 +1,26 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
package="com.topjohnwu.magisk">
|
|
||||||
|
<permission
|
||||||
|
android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
|
||||||
|
android:protectionLevel="signature"
|
||||||
|
tools:node="remove" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
|
||||||
android:maxSdkVersion="28" />
|
tools:node="remove" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".core.App"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:name="a.e"
|
android:multiArch="true"
|
||||||
android:allowBackup="true"
|
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning"
|
||||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
|
tools:remove="android:appComponentFactory">
|
||||||
|
|
||||||
<!-- Splash -->
|
|
||||||
<activity
|
<activity
|
||||||
android:name="a.c"
|
android:name=".ui.MainActivity"
|
||||||
|
android:exported="true"
|
||||||
android:theme="@style/SplashTheme">
|
android:theme="@style/SplashTheme">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
@@ -30,29 +32,25 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<!-- Main -->
|
|
||||||
<activity android:name="a.b" />
|
|
||||||
|
|
||||||
<!-- Superuser -->
|
|
||||||
<activity
|
<activity
|
||||||
android:name="a.m"
|
android:name=".ui.surequest.SuRequestActivity"
|
||||||
android:directBootAware="true"
|
android:directBootAware="true"
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
android:taskAffinity=""
|
||||||
tools:ignore="AppLinkUrlError">
|
tools:ignore="AppLinkUrlError">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<!-- Receiver -->
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name="a.h"
|
android:name=".core.Receiver"
|
||||||
android:directBootAware="true">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.REBOOT" />
|
|
||||||
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
||||||
|
<action android:name="android.intent.action.UID_REMOVED" />
|
||||||
|
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.PACKAGE_REPLACED" />
|
<action android:name="android.intent.action.PACKAGE_REPLACED" />
|
||||||
@@ -62,28 +60,33 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<!-- DownloadService -->
|
<service
|
||||||
<service android:name="a.j" />
|
android:name=".core.download.DownloadService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
|
||||||
<!-- Hardcode GMS version -->
|
<service
|
||||||
<meta-data
|
android:name=".core.JobService"
|
||||||
android:name="com.google.android.gms.version"
|
android:exported="false"
|
||||||
android:value="12451000" />
|
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||||
|
|
||||||
<!-- Initialize WorkManager on-demand -->
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.work.impl.WorkManagerInitializer"
|
android:name=".core.Provider"
|
||||||
android:authorities="${applicationId}.workmanager-init"
|
android:authorities="${applicationId}.provider"
|
||||||
tools:node="remove" />
|
android:directBootAware="true"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true" />
|
||||||
|
|
||||||
<!-- We don't invalidate Room -->
|
<!-- We don't invalidate Room -->
|
||||||
<service
|
<service
|
||||||
android:name="androidx.room.MultiInstanceInvalidationService"
|
android:name="androidx.room.MultiInstanceInvalidationService"
|
||||||
tools:node="remove"/>
|
tools:node="remove" />
|
||||||
|
|
||||||
<!-- We don't use Device Credentials -->
|
<!-- We don't need emoji compat -->
|
||||||
<activity
|
<provider
|
||||||
android:name="androidx.biometric.DeviceCredentialHandlerActivity"
|
android:name="androidx.startup.InitializationProvider"
|
||||||
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
|
android:exported="false"
|
||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// IRootUtils.aidl
|
||||||
|
package com.topjohnwu.magisk.core.utils;
|
||||||
|
|
||||||
|
// Declare any non-default types here with import statements
|
||||||
|
|
||||||
|
interface IRootUtils {
|
||||||
|
android.app.ActivityManager.RunningAppProcessInfo getAppProcess(int pid);
|
||||||
|
IBinder getFileSystem();
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
@file:JvmName("a")
|
|
||||||
package a
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.core.App
|
|
||||||
import com.topjohnwu.magisk.core.GeneralReceiver
|
|
||||||
import com.topjohnwu.magisk.core.SplashActivity
|
|
||||||
import com.topjohnwu.magisk.core.download.DownloadService
|
|
||||||
import com.topjohnwu.magisk.ui.MainActivity
|
|
||||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
|
||||||
import com.topjohnwu.signing.BootSigner
|
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
BootSigner.main(args)
|
|
||||||
}
|
|
||||||
|
|
||||||
class b : MainActivity()
|
|
||||||
|
|
||||||
class c : SplashActivity()
|
|
||||||
|
|
||||||
class e : App {
|
|
||||||
constructor() : super()
|
|
||||||
constructor(o: Any) : super(o)
|
|
||||||
}
|
|
||||||
|
|
||||||
class h : GeneralReceiver()
|
|
||||||
|
|
||||||
class j : DownloadService()
|
|
||||||
|
|
||||||
class m : SuRequestActivity()
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package androidx.lifecycle;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public class ProcessLifecycleAccessor {
|
||||||
|
public static void init(@NonNull Context context) {
|
||||||
|
LifecycleDispatcher.init(context);
|
||||||
|
ProcessLifecycleOwner.init(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,30 +5,27 @@ import android.view.KeyEvent
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.databinding.OnRebindCallback
|
import androidx.databinding.OnRebindCallback
|
||||||
import androidx.databinding.ViewDataBinding
|
import androidx.databinding.ViewDataBinding
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
import com.topjohnwu.magisk.ktx.startAnimations
|
|
||||||
|
|
||||||
abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
|
abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHolder {
|
||||||
Fragment(), BaseUIComponent<VM> {
|
|
||||||
|
|
||||||
protected val activity get() = requireActivity() as BaseUIActivity<*, *>
|
val activity get() = getActivity() as? NavigationActivity<*>
|
||||||
protected lateinit var binding: Binding
|
protected lateinit var binding: Binding
|
||||||
protected abstract val layoutRes: Int
|
protected abstract val layoutRes: Int
|
||||||
|
|
||||||
override val viewRoot: View get() = binding.root
|
private val navigation get() = activity?.navigation
|
||||||
private val navigation get() = activity.navigation
|
open val snackbarView: View? get() = null
|
||||||
|
open val snackbarAnchorView: View? get() = null
|
||||||
override fun consumeSystemWindowInsets(insets: Insets) = insets
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
startObserveEvents()
|
startObserveLiveData()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
@@ -38,14 +35,24 @@ abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
|
|||||||
): View? {
|
): View? {
|
||||||
binding = DataBindingUtil.inflate<Binding>(inflater, layoutRes, container, false).also {
|
binding = DataBindingUtil.inflate<Binding>(inflater, layoutRes, container, false).also {
|
||||||
it.setVariable(BR.viewModel, viewModel)
|
it.setVariable(BR.viewModel, viewModel)
|
||||||
it.lifecycleOwner = this
|
it.lifecycleOwner = viewLifecycleOwner
|
||||||
}
|
}
|
||||||
|
savedInstanceState?.let { viewModel.onRestoreState(it) }
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
viewModel.onSaveState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
activity?.supportActionBar?.subtitle = null
|
||||||
|
}
|
||||||
|
|
||||||
override fun onEventDispatched(event: ViewEvent) = when(event) {
|
override fun onEventDispatched(event: ViewEvent) = when(event) {
|
||||||
is ContextExecutor -> event(requireContext())
|
is ContextExecutor -> event(requireContext())
|
||||||
is ActivityExecutor -> event(activity)
|
is ActivityExecutor -> activity?.let { event(it) } ?: Unit
|
||||||
is FragmentExecutor -> event(this)
|
is FragmentExecutor -> event(this)
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
@@ -59,18 +66,22 @@ abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
if (this is MenuProvider)
|
||||||
|
activity?.addMenuProvider(this, viewLifecycleOwner)
|
||||||
binding.addOnRebindCallback(object : OnRebindCallback<Binding>() {
|
binding.addOnRebindCallback(object : OnRebindCallback<Binding>() {
|
||||||
override fun onPreBind(binding: Binding): Boolean {
|
override fun onPreBind(binding: Binding): Boolean {
|
||||||
this@BaseUIFragment.onPreBind(binding)
|
this@BaseFragment.onPreBind(binding)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
ensureInsets()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,13 +89,7 @@ abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun NavDirections.navigate() {
|
fun NavDirections.navigate() {
|
||||||
navigation?.navigate(this)
|
navigation?.currentDestination?.getAction(actionId)?.let { navigation!!.navigate(this) }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReselectionTarget {
|
|
||||||
|
|
||||||
fun onReselected()
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.view.View
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
|
||||||
import androidx.core.content.res.use
|
|
||||||
import androidx.databinding.DataBindingUtil
|
|
||||||
import androidx.databinding.ViewDataBinding
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.NavDirections
|
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
|
||||||
import com.topjohnwu.magisk.BR
|
|
||||||
import com.topjohnwu.magisk.core.Config
|
|
||||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
|
||||||
import com.topjohnwu.magisk.ui.theme.Theme
|
|
||||||
|
|
||||||
abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
|
|
||||||
BaseActivity(), BaseUIComponent<VM> {
|
|
||||||
|
|
||||||
protected lateinit var binding: Binding
|
|
||||||
protected abstract val layoutRes: Int
|
|
||||||
protected open val themeRes: Int = Theme.selected.themeRes
|
|
||||||
|
|
||||||
private val navHostFragment by lazy {
|
|
||||||
supportFragmentManager.findFragmentById(navHost) as? NavHostFragment
|
|
||||||
}
|
|
||||||
private val topFragment get() = navHostFragment?.childFragmentManager?.fragments?.getOrNull(0)
|
|
||||||
protected val currentFragment get() = topFragment as? BaseUIFragment<*, *>
|
|
||||||
|
|
||||||
override val viewRoot: View get() = binding.root
|
|
||||||
open val navigation: NavController? get() = navHostFragment?.navController
|
|
||||||
|
|
||||||
open val navHost: Int = 0
|
|
||||||
open val snackbarView get() = binding.root
|
|
||||||
|
|
||||||
init {
|
|
||||||
val theme = Config.darkTheme
|
|
||||||
AppCompatDelegate.setDefaultNightMode(theme)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
currentFragment?.onActivityResult(requestCode, resultCode, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
setTheme(themeRes)
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
startObserveEvents()
|
|
||||||
|
|
||||||
// We need to set the window background explicitly since for whatever reason it's not
|
|
||||||
// propagated upstream
|
|
||||||
obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
|
|
||||||
.use { it.getDrawable(0) }
|
|
||||||
.also { window.setBackgroundDrawable(it) }
|
|
||||||
|
|
||||||
directionsDispatcher.observe(this) {
|
|
||||||
it?.navigate()
|
|
||||||
// we don't want the directions to be re-dispatched, so we preemptively set them to null
|
|
||||||
if (it != null) {
|
|
||||||
directionsDispatcher.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setContentView() {
|
|
||||||
binding = DataBindingUtil.setContentView<Binding>(this, layoutRes).also {
|
|
||||||
it.setVariable(BR.viewModel, viewModel)
|
|
||||||
it.lifecycleOwner = this
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureInsets()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
viewModel.requestRefresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
|
||||||
return currentFragment?.onKeyEvent(event) == true || super.dispatchKeyEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onEventDispatched(event: ViewEvent) = when(event) {
|
|
||||||
is ContextExecutor -> event(this)
|
|
||||||
is ActivityExecutor -> event(this)
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed() {
|
|
||||||
if (navigation == null || currentFragment?.onBackPressed()?.not() == true) {
|
|
||||||
super.onBackPressed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun NavDirections.navigate() {
|
|
||||||
navigation?.navigate(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private val directionsDispatcher = MutableLiveData<NavDirections?>()
|
|
||||||
|
|
||||||
fun postDirections(navDirections: NavDirections) =
|
|
||||||
directionsDispatcher.postValue(navDirections)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.core.graphics.Insets
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
|
|
||||||
interface BaseUIComponent<VM : BaseViewModel>: LifecycleOwner {
|
|
||||||
|
|
||||||
val viewRoot: View
|
|
||||||
val viewModel: VM
|
|
||||||
|
|
||||||
fun startObserveEvents() {
|
|
||||||
viewModel.viewEvents.observe(this) {
|
|
||||||
onEventDispatched(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun consumeSystemWindowInsets(insets: Insets): Insets? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called for all [ViewEvent]s published by associated viewModel.
|
|
||||||
*/
|
|
||||||
fun onEventDispatched(event: ViewEvent) {}
|
|
||||||
|
|
||||||
fun ensureInsets() {
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(viewRoot) { _, insets ->
|
|
||||||
insets.asInsets()
|
|
||||||
.also { viewModel.insets = it }
|
|
||||||
.let { consumeSystemWindowInsets(it) }
|
|
||||||
?.subtractBy(insets) ?: insets
|
|
||||||
}
|
|
||||||
if (ViewCompat.isAttachedToWindow(viewRoot)) {
|
|
||||||
ViewCompat.requestApplyInsets(viewRoot)
|
|
||||||
} else {
|
|
||||||
viewRoot.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
|
|
||||||
override fun onViewDetachedFromWindow(v: View) = Unit
|
|
||||||
override fun onViewAttachedToWindow(v: View) {
|
|
||||||
ViewCompat.requestApplyInsets(v)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun WindowInsetsCompat.asInsets() = Insets.of(
|
|
||||||
systemWindowInsetLeft,
|
|
||||||
systemWindowInsetTop,
|
|
||||||
systemWindowInsetRight,
|
|
||||||
systemWindowInsetBottom
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun Insets.subtractBy(insets: WindowInsetsCompat) =
|
|
||||||
WindowInsetsCompat.Builder(insets).setSystemWindowInsets(
|
|
||||||
Insets.of(
|
|
||||||
insets.systemWindowInsetLeft - left,
|
|
||||||
insets.systemWindowInsetTop - top,
|
|
||||||
insets.systemWindowInsetRight - right,
|
|
||||||
insets.systemWindowInsetBottom - bottom
|
|
||||||
)
|
|
||||||
).build()
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,93 +1,39 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
package com.topjohnwu.magisk.arch
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest.permission.*
|
||||||
import android.os.Build
|
import android.annotation.SuppressLint
|
||||||
import androidx.annotation.CallSuper
|
import android.os.Bundle
|
||||||
import androidx.core.graphics.Insets
|
|
||||||
import androidx.databinding.Bindable
|
|
||||||
import androidx.databinding.Observable
|
|
||||||
import androidx.databinding.PropertyChangeRegistry
|
import androidx.databinding.PropertyChangeRegistry
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
import com.topjohnwu.magisk.BR
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.databinding.ObservableHost
|
||||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
import com.topjohnwu.magisk.events.BackPressEvent
|
||||||
import com.topjohnwu.magisk.events.*
|
import com.topjohnwu.magisk.events.DialogBuilder
|
||||||
import com.topjohnwu.magisk.utils.ObservableHost
|
import com.topjohnwu.magisk.events.DialogEvent
|
||||||
import com.topjohnwu.magisk.utils.set
|
import com.topjohnwu.magisk.events.NavigationEvent
|
||||||
import kotlinx.coroutines.Job
|
import com.topjohnwu.magisk.events.PermissionEvent
|
||||||
import org.koin.core.KoinComponent
|
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||||
|
|
||||||
abstract class BaseViewModel(
|
abstract class BaseViewModel : ViewModel(), ObservableHost {
|
||||||
initialState: State = State.LOADING
|
|
||||||
) : ViewModel(), ObservableHost, KoinComponent {
|
|
||||||
|
|
||||||
override var callbacks: PropertyChangeRegistry? = null
|
override var callbacks: PropertyChangeRegistry? = null
|
||||||
|
|
||||||
enum class State {
|
private val _viewEvents = MutableLiveData<ViewEvent>()
|
||||||
LOADED, LOADING, LOADING_FAILED
|
|
||||||
}
|
|
||||||
|
|
||||||
@get:Bindable
|
|
||||||
val loading get() = state == State.LOADING
|
|
||||||
@get:Bindable
|
|
||||||
val loaded get() = state == State.LOADED
|
|
||||||
@get:Bindable
|
|
||||||
val loadFailed get() = state == State.LOADING_FAILED
|
|
||||||
|
|
||||||
val isConnected get() = Info.isConnected
|
|
||||||
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
|
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
|
||||||
|
|
||||||
@get:Bindable
|
open fun onSaveState(state: Bundle) {}
|
||||||
var insets = Insets.NONE
|
open fun onRestoreState(state: Bundle) {}
|
||||||
set(value) = set(value, field, { field = it }, BR.insets)
|
open fun onNetworkChanged(network: Boolean) {}
|
||||||
|
|
||||||
var state= initialState
|
|
||||||
set(value) = set(value, field, { field = it }, BR.loading, BR.loaded, BR.loadFailed)
|
|
||||||
|
|
||||||
private val _viewEvents = MutableLiveData<ViewEvent>()
|
|
||||||
private var runningJob: Job? = null
|
|
||||||
private val refreshCallback = object : Observable.OnPropertyChangedCallback() {
|
|
||||||
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
|
||||||
requestRefresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
isConnected.addOnPropertyChangedCallback(refreshCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** This should probably never be called manually, it's called manually via delegate. */
|
|
||||||
@Synchronized
|
|
||||||
fun requestRefresh() {
|
|
||||||
if (runningJob?.isActive == true) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
runningJob = refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun refresh(): Job? = null
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
override fun onCleared() {
|
|
||||||
isConnected.removeOnPropertyChangedCallback(refreshCallback)
|
|
||||||
super.onCleared()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun withView(action: BaseActivity.() -> Unit) {
|
|
||||||
ViewActionEvent(action).publish()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
||||||
PermissionEvent(permission, callback).publish()
|
PermissionEvent(permission, callback).publish()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun withExternalRW(callback: () -> Unit) {
|
inline fun withExternalRW(crossinline callback: () -> Unit) {
|
||||||
withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) {
|
withPermission(WRITE_EXTERNAL_STORAGE) {
|
||||||
if (!it) {
|
if (!it) {
|
||||||
SnackbarEvent(R.string.external_rw_permission_denied).publish()
|
SnackbarEvent(R.string.external_rw_permission_denied).publish()
|
||||||
} else {
|
} else {
|
||||||
@@ -96,19 +42,40 @@ abstract class BaseViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
inline fun withInstallPermission(crossinline callback: () -> Unit) {
|
||||||
|
withPermission(REQUEST_INSTALL_PACKAGES) {
|
||||||
|
if (!it) {
|
||||||
|
SnackbarEvent(R.string.install_unknown_denied).publish()
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
inline fun withPostNotificationPermission(crossinline callback: () -> Unit) {
|
||||||
|
withPermission(POST_NOTIFICATIONS) {
|
||||||
|
if (!it) {
|
||||||
|
SnackbarEvent(R.string.post_notifications_denied).publish()
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun back() = BackPressEvent().publish()
|
fun back() = BackPressEvent().publish()
|
||||||
|
|
||||||
fun <Event : ViewEvent> Event.publish() {
|
fun ViewEvent.publish() {
|
||||||
_viewEvents.postValue(this)
|
_viewEvents.postValue(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <Event : ViewEventWithScope> Event.publish() {
|
fun DialogBuilder.show() {
|
||||||
scope = viewModelScope
|
DialogEvent(this).publish()
|
||||||
_viewEvents.postValue(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NavDirections.publish() {
|
fun NavDirections.navigate(pop: Boolean = false) {
|
||||||
_viewEvents.postValue(NavigationEvent(this))
|
_viewEvents.postValue(NavigationEvent(this, pop))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
|
||||||
|
|
||||||
import androidx.databinding.ViewDataBinding
|
|
||||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
|
||||||
import com.topjohnwu.magisk.databinding.RvItem
|
|
||||||
import com.topjohnwu.magisk.utils.DiffObservableList
|
|
||||||
import com.topjohnwu.magisk.utils.FilterableDiffObservableList
|
|
||||||
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
|
|
||||||
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
|
||||||
import me.tatarka.bindingcollectionadapter2.OnItemBind
|
|
||||||
|
|
||||||
inline fun <T : ComparableRvItem<*>> diffListOf(
|
|
||||||
vararg newItems: T
|
|
||||||
) = diffListOf(newItems.toList())
|
|
||||||
|
|
||||||
inline fun <T : ComparableRvItem<*>> diffListOf(
|
|
||||||
newItems: List<T>
|
|
||||||
) = DiffObservableList(object : DiffObservableList.Callback<T> {
|
|
||||||
override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.genericItemSameAs(newItem)
|
|
||||||
override fun areContentsTheSame(oldItem: T, newItem: T) = oldItem.genericContentSameAs(newItem)
|
|
||||||
}).also { it.update(newItems) }
|
|
||||||
|
|
||||||
inline fun <T : ComparableRvItem<*>> filterableListOf(
|
|
||||||
vararg newItems: T
|
|
||||||
) = FilterableDiffObservableList(object : DiffObservableList.Callback<T> {
|
|
||||||
override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.genericItemSameAs(newItem)
|
|
||||||
override fun areContentsTheSame(oldItem: T, newItem: T) = oldItem.genericContentSameAs(newItem)
|
|
||||||
}).also { it.update(newItems.toList()) }
|
|
||||||
|
|
||||||
fun <T : RvItem> adapterOf() = object : BindingRecyclerViewAdapter<T>() {
|
|
||||||
override fun onBindBinding(
|
|
||||||
binding: ViewDataBinding,
|
|
||||||
variableId: Int,
|
|
||||||
layoutRes: Int,
|
|
||||||
position: Int,
|
|
||||||
item: T
|
|
||||||
) {
|
|
||||||
super.onBindBinding(binding, variableId, layoutRes, position, item)
|
|
||||||
item.onBindingBound(binding)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <T : RvItem> itemBindingOf(
|
|
||||||
crossinline body: (ItemBinding<*>) -> Unit = {}
|
|
||||||
) = OnItemBind<T> { itemBinding, _, item ->
|
|
||||||
item.bind(itemBinding)
|
|
||||||
body(itemBinding)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.topjohnwu.magisk.arch
|
||||||
|
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import androidx.databinding.ViewDataBinding
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavDirections
|
||||||
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
|
|
||||||
|
abstract class NavigationActivity<Binding : ViewDataBinding> : UIActivity<Binding>() {
|
||||||
|
|
||||||
|
abstract val navHostId: Int
|
||||||
|
|
||||||
|
private val navHostFragment by lazy {
|
||||||
|
supportFragmentManager.findFragmentById(navHostId) as NavHostFragment
|
||||||
|
}
|
||||||
|
|
||||||
|
protected val currentFragment get() =
|
||||||
|
navHostFragment.childFragmentManager.fragments.getOrNull(0) as? BaseFragment<*>
|
||||||
|
|
||||||
|
val navigation: NavController get() = navHostFragment.navController
|
||||||
|
|
||||||
|
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||||
|
return if (binded && currentFragment?.onKeyEvent(event) == true) true else super.dispatchKeyEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
if (binded) {
|
||||||
|
if (currentFragment?.onBackPressed() == false) {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavDirections.navigate() {
|
||||||
|
navigation.navigate(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
|
||||||
|
|
||||||
import android.os.Handler
|
|
||||||
import androidx.core.os.postDelayed
|
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
|
||||||
|
|
||||||
interface Queryable {
|
|
||||||
|
|
||||||
val queryDelay: Long
|
|
||||||
val queryHandler: Handler get() = UiThreadHandler.handler
|
|
||||||
|
|
||||||
fun submitQuery() {
|
|
||||||
queryHandler.postDelayed(queryDelay) { query() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun query()
|
|
||||||
}
|
|
||||||
115
app/src/main/java/com/topjohnwu/magisk/arch/UIActivity.kt
Normal file
115
app/src/main/java/com/topjohnwu/magisk/arch/UIActivity.kt
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package com.topjohnwu.magisk.arch
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.core.content.res.use
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.databinding.DataBindingUtil
|
||||||
|
import androidx.databinding.ViewDataBinding
|
||||||
|
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||||
|
import androidx.transition.AutoTransition
|
||||||
|
import androidx.transition.TransitionManager
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.topjohnwu.magisk.BR
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.core.Config
|
||||||
|
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||||
|
import rikka.insets.WindowInsetsHelper
|
||||||
|
import rikka.layoutinflater.view.LayoutInflaterFactory
|
||||||
|
|
||||||
|
abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModelHolder {
|
||||||
|
|
||||||
|
protected lateinit var binding: Binding
|
||||||
|
protected abstract val layoutRes: Int
|
||||||
|
|
||||||
|
protected val binded get() = ::binding.isInitialized
|
||||||
|
|
||||||
|
open val snackbarView get() = binding.root
|
||||||
|
open val snackbarAnchorView: View? get() = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
AppCompatDelegate.setDefaultNightMode(Config.darkTheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
layoutInflater.factory2 = LayoutInflaterFactory(delegate)
|
||||||
|
.addOnViewCreatedListener(WindowInsetsHelper.LISTENER)
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
startObserveLiveData()
|
||||||
|
|
||||||
|
// We need to set the window background explicitly since for whatever reason it's not
|
||||||
|
// propagated upstream
|
||||||
|
obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
|
||||||
|
.use { it.getDrawable(0) }
|
||||||
|
.also { window.setBackgroundDrawable(it) }
|
||||||
|
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
window?.decorView?.post {
|
||||||
|
// If navigation bar is short enough (gesture navigation enabled), make it transparent
|
||||||
|
if ((window.decorView.rootWindowInsets?.systemWindowInsetBottom
|
||||||
|
?: 0) < Resources.getSystem().displayMetrics.density * 40) {
|
||||||
|
window.navigationBarColor = Color.TRANSPARENT
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
window.navigationBarDividerColor = Color.TRANSPARENT
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
window.isNavigationBarContrastEnforced = false
|
||||||
|
window.isStatusBarContrastEnforced = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setContentView() {
|
||||||
|
binding = DataBindingUtil.setContentView<Binding>(this, layoutRes).also {
|
||||||
|
it.setVariable(BR.viewModel, viewModel)
|
||||||
|
it.lifecycleOwner = this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAccessibilityDelegate(delegate: View.AccessibilityDelegate?) {
|
||||||
|
binding.root.rootView.accessibilityDelegate = delegate
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showSnackbar(
|
||||||
|
message: CharSequence,
|
||||||
|
length: Int = Snackbar.LENGTH_SHORT,
|
||||||
|
builder: Snackbar.() -> Unit = {}
|
||||||
|
) = Snackbar.make(snackbarView, message, length)
|
||||||
|
.setAnchorView(snackbarAnchorView).apply(builder).show()
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
viewModel.let {
|
||||||
|
if (it is AsyncLoadViewModel)
|
||||||
|
it.startLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEventDispatched(event: ViewEvent) = when (event) {
|
||||||
|
is ContextExecutor -> event(this)
|
||||||
|
is ActivityExecutor -> event(this)
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ViewGroup.startAnimations() {
|
||||||
|
val transition = AutoTransition()
|
||||||
|
.setInterpolator(FastOutSlowInInterpolator())
|
||||||
|
.setDuration(400)
|
||||||
|
.excludeTarget(R.id.main_toolbar, true)
|
||||||
|
TransitionManager.beginDelayedTransition(
|
||||||
|
this,
|
||||||
|
transition
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
package com.topjohnwu.magisk.arch
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for passing events from ViewModels to Activities/Fragments
|
* Class for passing events from ViewModels to Activities/Fragments
|
||||||
@@ -10,18 +8,14 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
*/
|
*/
|
||||||
abstract class ViewEvent
|
abstract class ViewEvent
|
||||||
|
|
||||||
abstract class ViewEventWithScope: ViewEvent() {
|
|
||||||
lateinit var scope: CoroutineScope
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContextExecutor {
|
interface ContextExecutor {
|
||||||
operator fun invoke(context: Context)
|
operator fun invoke(context: Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ActivityExecutor {
|
interface ActivityExecutor {
|
||||||
operator fun invoke(activity: BaseUIActivity<*, *>)
|
operator fun invoke(activity: UIActivity<*>)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FragmentExecutor {
|
interface FragmentExecutor {
|
||||||
operator fun invoke(fragment: Fragment)
|
operator fun invoke(fragment: BaseFragment<*>)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.topjohnwu.magisk.arch
|
||||||
|
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.ViewModelStoreOwner
|
||||||
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
||||||
|
import com.topjohnwu.magisk.ui.install.InstallViewModel
|
||||||
|
import com.topjohnwu.magisk.ui.log.LogViewModel
|
||||||
|
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
|
||||||
|
import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel
|
||||||
|
|
||||||
|
interface ViewModelHolder : LifecycleOwner, ViewModelStoreOwner {
|
||||||
|
|
||||||
|
val viewModel: BaseViewModel
|
||||||
|
|
||||||
|
fun startObserveLiveData() {
|
||||||
|
viewModel.viewEvents.observe(this, this::onEventDispatched)
|
||||||
|
Info.isConnected.observe(this, viewModel::onNetworkChanged)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called for all [ViewEvent]s published by associated viewModel.
|
||||||
|
*/
|
||||||
|
fun onEventDispatched(event: ViewEvent) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
object VMFactory : ViewModelProvider.Factory {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return when (modelClass) {
|
||||||
|
HomeViewModel::class.java -> HomeViewModel(ServiceLocator.networkService)
|
||||||
|
LogViewModel::class.java -> LogViewModel(ServiceLocator.logRepo)
|
||||||
|
SuperuserViewModel::class.java -> SuperuserViewModel(ServiceLocator.policyDB)
|
||||||
|
InstallViewModel::class.java ->
|
||||||
|
InstallViewModel(ServiceLocator.networkService, ServiceLocator.markwon)
|
||||||
|
SuRequestViewModel::class.java ->
|
||||||
|
SuRequestViewModel(ServiceLocator.policyDB, ServiceLocator.timeoutPrefs)
|
||||||
|
else -> modelClass.newInstance()
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified VM : ViewModel> ViewModelHolder.viewModel() =
|
||||||
|
lazy(LazyThreadSafetyMode.NONE) {
|
||||||
|
ViewModelProvider(this, VMFactory)[VM::class.java]
|
||||||
|
}
|
||||||
@@ -5,39 +5,36 @@ import android.app.Application
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.lifecycle.ProcessLifecycleAccessor
|
||||||
import androidx.multidex.MultiDex
|
import com.topjohnwu.magisk.StubApk
|
||||||
import androidx.work.WorkManager
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.core.utils.DispatcherExecutor
|
||||||
import com.topjohnwu.magisk.DynAPK
|
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||||
import com.topjohnwu.magisk.FileProvider
|
import com.topjohnwu.magisk.core.utils.ShellInit
|
||||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
import com.topjohnwu.magisk.core.utils.refreshLocale
|
||||||
import com.topjohnwu.magisk.core.utils.IODispatcherExecutor
|
import com.topjohnwu.magisk.core.utils.setConfig
|
||||||
import com.topjohnwu.magisk.core.utils.RootInit
|
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
||||||
import com.topjohnwu.magisk.core.utils.updateConfig
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import com.topjohnwu.magisk.di.koinModules
|
|
||||||
import com.topjohnwu.magisk.ktx.unwrap
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import org.koin.android.ext.koin.androidContext
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
import org.koin.core.context.startKoin
|
import com.topjohnwu.superuser.ipc.RootService
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
open class App() : Application() {
|
open class App() : Application() {
|
||||||
|
|
||||||
constructor(o: Any) : this() {
|
constructor(o: Any) : this() {
|
||||||
Info.stub = DynAPK.load(o)
|
val data = StubApk.Data(o)
|
||||||
|
// Add the root service name mapping
|
||||||
|
data.classToComponent[RootUtils::class.java.name] = data.rootService.name
|
||||||
|
// Send back the actual root service class
|
||||||
|
data.rootService = RootUtils::class.java
|
||||||
|
Info.stub = data
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
|
||||||
Shell.setDefaultBuilder(Shell.Builder.create()
|
|
||||||
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
|
||||||
.setInitializers(RootInit::class.java)
|
|
||||||
.setTimeout(2))
|
|
||||||
Shell.EXECUTOR = IODispatcherExecutor()
|
|
||||||
FileProvider.callHandler = SuCallbackHandler
|
|
||||||
|
|
||||||
// Always log full stack trace with Timber
|
// Always log full stack trace with Timber
|
||||||
Timber.plant(Timber.DebugTree())
|
Timber.plant(Timber.DebugTree())
|
||||||
Thread.setDefaultUncaughtExceptionHandler { _, e ->
|
Thread.setDefaultUncaughtExceptionHandler { _, e ->
|
||||||
@@ -46,59 +43,71 @@ open class App() : Application() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
override fun attachBaseContext(context: Context) {
|
||||||
// Basic setup
|
// Get the actual ContextImpl
|
||||||
if (BuildConfig.DEBUG)
|
|
||||||
MultiDex.install(base)
|
|
||||||
|
|
||||||
// Some context magic
|
|
||||||
val app: Application
|
val app: Application
|
||||||
val impl: Context
|
val base: Context
|
||||||
if (base is Application) {
|
if (context is Application) {
|
||||||
app = base
|
app = context
|
||||||
impl = base.baseContext
|
base = context.baseContext
|
||||||
|
AppApkPath = StubApk.current(base).path
|
||||||
} else {
|
} else {
|
||||||
app = this
|
app = this
|
||||||
impl = base
|
base = context
|
||||||
|
AppApkPath = base.packageResourcePath
|
||||||
}
|
}
|
||||||
val wrapped = impl.wrap()
|
super.attachBaseContext(base)
|
||||||
super.attachBaseContext(wrapped)
|
ServiceLocator.context = base
|
||||||
|
app.registerActivityLifecycleCallbacks(ActivityTracker)
|
||||||
|
|
||||||
// Normal startup
|
Shell.setDefaultBuilder(Shell.Builder.create()
|
||||||
startKoin {
|
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
||||||
androidContext(wrapped)
|
.setInitializers(ShellInit::class.java)
|
||||||
modules(koinModules)
|
.setContext(base)
|
||||||
}
|
.setTimeout(2))
|
||||||
ResMgr.init(impl)
|
Shell.EXECUTOR = DispatcherExecutor(Dispatchers.IO)
|
||||||
app.registerActivityLifecycleCallbacks(ForegroundTracker)
|
RootUtils.bindTask = RootService.bindOrTask(
|
||||||
WorkManager.initialize(impl.wrapJob(), androidx.work.Configuration.Builder().build())
|
intent<RootUtils>(),
|
||||||
|
UiThreadHandler.executor,
|
||||||
|
RootUtils.Connection
|
||||||
|
)
|
||||||
|
// Pre-heat the shell ASAP
|
||||||
|
Shell.getShell(null) {}
|
||||||
|
|
||||||
|
refreshLocale()
|
||||||
|
resources.patch()
|
||||||
|
Notifications.setup()
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is required as some platforms expect ContextImpl
|
override fun onCreate() {
|
||||||
override fun getBaseContext(): Context {
|
super.onCreate()
|
||||||
return super.getBaseContext().unwrap()
|
ProcessLifecycleAccessor.init(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
resources.updateConfig(newConfig)
|
if (resources.configuration.diff(newConfig) != 0) {
|
||||||
|
resources.setConfig(newConfig)
|
||||||
|
}
|
||||||
if (!isRunningAsStub)
|
if (!isRunningAsStub)
|
||||||
super.onConfigurationChanged(newConfig)
|
super.onConfigurationChanged(newConfig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object ForegroundTracker : Application.ActivityLifecycleCallbacks {
|
object ActivityTracker : Application.ActivityLifecycleCallbacks {
|
||||||
|
|
||||||
|
val foreground: Activity? get() = ref.get()
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
var foreground: Activity? = null
|
private var ref = WeakReference<Activity>(null)
|
||||||
|
|
||||||
val hasForeground get() = foreground != null
|
|
||||||
|
|
||||||
override fun onActivityResumed(activity: Activity) {
|
override fun onActivityResumed(activity: Activity) {
|
||||||
foreground = activity
|
if (activity is SuRequestActivity) return
|
||||||
|
ref = WeakReference(activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityPaused(activity: Activity) {
|
override fun onActivityPaused(activity: Activity) {
|
||||||
foreground = null
|
if (activity is SuRequestActivity) return
|
||||||
|
ref.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
||||||
|
|||||||
@@ -1,33 +1,38 @@
|
|||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import android.content.Context
|
import android.annotation.SuppressLint
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.util.Xml
|
import android.util.Xml
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
import com.topjohnwu.magisk.core.magiskdb.StringDao
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
import com.topjohnwu.magisk.core.utils.BiometricHelper
|
import com.topjohnwu.magisk.core.repository.BoolDBPropertyNoWrite
|
||||||
import com.topjohnwu.magisk.core.utils.defaultLocale
|
import com.topjohnwu.magisk.core.repository.DBConfig
|
||||||
|
import com.topjohnwu.magisk.core.repository.PreferenceConfig
|
||||||
import com.topjohnwu.magisk.core.utils.refreshLocale
|
import com.topjohnwu.magisk.core.utils.refreshLocale
|
||||||
import com.topjohnwu.magisk.data.preference.PreferenceModel
|
|
||||||
import com.topjohnwu.magisk.data.repository.DBConfig
|
|
||||||
import com.topjohnwu.magisk.di.Protected
|
|
||||||
import com.topjohnwu.magisk.ktx.get
|
|
||||||
import com.topjohnwu.magisk.ktx.inject
|
|
||||||
import com.topjohnwu.magisk.ui.theme.Theme
|
import com.topjohnwu.magisk.ui.theme.Theme
|
||||||
import com.topjohnwu.superuser.Shell
|
import kotlinx.coroutines.GlobalScope
|
||||||
import com.topjohnwu.superuser.io.SuFile
|
|
||||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
|
||||||
import org.xmlpull.v1.XmlPullParser
|
import org.xmlpull.v1.XmlPullParser
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
object Config : PreferenceModel, DBConfig {
|
object Config : PreferenceConfig, DBConfig {
|
||||||
|
|
||||||
override val stringDao: StringDao by inject()
|
override val stringDB get() = ServiceLocator.stringDB
|
||||||
override val settingsDao: SettingsDao by inject()
|
override val settingsDB get() = ServiceLocator.settingsDB
|
||||||
override val context: Context by inject(Protected)
|
override val context get() = ServiceLocator.deContext
|
||||||
|
override val coroutineScope get() = GlobalScope
|
||||||
|
|
||||||
|
@get:SuppressLint("ApplySharedPref")
|
||||||
|
val prefsFile: File get() {
|
||||||
|
// Flush prefs to disk
|
||||||
|
prefs.edit().apply {
|
||||||
|
remove(Key.ASKED_HOME)
|
||||||
|
}.commit()
|
||||||
|
return File("${context.filesDir.parent}/shared_prefs", "${fileName}.xml")
|
||||||
|
}
|
||||||
|
|
||||||
object Key {
|
object Key {
|
||||||
// db configs
|
// db configs
|
||||||
@@ -35,6 +40,8 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
const val SU_MULTIUSER_MODE = "multiuser_mode"
|
const val SU_MULTIUSER_MODE = "multiuser_mode"
|
||||||
const val SU_MNT_NS = "mnt_ns"
|
const val SU_MNT_NS = "mnt_ns"
|
||||||
const val SU_BIOMETRIC = "su_biometric"
|
const val SU_BIOMETRIC = "su_biometric"
|
||||||
|
const val ZYGISK = "zygisk"
|
||||||
|
const val DENYLIST = "denylist"
|
||||||
const val SU_MANAGER = "requester"
|
const val SU_MANAGER = "requester"
|
||||||
const val KEYSTORE = "keystore"
|
const val KEYSTORE = "keystore"
|
||||||
|
|
||||||
@@ -43,6 +50,7 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
const val SU_AUTO_RESPONSE = "su_auto_response"
|
const val SU_AUTO_RESPONSE = "su_auto_response"
|
||||||
const val SU_NOTIFICATION = "su_notification"
|
const val SU_NOTIFICATION = "su_notification"
|
||||||
const val SU_REAUTH = "su_reauth"
|
const val SU_REAUTH = "su_reauth"
|
||||||
|
const val SU_TAPJACK = "su_tapjack"
|
||||||
const val CHECK_UPDATES = "check_update"
|
const val CHECK_UPDATES = "check_update"
|
||||||
const val UPDATE_CHANNEL = "update_channel"
|
const val UPDATE_CHANNEL = "update_channel"
|
||||||
const val CUSTOM_CHANNEL = "custom_channel"
|
const val CUSTOM_CHANNEL = "custom_channel"
|
||||||
@@ -56,9 +64,6 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
const val BOOT_ID = "boot_id"
|
const val BOOT_ID = "boot_id"
|
||||||
const val ASKED_HOME = "asked_home"
|
const val ASKED_HOME = "asked_home"
|
||||||
const val DOH = "doh"
|
const val DOH = "doh"
|
||||||
|
|
||||||
// system state
|
|
||||||
const val MAGISKHIDE = "magiskhide"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object Value {
|
object Value {
|
||||||
@@ -68,6 +73,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
|
||||||
@@ -104,13 +110,16 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
|
|
||||||
private val defaultChannel =
|
private val defaultChannel =
|
||||||
if (BuildConfig.DEBUG)
|
if (BuildConfig.DEBUG)
|
||||||
|
Value.DEBUG_CHANNEL
|
||||||
|
else if (Const.APP_IS_CANARY)
|
||||||
Value.CANARY_CHANNEL
|
Value.CANARY_CHANNEL
|
||||||
else
|
else
|
||||||
Value.DEFAULT_CHANNEL
|
Value.DEFAULT_CHANNEL
|
||||||
|
|
||||||
@JvmStatic var keepVerity = false
|
@JvmField var keepVerity = false
|
||||||
@JvmStatic var keepEnc = false
|
@JvmField var keepEnc = false
|
||||||
@JvmStatic var recovery = false
|
@JvmField var patchVbmeta = false
|
||||||
|
@JvmField var recovery = false
|
||||||
|
|
||||||
var bootId by preference(Key.BOOT_ID, "")
|
var bootId by preference(Key.BOOT_ID, "")
|
||||||
var askedHome by preference(Key.ASKED_HOME, false)
|
var askedHome by preference(Key.ASKED_HOME, false)
|
||||||
@@ -119,7 +128,7 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
var repoOrder by preference(Key.REPO_ORDER, Value.ORDER_DATE)
|
var repoOrder by preference(Key.REPO_ORDER, Value.ORDER_DATE)
|
||||||
|
|
||||||
var suDefaultTimeout by preferenceStrInt(Key.SU_REQUEST_TIMEOUT, 10)
|
var suDefaultTimeout by preferenceStrInt(Key.SU_REQUEST_TIMEOUT, 10)
|
||||||
var suAutoReponse by preferenceStrInt(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT)
|
var suAutoResponse by preferenceStrInt(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT)
|
||||||
var suNotification by preferenceStrInt(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST)
|
var suNotification by preferenceStrInt(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST)
|
||||||
var updateChannel by preferenceStrInt(Key.UPDATE_CHANNEL, defaultChannel)
|
var updateChannel by preferenceStrInt(Key.UPDATE_CHANNEL, defaultChannel)
|
||||||
|
|
||||||
@@ -127,9 +136,17 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
var darkTheme by preference(Key.DARK_THEME, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
var darkTheme by preference(Key.DARK_THEME, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||||
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 checkUpdate by preference(Key.CHECK_UPDATES, true)
|
var suTapjack by preference(Key.SU_TAPJACK, true)
|
||||||
var doh by preference(Key.DOH, defaultLocale.country == "CN")
|
private var checkUpdatePrefs by preference(Key.CHECK_UPDATES, true)
|
||||||
var magiskHide by preference(Key.MAGISKHIDE, true)
|
var checkUpdate
|
||||||
|
get() = checkUpdatePrefs
|
||||||
|
set(value) {
|
||||||
|
if (checkUpdatePrefs != value) {
|
||||||
|
checkUpdatePrefs = value
|
||||||
|
JobService.schedule(AppContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var doh by preference(Key.DOH, false)
|
||||||
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
|
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
|
||||||
|
|
||||||
var customChannelUrl by preference(Key.CUSTOM_CHANNEL, "")
|
var customChannelUrl by preference(Key.CUSTOM_CHANNEL, "")
|
||||||
@@ -145,13 +162,20 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
var suMntNamespaceMode by dbSettings(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER)
|
var suMntNamespaceMode by dbSettings(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER)
|
||||||
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
|
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
|
||||||
var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
|
var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
|
||||||
|
var zygisk by dbSettings(Key.ZYGISK, false)
|
||||||
|
var denyList by BoolDBPropertyNoWrite(Key.DENYLIST, false)
|
||||||
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
||||||
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
|
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
|
||||||
|
|
||||||
private const val SU_FINGERPRINT = "su_fingerprint"
|
private const val SU_FINGERPRINT = "su_fingerprint"
|
||||||
|
|
||||||
fun initialize() {
|
fun load(pkg: String?) {
|
||||||
prefs.edit { parsePrefs() }
|
// Only try to load prefs when fresh install and a previous package name is set
|
||||||
|
if (pkg != null && prefs.all.isEmpty()) runCatching {
|
||||||
|
context.contentResolver.openInputStream(Provider.preferencesUri(pkg))?.use {
|
||||||
|
prefs.edit { parsePrefs(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
// Settings migration
|
// Settings migration
|
||||||
@@ -159,24 +183,17 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
suBiometric = true
|
suBiometric = true
|
||||||
remove(SU_FINGERPRINT)
|
remove(SU_FINGERPRINT)
|
||||||
prefs.getString(Key.UPDATE_CHANNEL, null).also {
|
prefs.getString(Key.UPDATE_CHANNEL, null).also {
|
||||||
if (it == null)
|
if (it == null ||
|
||||||
|
it.toInt() > Value.DEBUG_CHANNEL ||
|
||||||
|
it.toInt() < Value.DEFAULT_CHANNEL) {
|
||||||
putString(Key.UPDATE_CHANNEL, defaultChannel.toString())
|
putString(Key.UPDATE_CHANNEL, defaultChannel.toString())
|
||||||
else if (it.toInt() > Value.CANARY_CHANNEL)
|
}
|
||||||
putString(Key.UPDATE_CHANNEL, Value.CANARY_CHANNEL.toString())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write database configs
|
|
||||||
putString(Key.ROOT_ACCESS, rootMode.toString())
|
|
||||||
putString(Key.SU_MNT_NS, suMntNamespaceMode.toString())
|
|
||||||
putString(Key.SU_MULTIUSER_MODE, suMultiuserMode.toString())
|
|
||||||
putBoolean(Key.SU_BIOMETRIC, BiometricHelper.isEnabled)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun SharedPreferences.Editor.parsePrefs() {
|
private fun SharedPreferences.Editor.parsePrefs(input: InputStream) {
|
||||||
val config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS)
|
runCatching {
|
||||||
if (config.exists()) runCatching {
|
|
||||||
val input = SuFileInputStream(config)
|
|
||||||
val parser = Xml.newPullParser()
|
val parser = Xml.newPullParser()
|
||||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
|
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
|
||||||
parser.setInput(input, "UTF-8")
|
parser.setInput(input, "UTF-8")
|
||||||
@@ -220,21 +237,6 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
else -> parser.next()
|
else -> parser.next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
config.delete()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun export() {
|
|
||||||
// Flush prefs to disk
|
|
||||||
prefs.edit().apply {
|
|
||||||
remove(Key.ASKED_HOME)
|
|
||||||
}.commit()
|
|
||||||
val context = get<Context>(Protected)
|
|
||||||
val xml = File(
|
|
||||||
"${context.filesDir.parent}/shared_prefs",
|
|
||||||
"${context.packageName}_preferences.xml"
|
|
||||||
)
|
|
||||||
Shell.su("cat $xml > /data/adb/${Const.MANAGER_CONFIGS}").exec()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +1,60 @@
|
|||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
object Const {
|
object Const {
|
||||||
|
|
||||||
|
val CPU_ABI: String get() = Build.SUPPORTED_ABIS[0]
|
||||||
|
|
||||||
|
// Null if 32-bit only or 64-bit only
|
||||||
|
val CPU_ABI_32 =
|
||||||
|
if (Build.SUPPORTED_64_BIT_ABIS.isEmpty()) null
|
||||||
|
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 TMP_FOLDER_PATH = "/dev/tmp"
|
|
||||||
const val MAGISK_LOG = "/cache/magisk.log"
|
const val MAGISK_LOG = "/cache/magisk.log"
|
||||||
|
|
||||||
// Versions
|
|
||||||
const val SNET_EXT_VER = 15
|
|
||||||
const val SNET_REVISION = "d494bc726e86166913a13629e3b1336728ec5d7f"
|
|
||||||
const val BOOTCTL_REVISION = "a6c47f86f10b310358afa9dbe837037dd5d561df"
|
|
||||||
|
|
||||||
// Misc
|
// Misc
|
||||||
const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
|
||||||
const val MAGISK_INSTALL_LOG_FILENAME = "magisk_install_log_%s.log"
|
|
||||||
const val MANAGER_CONFIGS = ".tmp.magisk.config"
|
|
||||||
val USER_ID = Process.myUid() / 100000
|
val USER_ID = Process.myUid() / 100000
|
||||||
|
val APP_IS_CANARY get() = Version.isCanary(BuildConfig.VERSION_CODE)
|
||||||
|
|
||||||
object Version {
|
object Version {
|
||||||
const val MIN_VERSION = "v19.0"
|
const val MIN_VERSION = "v22.0"
|
||||||
const val MIN_VERCODE = 19000
|
const val MIN_VERCODE = 22000
|
||||||
|
|
||||||
fun atLeast_20_2() = Info.env.magiskVersionCode >= 20200 || isCanary()
|
fun atLeast_24_0() = Info.env.versionCode >= 24000 || isCanary()
|
||||||
fun atLeast_20_4() = Info.env.magiskVersionCode >= 20400 || isCanary()
|
fun atLeast_25_0() = Info.env.versionCode >= 25000 || isCanary()
|
||||||
fun atLeast_21_0() = Info.env.magiskVersionCode >= 21000 || isCanary()
|
fun isCanary() = isCanary(Info.env.versionCode)
|
||||||
fun isCanary() = Info.env.magiskVersionCode % 100 != 0
|
|
||||||
|
fun isCanary(ver: Int) = ver > 0 && ver % 100 != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
object ID {
|
object ID {
|
||||||
const val FETCH_ZIP = 2
|
const val JOB_SERVICE_ID = 7
|
||||||
const val SELECT_BOOT = 3
|
|
||||||
|
|
||||||
// notifications
|
|
||||||
const val MAGISK_UPDATE_NOTIFICATION_ID = 4
|
|
||||||
const val APK_UPDATE_NOTIFICATION_ID = 5
|
|
||||||
const val DTBO_NOTIFICATION_ID = 7
|
|
||||||
const val HIDE_MANAGER_NOTIFICATION_ID = 8
|
|
||||||
const val UPDATE_NOTIFICATION_CHANNEL = "update"
|
|
||||||
const val PROGRESS_NOTIFICATION_CHANNEL = "progress"
|
|
||||||
const val CHECK_MAGISK_UPDATE_WORKER_ID = "magisk_update"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object Url {
|
object Url {
|
||||||
const val ZIP_URL = "https://github.com/Magisk-Modules-Repo/%s/archive/master.zip"
|
|
||||||
const val PATREON_URL = "https://www.patreon.com/topjohnwu"
|
const val PATREON_URL = "https://www.patreon.com/topjohnwu"
|
||||||
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
|
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
|
||||||
|
|
||||||
|
val CHANGELOG_URL = if (APP_IS_CANARY) Info.remote.magisk.note
|
||||||
|
else "https://topjohnwu.github.io/Magisk/releases/${BuildConfig.VERSION_CODE}.md"
|
||||||
|
|
||||||
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com/"
|
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com/"
|
||||||
const val GITHUB_API_URL = "https://api.github.com/users/Magisk-Modules-Repo/"
|
const val GITHUB_API_URL = "https://api.github.com/"
|
||||||
const val GITHUB_PAGE_URL = "https://topjohnwu.github.io/magisk_files/"
|
const val GITHUB_PAGE_URL = "https://topjohnwu.github.io/magisk-files/"
|
||||||
|
const val JS_DELIVR_URL = "https://cdn.jsdelivr.net/gh/"
|
||||||
}
|
}
|
||||||
|
|
||||||
object Key {
|
object Key {
|
||||||
// others
|
|
||||||
const val LINK_KEY = "Link"
|
|
||||||
const val IF_NONE_MATCH = "If-None-Match"
|
|
||||||
const val ETAG_KEY = "ETag"
|
|
||||||
// intents
|
// intents
|
||||||
const val OPEN_SECTION = "section"
|
const val OPEN_SECTION = "section"
|
||||||
|
const val PREV_PKG = "prev_pkg"
|
||||||
}
|
}
|
||||||
|
|
||||||
object Value {
|
object Value {
|
||||||
@@ -75,7 +68,6 @@ object Const {
|
|||||||
object Nav {
|
object Nav {
|
||||||
const val HOME = "home"
|
const val HOME = "home"
|
||||||
const val SETTINGS = "settings"
|
const val SETTINGS = "settings"
|
||||||
const val HIDE = "hide"
|
|
||||||
const val MODULES = "modules"
|
const val MODULES = "modules"
|
||||||
const val SUPERUSER = "superuser"
|
const val SUPERUSER = "superuser"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core
|
|
||||||
|
|
||||||
import android.content.ContextWrapper
|
|
||||||
import android.content.Intent
|
|
||||||
import com.topjohnwu.magisk.core.base.BaseReceiver
|
|
||||||
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
|
||||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
|
||||||
import com.topjohnwu.magisk.view.Shortcuts
|
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koin.core.inject
|
|
||||||
|
|
||||||
open class GeneralReceiver : BaseReceiver() {
|
|
||||||
|
|
||||||
private val policyDB: PolicyDao by inject()
|
|
||||||
|
|
||||||
private fun getPkg(intent: Intent): String {
|
|
||||||
return intent.data?.encodedSchemeSpecificPart.orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReceive(context: ContextWrapper, intent: Intent?) {
|
|
||||||
intent ?: return
|
|
||||||
|
|
||||||
fun rmPolicy(pkg: String) = GlobalScope.launch {
|
|
||||||
policyDB.delete(pkg)
|
|
||||||
}
|
|
||||||
|
|
||||||
when (intent.action ?: return) {
|
|
||||||
Intent.ACTION_REBOOT -> {
|
|
||||||
SuCallbackHandler(context, intent.getStringExtra("action"), intent.extras)
|
|
||||||
}
|
|
||||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
|
||||||
// This will only work pre-O
|
|
||||||
if (Config.suReAuth)
|
|
||||||
rmPolicy(getPkg(intent))
|
|
||||||
}
|
|
||||||
Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
|
|
||||||
val pkg = getPkg(intent)
|
|
||||||
rmPolicy(pkg)
|
|
||||||
Shell.su("magiskhide --rm $pkg").submit()
|
|
||||||
}
|
|
||||||
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setupDynamic(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,11 +2,6 @@
|
|||||||
|
|
||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.job.JobInfo
|
|
||||||
import android.app.job.JobScheduler
|
|
||||||
import android.app.job.JobWorkItem
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
@@ -14,148 +9,53 @@ import android.content.Intent
|
|||||||
import android.content.res.AssetManager
|
import android.content.res.AssetManager
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import androidx.annotation.RequiresApi
|
import android.util.DisplayMetrics
|
||||||
import com.topjohnwu.magisk.DynAPK
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.download.DownloadService
|
import com.topjohnwu.magisk.StubApk
|
||||||
import com.topjohnwu.magisk.core.utils.refreshLocale
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
import com.topjohnwu.magisk.core.utils.updateConfig
|
import com.topjohnwu.magisk.core.ktx.unwrap
|
||||||
import com.topjohnwu.magisk.ktx.forceGetDeclaredField
|
import com.topjohnwu.magisk.core.utils.syncLocale
|
||||||
import com.topjohnwu.magisk.ui.MainActivity
|
|
||||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
|
||||||
|
|
||||||
fun AssetManager.addAssetPath(path: String) {
|
lateinit var AppApkPath: String
|
||||||
DynAPK.addAssetPath(this, path)
|
|
||||||
|
fun Resources.addAssetPath(path: String) = StubApk.addAssetPath(this, path)
|
||||||
|
|
||||||
|
fun Resources.patch(): Resources {
|
||||||
|
if (isRunningAsStub)
|
||||||
|
addAssetPath(AppApkPath)
|
||||||
|
syncLocale()
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.wrap(global: Boolean = true): Context =
|
fun Context.patch(): Context {
|
||||||
if (global) GlobalResContext(this) else ResContext(this)
|
unwrap().resources.patch()
|
||||||
|
return this
|
||||||
fun Context.wrapJob(): Context = object : GlobalResContext(this) {
|
|
||||||
|
|
||||||
override fun getApplicationContext(): Context {
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
override fun getSystemService(name: String): Any? {
|
|
||||||
return if (!isRunningAsStub) super.getSystemService(name) else
|
|
||||||
when (name) {
|
|
||||||
Context.JOB_SCHEDULER_SERVICE ->
|
|
||||||
JobSchedulerWrapper(super.getSystemService(name) as JobScheduler)
|
|
||||||
else -> super.getSystemService(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Class<*>.cmp(pkg: String): ComponentName {
|
// Wrapping is only necessary for ContextThemeWrapper to support configuration overrides
|
||||||
val name = ClassMap[this].name
|
fun Context.wrap(): Context {
|
||||||
return ComponentName(pkg, Info.stub?.classToComponent?.get(name) ?: name)
|
patch()
|
||||||
}
|
return object : ContextWrapper(this) {
|
||||||
|
override fun createConfigurationContext(config: Configuration): Context {
|
||||||
inline fun <reified T> Activity.redirect() = Intent(intent)
|
return super.createConfigurationContext(config).wrap()
|
||||||
.setComponent(T::class.java.cmp(packageName))
|
|
||||||
.setFlags(0)
|
|
||||||
|
|
||||||
inline fun <reified T> Context.intent() = Intent().setComponent(T::class.java.cmp(packageName))
|
|
||||||
|
|
||||||
private open class GlobalResContext(base: Context) : ContextWrapper(base) {
|
|
||||||
open val mRes: Resources get() = ResMgr.resource
|
|
||||||
|
|
||||||
override fun getResources(): Resources {
|
|
||||||
return mRes
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getClassLoader(): ClassLoader {
|
|
||||||
return javaClass.classLoader!!
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createConfigurationContext(config: Configuration): Context {
|
|
||||||
return ResContext(super.createConfigurationContext(config))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ResContext(base: Context) : GlobalResContext(base) {
|
|
||||||
override val mRes by lazy { base.resources.patch() }
|
|
||||||
|
|
||||||
private fun Resources.patch(): Resources {
|
|
||||||
updateConfig()
|
|
||||||
if (isRunningAsStub)
|
|
||||||
assets.addAssetPath(ResMgr.apk)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object ResMgr {
|
|
||||||
|
|
||||||
lateinit var resource: Resources
|
|
||||||
lateinit var apk: String
|
|
||||||
|
|
||||||
fun init(context: Context) {
|
|
||||||
resource = context.resources
|
|
||||||
refreshLocale()
|
|
||||||
if (isRunningAsStub) {
|
|
||||||
apk = DynAPK.current(context).path
|
|
||||||
resource.assets.addAssetPath(apk)
|
|
||||||
} else {
|
|
||||||
apk = context.packageResourcePath
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(28)
|
fun createNewResources(): Resources {
|
||||||
private class JobSchedulerWrapper(private val base: JobScheduler) : JobScheduler() {
|
val asset = AssetManager::class.java.newInstance()
|
||||||
|
val config = Configuration(AppContext.resources.configuration)
|
||||||
override fun schedule(job: JobInfo): Int {
|
val metrics = DisplayMetrics()
|
||||||
return base.schedule(job.patch())
|
metrics.setTo(AppContext.resources.displayMetrics)
|
||||||
}
|
val res = Resources(asset, metrics, config)
|
||||||
|
res.addAssetPath(AppApkPath)
|
||||||
override fun enqueue(job: JobInfo, work: JobWorkItem): Int {
|
return res
|
||||||
return base.enqueue(job.patch(), work)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancel(jobId: Int) {
|
|
||||||
base.cancel(jobId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancelAll() {
|
|
||||||
base.cancelAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getAllPendingJobs(): List<JobInfo> {
|
|
||||||
return base.allPendingJobs
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPendingJob(jobId: Int): JobInfo? {
|
|
||||||
return base.getPendingJob(jobId)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun JobInfo.patch(): JobInfo {
|
|
||||||
// We need to swap out the service of JobInfo
|
|
||||||
val name = service.className
|
|
||||||
val component = ComponentName(
|
|
||||||
service.packageName,
|
|
||||||
Info.stubChk.classToComponent[name] ?: name
|
|
||||||
)
|
|
||||||
|
|
||||||
javaClass.forceGetDeclaredField("service")?.set(this, component)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private object ClassMap {
|
fun Class<*>.cmp(pkg: String) =
|
||||||
|
ComponentName(pkg, Info.stub?.classToComponent?.get(name) ?: name)
|
||||||
|
|
||||||
private val map = mapOf(
|
inline fun <reified T> Context.intent() = Intent().setComponent(T::class.java.cmp(packageName))
|
||||||
App::class.java to a.e::class.java,
|
|
||||||
MainActivity::class.java to a.b::class.java,
|
|
||||||
SplashActivity::class.java to a.c::class.java,
|
|
||||||
GeneralReceiver::class.java to a.h::class.java,
|
|
||||||
DownloadService::class.java to a.j::class.java,
|
|
||||||
SuRequestActivity::class.java to a.m::class.java
|
|
||||||
)
|
|
||||||
|
|
||||||
operator fun get(c: Class<*>) = map.getOrElse(c) { c }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep a reference to these resources to prevent it from
|
// Keep a reference to these resources to prevent it from
|
||||||
// being removed when running "remove unused resources"
|
// being removed when running "remove unused resources"
|
||||||
@@ -164,10 +64,11 @@ val shouldKeepResources = listOf(
|
|||||||
R.string.release_notes,
|
R.string.release_notes,
|
||||||
R.string.invalid_update_channel,
|
R.string.invalid_update_channel,
|
||||||
R.string.update_available,
|
R.string.update_available,
|
||||||
R.string.safetynet_api_error,
|
|
||||||
R.raw.changelog,
|
|
||||||
R.drawable.ic_device,
|
R.drawable.ic_device,
|
||||||
R.drawable.ic_hide_select_md2,
|
|
||||||
R.drawable.ic_more,
|
R.drawable.ic_more,
|
||||||
R.drawable.ic_magisk_delete
|
R.drawable.ic_magisk_delete,
|
||||||
|
R.drawable.ic_refresh_data_md2,
|
||||||
|
R.drawable.ic_order_date,
|
||||||
|
R.drawable.ic_order_name,
|
||||||
|
R.array.allow_timeout,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,87 +1,81 @@
|
|||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import androidx.databinding.ObservableBoolean
|
import android.os.Build
|
||||||
import com.topjohnwu.magisk.DynAPK
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import com.topjohnwu.magisk.StubApk
|
||||||
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
|
import com.topjohnwu.magisk.core.ktx.getProperty
|
||||||
import com.topjohnwu.magisk.core.model.UpdateInfo
|
import com.topjohnwu.magisk.core.model.UpdateInfo
|
||||||
import com.topjohnwu.magisk.core.utils.net.NetworkObserver
|
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||||
import com.topjohnwu.magisk.ktx.get
|
import com.topjohnwu.magisk.core.utils.NetworkObserver
|
||||||
import com.topjohnwu.magisk.utils.CachedValue
|
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import com.topjohnwu.superuser.ShellUtils.fastCmd
|
import com.topjohnwu.superuser.ShellUtils.fastCmd
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
val isRunningAsStub get() = Info.stub != null
|
val isRunningAsStub get() = Info.stub != null
|
||||||
|
|
||||||
object Info {
|
object Info {
|
||||||
|
|
||||||
val envRef = CachedValue { loadState() }
|
var stub: StubApk.Data? = null
|
||||||
|
|
||||||
@JvmStatic val env by envRef
|
val EMPTY_REMOTE = UpdateInfo()
|
||||||
|
var remote = EMPTY_REMOTE
|
||||||
var stub: DynAPK.Data? = null
|
suspend fun getRemote(svc: NetworkService): UpdateInfo? {
|
||||||
val stubChk: DynAPK.Data
|
return if (remote === EMPTY_REMOTE) {
|
||||||
get() = stub as DynAPK.Data
|
svc.fetchUpdate()?.apply { remote = this }
|
||||||
|
} else remote
|
||||||
var remote = UpdateInfo()
|
}
|
||||||
|
|
||||||
// Device state
|
// Device state
|
||||||
var crypto = ""
|
@JvmStatic val env by lazy { loadState() }
|
||||||
@JvmStatic var isSAR = false
|
@JvmField var isSAR = false
|
||||||
@JvmStatic var isAB = false
|
var isAB = false
|
||||||
|
@JvmField val isZygiskEnabled = System.getenv("ZYGISK_ENABLED") == "1"
|
||||||
@JvmStatic val isFDE get() = crypto == "block"
|
@JvmStatic val isFDE get() = crypto == "block"
|
||||||
@JvmStatic var ramdisk = false
|
@JvmField var ramdisk = false
|
||||||
@JvmStatic var hasGMS = true
|
@JvmField var vbmeta = false
|
||||||
@JvmStatic var isPixel = false
|
var crypto = ""
|
||||||
@JvmStatic val cryptoText get() = crypto.capitalize(Locale.US)
|
var noDataExec = false
|
||||||
|
var isRooted = false
|
||||||
|
|
||||||
val isConnected by lazy {
|
@JvmField var hasGMS = true
|
||||||
ObservableBoolean(false).also { field ->
|
val isSamsung = Build.MANUFACTURER.equals("samsung", ignoreCase = true)
|
||||||
NetworkObserver.observe(get()) {
|
@JvmField val isEmulator =
|
||||||
UiThreadHandler.run { field.set(it) }
|
getProperty("ro.kernel.qemu", "0") == "1" ||
|
||||||
|
getProperty("ro.boot.qemu", "0") == "1"
|
||||||
|
|
||||||
|
val isConnected: LiveData<Boolean> by lazy {
|
||||||
|
MutableLiveData(false).also { field ->
|
||||||
|
NetworkObserver.observe(AppContext) {
|
||||||
|
remote = EMPTY_REMOTE
|
||||||
|
field.postValue(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val isNewReboot by lazy {
|
val showSuperUser: Boolean get() {
|
||||||
try {
|
return env.isActive && (Const.USER_ID == 0
|
||||||
FileInputStream("/proc/sys/kernel/random/boot_id").bufferedReader().use {
|
|| Config.suMultiuserMode == Config.Value.MULTIUSER_MODE_USER)
|
||||||
val id = it.readLine()
|
|
||||||
if (id != Config.bootId) {
|
|
||||||
Config.bootId = id
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadState() = Env(
|
private fun loadState(): Env {
|
||||||
fastCmd("magisk -v").split(":".toRegex())[0],
|
val v = fastCmd("magisk -v").split(":".toRegex())
|
||||||
runCatching { fastCmd("magisk -V").toInt() }.getOrDefault(-1),
|
return Env(
|
||||||
Shell.su("magiskhide --status").exec().isSuccess
|
v[0], v.size >= 3 && v[2] == "D",
|
||||||
)
|
runCatching { fastCmd("magisk -V").toInt() }.getOrDefault(-1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
class Env(
|
class Env(
|
||||||
val magiskVersionString: String = "",
|
val versionString: String = "",
|
||||||
code: Int = -1,
|
val isDebug: Boolean = false,
|
||||||
hide: Boolean = false
|
code: Int = -1
|
||||||
) {
|
) {
|
||||||
val magiskHide get() = Config.magiskHide
|
val versionCode = when {
|
||||||
val magiskVersionCode = when (code) {
|
code < Const.Version.MIN_VERCODE -> -1
|
||||||
in Int.MIN_VALUE..Const.Version.MIN_VERCODE -> -1
|
isRooted -> code
|
||||||
else -> if (Shell.rootAccess()) code else -1
|
else -> -1
|
||||||
}
|
}
|
||||||
val isUnsupported = code > 0 && code < Const.Version.MIN_VERCODE
|
val isUnsupported = code > 0 && code < Const.Version.MIN_VERCODE
|
||||||
val isActive = magiskVersionCode >= 0
|
val isActive = versionCode > 0
|
||||||
|
|
||||||
init {
|
|
||||||
Config.magiskHide = hide
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
61
app/src/main/java/com/topjohnwu/magisk/core/JobService.kt
Normal file
61
app/src/main/java/com/topjohnwu/magisk/core/JobService.kt
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
|
import android.app.job.JobInfo
|
||||||
|
import android.app.job.JobParameters
|
||||||
|
import android.app.job.JobScheduler
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
|
import com.topjohnwu.magisk.core.base.BaseJobService
|
||||||
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class JobService : BaseJobService() {
|
||||||
|
|
||||||
|
private val job = Job()
|
||||||
|
private val svc get() = ServiceLocator.networkService
|
||||||
|
|
||||||
|
override fun onStartJob(params: JobParameters): Boolean {
|
||||||
|
val coroutineScope = CoroutineScope(Dispatchers.IO + job)
|
||||||
|
coroutineScope.launch {
|
||||||
|
doWork()
|
||||||
|
jobFinished(params, false)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun doWork() {
|
||||||
|
svc.fetchUpdate()?.let {
|
||||||
|
Info.remote = it
|
||||||
|
if (Info.env.isActive && BuildConfig.VERSION_CODE < it.magisk.versionCode)
|
||||||
|
Notifications.updateAvailable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopJob(params: JobParameters): Boolean {
|
||||||
|
job.cancel()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun schedule(context: Context) {
|
||||||
|
val scheduler = context.getSystemService<JobScheduler>() ?: return
|
||||||
|
if (Config.checkUpdate) {
|
||||||
|
val cmp = JobService::class.java.cmp(context.packageName)
|
||||||
|
val info = JobInfo.Builder(Const.ID.JOB_SERVICE_ID, cmp)
|
||||||
|
.setPeriodic(TimeUnit.HOURS.toMillis(12))
|
||||||
|
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
|
||||||
|
.setRequiresDeviceIdle(true)
|
||||||
|
.build()
|
||||||
|
scheduler.schedule(info)
|
||||||
|
} else {
|
||||||
|
scheduler.cancel(Const.ID.JOB_SERVICE_ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
app/src/main/java/com/topjohnwu/magisk/core/Provider.kt
Normal file
28
app/src/main/java/com/topjohnwu/magisk/core/Provider.kt
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.ParcelFileDescriptor
|
||||||
|
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
||||||
|
import com.topjohnwu.magisk.core.base.BaseProvider
|
||||||
|
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
||||||
|
|
||||||
|
class Provider : BaseProvider() {
|
||||||
|
|
||||||
|
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
|
||||||
|
SuCallbackHandler.run(context!!, method, extras)
|
||||||
|
return Bundle.EMPTY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
|
||||||
|
return when (uri.encodedPath ?: return null) {
|
||||||
|
"/prefs_file" -> ParcelFileDescriptor.open(Config.prefsFile, MODE_READ_ONLY)
|
||||||
|
else -> super.openFile(uri, mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun preferencesUri(pkg: String): Uri =
|
||||||
|
Uri.Builder().scheme("content").authority("$pkg.provider").path("prefs_file").build()
|
||||||
|
}
|
||||||
|
}
|
||||||
59
app/src/main/java/com/topjohnwu/magisk/core/Receiver.kt
Normal file
59
app/src/main/java/com/topjohnwu/magisk/core/Receiver.kt
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import com.topjohnwu.magisk.core.base.BaseReceiver
|
||||||
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
|
import com.topjohnwu.magisk.view.Shortcuts
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
open class Receiver : BaseReceiver() {
|
||||||
|
|
||||||
|
private val policyDB get() = ServiceLocator.policyDB
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
private fun getPkg(intent: Intent): String? {
|
||||||
|
val pkg = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME)
|
||||||
|
return pkg ?: intent.data?.schemeSpecificPart
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUid(intent: Intent): Int? {
|
||||||
|
val uid = intent.getIntExtra(Intent.EXTRA_UID, -1)
|
||||||
|
return if (uid == -1) null else uid
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
|
intent ?: return
|
||||||
|
super.onReceive(context, intent)
|
||||||
|
|
||||||
|
fun rmPolicy(uid: Int) = GlobalScope.launch {
|
||||||
|
policyDB.delete(uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
when (intent.action ?: return) {
|
||||||
|
Intent.ACTION_PACKAGE_REPLACED -> {
|
||||||
|
// This will only work pre-O
|
||||||
|
if (Config.suReAuth)
|
||||||
|
getUid(intent)?.let { rmPolicy(it) }
|
||||||
|
}
|
||||||
|
Intent.ACTION_UID_REMOVED -> {
|
||||||
|
getUid(intent)?.let { rmPolicy(it) }
|
||||||
|
}
|
||||||
|
Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
|
||||||
|
getPkg(intent)?.let { Shell.cmd("magisk --denylist rm $it").submit() }
|
||||||
|
}
|
||||||
|
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setupDynamic(context)
|
||||||
|
Intent.ACTION_MY_PACKAGE_REPLACED -> {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val installer = context.packageManager.getInstallerPackageName(context.packageName)
|
||||||
|
if (installer == context.packageName) {
|
||||||
|
Notifications.updateDone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
|
||||||
import com.topjohnwu.magisk.R
|
|
||||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
|
||||||
import com.topjohnwu.magisk.ktx.get
|
|
||||||
import com.topjohnwu.magisk.ui.MainActivity
|
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
|
||||||
import com.topjohnwu.magisk.view.Shortcuts
|
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
open class SplashActivity : Activity() {
|
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
|
||||||
super.attachBaseContext(base.wrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
setTheme(R.style.SplashTheme)
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
|
||||||
initAndStart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleRepackage() {
|
|
||||||
val pkg = Config.suManager
|
|
||||||
if (Config.suManager.isNotEmpty() && packageName == BuildConfig.APPLICATION_ID) {
|
|
||||||
Config.suManager = ""
|
|
||||||
Shell.su("(pm uninstall $pkg)& >/dev/null 2>&1").exec()
|
|
||||||
}
|
|
||||||
if (pkg == packageName) {
|
|
||||||
runCatching {
|
|
||||||
// We are the manager, remove com.topjohnwu.magisk as it could be malware
|
|
||||||
packageManager.getApplicationInfo(BuildConfig.APPLICATION_ID, 0)
|
|
||||||
Shell.su("(pm uninstall ${BuildConfig.APPLICATION_ID})& >/dev/null 2>&1").exec()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initAndStart() {
|
|
||||||
// Pre-initialize root shell
|
|
||||||
Shell.getShell()
|
|
||||||
|
|
||||||
Config.initialize()
|
|
||||||
handleRepackage()
|
|
||||||
Notifications.setup(this)
|
|
||||||
UpdateCheckService.schedule(this)
|
|
||||||
Shortcuts.setupDynamic(this)
|
|
||||||
|
|
||||||
// Pre-fetch network stuffs
|
|
||||||
get<GithubRawServices>()
|
|
||||||
|
|
||||||
DONE = true
|
|
||||||
|
|
||||||
redirect<MainActivity>().also { startActivity(it) }
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
var DONE = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.work.*
|
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
|
||||||
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.koin.core.KoinComponent
|
|
||||||
import org.koin.core.inject
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class UpdateCheckService(context: Context, workerParams: WorkerParameters)
|
|
||||||
: CoroutineWorker(context, workerParams), KoinComponent {
|
|
||||||
|
|
||||||
private val magiskRepo: MagiskRepository by inject()
|
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
|
||||||
// Make sure shell initializer was ran
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
Shell.getShell()
|
|
||||||
}
|
|
||||||
return magiskRepo.fetchUpdate()?.let {
|
|
||||||
if (BuildConfig.VERSION_CODE < it.app.versionCode)
|
|
||||||
Notifications.managerUpdate(applicationContext)
|
|
||||||
else if (Info.env.isActive && Info.env.magiskVersionCode < it.magisk.versionCode)
|
|
||||||
Notifications.magiskUpdate(applicationContext)
|
|
||||||
Result.success()
|
|
||||||
} ?: Result.failure()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun schedule(context: Context) {
|
|
||||||
if (Config.checkUpdate) {
|
|
||||||
val constraints = Constraints.Builder()
|
|
||||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
|
||||||
.setRequiresDeviceIdle(true)
|
|
||||||
.build()
|
|
||||||
val request = PeriodicWorkRequestBuilder<UpdateCheckService>(12, TimeUnit.HOURS)
|
|
||||||
.setConstraints(constraints)
|
|
||||||
.build()
|
|
||||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
|
||||||
Const.ID.CHECK_MAGISK_UPDATE_WORKER_ID,
|
|
||||||
ExistingPeriodicWorkPolicy.REPLACE, request)
|
|
||||||
} else {
|
|
||||||
WorkManager.getInstance(context)
|
|
||||||
.cancelUniqueWork(Const.ID.CHECK_MAGISK_UPDATE_WORKER_ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,98 +1,116 @@
|
|||||||
package com.topjohnwu.magisk.core.base
|
package com.topjohnwu.magisk.core.base
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
||||||
|
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
|
import android.app.Activity
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.net.Uri
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.ActivityResultCallback
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts.GetContent
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.collection.SparseArrayCompat
|
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
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.wrap
|
import com.topjohnwu.magisk.core.wrap
|
||||||
import com.topjohnwu.magisk.ktx.set
|
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
typealias RequestCallback = BaseActivity.(Int, Intent?) -> Unit
|
interface ContentResultCallback: ActivityResultCallback<Uri>, Parcelable {
|
||||||
|
fun onActivityLaunch() {}
|
||||||
|
// Make the result type explicitly non-null
|
||||||
|
override fun onActivityResult(result: Uri)
|
||||||
|
}
|
||||||
|
|
||||||
abstract class BaseActivity : AppCompatActivity() {
|
abstract class BaseActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private val resultCallbacks by lazy { SparseArrayCompat<RequestCallback>() }
|
private var permissionCallback: ((Boolean) -> Unit)? = null
|
||||||
|
private val requestPermission = registerForActivityResult(RequestPermission()) {
|
||||||
|
permissionCallback?.invoke(it)
|
||||||
|
permissionCallback = null
|
||||||
|
}
|
||||||
|
|
||||||
override fun applyOverrideConfiguration(config: Configuration?) {
|
private var installCallback: ((Boolean) -> Unit)? = null
|
||||||
// Force applying our preferred local
|
private val requestInstall = registerForActivityResult(RequestInstall()) {
|
||||||
config?.setLocale(currentLocale)
|
installCallback?.invoke(it)
|
||||||
super.applyOverrideConfiguration(config)
|
installCallback = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private var contentCallback: ContentResultCallback? = null
|
||||||
|
private val getContent = registerForActivityResult(GetContent()) {
|
||||||
|
if (it != null) contentCallback?.onActivityResult(it)
|
||||||
|
contentCallback = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private val mReferrerField by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
|
Activity::class.java.reflectField("mReferrer")
|
||||||
|
}
|
||||||
|
|
||||||
|
val realCallingPackage: String? get() {
|
||||||
|
callingPackage?.let { return it }
|
||||||
|
mReferrerField.get(this)?.let { return it as String }
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
override fun attachBaseContext(base: Context) {
|
||||||
super.attachBaseContext(base.wrap(false))
|
super.attachBaseContext(base.wrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun withPermission(permission: String, builder: PermissionRequestBuilder.() -> Unit) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
val request = PermissionRequestBuilder().apply(builder).build()
|
if (isRunningAsStub) {
|
||||||
|
// Overwrite private members to avoid nasty "false" stack traces being logged
|
||||||
|
val delegate = delegate
|
||||||
|
val clz = delegate.javaClass
|
||||||
|
clz.reflectField("mActivityHandlesConfigFlagsChecked").set(delegate, true)
|
||||||
|
clz.reflectField("mActivityHandlesConfigFlags").set(delegate, 0)
|
||||||
|
}
|
||||||
|
contentCallback = savedInstanceState?.getParcelable(CONTENT_CALLBACK_KEY)
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
if (permission == Manifest.permission.WRITE_EXTERNAL_STORAGE &&
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
Build.VERSION.SDK_INT >= 29) {
|
super.onSaveInstanceState(outState)
|
||||||
// We do not need external rw on 29+
|
contentCallback?.let {
|
||||||
request.onSuccess()
|
outState.putParcelable(CONTENT_CALLBACK_KEY, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
|
||||||
|
permission == WRITE_EXTERNAL_STORAGE) {
|
||||||
|
// We do not need external rw on R+
|
||||||
|
callback(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU &&
|
||||||
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
|
permission == POST_NOTIFICATIONS) {
|
||||||
request.onSuccess()
|
// All apps have notification permissions before T
|
||||||
|
callback(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (permission == REQUEST_INSTALL_PACKAGES) {
|
||||||
|
installCallback = callback
|
||||||
|
requestInstall.launch(Unit)
|
||||||
} else {
|
} else {
|
||||||
val requestCode = Random.nextInt(256, 512)
|
permissionCallback = callback
|
||||||
resultCallbacks[requestCode] = { result, _ ->
|
requestPermission.launch(permission)
|
||||||
if (result > 0)
|
|
||||||
request.onSuccess()
|
|
||||||
else
|
|
||||||
request.onFailure()
|
|
||||||
}
|
|
||||||
ActivityCompat.requestPermissions(this, arrayOf(permission), requestCode)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun withExternalRW(builder: PermissionRequestBuilder.() -> Unit) {
|
fun getContent(type: String, callback: ContentResultCallback) {
|
||||||
withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, builder = builder)
|
contentCallback = callback
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(
|
|
||||||
requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
|
||||||
var success = true
|
|
||||||
for (res in grantResults) {
|
|
||||||
if (res != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
success = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resultCallbacks[requestCode]?.also {
|
|
||||||
resultCallbacks.remove(requestCode)
|
|
||||||
it(this, if (success) 1 else -1, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
resultCallbacks[requestCode]?.also {
|
|
||||||
resultCallbacks.remove(requestCode)
|
|
||||||
it(this, resultCode, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startActivityForResult(intent: Intent, requestCode: Int, listener: RequestCallback) {
|
|
||||||
resultCallbacks[requestCode] = listener
|
|
||||||
try {
|
try {
|
||||||
startActivityForResult(intent, requestCode)
|
getContent.launch(type)
|
||||||
|
callback.onActivityLaunch()
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Utils.toast(R.string.app_not_found, Toast.LENGTH_SHORT)
|
toast(R.string.app_not_found, Toast.LENGTH_SHORT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,4 +119,12 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun relaunch() {
|
||||||
|
startActivity(Intent(intent).setFlags(0))
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val CONTENT_CALLBACK_KEY = "content_callback"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.topjohnwu.magisk.core.base
|
||||||
|
|
||||||
|
import android.app.job.JobService
|
||||||
|
import android.content.Context
|
||||||
|
import com.topjohnwu.magisk.core.patch
|
||||||
|
|
||||||
|
abstract class BaseJobService : JobService() {
|
||||||
|
override fun attachBaseContext(base: Context) {
|
||||||
|
super.attachBaseContext(base.patch())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.topjohnwu.magisk.core.base
|
||||||
|
|
||||||
|
import android.content.ContentProvider
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.ProviderInfo
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.net.Uri
|
||||||
|
import com.topjohnwu.magisk.core.patch
|
||||||
|
|
||||||
|
open class BaseProvider : ContentProvider() {
|
||||||
|
override fun attachInfo(context: Context, info: ProviderInfo) {
|
||||||
|
super.attachInfo(context.patch(), info)
|
||||||
|
}
|
||||||
|
override fun onCreate() = true
|
||||||
|
override fun getType(uri: Uri): String? = null
|
||||||
|
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
|
||||||
|
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?) = 0
|
||||||
|
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?) = 0
|
||||||
|
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? = null
|
||||||
|
}
|
||||||
@@ -2,16 +2,13 @@ package com.topjohnwu.magisk.core.base
|
|||||||
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.topjohnwu.magisk.core.wrap
|
import androidx.annotation.CallSuper
|
||||||
import org.koin.core.KoinComponent
|
import com.topjohnwu.magisk.core.patch
|
||||||
|
|
||||||
abstract class BaseReceiver : BroadcastReceiver(), KoinComponent {
|
abstract class BaseReceiver : BroadcastReceiver() {
|
||||||
|
@CallSuper
|
||||||
final override fun onReceive(context: Context, intent: Intent?) {
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
onReceive(context.wrap() as ContextWrapper, intent)
|
context.patch()
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun onReceive(context: ContextWrapper, intent: Intent?)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ package com.topjohnwu.magisk.core.base
|
|||||||
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.topjohnwu.magisk.core.wrap
|
import android.content.Intent
|
||||||
import org.koin.core.KoinComponent
|
import android.os.IBinder
|
||||||
|
import com.topjohnwu.magisk.core.patch
|
||||||
|
|
||||||
abstract class BaseService : Service(), KoinComponent {
|
open class BaseService : Service() {
|
||||||
override fun attachBaseContext(base: Context) {
|
override fun attachBaseContext(base: Context) {
|
||||||
super.attachBaseContext(base.wrap())
|
super.attachBaseContext(base.patch())
|
||||||
}
|
}
|
||||||
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.base
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Network
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.annotation.MainThread
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.work.Data
|
|
||||||
import androidx.work.ListenableWorker
|
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
abstract class BaseWorkerWrapper {
|
|
||||||
|
|
||||||
private lateinit var worker: ListenableWorker
|
|
||||||
|
|
||||||
val applicationContext: Context
|
|
||||||
get() = worker.applicationContext
|
|
||||||
|
|
||||||
val id: UUID
|
|
||||||
get() = worker.id
|
|
||||||
|
|
||||||
val inputData: Data
|
|
||||||
get() = worker.inputData
|
|
||||||
|
|
||||||
val tags: Set<String>
|
|
||||||
get() = worker.tags
|
|
||||||
|
|
||||||
val triggeredContentUris: List<Uri>
|
|
||||||
@RequiresApi(24)
|
|
||||||
get() = worker.triggeredContentUris
|
|
||||||
|
|
||||||
val triggeredContentAuthorities: List<String>
|
|
||||||
@RequiresApi(24)
|
|
||||||
get() = worker.triggeredContentAuthorities
|
|
||||||
|
|
||||||
val network: Network?
|
|
||||||
@RequiresApi(28)
|
|
||||||
get() = worker.network
|
|
||||||
|
|
||||||
val runAttemptCount: Int
|
|
||||||
get() = worker.runAttemptCount
|
|
||||||
|
|
||||||
val isStopped: Boolean
|
|
||||||
get() = worker.isStopped
|
|
||||||
|
|
||||||
abstract fun doWork(): ListenableWorker.Result
|
|
||||||
|
|
||||||
fun onStopped() {}
|
|
||||||
|
|
||||||
fun attachWorker(w: ListenableWorker) {
|
|
||||||
worker = w
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainThread
|
|
||||||
fun startWork(): ListenableFuture<ListenableWorker.Result> {
|
|
||||||
return worker.startWork()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.base
|
|
||||||
|
|
||||||
typealias SimpleCallback = () -> Unit
|
|
||||||
typealias PermissionRationaleCallback = (List<String>) -> Unit
|
|
||||||
|
|
||||||
class PermissionRequestBuilder {
|
|
||||||
|
|
||||||
private var onSuccessCallback: SimpleCallback = {}
|
|
||||||
private var onFailureCallback: SimpleCallback = {}
|
|
||||||
private var onShowRationaleCallback: PermissionRationaleCallback = {}
|
|
||||||
|
|
||||||
fun onSuccess(callback: SimpleCallback) {
|
|
||||||
onSuccessCallback = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onFailure(callback: SimpleCallback) {
|
|
||||||
onFailureCallback = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onShowRationale(callback: PermissionRationaleCallback) {
|
|
||||||
onShowRationaleCallback = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
fun build(): PermissionRequest {
|
|
||||||
return PermissionRequest(onSuccessCallback, onFailureCallback, onShowRationaleCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class PermissionRequest(
|
|
||||||
private val onSuccessCallback: SimpleCallback,
|
|
||||||
private val onFailureCallback: SimpleCallback,
|
|
||||||
private val onShowRationaleCallback: PermissionRationaleCallback
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun onSuccess() = onSuccessCallback()
|
|
||||||
fun onFailure() = onFailureCallback()
|
|
||||||
fun onShowRationale(permissions: List<String>) = onShowRationaleCallback(permissions)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.topjohnwu.magisk.core.data
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.core.model.BranchInfo
|
||||||
|
import com.topjohnwu.magisk.core.model.ModuleJson
|
||||||
|
import com.topjohnwu.magisk.core.model.UpdateInfo
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import retrofit2.http.*
|
||||||
|
|
||||||
|
private const val BRANCH = "branch"
|
||||||
|
private const val REPO = "repo"
|
||||||
|
private const val FILE = "file"
|
||||||
|
|
||||||
|
interface GithubPageServices {
|
||||||
|
|
||||||
|
@GET
|
||||||
|
suspend fun fetchUpdateJSON(@Url file: String): UpdateInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RawServices {
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Streaming
|
||||||
|
suspend fun fetchFile(@Url url: String): ResponseBody
|
||||||
|
|
||||||
|
@GET
|
||||||
|
suspend fun fetchString(@Url url: String): String
|
||||||
|
|
||||||
|
@GET
|
||||||
|
suspend fun fetchModuleJson(@Url url: String): ModuleJson
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GithubApiServices {
|
||||||
|
|
||||||
|
@GET("repos/{$REPO}/branches/{$BRANCH}")
|
||||||
|
@Headers("Accept: application/vnd.github.v3+json")
|
||||||
|
suspend fun fetchBranch(
|
||||||
|
@Path(REPO, encoded = true) repo: String,
|
||||||
|
@Path(BRANCH) branch: String
|
||||||
|
): BranchInfo
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.topjohnwu.magisk.data.database
|
package com.topjohnwu.magisk.core.data
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import com.topjohnwu.magisk.core.model.su.SuLog
|
import com.topjohnwu.magisk.core.model.su.SuLog
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
99
app/src/main/java/com/topjohnwu/magisk/core/di/Networking.kt
Normal file
99
app/src/main/java/com/topjohnwu/magisk/core/di/Networking.kt
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package com.topjohnwu.magisk.core.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
|
import com.topjohnwu.magisk.ProviderInstaller
|
||||||
|
import com.topjohnwu.magisk.core.Config
|
||||||
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||||
|
import okhttp3.Cache
|
||||||
|
import okhttp3.ConnectionSpec
|
||||||
|
import okhttp3.Dns
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.dnsoverhttps.DnsOverHttps
|
||||||
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||||
|
import retrofit2.converter.scalars.ScalarsConverterFactory
|
||||||
|
import java.io.File
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.net.UnknownHostException
|
||||||
|
|
||||||
|
private class DnsResolver(client: OkHttpClient) : Dns {
|
||||||
|
|
||||||
|
private val doh by lazy {
|
||||||
|
DnsOverHttps.Builder().client(client)
|
||||||
|
.url("https://cloudflare-dns.com/dns-query".toHttpUrl())
|
||||||
|
.bootstrapDnsHosts(listOf(
|
||||||
|
InetAddress.getByName("162.159.36.1"),
|
||||||
|
InetAddress.getByName("162.159.46.1"),
|
||||||
|
InetAddress.getByName("1.1.1.1"),
|
||||||
|
InetAddress.getByName("1.0.0.1"),
|
||||||
|
InetAddress.getByName("2606:4700:4700::1111"),
|
||||||
|
InetAddress.getByName("2606:4700:4700::1001"),
|
||||||
|
InetAddress.getByName("2606:4700:4700::0064"),
|
||||||
|
InetAddress.getByName("2606:4700:4700::6400")
|
||||||
|
))
|
||||||
|
.resolvePrivateAddresses(true) /* To make PublicSuffixDatabase never used */
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun lookup(hostname: String): List<InetAddress> {
|
||||||
|
if (Config.doh) {
|
||||||
|
try {
|
||||||
|
return doh.lookup(hostname)
|
||||||
|
} catch (e: UnknownHostException) {}
|
||||||
|
}
|
||||||
|
return Dns.SYSTEM.lookup(hostname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun createOkHttpClient(context: Context): OkHttpClient {
|
||||||
|
val appCache = Cache(File(context.cacheDir, "okhttp"), 10 * 1024 * 1024)
|
||||||
|
val builder = OkHttpClient.Builder().cache(appCache)
|
||||||
|
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
builder.addInterceptor(HttpLoggingInterceptor().apply {
|
||||||
|
level = HttpLoggingInterceptor.Level.BASIC
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
builder.connectionSpecs(listOf(ConnectionSpec.MODERN_TLS))
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.dns(DnsResolver(builder.build()))
|
||||||
|
|
||||||
|
builder.addInterceptor { chain ->
|
||||||
|
val request = chain.request().newBuilder()
|
||||||
|
request.header("User-Agent", "Magisk/${BuildConfig.VERSION_CODE}")
|
||||||
|
request.header("Accept-Language", currentLocale.toLanguageTag())
|
||||||
|
chain.proceed(request.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ProviderInstaller.install(context)) {
|
||||||
|
Info.hasGMS = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createMoshiConverterFactory(): MoshiConverterFactory {
|
||||||
|
val moshi = Moshi.Builder().build()
|
||||||
|
return MoshiConverterFactory.create(moshi)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createRetrofit(okHttpClient: OkHttpClient): Retrofit.Builder {
|
||||||
|
return Retrofit.Builder()
|
||||||
|
.addConverterFactory(ScalarsConverterFactory.create())
|
||||||
|
.addConverterFactory(createMoshiConverterFactory())
|
||||||
|
.client(okHttpClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseUrl: String): T {
|
||||||
|
return retrofitBuilder
|
||||||
|
.baseUrl(baseUrl)
|
||||||
|
.build()
|
||||||
|
.create(T::class.java)
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package com.topjohnwu.magisk.core.di
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
|
import androidx.room.Room
|
||||||
|
import com.topjohnwu.magisk.core.Const
|
||||||
|
import com.topjohnwu.magisk.core.data.SuLogDatabase
|
||||||
|
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
||||||
|
import com.topjohnwu.magisk.core.data.magiskdb.SettingsDao
|
||||||
|
import com.topjohnwu.magisk.core.data.magiskdb.StringDao
|
||||||
|
import com.topjohnwu.magisk.core.ktx.deviceProtectedContext
|
||||||
|
import com.topjohnwu.magisk.core.repository.LogRepository
|
||||||
|
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||||
|
import io.noties.markwon.Markwon
|
||||||
|
import io.noties.markwon.utils.NoCopySpannableFactory
|
||||||
|
|
||||||
|
val AppContext: Context inline get() = ServiceLocator.context
|
||||||
|
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
object ServiceLocator {
|
||||||
|
|
||||||
|
lateinit var context: Context
|
||||||
|
val deContext by lazy { context.deviceProtectedContext }
|
||||||
|
val timeoutPrefs by lazy { deContext.getSharedPreferences("su_timeout", 0) }
|
||||||
|
|
||||||
|
// Database
|
||||||
|
val policyDB = PolicyDao()
|
||||||
|
val settingsDB = SettingsDao()
|
||||||
|
val stringDB = StringDao()
|
||||||
|
val sulogDB by lazy { createSuLogDatabase(deContext).suLogDao() }
|
||||||
|
val logRepo by lazy { LogRepository(sulogDB) }
|
||||||
|
|
||||||
|
// Networking
|
||||||
|
val okhttp by lazy { createOkHttpClient(context) }
|
||||||
|
val retrofit by lazy { createRetrofit(okhttp) }
|
||||||
|
val markwon by lazy { createMarkwon(context) }
|
||||||
|
val networkService by lazy {
|
||||||
|
NetworkService(
|
||||||
|
createApiService(retrofit, Const.Url.GITHUB_PAGE_URL),
|
||||||
|
createApiService(retrofit, Const.Url.GITHUB_RAW_URL),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSuLogDatabase(context: Context) =
|
||||||
|
Room.databaseBuilder(context, SuLogDatabase::class.java, "sulogs.db")
|
||||||
|
.fallbackToDestructiveMigration()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private fun createMarkwon(context: Context) =
|
||||||
|
Markwon.builder(context).textSetter { textView, spanned, bufferType, onComplete ->
|
||||||
|
textView.apply {
|
||||||
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
setSpannableFactory(NoCopySpannableFactory.getInstance())
|
||||||
|
setText(spanned, bufferType)
|
||||||
|
onComplete.run()
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.download
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.android.parcel.Parcelize
|
|
||||||
|
|
||||||
sealed class Action : Parcelable {
|
|
||||||
|
|
||||||
sealed class Flash : Action() {
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
object Primary : Flash()
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
object Secondary : Flash()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class APK : Action() {
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
object Upgrade : APK()
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
object Restore : APK()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
object Download : Action()
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
object Uninstall : Action()
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
object EnvFix : Action()
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Patch(val fileUri: Uri) : Action()
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.download
|
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.IBinder
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import com.topjohnwu.magisk.R
|
|
||||||
import com.topjohnwu.magisk.core.ForegroundTracker
|
|
||||||
import com.topjohnwu.magisk.core.base.BaseService
|
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.checkSum
|
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
|
||||||
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
|
||||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
|
||||||
import com.topjohnwu.magisk.ktx.withStreams
|
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import okhttp3.ResponseBody
|
|
||||||
import org.koin.android.ext.android.inject
|
|
||||||
import org.koin.core.KoinComponent
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.collections.HashMap
|
|
||||||
import kotlin.random.Random.Default.nextInt
|
|
||||||
|
|
||||||
abstract class BaseDownloader : BaseService(), KoinComponent {
|
|
||||||
|
|
||||||
private val hasNotifications get() = notifications.isNotEmpty()
|
|
||||||
private val notifications = Collections.synchronizedMap(HashMap<Int, Notification.Builder>())
|
|
||||||
private val coroutineScope = CoroutineScope(Dispatchers.IO)
|
|
||||||
|
|
||||||
val service: GithubRawServices by inject()
|
|
||||||
|
|
||||||
// -- Service overrides
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? = null
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
|
||||||
intent.getParcelableExtra<Subject>(ACTION_KEY)?.let { subject ->
|
|
||||||
update(subject.notifyID())
|
|
||||||
coroutineScope.launch {
|
|
||||||
try {
|
|
||||||
subject.startDownload()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Timber.e(e)
|
|
||||||
notifyFail(subject)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return START_REDELIVER_INTENT
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
|
||||||
super.onTaskRemoved(rootIntent)
|
|
||||||
notifications.forEach { cancel(it.key) }
|
|
||||||
notifications.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
coroutineScope.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Download logic
|
|
||||||
|
|
||||||
private suspend fun Subject.startDownload() {
|
|
||||||
val skip = this is Subject.Magisk && file.checkSum("MD5", magisk.md5)
|
|
||||||
if (!skip) {
|
|
||||||
val stream = service.fetchFile(url).toProgressStream(this)
|
|
||||||
when (this) {
|
|
||||||
is Subject.Module -> // Download and process on-the-fly
|
|
||||||
stream.toModule(file, service.fetchInstaller().byteStream())
|
|
||||||
else -> {
|
|
||||||
withStreams(stream, file.outputStream()) { it, out -> it.copyTo(out) }
|
|
||||||
if (this is Subject.Manager)
|
|
||||||
handleAPK(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val newId = notifyFinish(this)
|
|
||||||
if (ForegroundTracker.hasForeground)
|
|
||||||
onFinish(this, newId)
|
|
||||||
if (!hasNotifications)
|
|
||||||
stopSelf()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ResponseBody.toProgressStream(subject: Subject): InputStream {
|
|
||||||
val max = contentLength()
|
|
||||||
val total = max.toFloat() / 1048576
|
|
||||||
val id = subject.notifyID()
|
|
||||||
|
|
||||||
update(id) { it.setContentTitle(subject.title) }
|
|
||||||
|
|
||||||
return ProgressInputStream(byteStream()) {
|
|
||||||
val progress = it.toFloat() / 1048576
|
|
||||||
update(id) { notification ->
|
|
||||||
if (max > 0) {
|
|
||||||
broadcast(progress / total, subject)
|
|
||||||
notification
|
|
||||||
.setProgress(max.toInt(), it.toInt(), false)
|
|
||||||
.setContentText("%.2f / %.2f MB".format(progress, total))
|
|
||||||
} else {
|
|
||||||
broadcast(-1f, subject)
|
|
||||||
notification.setContentText("%.2f MB / ??".format(progress))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Notification managements
|
|
||||||
|
|
||||||
fun Subject.notifyID() = hashCode()
|
|
||||||
|
|
||||||
private fun notifyFail(subject: Subject) = lastNotify(subject.notifyID()) {
|
|
||||||
broadcast(-1f, subject)
|
|
||||||
it.setContentText(getString(R.string.download_file_error))
|
|
||||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
|
||||||
.setOngoing(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun notifyFinish(subject: Subject) = lastNotify(subject.notifyID()) {
|
|
||||||
broadcast(1f, subject)
|
|
||||||
it.setIntent(subject)
|
|
||||||
.setContentTitle(subject.title)
|
|
||||||
.setContentText(getString(R.string.download_complete))
|
|
||||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
|
||||||
.setProgress(0, 0, false)
|
|
||||||
.setOngoing(false)
|
|
||||||
.setAutoCancel(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun create() = Notifications.progress(this, "")
|
|
||||||
|
|
||||||
fun update(id: Int, editor: (Notification.Builder) -> Unit = {}) {
|
|
||||||
val wasEmpty = !hasNotifications
|
|
||||||
val notification = notifications.getOrPut(id, ::create).also(editor)
|
|
||||||
if (wasEmpty)
|
|
||||||
updateForeground()
|
|
||||||
else
|
|
||||||
notify(id, notification.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun lastNotify(
|
|
||||||
id: Int,
|
|
||||||
editor: (Notification.Builder) -> Notification.Builder? = { null }
|
|
||||||
) : Int {
|
|
||||||
val notification = remove(id)?.run(editor) ?: return -1
|
|
||||||
val newId: Int = nextInt()
|
|
||||||
notify(newId, notification.build())
|
|
||||||
return newId
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun remove(id: Int) = notifications.remove(id)
|
|
||||||
?.also { updateForeground(); cancel(id) }
|
|
||||||
?: { cancel(id); null }()
|
|
||||||
|
|
||||||
private fun notify(id: Int, notification: Notification) {
|
|
||||||
Notifications.mgr.notify(id, notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cancel(id: Int) {
|
|
||||||
Notifications.mgr.cancel(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateForeground() {
|
|
||||||
if (hasNotifications) {
|
|
||||||
val (id, notification) = notifications.entries.first()
|
|
||||||
startForeground(id, notification.build())
|
|
||||||
} else {
|
|
||||||
stopForeground(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Implement custom logic
|
|
||||||
|
|
||||||
protected abstract suspend fun onFinish(subject: Subject, id: Int)
|
|
||||||
|
|
||||||
protected abstract fun Notification.Builder.setIntent(subject: Subject): Notification.Builder
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
companion object : KoinComponent {
|
|
||||||
const val ACTION_KEY = "download_action"
|
|
||||||
|
|
||||||
private val progressBroadcast = MutableLiveData<Pair<Float, Subject>>()
|
|
||||||
|
|
||||||
fun observeProgress(owner: LifecycleOwner, callback: (Float, Subject) -> Unit) {
|
|
||||||
progressBroadcast.value = null
|
|
||||||
progressBroadcast.observe(owner) {
|
|
||||||
val (progress, subject) = it ?: return@observe
|
|
||||||
callback(progress, subject)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun broadcast(progress: Float, subject: Subject) {
|
|
||||||
progressBroadcast.postValue(progress to subject)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,113 +1,220 @@
|
|||||||
package com.topjohnwu.magisk.core.download
|
package com.topjohnwu.magisk.core.download
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Notification
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
|
import android.app.PendingIntent.*
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
import com.topjohnwu.magisk.core.download.Action.*
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import com.topjohnwu.magisk.core.download.Action.Flash.Secondary
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.download.Subject.*
|
import com.topjohnwu.magisk.StubApk
|
||||||
|
import com.topjohnwu.magisk.core.ActivityTracker
|
||||||
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||||
import com.topjohnwu.magisk.core.intent
|
import com.topjohnwu.magisk.core.intent
|
||||||
import com.topjohnwu.magisk.core.tasks.EnvFixTask
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
import com.topjohnwu.magisk.core.ktx.*
|
||||||
|
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||||
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||||
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||||
import com.topjohnwu.magisk.utils.APKInstall
|
import com.topjohnwu.magisk.utils.APKInstall
|
||||||
import kotlin.random.Random.Default.nextInt
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.util.Properties
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipFile
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
@SuppressLint("Registered")
|
class DownloadService : NotificationService() {
|
||||||
open class DownloadService : BaseDownloader() {
|
|
||||||
|
|
||||||
private val context get() = this
|
private val job = Job()
|
||||||
|
|
||||||
override suspend fun onFinish(subject: Subject, id: Int) = when (subject) {
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||||
is Magisk -> subject.onFinish(id)
|
intent.getParcelableExtra<Subject>(SUBJECT_KEY)?.let { download(it) }
|
||||||
is Module -> subject.onFinish(id)
|
return START_NOT_STICKY
|
||||||
is Manager -> subject.onFinish(id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun Magisk.onFinish(id: Int) = when (val action = action) {
|
override fun onDestroy() {
|
||||||
Uninstall -> FlashFragment.uninstall(file, id)
|
job.cancel()
|
||||||
EnvFix -> {
|
}
|
||||||
remove(id)
|
|
||||||
EnvFixTask(file).exec()
|
private fun download(subject: Subject) {
|
||||||
Unit
|
notifyUpdate(subject.notifyId)
|
||||||
|
CoroutineScope(job + Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val stream = service.fetchFile(subject.url).toProgressStream(subject)
|
||||||
|
when (subject) {
|
||||||
|
is Subject.App -> handleApp(stream, subject)
|
||||||
|
is Subject.Module -> handleModule(stream, subject.file)
|
||||||
|
}
|
||||||
|
val activity = ActivityTracker.foreground
|
||||||
|
if (activity != null && subject.autoLaunch) {
|
||||||
|
notifyRemove(subject.notifyId)
|
||||||
|
subject.pendingIntent(activity)?.send()
|
||||||
|
} else {
|
||||||
|
notifyFinish(subject)
|
||||||
|
}
|
||||||
|
subject.postDownload?.invoke()
|
||||||
|
if (!hasNotifications)
|
||||||
|
stopSelf()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
notifyFail(subject)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is Patch -> FlashFragment.patch(file, action.fileUri, id)
|
|
||||||
is Flash -> FlashFragment.flash(file, action is Secondary, id)
|
|
||||||
else -> Unit
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Module.onFinish(id: Int) = when (action) {
|
private fun handleApp(stream: InputStream, subject: Subject.App) {
|
||||||
is Flash -> FlashFragment.install(file, id)
|
fun writeTee(output: OutputStream) {
|
||||||
else -> Unit
|
val uri = MediaStoreUtils.getFile("${subject.title}.apk").uri
|
||||||
|
val external = uri.outputStream()
|
||||||
|
stream.copyAndClose(TeeOutputStream(external, output))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRunningAsStub) {
|
||||||
|
val updateApk = StubApk.update(this)
|
||||||
|
try {
|
||||||
|
// Download full APK to stub update path
|
||||||
|
writeTee(updateApk.outputStream())
|
||||||
|
|
||||||
|
val zf = ZipFile(updateApk)
|
||||||
|
val prop = Properties()
|
||||||
|
prop.load(ByteArrayInputStream(zf.comment.toByteArray()))
|
||||||
|
val stubVersion = prop.getProperty("stubVersion").toIntOrNull() ?: -1
|
||||||
|
if (Info.stub!!.version < stubVersion) {
|
||||||
|
// Also upgrade stub
|
||||||
|
notifyUpdate(subject.notifyId) {
|
||||||
|
it.setProgress(0, 0, true)
|
||||||
|
.setContentTitle(getString(R.string.hide_app_title))
|
||||||
|
.setContentText("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract stub
|
||||||
|
val apk = subject.file.toFile()
|
||||||
|
zf.getInputStream(zf.getEntry("assets/stub.apk")).writeTo(apk)
|
||||||
|
zf.close()
|
||||||
|
|
||||||
|
// Patch and install
|
||||||
|
subject.intent = HideAPK.upgrade(this, apk)
|
||||||
|
?: throw IOException("HideAPK patch error")
|
||||||
|
apk.delete()
|
||||||
|
} else {
|
||||||
|
ActivityTracker.foreground?.let {
|
||||||
|
// Relaunch the process if we are foreground
|
||||||
|
StubApk.restartProcess(it)
|
||||||
|
} ?: run {
|
||||||
|
// Or else kill the current process after posting notification
|
||||||
|
subject.intent = selfLaunchIntent()
|
||||||
|
subject.postDownload = { Runtime.getRuntime().exit(0) }
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// If any error occurred, do not let stub load the new APK
|
||||||
|
updateApk.delete()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val session = APKInstall.startSession(this)
|
||||||
|
writeTee(session.openStream(this))
|
||||||
|
subject.intent = session.waitIntent()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Manager.onFinish(id: Int) {
|
private fun handleModule(src: InputStream, file: Uri) {
|
||||||
remove(id)
|
val input = ZipInputStream(src.buffered())
|
||||||
APKInstall.install(context, file.toFile())
|
val output = ZipOutputStream(file.outputStream().buffered())
|
||||||
|
|
||||||
|
withStreams(input, output) { zin, zout ->
|
||||||
|
zout.putNextEntry(ZipEntry("META-INF/"))
|
||||||
|
zout.putNextEntry(ZipEntry("META-INF/com/"))
|
||||||
|
zout.putNextEntry(ZipEntry("META-INF/com/google/"))
|
||||||
|
zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
|
||||||
|
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
|
||||||
|
assets.open("module_installer.sh").copyTo(zout)
|
||||||
|
|
||||||
|
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
|
||||||
|
zout.write("#MAGISK\n".toByteArray())
|
||||||
|
|
||||||
|
zin.forEach { entry ->
|
||||||
|
val path = entry.name
|
||||||
|
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
|
||||||
|
zout.putNextEntry(ZipEntry(path))
|
||||||
|
if (!entry.isDirectory) {
|
||||||
|
zin.copyTo(zout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Customize finish notification
|
private class TeeOutputStream(
|
||||||
|
private val o1: OutputStream,
|
||||||
override fun Notification.Builder.setIntent(subject: Subject)
|
private val o2: OutputStream
|
||||||
= when (subject) {
|
) : OutputStream() {
|
||||||
is Magisk -> setIntent(subject)
|
override fun write(b: Int) {
|
||||||
is Module -> setIntent(subject)
|
o1.write(b)
|
||||||
is Manager -> setIntent(subject)
|
o2.write(b)
|
||||||
|
}
|
||||||
|
override fun write(b: ByteArray?, off: Int, len: Int) {
|
||||||
|
o1.write(b, off, len)
|
||||||
|
o2.write(b, off, len)
|
||||||
|
}
|
||||||
|
override fun close() {
|
||||||
|
o1.close()
|
||||||
|
o2.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Notification.Builder.setIntent(subject: Magisk)
|
|
||||||
= when (val action = subject.action) {
|
|
||||||
Uninstall -> setContentIntent(FlashFragment.uninstallIntent(context, subject.file))
|
|
||||||
is Flash -> setContentIntent(FlashFragment.flashIntent(context, subject.file, action is Secondary))
|
|
||||||
is Patch -> setContentIntent(FlashFragment.patchIntent(context, subject.file, action.fileUri))
|
|
||||||
else -> setContentIntent(Intent())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Notification.Builder.setIntent(subject: Module)
|
|
||||||
= when (subject.action) {
|
|
||||||
is Flash -> setContentIntent(FlashFragment.installIntent(context, subject.file))
|
|
||||||
else -> setContentIntent(Intent())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Notification.Builder.setIntent(subject: Manager)
|
|
||||||
= when (subject.action) {
|
|
||||||
APK.Upgrade -> setContentIntent(APKInstall.installIntent(context, subject.file.toFile()))
|
|
||||||
else -> setContentIntent(Intent())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Notification.Builder.setContentIntent(intent: Intent) =
|
|
||||||
setContentIntent(
|
|
||||||
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
|
||||||
)
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val SUBJECT_KEY = "subject"
|
||||||
|
private const val REQUEST_CODE = 1
|
||||||
|
|
||||||
private fun intent(context: Context, subject: Subject) =
|
fun observeProgress(owner: LifecycleOwner, callback: (Float, Subject) -> Unit) {
|
||||||
context.intent<DownloadService>().putExtra(ACTION_KEY, subject)
|
progressBroadcast.value = null
|
||||||
|
progressBroadcast.observe(owner) {
|
||||||
fun pendingIntent(context: Context, subject: Subject): PendingIntent {
|
val (progress, subject) = it ?: return@observe
|
||||||
return if (Build.VERSION.SDK_INT >= 26) {
|
callback(progress, subject)
|
||||||
PendingIntent.getForegroundService(context, nextInt(),
|
|
||||||
intent(context, subject), PendingIntent.FLAG_UPDATE_CURRENT)
|
|
||||||
} else {
|
|
||||||
PendingIntent.getService(context, nextInt(),
|
|
||||||
intent(context, subject), PendingIntent.FLAG_UPDATE_CURRENT)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start(context: Context, subject: Subject) {
|
private fun intent(context: Context, subject: Subject) =
|
||||||
val app = context.applicationContext
|
context.intent<DownloadService>().putExtra(SUBJECT_KEY, subject)
|
||||||
if (Build.VERSION.SDK_INT >= 26) {
|
|
||||||
app.startForegroundService(intent(app, subject))
|
@SuppressLint("InlinedApi")
|
||||||
|
fun getPendingIntent(context: Context, subject: Subject): PendingIntent {
|
||||||
|
val flag = FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT or FLAG_ONE_SHOT
|
||||||
|
val intent = intent(context, subject)
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
getForegroundService(context, REQUEST_CODE, intent, flag)
|
||||||
} else {
|
} else {
|
||||||
app.startService(intent(app, subject))
|
getService(context, REQUEST_CODE, intent, flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
fun start(activity: BaseActivity, subject: Subject) {
|
||||||
|
activity.withPermission(Manifest.permission.POST_NOTIFICATIONS) {
|
||||||
|
// Always download regardless of notification permission status
|
||||||
|
val app = activity.applicationContext
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
app.startForegroundService(intent(app, subject))
|
||||||
|
} else {
|
||||||
|
app.startService(intent(app, subject))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.download
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.core.net.toFile
|
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
|
||||||
import com.topjohnwu.magisk.DynAPK
|
|
||||||
import com.topjohnwu.magisk.R
|
|
||||||
import com.topjohnwu.magisk.core.Config
|
|
||||||
import com.topjohnwu.magisk.core.Info
|
|
||||||
import com.topjohnwu.magisk.core.download.Action.APK.Restore
|
|
||||||
import com.topjohnwu.magisk.core.download.Action.APK.Upgrade
|
|
||||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
|
||||||
import com.topjohnwu.magisk.core.tasks.PatchAPK
|
|
||||||
import com.topjohnwu.magisk.ktx.relaunchApp
|
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
private fun Context.patch(apk: File) {
|
|
||||||
val patched = File(apk.parent, "patched.apk")
|
|
||||||
PatchAPK.patch(this, apk.path, patched.path, packageName, applicationInfo.nonLocalizedLabel)
|
|
||||||
apk.delete()
|
|
||||||
patched.renameTo(apk)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun BaseDownloader.notifyHide(id: Int) {
|
|
||||||
update(id) {
|
|
||||||
it.setProgress(0, 0, true)
|
|
||||||
.setContentTitle(getString(R.string.hide_manager_title))
|
|
||||||
.setContentText("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun BaseDownloader.upgrade(subject: Subject.Manager) {
|
|
||||||
val apk = subject.file.toFile()
|
|
||||||
val id = subject.notifyID()
|
|
||||||
if (isRunningAsStub) {
|
|
||||||
// Move to upgrade location
|
|
||||||
apk.copyTo(DynAPK.update(this), overwrite = true)
|
|
||||||
apk.delete()
|
|
||||||
if (Info.stubChk.version < subject.stub.versionCode) {
|
|
||||||
notifyHide(id)
|
|
||||||
// Also upgrade stub
|
|
||||||
service.fetchFile(subject.stub.link).byteStream().use { it.writeTo(apk) }
|
|
||||||
patch(apk)
|
|
||||||
} else {
|
|
||||||
// Simply relaunch the app
|
|
||||||
stopSelf()
|
|
||||||
relaunchApp(this)
|
|
||||||
}
|
|
||||||
} else if (packageName != BuildConfig.APPLICATION_ID) {
|
|
||||||
notifyHide(id)
|
|
||||||
patch(apk)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun BaseDownloader.restore(apk: File, id: Int) {
|
|
||||||
update(id) {
|
|
||||||
it.setProgress(0, 0, true)
|
|
||||||
.setProgress(0, 0, true)
|
|
||||||
.setContentTitle(getString(R.string.restore_img_msg))
|
|
||||||
.setContentText("")
|
|
||||||
}
|
|
||||||
Config.export()
|
|
||||||
Shell.su("pm install $apk && pm uninstall $packageName").exec()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun BaseDownloader.handleAPK(subject: Subject.Manager) =
|
|
||||||
when (subject.action) {
|
|
||||||
is Upgrade -> upgrade(subject)
|
|
||||||
is Restore -> restore(subject.file.toFile(), subject.notifyID())
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.download
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import com.topjohnwu.magisk.ktx.withStreams
|
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.util.zip.ZipEntry
|
|
||||||
import java.util.zip.ZipInputStream
|
|
||||||
import java.util.zip.ZipOutputStream
|
|
||||||
|
|
||||||
fun InputStream.toModule(file: Uri, installer: InputStream) {
|
|
||||||
|
|
||||||
val input = ZipInputStream(buffered())
|
|
||||||
val output = ZipOutputStream(file.outputStream().buffered())
|
|
||||||
|
|
||||||
withStreams(input, output) { zin, zout ->
|
|
||||||
zout.putNextEntry(ZipEntry("META-INF/"))
|
|
||||||
zout.putNextEntry(ZipEntry("META-INF/com/"))
|
|
||||||
zout.putNextEntry(ZipEntry("META-INF/com/google/"))
|
|
||||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
|
|
||||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
|
|
||||||
installer.copyTo(zout)
|
|
||||||
|
|
||||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
|
|
||||||
zout.write("#MAGISK\n".toByteArray(charset("UTF-8")))
|
|
||||||
|
|
||||||
var off = -1
|
|
||||||
var entry: ZipEntry? = zin.nextEntry
|
|
||||||
while (entry != null) {
|
|
||||||
if (off < 0) {
|
|
||||||
off = entry.name.indexOf('/') + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
val path = entry.name.substring(off)
|
|
||||||
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
|
|
||||||
zout.putNextEntry(ZipEntry(path))
|
|
||||||
if (!entry.isDirectory) {
|
|
||||||
zin.copyTo(zout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
entry = zin.nextEntry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package com.topjohnwu.magisk.core.download
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.core.base.BaseService
|
||||||
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.core.ktx.synchronized
|
||||||
|
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
||||||
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
open class NotificationService : BaseService() {
|
||||||
|
|
||||||
|
private val notifications = HashMap<Int, Notification.Builder>().synchronized()
|
||||||
|
protected val hasNotifications get() = notifications.isNotEmpty()
|
||||||
|
|
||||||
|
protected val service get() = ServiceLocator.networkService
|
||||||
|
|
||||||
|
private var attachedNotificationId = 0
|
||||||
|
|
||||||
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
|
super.onTaskRemoved(rootIntent)
|
||||||
|
notifications.forEach { Notifications.mgr.cancel(it.key) }
|
||||||
|
notifications.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun ResponseBody.toProgressStream(subject: Subject): InputStream {
|
||||||
|
val max = contentLength()
|
||||||
|
val total = max.toFloat() / 1048576
|
||||||
|
val id = subject.notifyId
|
||||||
|
|
||||||
|
notifyUpdate(id) { it.setContentTitle(subject.title) }
|
||||||
|
|
||||||
|
return ProgressInputStream(byteStream()) {
|
||||||
|
val progress = it.toFloat() / 1048576
|
||||||
|
notifyUpdate(id) { notification ->
|
||||||
|
if (max > 0) {
|
||||||
|
broadcast(progress / total, subject)
|
||||||
|
notification
|
||||||
|
.setProgress(max.toInt(), it.toInt(), false)
|
||||||
|
.setContentText("%.2f / %.2f MB".format(progress, total))
|
||||||
|
} else {
|
||||||
|
broadcast(-1f, subject)
|
||||||
|
notification.setContentText("%.2f MB / ??".format(progress))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
|
||||||
|
val notification = notifyRemove(id)?.also(editor) ?: return -1
|
||||||
|
val newId = Notifications.nextId()
|
||||||
|
Notifications.mgr.notify(newId, notification.build())
|
||||||
|
return newId
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun notifyFail(subject: Subject) = finalNotify(subject.notifyId) {
|
||||||
|
broadcast(-2f, subject)
|
||||||
|
it.setContentText(getString(R.string.download_file_error))
|
||||||
|
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||||
|
.setOngoing(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun notifyFinish(subject: Subject) = finalNotify(subject.notifyId) {
|
||||||
|
broadcast(1f, subject)
|
||||||
|
it.setContentTitle(subject.title)
|
||||||
|
.setContentText(getString(R.string.download_complete))
|
||||||
|
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
|
.setProgress(0, 0, false)
|
||||||
|
.setOngoing(false)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
subject.pendingIntent(this)?.let { intent -> it.setContentIntent(intent) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun attachNotification(id: Int, notification: Notification) {
|
||||||
|
attachedNotificationId = id
|
||||||
|
startForeground(id, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun maybeDetachNotification(id: Int) : Boolean {
|
||||||
|
if (attachedNotificationId != id) return false
|
||||||
|
if (hasNotifications) {
|
||||||
|
val (anotherId, notification) = notifications.entries.first()
|
||||||
|
// Attaching a new notification will remove the current showing one
|
||||||
|
attachNotification(anotherId, notification.build())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
stopForeground(true)
|
||||||
|
}
|
||||||
|
attachedNotificationId = 0
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit = {}) {
|
||||||
|
fun create() = Notifications.startProgress("")
|
||||||
|
|
||||||
|
val wasEmpty = !hasNotifications
|
||||||
|
val notification = notifications.getOrPut(id, ::create).also(editor).build()
|
||||||
|
if (wasEmpty)
|
||||||
|
attachNotification(id, notification)
|
||||||
|
else
|
||||||
|
Notifications.mgr.notify(id, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun notifyRemove(id: Int): Notification.Builder? {
|
||||||
|
val n = notifications.remove(id)
|
||||||
|
if (n == null || !maybeDetachNotification(id))
|
||||||
|
Notifications.mgr.cancel(id)
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
protected val progressBroadcast = MutableLiveData<Pair<Float, Subject>?>()
|
||||||
|
|
||||||
|
private fun broadcast(progress: Float, subject: Subject) {
|
||||||
|
progressBroadcast.postValue(progress to subject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,114 +1,84 @@
|
|||||||
package com.topjohnwu.magisk.core.download
|
package com.topjohnwu.magisk.core.download
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
|
import com.topjohnwu.magisk.core.ktx.cachedFile
|
||||||
import com.topjohnwu.magisk.core.model.MagiskJson
|
import com.topjohnwu.magisk.core.model.MagiskJson
|
||||||
import com.topjohnwu.magisk.core.model.ManagerJson
|
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||||
import com.topjohnwu.magisk.core.model.StubJson
|
|
||||||
import com.topjohnwu.magisk.core.model.module.Repo
|
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||||
import com.topjohnwu.magisk.ktx.cachedFile
|
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
||||||
import com.topjohnwu.magisk.ktx.get
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import kotlinx.android.parcel.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
private fun cachedFile(name: String) = get<Context>().cachedFile(name).apply { delete() }.toUri()
|
private fun cachedFile(name: String) = AppContext.cachedFile(name).apply { delete() }.toUri()
|
||||||
|
|
||||||
|
enum class Action {
|
||||||
|
Flash,
|
||||||
|
Download
|
||||||
|
}
|
||||||
|
|
||||||
sealed class Subject : Parcelable {
|
sealed class Subject : Parcelable {
|
||||||
|
|
||||||
abstract val url: String
|
abstract val url: String
|
||||||
abstract val file: Uri
|
abstract val file: Uri
|
||||||
abstract val action: Action
|
|
||||||
abstract val title: String
|
abstract val title: String
|
||||||
|
abstract val notifyId: Int
|
||||||
|
open val autoLaunch: Boolean get() = true
|
||||||
|
open val postDownload: (() -> Unit)? get() = null
|
||||||
|
|
||||||
|
abstract fun pendingIntent(context: Context): PendingIntent?
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class Module(
|
class Module(
|
||||||
val module: Repo,
|
val module: OnlineModule,
|
||||||
override val action: Action
|
val action: Action,
|
||||||
|
override val notifyId: Int = Notifications.nextId()
|
||||||
) : Subject() {
|
) : Subject() {
|
||||||
override val url: String get() = module.zipUrl
|
override val url: String get() = module.zipUrl
|
||||||
override val title: String get() = module.downloadFilename
|
override val title: String get() = module.downloadFilename
|
||||||
|
override val autoLaunch: Boolean get() = action == Action.Flash
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
override val file by lazy {
|
override val file by lazy {
|
||||||
MediaStoreUtils.getFile(title).uri
|
MediaStoreUtils.getFile(title).uri
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun pendingIntent(context: Context) =
|
||||||
|
FlashFragment.installIntent(context, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class Manager(
|
class App(
|
||||||
override val action: Action.APK,
|
private val json: MagiskJson = Info.remote.magisk,
|
||||||
private val app: ManagerJson = Info.remote.app,
|
override val notifyId: Int = Notifications.nextId()
|
||||||
val stub: StubJson = Info.remote.stub
|
|
||||||
) : Subject() {
|
) : Subject() {
|
||||||
|
override val title: String get() = "Magisk-${json.version}(${json.versionCode})"
|
||||||
override val title: String
|
override val url: String get() = json.link
|
||||||
get() = "MagiskManager-${app.version}(${app.versionCode})"
|
|
||||||
|
|
||||||
override val url: String
|
|
||||||
get() = app.link
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
override val file by lazy {
|
override val file by lazy {
|
||||||
cachedFile("manager.apk")
|
cachedFile("manager.apk")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@IgnoredOnParcel
|
||||||
|
override var postDownload: (() -> Unit)? = null
|
||||||
|
|
||||||
|
@IgnoredOnParcel
|
||||||
|
var intent: Intent? = null
|
||||||
|
override fun pendingIntent(context: Context) = intent?.toPending(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class Magisk : Subject() {
|
@SuppressLint("InlinedApi")
|
||||||
|
protected fun Intent.toPending(context: Context): PendingIntent {
|
||||||
val magisk: MagiskJson = Info.remote.magisk
|
return PendingIntent.getActivity(context, notifyId, this,
|
||||||
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT)
|
||||||
@Parcelize
|
|
||||||
private class Internal(
|
|
||||||
override val action: Action
|
|
||||||
) : Magisk() {
|
|
||||||
override val url: String get() = magisk.link
|
|
||||||
override val title: String get() = "Magisk-${magisk.version}(${magisk.versionCode})"
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
override val file by lazy {
|
|
||||||
cachedFile("magisk.zip")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
private class Uninstall : Magisk() {
|
|
||||||
override val action get() = Action.Uninstall
|
|
||||||
override val url: String get() = Info.remote.uninstaller.link
|
|
||||||
override val title: String get() = "uninstall.zip"
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
override val file by lazy {
|
|
||||||
cachedFile(title)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
private class Download : Magisk() {
|
|
||||||
override val action get() = Action.Download
|
|
||||||
override val url: String get() = magisk.link
|
|
||||||
override val title: String get() = "Magisk-${magisk.version}(${magisk.versionCode}).zip"
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
override val file by lazy {
|
|
||||||
MediaStoreUtils.getFile(title).uri
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
operator fun invoke(config: Action) = when (config) {
|
|
||||||
Action.Download -> Download()
|
|
||||||
Action.Uninstall -> Uninstall()
|
|
||||||
Action.EnvFix, is Action.Flash, is Action.Patch -> Internal(config)
|
|
||||||
else -> throw IllegalArgumentException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
261
app/src/main/java/com/topjohnwu/magisk/core/ktx/XAndroid.kt
Normal file
261
app/src/main/java/com/topjohnwu/magisk/core/ktx/XAndroid.kt
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
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.net.Uri
|
||||||
|
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.Const
|
||||||
|
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||||
|
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||||
|
import com.topjohnwu.magisk.utils.APKInstall
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.Array
|
||||||
|
import kotlin.String
|
||||||
|
import java.lang.reflect.Array as JArray
|
||||||
|
|
||||||
|
fun Context.rawResource(id: Int) = resources.openRawResource(id)
|
||||||
|
|
||||||
|
fun Context.getBitmap(id: Int): Bitmap {
|
||||||
|
var drawable = AppCompatResources.getDrawable(this, id)!!
|
||||||
|
if (drawable is BitmapDrawable)
|
||||||
|
return drawable.bitmap
|
||||||
|
if (SDK_INT >= 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 Intent.startActivityWithRoot() {
|
||||||
|
val args = mutableListOf("am", "start", "--user", Const.USER_ID.toString())
|
||||||
|
val cmd = toCommand(args).joinToString(" ")
|
||||||
|
Shell.cmd(cmd).submit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.toCommand(args: MutableList<String> = mutableListOf()): MutableList<String> {
|
||||||
|
action?.also {
|
||||||
|
args.add("-a")
|
||||||
|
args.add(it)
|
||||||
|
}
|
||||||
|
component?.also {
|
||||||
|
args.add("-n")
|
||||||
|
args.add(it.flattenToString())
|
||||||
|
}
|
||||||
|
data?.also {
|
||||||
|
args.add("-d")
|
||||||
|
args.add(it.toString())
|
||||||
|
}
|
||||||
|
categories?.also {
|
||||||
|
for (cat in it) {
|
||||||
|
args.add("-c")
|
||||||
|
args.add(cat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type?.also {
|
||||||
|
args.add("-t")
|
||||||
|
args.add(it)
|
||||||
|
}
|
||||||
|
extras?.also {
|
||||||
|
loop@ for (key in it.keySet()) {
|
||||||
|
val v = it[key] ?: continue
|
||||||
|
var value: Any = v
|
||||||
|
val arg: String
|
||||||
|
when {
|
||||||
|
v is String -> arg = "--es"
|
||||||
|
v is Boolean -> arg = "--ez"
|
||||||
|
v is Int -> arg = "--ei"
|
||||||
|
v is Long -> arg = "--el"
|
||||||
|
v is Float -> arg = "--ef"
|
||||||
|
v is Uri -> arg = "--eu"
|
||||||
|
v is ComponentName -> {
|
||||||
|
arg = "--ecn"
|
||||||
|
value = v.flattenToString()
|
||||||
|
}
|
||||||
|
v is List<*> -> {
|
||||||
|
if (v.isEmpty())
|
||||||
|
continue@loop
|
||||||
|
|
||||||
|
arg = if (v[0] is Int)
|
||||||
|
"--eial"
|
||||||
|
else if (v[0] is Long)
|
||||||
|
"--elal"
|
||||||
|
else if (v[0] is Float)
|
||||||
|
"--efal"
|
||||||
|
else if (v[0] is String)
|
||||||
|
"--esal"
|
||||||
|
else
|
||||||
|
continue@loop /* Unsupported */
|
||||||
|
|
||||||
|
val sb = StringBuilder()
|
||||||
|
for (o in v) {
|
||||||
|
sb.append(o.toString().replace(",", "\\,"))
|
||||||
|
sb.append(',')
|
||||||
|
}
|
||||||
|
// Remove trailing comma
|
||||||
|
sb.deleteCharAt(sb.length - 1)
|
||||||
|
value = sb
|
||||||
|
}
|
||||||
|
v.javaClass.isArray -> {
|
||||||
|
arg = if (v is IntArray)
|
||||||
|
"--eia"
|
||||||
|
else if (v is LongArray)
|
||||||
|
"--ela"
|
||||||
|
else if (v is FloatArray)
|
||||||
|
"--efa"
|
||||||
|
else if (v is Array<*> && v.isArrayOf<String>())
|
||||||
|
"--esa"
|
||||||
|
else
|
||||||
|
continue@loop /* Unsupported */
|
||||||
|
|
||||||
|
val sb = StringBuilder()
|
||||||
|
val len = JArray.getLength(v)
|
||||||
|
for (i in 0 until len) {
|
||||||
|
sb.append(JArray.get(v, i)!!.toString().replace(",", "\\,"))
|
||||||
|
sb.append(',')
|
||||||
|
}
|
||||||
|
// Remove trailing comma
|
||||||
|
sb.deleteCharAt(sb.length - 1)
|
||||||
|
value = sb
|
||||||
|
}
|
||||||
|
else -> continue@loop
|
||||||
|
} /* Unsupported */
|
||||||
|
|
||||||
|
args.add(arg)
|
||||||
|
args.add(key)
|
||||||
|
args.add(value.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
args.add("-f")
|
||||||
|
args.add(flags.toString())
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.cachedFile(name: String) = File(cacheDir, name)
|
||||||
|
|
||||||
|
fun ApplicationInfo.getLabel(pm: PackageManager): String {
|
||||||
|
runCatching {
|
||||||
|
if (labelRes > 0) {
|
||||||
|
val res = pm.getResourcesForApplication(this)
|
||||||
|
val config = Configuration()
|
||||||
|
config.setLocale(currentLocale)
|
||||||
|
res.updateConfiguration(config, res.displayMetrics)
|
||||||
|
return res.getString(labelRes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadLabel(pm).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.unwrap(): Context {
|
||||||
|
var context = this
|
||||||
|
while (context is ContextWrapper)
|
||||||
|
context = context.baseContext
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Activity.hideKeyboard() {
|
||||||
|
val view = currentFocus ?: return
|
||||||
|
getSystemService<InputMethodManager>()
|
||||||
|
?.hideSoftInputFromWindow(view.windowToken, 0)
|
||||||
|
view.clearFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
val View.activity: Activity get() {
|
||||||
|
var context = context
|
||||||
|
while(true) {
|
||||||
|
if (context !is ContextWrapper)
|
||||||
|
error("View is not attached to activity")
|
||||||
|
if (context is Activity)
|
||||||
|
return context
|
||||||
|
context = context.baseContext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("PrivateApi")
|
||||||
|
fun getProperty(key: String, def: String): String {
|
||||||
|
runCatching {
|
||||||
|
val clazz = Class.forName("android.os.SystemProperties")
|
||||||
|
val get = clazz.getMethod("get", String::class.java, String::class.java)
|
||||||
|
return get.invoke(clazz, key, def) as String
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
@Throws(PackageManager.NameNotFoundException::class)
|
||||||
|
fun PackageManager.getPackageInfo(uid: Int, pid: Int): PackageInfo? {
|
||||||
|
val flag = PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||||
|
val pkgs = getPackagesForUid(uid) ?: throw PackageManager.NameNotFoundException()
|
||||||
|
if (pkgs.size > 1) {
|
||||||
|
if (pid <= 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// Try to find package name from PID
|
||||||
|
val proc = RootUtils.obj?.getAppProcess(pid)
|
||||||
|
if (proc == null) {
|
||||||
|
if (uid == Process.SHELL_UID) {
|
||||||
|
// It is possible that some apps installed are sharing UID with shell.
|
||||||
|
// We will not be able to find a package from the active process list,
|
||||||
|
// because the client is forked from ADB shell, not any app process.
|
||||||
|
return getPackageInfo("com.android.shell", flag)
|
||||||
|
}
|
||||||
|
} else if (uid == proc.uid) {
|
||||||
|
return getPackageInfo(proc.pkgList[0], flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (pkgs.size == 1) {
|
||||||
|
return getPackageInfo(pkgs[0], flag)
|
||||||
|
}
|
||||||
|
throw PackageManager.NameNotFoundException()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.registerRuntimeReceiver(receiver: BroadcastReceiver, filter: IntentFilter) {
|
||||||
|
APKInstall.registerReceiver(this, receiver, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.selfLaunchIntent(): Intent {
|
||||||
|
val pm = packageManager
|
||||||
|
val intent = pm.getLaunchIntentForPackage(packageName)!!
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.toast(msg: CharSequence, duration: Int) {
|
||||||
|
UiThreadHandler.run { Toast.makeText(this, msg, duration).show() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.toast(resId: Int, duration: Int) {
|
||||||
|
UiThreadHandler.run { Toast.makeText(this, resId, duration).show() }
|
||||||
|
}
|
||||||
76
app/src/main/java/com/topjohnwu/magisk/core/ktx/XJVM.kt
Normal file
76
app/src/main/java/com/topjohnwu/magisk/core/ktx/XJVM.kt
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package com.topjohnwu.magisk.core.ktx
|
||||||
|
|
||||||
|
import androidx.collection.SparseArrayCompat
|
||||||
|
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flatMapMerge
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.lang.reflect.Field
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
|
inline fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
|
||||||
|
var entry: ZipEntry? = nextEntry
|
||||||
|
while (entry != null) {
|
||||||
|
callback(entry)
|
||||||
|
entry = nextEntry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <In : InputStream, Out : OutputStream> withStreams(
|
||||||
|
inStream: In,
|
||||||
|
outStream: Out,
|
||||||
|
withBoth: (In, Out) -> Unit
|
||||||
|
) {
|
||||||
|
inStream.use { reader ->
|
||||||
|
outStream.use { writer ->
|
||||||
|
withBoth(reader, writer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun InputStream.copyAndClose(out: OutputStream) = withStreams(this, out) { i, o -> i.copyTo(o) }
|
||||||
|
|
||||||
|
fun InputStream.writeTo(file: File) = copyAndClose(file.outputStream())
|
||||||
|
|
||||||
|
operator fun <E> SparseArrayCompat<E>.set(key: Int, value: E) {
|
||||||
|
put(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> MutableList<T>.synchronized(): MutableList<T> = Collections.synchronizedList(this)
|
||||||
|
|
||||||
|
fun <T> MutableSet<T>.synchronized(): MutableSet<T> = Collections.synchronizedSet(this)
|
||||||
|
|
||||||
|
fun <K, V> MutableMap<K, V>.synchronized(): MutableMap<K, V> = Collections.synchronizedMap(this)
|
||||||
|
|
||||||
|
fun Class<*>.reflectField(name: String): Field =
|
||||||
|
getDeclaredField(name).apply { isAccessible = true }
|
||||||
|
|
||||||
|
inline fun <T, R> Flow<T>.concurrentMap(crossinline transform: suspend (T) -> R): Flow<R> {
|
||||||
|
return flatMapMerge { value ->
|
||||||
|
flow { emit(transform(value)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Long.toTime(format: DateFormat) = format.format(this).orEmpty()
|
||||||
|
|
||||||
|
// Some devices don't allow filenames containing ":"
|
||||||
|
val timeFormatStandard by lazy {
|
||||||
|
SimpleDateFormat(
|
||||||
|
"yyyy-MM-dd'T'HH.mm.ss",
|
||||||
|
currentLocale
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val timeDateFormat: DateFormat by lazy {
|
||||||
|
DateFormat.getDateTimeInstance(
|
||||||
|
DateFormat.DEFAULT,
|
||||||
|
DateFormat.DEFAULT,
|
||||||
|
currentLocale
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.topjohnwu.magisk.ktx
|
package com.topjohnwu.magisk.core.ktx
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
@@ -8,14 +8,18 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
fun reboot(reason: String = if (Config.recovery) "recovery" else "") {
|
fun reboot(reason: String = if (Config.recovery) "recovery" else "") {
|
||||||
Shell.su("/system/bin/svc power reboot $reason || /system/bin/reboot $reason").submit()
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun relaunchApp(context: Context) {
|
fun relaunchApp(context: Context) {
|
||||||
val intent = context.packageManager.getLaunchIntentForPackage(context.packageName) ?: return
|
val intent = context.packageManager.getLaunchIntentForPackage(context.packageName) ?: return
|
||||||
val args = mutableListOf("am", "start", "--user", Const.USER_ID.toString())
|
val args = mutableListOf("am", "start", "--user", Const.USER_ID.toString())
|
||||||
val cmd = intent.toCommand(args).joinToString(separator = " ")
|
val cmd = intent.toCommand(args).joinToString(separator = " ")
|
||||||
Shell.su("run_delay 1 \"$cmd\"").exec()
|
Shell.cmd("run_delay 1 \"$cmd\"").exec()
|
||||||
Runtime.getRuntime().exit(0)
|
Runtime.getRuntime().exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.magiskdb
|
|
||||||
|
|
||||||
import androidx.annotation.StringDef
|
|
||||||
|
|
||||||
abstract class BaseDao {
|
|
||||||
|
|
||||||
object Table {
|
|
||||||
const val POLICY = "policies"
|
|
||||||
const val LOG = "logs"
|
|
||||||
const val SETTINGS = "settings"
|
|
||||||
const val STRINGS = "strings"
|
|
||||||
}
|
|
||||||
|
|
||||||
@StringDef(Table.POLICY, Table.LOG, Table.SETTINGS, Table.STRINGS)
|
|
||||||
@Retention(AnnotationRetention.SOURCE)
|
|
||||||
annotation class TableStrict
|
|
||||||
|
|
||||||
@TableStrict
|
|
||||||
abstract val table: String
|
|
||||||
|
|
||||||
inline fun <reified Builder : Query.Builder> buildQuery(builder: Builder.() -> Unit = {}) =
|
|
||||||
Builder::class.java.newInstance()
|
|
||||||
.apply { table = this@BaseDao.table }
|
|
||||||
.apply(builder)
|
|
||||||
.toString()
|
|
||||||
.let { Query(it) }
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.magiskdb
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import com.topjohnwu.magisk.core.Const
|
|
||||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
|
||||||
import com.topjohnwu.magisk.core.model.su.toMap
|
|
||||||
import com.topjohnwu.magisk.core.model.su.toPolicy
|
|
||||||
import com.topjohnwu.magisk.ktx.now
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
|
|
||||||
class PolicyDao(
|
|
||||||
private val context: Context
|
|
||||||
) : BaseDao() {
|
|
||||||
|
|
||||||
override val table: String = Table.POLICY
|
|
||||||
|
|
||||||
suspend fun deleteOutdated() = buildQuery<Delete> {
|
|
||||||
condition {
|
|
||||||
greaterThan("until", "0")
|
|
||||||
and {
|
|
||||||
lessThan("until", TimeUnit.MILLISECONDS.toSeconds(now).toString())
|
|
||||||
}
|
|
||||||
or {
|
|
||||||
lessThan("until", "0")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.commit()
|
|
||||||
|
|
||||||
suspend fun delete(packageName: String) = buildQuery<Delete> {
|
|
||||||
condition {
|
|
||||||
equals("package_name", packageName)
|
|
||||||
}
|
|
||||||
}.commit()
|
|
||||||
|
|
||||||
suspend fun delete(uid: Int) = buildQuery<Delete> {
|
|
||||||
condition {
|
|
||||||
equals("uid", uid)
|
|
||||||
}
|
|
||||||
}.commit()
|
|
||||||
|
|
||||||
suspend fun fetch(uid: Int) = buildQuery<Select> {
|
|
||||||
condition {
|
|
||||||
equals("uid", uid)
|
|
||||||
}
|
|
||||||
}.query().first().toPolicyOrNull()
|
|
||||||
|
|
||||||
suspend fun update(policy: SuPolicy) = buildQuery<Replace> {
|
|
||||||
values(policy.toMap())
|
|
||||||
}.commit()
|
|
||||||
|
|
||||||
suspend fun <R: Any> fetchAll(mapper: (SuPolicy) -> R) = buildQuery<Select> {
|
|
||||||
condition {
|
|
||||||
equals("uid/100000", Const.USER_ID)
|
|
||||||
}
|
|
||||||
}.query {
|
|
||||||
it.toPolicyOrNull()?.let(mapper)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Map<String, String>.toPolicyOrNull(): SuPolicy? {
|
|
||||||
return runCatching { toPolicy(context.packageManager) }.getOrElse {
|
|
||||||
Timber.e(it)
|
|
||||||
if (it is PackageManager.NameNotFoundException) {
|
|
||||||
val uid = getOrElse("uid") { null } ?: return null
|
|
||||||
GlobalScope.launch {
|
|
||||||
delete(uid.toInt())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.magiskdb
|
|
||||||
|
|
||||||
import androidx.annotation.StringDef
|
|
||||||
import com.topjohnwu.magisk.ktx.await
|
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class Query(private val _query: String) {
|
|
||||||
val query get() = "magisk --sqlite '$_query'"
|
|
||||||
|
|
||||||
interface Builder {
|
|
||||||
val requestType: String
|
|
||||||
var table: String
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend inline fun <R : Any> query(crossinline mapper: (Map<String, String>) -> R?): List<R> =
|
|
||||||
withContext(Dispatchers.Default) {
|
|
||||||
Shell.su(query).await().out.map { line ->
|
|
||||||
async {
|
|
||||||
line.split("\\|".toRegex())
|
|
||||||
.map { it.split("=", limit = 2) }
|
|
||||||
.filter { it.size == 2 }
|
|
||||||
.map { it[0] to it[1] }
|
|
||||||
.toMap()
|
|
||||||
.let(mapper)
|
|
||||||
}
|
|
||||||
}.awaitAll().filterNotNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend inline fun query() = query { it }
|
|
||||||
|
|
||||||
suspend inline fun commit() = Shell.su(query).to(null).await()
|
|
||||||
}
|
|
||||||
|
|
||||||
class Delete : Query.Builder {
|
|
||||||
override val requestType: String = "DELETE FROM"
|
|
||||||
override var table = ""
|
|
||||||
|
|
||||||
private var condition = ""
|
|
||||||
|
|
||||||
fun condition(builder: Condition.() -> Unit) {
|
|
||||||
condition = Condition().apply(builder).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return listOf(requestType, table, condition).joinToString(" ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Select : Query.Builder {
|
|
||||||
override val requestType: String get() = "SELECT $fields FROM"
|
|
||||||
override lateinit var table: String
|
|
||||||
|
|
||||||
private var fields = "*"
|
|
||||||
private var condition = ""
|
|
||||||
private var orderField = ""
|
|
||||||
|
|
||||||
fun fields(vararg newFields: String) {
|
|
||||||
if (newFields.isEmpty()) {
|
|
||||||
fields = "*"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fields = newFields.joinToString(", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun condition(builder: Condition.() -> Unit) {
|
|
||||||
condition = Condition().apply(builder).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun orderBy(field: String, @OrderStrict order: String) {
|
|
||||||
orderField = "ORDER BY $field $order"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return listOf(requestType, table, condition, orderField).joinToString(" ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Replace : Insert() {
|
|
||||||
override val requestType: String = "REPLACE INTO"
|
|
||||||
}
|
|
||||||
|
|
||||||
open class Insert : Query.Builder {
|
|
||||||
override val requestType: String = "INSERT INTO"
|
|
||||||
override lateinit var table: String
|
|
||||||
|
|
||||||
private val keys get() = _values.keys.joinToString(",")
|
|
||||||
private val values get() = _values.values.joinToString(",") {
|
|
||||||
when (it) {
|
|
||||||
is Boolean -> if (it) "1" else "0"
|
|
||||||
is Number -> it.toString()
|
|
||||||
else -> "\"$it\""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private var _values: Map<String, Any> = mapOf()
|
|
||||||
|
|
||||||
fun values(vararg pairs: Pair<String, Any>) {
|
|
||||||
_values = pairs.toMap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun values(values: Map<String, Any>) {
|
|
||||||
_values = values
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return listOf(requestType, table, "($keys) VALUES($values)").joinToString(" ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Condition {
|
|
||||||
|
|
||||||
private val conditionWord = "WHERE %s"
|
|
||||||
private var condition: String = ""
|
|
||||||
|
|
||||||
fun equals(field: String, value: Any) {
|
|
||||||
condition = when (value) {
|
|
||||||
is String -> "$field=\"$value\""
|
|
||||||
else -> "$field=$value"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun greaterThan(field: String, value: String) {
|
|
||||||
condition = "$field > $value"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun lessThan(field: String, value: String) {
|
|
||||||
condition = "$field < $value"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun greaterOrEqualTo(field: String, value: String) {
|
|
||||||
condition = "$field >= $value"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun lessOrEqualTo(field: String, value: String) {
|
|
||||||
condition = "$field <= $value"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun and(builder: Condition.() -> Unit) {
|
|
||||||
condition = "($condition AND ${Condition().apply(builder).condition})"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun or(builder: Condition.() -> Unit) {
|
|
||||||
condition = "($condition OR ${Condition().apply(builder).condition})"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return conditionWord.format(condition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object Order {
|
|
||||||
const val ASC = "ASC"
|
|
||||||
const val DESC = "DESC"
|
|
||||||
}
|
|
||||||
|
|
||||||
@StringDef(Order.ASC, Order.DESC)
|
|
||||||
@Retention(AnnotationRetention.SOURCE)
|
|
||||||
annotation class OrderStrict
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.magiskdb
|
|
||||||
|
|
||||||
class SettingsDao : BaseDao() {
|
|
||||||
|
|
||||||
override val table = Table.SETTINGS
|
|
||||||
|
|
||||||
suspend fun delete(key: String) = buildQuery<Delete> {
|
|
||||||
condition { equals("key", key) }
|
|
||||||
}.commit()
|
|
||||||
|
|
||||||
suspend fun put(key: String, value: Int) = buildQuery<Replace> {
|
|
||||||
values("key" to key, "value" to value)
|
|
||||||
}.commit()
|
|
||||||
|
|
||||||
suspend fun fetch(key: String, default: Int = -1) = buildQuery<Select> {
|
|
||||||
fields("value")
|
|
||||||
condition { equals("key", key) }
|
|
||||||
}.query {
|
|
||||||
it["value"]?.toIntOrNull()
|
|
||||||
}.firstOrNull() ?: default
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.magiskdb
|
|
||||||
|
|
||||||
class StringDao : BaseDao() {
|
|
||||||
|
|
||||||
override val table = Table.STRINGS
|
|
||||||
|
|
||||||
suspend fun delete(key: String) = buildQuery<Delete> {
|
|
||||||
condition { equals("key", key) }
|
|
||||||
}.commit()
|
|
||||||
|
|
||||||
suspend fun put(key: String, value: String) = buildQuery<Replace> {
|
|
||||||
values("key" to key, "value" to value)
|
|
||||||
}.commit()
|
|
||||||
|
|
||||||
suspend fun fetch(key: String, default: String = "") = buildQuery<Select> {
|
|
||||||
fields("value")
|
|
||||||
condition { equals("key", key) }
|
|
||||||
}.query {
|
|
||||||
it["value"]
|
|
||||||
}.firstOrNull() ?: default
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -2,42 +2,36 @@ package com.topjohnwu.magisk.core.model
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class UpdateInfo(
|
data class UpdateInfo(
|
||||||
val app: ManagerJson = ManagerJson(),
|
|
||||||
val uninstaller: UninstallerJson = UninstallerJson(),
|
|
||||||
val magisk: MagiskJson = MagiskJson(),
|
val magisk: MagiskJson = MagiskJson(),
|
||||||
val stub: StubJson = StubJson()
|
|
||||||
)
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class UninstallerJson(
|
|
||||||
val link: String = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class MagiskJson(
|
|
||||||
val version: String = "",
|
|
||||||
val versionCode: Int = -1,
|
|
||||||
val link: String = "",
|
|
||||||
val note: String = "",
|
|
||||||
val md5: String = ""
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class ManagerJson(
|
data class MagiskJson(
|
||||||
val version: String = "",
|
val version: String = "",
|
||||||
val versionCode: Int = -1,
|
val versionCode: Int = -1,
|
||||||
val link: String = "",
|
val link: String = "",
|
||||||
val note: String = ""
|
val note: String = ""
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class StubJson(
|
data class ModuleJson(
|
||||||
val versionCode: Int = -1,
|
val version: String,
|
||||||
val link: String = ""
|
val versionCode: Int,
|
||||||
) : Parcelable
|
val zipUrl: String,
|
||||||
|
val changelog: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class CommitInfo(
|
||||||
|
val sha: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class BranchInfo(
|
||||||
|
val commit: CommitInfo
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.model.module
|
|
||||||
|
|
||||||
abstract class BaseModule : Comparable<BaseModule> {
|
|
||||||
abstract var id: String
|
|
||||||
protected set
|
|
||||||
abstract var name: String
|
|
||||||
protected set
|
|
||||||
abstract var author: String
|
|
||||||
protected set
|
|
||||||
abstract var version: String
|
|
||||||
protected set
|
|
||||||
abstract var versionCode: Int
|
|
||||||
protected set
|
|
||||||
abstract var description: String
|
|
||||||
protected set
|
|
||||||
|
|
||||||
@Throws(NumberFormatException::class)
|
|
||||||
protected fun parseProps(props: List<String>) {
|
|
||||||
for (line in props) {
|
|
||||||
val prop = line.split("=".toRegex(), 2).map { it.trim() }
|
|
||||||
if (prop.size != 2)
|
|
||||||
continue
|
|
||||||
|
|
||||||
val key = prop[0]
|
|
||||||
val value = prop[1]
|
|
||||||
if (key.isEmpty() || key[0] == '#')
|
|
||||||
continue
|
|
||||||
|
|
||||||
when (key) {
|
|
||||||
"id" -> id = value
|
|
||||||
"name" -> name = value
|
|
||||||
"version" -> version = value
|
|
||||||
"versionCode" -> versionCode = value.toInt()
|
|
||||||
"author" -> author = value
|
|
||||||
"description" -> description = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override operator fun compareTo(other: BaseModule) = name.compareTo(other.name, true)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package com.topjohnwu.magisk.core.model.module
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonDataException
|
||||||
|
import com.topjohnwu.magisk.core.Const
|
||||||
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
data class LocalModule(
|
||||||
|
private val path: String,
|
||||||
|
) : Module() {
|
||||||
|
private val svc get() = ServiceLocator.networkService
|
||||||
|
|
||||||
|
override var id: String = ""
|
||||||
|
override var name: String = ""
|
||||||
|
override var version: String = ""
|
||||||
|
override var versionCode: Int = -1
|
||||||
|
var author: String = ""
|
||||||
|
var description: String = ""
|
||||||
|
var updateInfo: OnlineModule? = null
|
||||||
|
var outdated = false
|
||||||
|
|
||||||
|
private var updateUrl: String = ""
|
||||||
|
private val removeFile = RootUtils.fs.getFile(path, "remove")
|
||||||
|
private val disableFile = RootUtils.fs.getFile(path, "disable")
|
||||||
|
private val updateFile = RootUtils.fs.getFile(path, "update")
|
||||||
|
private val riruFolder = RootUtils.fs.getFile(path, "riru")
|
||||||
|
private val zygiskFolder = RootUtils.fs.getFile(path, "zygisk")
|
||||||
|
private val unloaded = RootUtils.fs.getFile(zygiskFolder, "unloaded")
|
||||||
|
|
||||||
|
val updated: Boolean get() = updateFile.exists()
|
||||||
|
val isRiru: Boolean get() = (id == "riru-core") || riruFolder.exists()
|
||||||
|
val isZygisk: Boolean get() = zygiskFolder.exists()
|
||||||
|
val zygiskUnloaded: Boolean get() = unloaded.exists()
|
||||||
|
|
||||||
|
var enable: Boolean
|
||||||
|
get() = !disableFile.exists()
|
||||||
|
set(enable) {
|
||||||
|
if (enable) {
|
||||||
|
disableFile.delete()
|
||||||
|
Shell.cmd("copy_preinit_files").submit()
|
||||||
|
} else {
|
||||||
|
!disableFile.createNewFile()
|
||||||
|
Shell.cmd("copy_preinit_files").submit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var remove: Boolean
|
||||||
|
get() = removeFile.exists()
|
||||||
|
set(remove) {
|
||||||
|
if (remove) {
|
||||||
|
if (updateFile.exists()) return
|
||||||
|
removeFile.createNewFile()
|
||||||
|
Shell.cmd("copy_preinit_files").submit()
|
||||||
|
} else {
|
||||||
|
removeFile.delete()
|
||||||
|
Shell.cmd("copy_preinit_files").submit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(NumberFormatException::class)
|
||||||
|
private fun parseProps(props: List<String>) {
|
||||||
|
for (line in props) {
|
||||||
|
val prop = line.split("=".toRegex(), 2).map { it.trim() }
|
||||||
|
if (prop.size != 2)
|
||||||
|
continue
|
||||||
|
|
||||||
|
val key = prop[0]
|
||||||
|
val value = prop[1]
|
||||||
|
if (key.isEmpty() || key[0] == '#')
|
||||||
|
continue
|
||||||
|
|
||||||
|
when (key) {
|
||||||
|
"id" -> id = value
|
||||||
|
"name" -> name = value
|
||||||
|
"version" -> version = value
|
||||||
|
"versionCode" -> versionCode = value.toInt()
|
||||||
|
"author" -> author = value
|
||||||
|
"description" -> description = value
|
||||||
|
"updateJson" -> updateUrl = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
runCatching {
|
||||||
|
parseProps(Shell.cmd("dos2unix < $path/module.prop").exec().out)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id.isEmpty()) {
|
||||||
|
val sep = path.lastIndexOf('/')
|
||||||
|
id = path.substring(sep + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
name = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetch(): Boolean {
|
||||||
|
if (updateUrl.isEmpty())
|
||||||
|
return false
|
||||||
|
|
||||||
|
try {
|
||||||
|
val json = svc.fetchModuleJson(updateUrl)
|
||||||
|
updateInfo = OnlineModule(this, json)
|
||||||
|
outdated = json.versionCode > versionCode
|
||||||
|
return true
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.w(e)
|
||||||
|
} catch (e: JsonDataException) {
|
||||||
|
Timber.w(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun loaded() = RootUtils.fs.getFile(Const.MAGISK_PATH).exists()
|
||||||
|
|
||||||
|
suspend fun installed() = withContext(Dispatchers.IO) {
|
||||||
|
RootUtils.fs.getFile(Const.MAGISK_PATH)
|
||||||
|
.listFiles()
|
||||||
|
.orEmpty()
|
||||||
|
.filter { !it.isFile && !it.isHidden }
|
||||||
|
.map { LocalModule("${Const.MAGISK_PATH}/${it.name}") }
|
||||||
|
.sortedBy { it.name.lowercase(Locale.ROOT) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,77 +1,14 @@
|
|||||||
package com.topjohnwu.magisk.core.model.module
|
package com.topjohnwu.magisk.core.model.module
|
||||||
|
|
||||||
import com.topjohnwu.magisk.core.Const
|
abstract class Module : Comparable<Module> {
|
||||||
import com.topjohnwu.superuser.Shell
|
abstract var id: String
|
||||||
import com.topjohnwu.superuser.io.SuFile
|
protected set
|
||||||
import kotlinx.coroutines.Dispatchers
|
abstract var name: String
|
||||||
import kotlinx.coroutines.withContext
|
protected set
|
||||||
|
abstract var version: String
|
||||||
|
protected set
|
||||||
|
abstract var versionCode: Int
|
||||||
|
protected set
|
||||||
|
|
||||||
class Module(path: String) : BaseModule() {
|
override operator fun compareTo(other: Module) = id.compareTo(other.id)
|
||||||
override var id: String = ""
|
|
||||||
override var name: String = ""
|
|
||||||
override var author: String = ""
|
|
||||||
override var version: String = ""
|
|
||||||
override var versionCode: Int = -1
|
|
||||||
override var description: String = ""
|
|
||||||
|
|
||||||
private val removeFile = SuFile(path, "remove")
|
|
||||||
private val disableFile = SuFile(path, "disable")
|
|
||||||
private val updateFile = SuFile(path, "update")
|
|
||||||
private val ruleFile = SuFile(path, "sepolicy.rule")
|
|
||||||
|
|
||||||
val updated: Boolean get() = updateFile.exists()
|
|
||||||
|
|
||||||
var enable: Boolean
|
|
||||||
get() = !disableFile.exists()
|
|
||||||
set(enable) {
|
|
||||||
val dir = "$PERSIST/$id"
|
|
||||||
if (enable) {
|
|
||||||
Shell.su("mkdir -p $dir", "cp -af $ruleFile $dir").submit()
|
|
||||||
disableFile.delete()
|
|
||||||
} else {
|
|
||||||
Shell.su("rm -rf $dir").submit()
|
|
||||||
!disableFile.createNewFile()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var remove: Boolean
|
|
||||||
get() = removeFile.exists()
|
|
||||||
set(remove) {
|
|
||||||
if (remove) {
|
|
||||||
Shell.su("rm -rf $PERSIST/$id").submit()
|
|
||||||
removeFile.createNewFile()
|
|
||||||
} else {
|
|
||||||
Shell.su("cp -af $ruleFile $PERSIST/$id").submit()
|
|
||||||
!removeFile.delete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
runCatching {
|
|
||||||
parseProps(Shell.su("dos2unix < $path/module.prop").exec().out)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id.isEmpty()) {
|
|
||||||
val sep = path.lastIndexOf('/')
|
|
||||||
id = path.substring(sep + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.isEmpty()) {
|
|
||||||
name = id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private val PERSIST get() = "${Const.MAGISKTMP}/mirror/persist/magisk"
|
|
||||||
|
|
||||||
suspend fun installed() = withContext(Dispatchers.IO) {
|
|
||||||
SuFile(Const.MAGISK_PATH)
|
|
||||||
.listFiles { _, name -> name != "lost+found" && name != ".core" }
|
|
||||||
.orEmpty()
|
|
||||||
.filter { !it.isFile }
|
|
||||||
.map { Module("${Const.MAGISK_PATH}/${it.name}") }
|
|
||||||
.sortedBy { it.name.toLowerCase() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.topjohnwu.magisk.core.model.module
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import com.topjohnwu.magisk.core.model.ModuleJson
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class OnlineModule(
|
||||||
|
override var id: String,
|
||||||
|
override var name: String,
|
||||||
|
override var version: String,
|
||||||
|
override var versionCode: Int,
|
||||||
|
val zipUrl: String,
|
||||||
|
val changelog: String,
|
||||||
|
) : Module(), Parcelable {
|
||||||
|
constructor(local: LocalModule, json: ModuleJson) :
|
||||||
|
this(local.id, local.name, json.version, json.versionCode, json.zipUrl, json.changelog)
|
||||||
|
|
||||||
|
val downloadFilename get() = "$name-$version($versionCode).zip".legalFilename()
|
||||||
|
|
||||||
|
private fun String.legalFilename() = replace(" ", "_")
|
||||||
|
.replace("'", "").replace("\"", "")
|
||||||
|
.replace("$", "").replace("`", "")
|
||||||
|
.replace("*", "").replace("/", "_")
|
||||||
|
.replace("#", "").replace("@", "")
|
||||||
|
.replace("\\", "_")
|
||||||
|
}
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.model.module
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import androidx.room.Entity
|
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
import com.topjohnwu.magisk.core.Const
|
|
||||||
import com.topjohnwu.magisk.data.repository.StringRepository
|
|
||||||
import com.topjohnwu.magisk.ktx.get
|
|
||||||
import com.topjohnwu.magisk.ktx.legalFilename
|
|
||||||
import kotlinx.android.parcel.Parcelize
|
|
||||||
import java.text.DateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Entity(tableName = "repos")
|
|
||||||
@Parcelize
|
|
||||||
data class Repo(
|
|
||||||
@PrimaryKey override var id: String,
|
|
||||||
override var name: String,
|
|
||||||
override var author: String,
|
|
||||||
override var version: String,
|
|
||||||
override var versionCode: Int,
|
|
||||||
override var description: String,
|
|
||||||
var last_update: Long
|
|
||||||
) : BaseModule(), Parcelable {
|
|
||||||
|
|
||||||
private val stringRepo: StringRepository get() = get()
|
|
||||||
|
|
||||||
val lastUpdate get() = Date(last_update)
|
|
||||||
|
|
||||||
val lastUpdateString: String get() = dateFormat.format(lastUpdate)
|
|
||||||
|
|
||||||
val downloadFilename: String get() = "$name-$version($versionCode).zip".legalFilename()
|
|
||||||
|
|
||||||
suspend fun readme() = stringRepo.getReadme(this)
|
|
||||||
|
|
||||||
val zipUrl: String get() = Const.Url.ZIP_URL.format(id)
|
|
||||||
|
|
||||||
constructor(id: String) : this(id, "", "", "", -1, "", 0)
|
|
||||||
|
|
||||||
@Throws(IllegalRepoException::class)
|
|
||||||
private fun loadProps(props: String) {
|
|
||||||
props.split("\\n".toRegex()).dropLastWhile { it.isEmpty() }.runCatching {
|
|
||||||
parseProps(this)
|
|
||||||
}.onFailure {
|
|
||||||
throw IllegalRepoException("Repo [$id] parse error: " + it.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (versionCode < 0) {
|
|
||||||
throw IllegalRepoException("Repo [$id] does not contain versionCode")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IllegalRepoException::class)
|
|
||||||
suspend fun update(lastUpdate: Date? = null) {
|
|
||||||
lastUpdate?.let { last_update = it.time }
|
|
||||||
loadProps(stringRepo.getMetadata(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
class IllegalRepoException(message: String) : Exception(message)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
package com.topjohnwu.magisk.core.model.su
|
package com.topjohnwu.magisk.core.model.su
|
||||||
|
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.Ignore
|
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.ALLOW
|
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||||
import com.topjohnwu.magisk.ktx.now
|
|
||||||
import com.topjohnwu.magisk.ktx.timeFormatTime
|
|
||||||
import com.topjohnwu.magisk.ktx.toTime
|
|
||||||
|
|
||||||
@Entity(tableName = "logs")
|
@Entity(tableName = "logs")
|
||||||
data class SuLog(
|
class SuLog(
|
||||||
val fromUid: Int,
|
val fromUid: Int,
|
||||||
val toUid: Int,
|
val toUid: Int,
|
||||||
val fromPid: Int,
|
val fromPid: Int,
|
||||||
@@ -17,14 +15,44 @@ data class SuLog(
|
|||||||
val appName: String,
|
val appName: String,
|
||||||
val command: String,
|
val command: String,
|
||||||
val action: Boolean,
|
val action: Boolean,
|
||||||
val time: Long = -1
|
val time: Long = System.currentTimeMillis()
|
||||||
) {
|
) {
|
||||||
@PrimaryKey(autoGenerate = true) var id: Int = 0
|
@PrimaryKey(autoGenerate = true) var id: Int = 0
|
||||||
@Ignore val timeString = time.toTime(timeFormatTime)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun SuPolicy.toLog(
|
fun PackageManager.createSuLog(
|
||||||
|
info: PackageInfo,
|
||||||
toUid: Int,
|
toUid: Int,
|
||||||
fromPid: Int,
|
fromPid: Int,
|
||||||
command: String
|
command: String,
|
||||||
) = SuLog(uid, toUid, fromPid, packageName, appName, command, policy == ALLOW, now)
|
policy: Int
|
||||||
|
): 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 == SuPolicy.ALLOW
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createSuLog(
|
||||||
|
fromUid: Int,
|
||||||
|
toUid: Int,
|
||||||
|
fromPid: Int,
|
||||||
|
command: String,
|
||||||
|
policy: Int
|
||||||
|
): SuLog {
|
||||||
|
return SuLog(
|
||||||
|
fromUid = fromUid,
|
||||||
|
toUid = toUid,
|
||||||
|
fromPid = fromPid,
|
||||||
|
packageName = "[UID] $fromUid",
|
||||||
|
appName = "[UID] $fromUid",
|
||||||
|
command = command,
|
||||||
|
action = policy == SuPolicy.ALLOW
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,70 +1,22 @@
|
|||||||
package com.topjohnwu.magisk.core.model.su
|
package com.topjohnwu.magisk.core.model.su
|
||||||
|
|
||||||
import android.content.pm.PackageManager
|
class SuPolicy(val uid: Int) {
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.INTERACTIVE
|
|
||||||
import com.topjohnwu.magisk.ktx.getLabel
|
|
||||||
|
|
||||||
|
|
||||||
data class SuPolicy(
|
|
||||||
var uid: Int,
|
|
||||||
val packageName: String,
|
|
||||||
val appName: String,
|
|
||||||
val icon: Drawable,
|
|
||||||
var policy: Int = INTERACTIVE,
|
|
||||||
var until: Long = -1L,
|
|
||||||
val logging: Boolean = true,
|
|
||||||
val notification: Boolean = true
|
|
||||||
) {
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val INTERACTIVE = 0
|
const val INTERACTIVE = 0
|
||||||
const val DENY = 1
|
const val DENY = 1
|
||||||
const val ALLOW = 2
|
const val ALLOW = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
var policy: Int = INTERACTIVE
|
||||||
|
var until: Long = -1L
|
||||||
|
var logging: Boolean = true
|
||||||
|
var notification: Boolean = true
|
||||||
|
|
||||||
fun SuPolicy.toMap() = mapOf(
|
fun toMap(): MutableMap<String, Any> = mutableMapOf(
|
||||||
"uid" to uid,
|
"uid" to uid,
|
||||||
"package_name" to packageName,
|
"policy" to policy,
|
||||||
"policy" to policy,
|
"until" to until,
|
||||||
"until" to until,
|
"logging" to logging,
|
||||||
"logging" to logging,
|
"notification" to notification
|
||||||
"notification" to notification
|
|
||||||
)
|
|
||||||
|
|
||||||
@Throws(PackageManager.NameNotFoundException::class)
|
|
||||||
fun Map<String, String>.toPolicy(pm: PackageManager): SuPolicy {
|
|
||||||
val uid = get("uid")?.toIntOrNull() ?: -1
|
|
||||||
val packageName = get("package_name").orEmpty()
|
|
||||||
val info = pm.getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES)
|
|
||||||
|
|
||||||
if (info.uid != uid)
|
|
||||||
throw PackageManager.NameNotFoundException()
|
|
||||||
|
|
||||||
return SuPolicy(
|
|
||||||
uid = uid,
|
|
||||||
packageName = packageName,
|
|
||||||
appName = info.getLabel(pm),
|
|
||||||
icon = info.loadIcon(pm),
|
|
||||||
policy = get("policy")?.toIntOrNull() ?: INTERACTIVE,
|
|
||||||
until = get("until")?.toLongOrNull() ?: -1L,
|
|
||||||
logging = get("logging")?.toIntOrNull() != 0,
|
|
||||||
notification = get("notification")?.toIntOrNull() != 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(PackageManager.NameNotFoundException::class)
|
|
||||||
fun Int.toPolicy(pm: PackageManager, policy: Int = INTERACTIVE): SuPolicy {
|
|
||||||
val pkg = pm.getPackagesForUid(this)?.firstOrNull()
|
|
||||||
?: throw PackageManager.NameNotFoundException()
|
|
||||||
val info = pm.getApplicationInfo(pkg, PackageManager.GET_UNINSTALLED_PACKAGES)
|
|
||||||
return SuPolicy(
|
|
||||||
uid = info.uid,
|
|
||||||
packageName = pkg,
|
|
||||||
appName = info.getLabel(pm),
|
|
||||||
icon = info.loadIcon(pm),
|
|
||||||
policy = policy
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,47 @@
|
|||||||
package com.topjohnwu.magisk.data.repository
|
package com.topjohnwu.magisk.core.repository
|
||||||
|
|
||||||
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
|
import com.topjohnwu.magisk.core.data.magiskdb.SettingsDao
|
||||||
import com.topjohnwu.magisk.core.magiskdb.StringDao
|
import com.topjohnwu.magisk.core.data.magiskdb.StringDao
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlin.properties.ReadWriteProperty
|
import kotlin.properties.ReadWriteProperty
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
interface DBConfig {
|
interface DBConfig {
|
||||||
val settingsDao: SettingsDao
|
val settingsDB: SettingsDao
|
||||||
val stringDao: StringDao
|
val stringDB: StringDao
|
||||||
|
val coroutineScope: CoroutineScope
|
||||||
|
|
||||||
fun dbSettings(
|
fun dbSettings(
|
||||||
name: String,
|
name: String,
|
||||||
default: Int
|
default: Int
|
||||||
) = DBSettingsValue(name, default)
|
) = IntDBProperty(name, default)
|
||||||
|
|
||||||
fun dbSettings(
|
fun dbSettings(
|
||||||
name: String,
|
name: String,
|
||||||
default: Boolean
|
default: Boolean
|
||||||
) = DBBoolSettings(name, default)
|
) = BoolDBProperty(name, default)
|
||||||
|
|
||||||
fun dbStrings(
|
fun dbStrings(
|
||||||
name: String,
|
name: String,
|
||||||
default: String,
|
default: String,
|
||||||
sync: Boolean = false
|
sync: Boolean = false
|
||||||
) = DBStringsValue(name, default, sync)
|
) = StringDBProperty(name, default, sync)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class DBSettingsValue(
|
class IntDBProperty(
|
||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: Int
|
private val default: Int
|
||||||
) : ReadWriteProperty<DBConfig, Int> {
|
) : ReadWriteProperty<DBConfig, Int> {
|
||||||
|
|
||||||
private var value: Int? = null
|
var value: Int? = null
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Int {
|
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Int {
|
||||||
if (value == null)
|
if (value == null)
|
||||||
value = runBlocking {
|
value = runBlocking { thisRef.settingsDB.fetch(name, default) }
|
||||||
thisRef.settingsDao.fetch(name, default)
|
|
||||||
}
|
|
||||||
return value as Int
|
return value as Int
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,18 +49,18 @@ class DBSettingsValue(
|
|||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
this.value = value
|
this.value = value
|
||||||
}
|
}
|
||||||
GlobalScope.launch {
|
thisRef.coroutineScope.launch {
|
||||||
thisRef.settingsDao.put(name, value)
|
thisRef.settingsDB.put(name, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DBBoolSettings(
|
open class BoolDBProperty(
|
||||||
name: String,
|
name: String,
|
||||||
default: Boolean
|
default: Boolean
|
||||||
) : ReadWriteProperty<DBConfig, Boolean> {
|
) : ReadWriteProperty<DBConfig, Boolean> {
|
||||||
|
|
||||||
val base = DBSettingsValue(name, if (default) 1 else 0)
|
val base = IntDBProperty(name, if (default) 1 else 0)
|
||||||
|
|
||||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Boolean =
|
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Boolean =
|
||||||
base.getValue(thisRef, property) != 0
|
base.getValue(thisRef, property) != 0
|
||||||
@@ -70,7 +69,18 @@ class DBBoolSettings(
|
|||||||
base.setValue(thisRef, property, if (value) 1 else 0)
|
base.setValue(thisRef, property, if (value) 1 else 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
class DBStringsValue(
|
class BoolDBPropertyNoWrite(
|
||||||
|
name: String,
|
||||||
|
default: Boolean
|
||||||
|
) : BoolDBProperty(name, default) {
|
||||||
|
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Boolean) {
|
||||||
|
synchronized(base) {
|
||||||
|
base.value = if (value) 1 else 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StringDBProperty(
|
||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: String,
|
private val default: String,
|
||||||
private val sync: Boolean
|
private val sync: Boolean
|
||||||
@@ -82,7 +92,7 @@ class DBStringsValue(
|
|||||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): String {
|
override fun getValue(thisRef: DBConfig, property: KProperty<*>): String {
|
||||||
if (value == null)
|
if (value == null)
|
||||||
value = runBlocking {
|
value = runBlocking {
|
||||||
thisRef.stringDao.fetch(name, default)
|
thisRef.stringDB.fetch(name, default)
|
||||||
}
|
}
|
||||||
return value!!
|
return value!!
|
||||||
}
|
}
|
||||||
@@ -94,21 +104,21 @@ class DBStringsValue(
|
|||||||
if (value.isEmpty()) {
|
if (value.isEmpty()) {
|
||||||
if (sync) {
|
if (sync) {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
thisRef.stringDao.delete(name)
|
thisRef.stringDB.delete(name)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
GlobalScope.launch {
|
thisRef.coroutineScope.launch {
|
||||||
thisRef.stringDao.delete(name)
|
thisRef.stringDB.delete(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (sync) {
|
if (sync) {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
thisRef.stringDao.put(name, value)
|
thisRef.stringDB.put(name, value)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
GlobalScope.launch {
|
thisRef.coroutineScope.launch {
|
||||||
thisRef.stringDao.put(name, value)
|
thisRef.stringDB.put(name, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
package com.topjohnwu.magisk.data.repository
|
package com.topjohnwu.magisk.core.repository
|
||||||
|
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
import com.topjohnwu.magisk.core.data.SuLogDao
|
||||||
|
import com.topjohnwu.magisk.core.ktx.await
|
||||||
import com.topjohnwu.magisk.core.model.su.SuLog
|
import com.topjohnwu.magisk.core.model.su.SuLog
|
||||||
import com.topjohnwu.magisk.data.database.SuLogDao
|
|
||||||
import com.topjohnwu.magisk.ktx.await
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
|
|
||||||
|
|
||||||
@@ -27,14 +28,18 @@ class LogRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Shell.su("cat ${Const.MAGISK_LOG}").to(list).await()
|
if (Info.env.isActive) {
|
||||||
|
Shell.cmd("cat ${Const.MAGISK_LOG} || logcat -d -s Magisk").to(list).await()
|
||||||
|
} else {
|
||||||
|
Shell.cmd("logcat -d").to(list).await()
|
||||||
|
}
|
||||||
return list.buf.toString()
|
return list.buf.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun clearLogs() = logDao.deleteAll()
|
suspend fun clearLogs() = logDao.deleteAll()
|
||||||
|
|
||||||
fun clearMagiskLogs(cb: (Shell.Result) -> Unit) =
|
fun clearMagiskLogs(cb: (Shell.Result) -> Unit) =
|
||||||
Shell.su("echo -n > ${Const.MAGISK_LOG}").submit(cb)
|
Shell.cmd("echo -n > ${Const.MAGISK_LOG}").submit(cb)
|
||||||
|
|
||||||
suspend fun insert(log: SuLog) = logDao.insert(log)
|
suspend fun insert(log: SuLog) = logDao.insert(log)
|
||||||
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.topjohnwu.magisk.core.repository
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.core.Config
|
||||||
|
import com.topjohnwu.magisk.core.Config.Value.BETA_CHANNEL
|
||||||
|
import com.topjohnwu.magisk.core.Config.Value.CANARY_CHANNEL
|
||||||
|
import com.topjohnwu.magisk.core.Config.Value.CUSTOM_CHANNEL
|
||||||
|
import com.topjohnwu.magisk.core.Config.Value.DEBUG_CHANNEL
|
||||||
|
import com.topjohnwu.magisk.core.Config.Value.DEFAULT_CHANNEL
|
||||||
|
import com.topjohnwu.magisk.core.Config.Value.STABLE_CHANNEL
|
||||||
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
import com.topjohnwu.magisk.core.data.GithubPageServices
|
||||||
|
import com.topjohnwu.magisk.core.data.RawServices
|
||||||
|
import retrofit2.HttpException
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class NetworkService(
|
||||||
|
private val pages: GithubPageServices,
|
||||||
|
private val raw: RawServices
|
||||||
|
) {
|
||||||
|
suspend fun fetchUpdate() = safe {
|
||||||
|
var info = when (Config.updateChannel) {
|
||||||
|
DEFAULT_CHANNEL, STABLE_CHANNEL -> fetchStableUpdate()
|
||||||
|
BETA_CHANNEL -> fetchBetaUpdate()
|
||||||
|
CANARY_CHANNEL -> fetchCanaryUpdate()
|
||||||
|
DEBUG_CHANNEL -> fetchDebugUpdate()
|
||||||
|
CUSTOM_CHANNEL -> fetchCustomUpdate(Config.customChannelUrl)
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
if (info.magisk.versionCode < Info.env.versionCode &&
|
||||||
|
Config.updateChannel == DEFAULT_CHANNEL) {
|
||||||
|
Config.updateChannel = BETA_CHANNEL
|
||||||
|
info = fetchBetaUpdate()
|
||||||
|
}
|
||||||
|
info
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateInfo
|
||||||
|
private suspend fun fetchStableUpdate() = pages.fetchUpdateJSON("stable.json")
|
||||||
|
private suspend fun fetchBetaUpdate() = pages.fetchUpdateJSON("beta.json")
|
||||||
|
private suspend fun fetchCanaryUpdate() = pages.fetchUpdateJSON("canary.json")
|
||||||
|
private suspend fun fetchDebugUpdate() = pages.fetchUpdateJSON("debug.json")
|
||||||
|
private suspend fun fetchCustomUpdate(url: String) = pages.fetchUpdateJSON(url)
|
||||||
|
|
||||||
|
private inline fun <T> safe(factory: () -> T): T? {
|
||||||
|
return try {
|
||||||
|
if (Info.isConnected.value == true)
|
||||||
|
factory()
|
||||||
|
else
|
||||||
|
null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <T> wrap(factory: () -> T): T {
|
||||||
|
return try {
|
||||||
|
factory()
|
||||||
|
} catch (e: HttpException) {
|
||||||
|
throw IOException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch files
|
||||||
|
suspend fun fetchFile(url: String) = wrap { raw.fetchFile(url) }
|
||||||
|
suspend fun fetchString(url: String) = wrap { raw.fetchString(url) }
|
||||||
|
suspend fun fetchModuleJson(url: String) = wrap { raw.fetchModuleJson(url) }
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
package com.topjohnwu.magisk.core.repository
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import kotlin.properties.ReadWriteProperty
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
interface PreferenceConfig {
|
||||||
|
|
||||||
|
val context: Context
|
||||||
|
|
||||||
|
val fileName: String
|
||||||
|
get() = "${context.packageName}_preferences"
|
||||||
|
|
||||||
|
val prefs: SharedPreferences
|
||||||
|
get() = context.getSharedPreferences(fileName, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
fun preferenceStrInt(
|
||||||
|
name: String,
|
||||||
|
default: Int,
|
||||||
|
commit: Boolean = false
|
||||||
|
) = object: ReadWriteProperty<PreferenceConfig, Int> {
|
||||||
|
val base = StringProperty(name, default.toString(), commit)
|
||||||
|
override fun getValue(thisRef: PreferenceConfig, property: KProperty<*>): Int =
|
||||||
|
base.getValue(thisRef, property).toInt()
|
||||||
|
|
||||||
|
override fun setValue(thisRef: PreferenceConfig, property: KProperty<*>, value: Int) =
|
||||||
|
base.setValue(thisRef, property, value.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun preference(
|
||||||
|
name: String,
|
||||||
|
default: Boolean,
|
||||||
|
commit: Boolean = false
|
||||||
|
) = BooleanProperty(name, default, commit)
|
||||||
|
|
||||||
|
fun preference(
|
||||||
|
name: String,
|
||||||
|
default: Float,
|
||||||
|
commit: Boolean = false
|
||||||
|
) = FloatProperty(name, default, commit)
|
||||||
|
|
||||||
|
fun preference(
|
||||||
|
name: String,
|
||||||
|
default: Int,
|
||||||
|
commit: Boolean = false
|
||||||
|
) = IntProperty(name, default, commit)
|
||||||
|
|
||||||
|
fun preference(
|
||||||
|
name: String,
|
||||||
|
default: Long,
|
||||||
|
commit: Boolean = false
|
||||||
|
) = LongProperty(name, default, commit)
|
||||||
|
|
||||||
|
fun preference(
|
||||||
|
name: String,
|
||||||
|
default: String,
|
||||||
|
commit: Boolean = false
|
||||||
|
) = StringProperty(name, default, commit)
|
||||||
|
|
||||||
|
fun preference(
|
||||||
|
name: String,
|
||||||
|
default: Set<String>,
|
||||||
|
commit: Boolean = false
|
||||||
|
) = StringSetProperty(name, default, commit)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class PreferenceProperty {
|
||||||
|
|
||||||
|
fun SharedPreferences.Editor.put(name: String, value: Boolean) = putBoolean(name, value)
|
||||||
|
fun SharedPreferences.Editor.put(name: String, value: Float) = putFloat(name, value)
|
||||||
|
fun SharedPreferences.Editor.put(name: String, value: Int) = putInt(name, value)
|
||||||
|
fun SharedPreferences.Editor.put(name: String, value: Long) = putLong(name, value)
|
||||||
|
fun SharedPreferences.Editor.put(name: String, value: String) = putString(name, value)
|
||||||
|
fun SharedPreferences.Editor.put(name: String, value: Set<String>) = putStringSet(name, value)
|
||||||
|
|
||||||
|
fun SharedPreferences.get(name: String, value: Boolean) = getBoolean(name, value)
|
||||||
|
fun SharedPreferences.get(name: String, value: Float) = getFloat(name, value)
|
||||||
|
fun SharedPreferences.get(name: String, value: Int) = getInt(name, value)
|
||||||
|
fun SharedPreferences.get(name: String, value: Long) = getLong(name, value)
|
||||||
|
fun SharedPreferences.get(name: String, value: String) = getString(name, value) ?: value
|
||||||
|
fun SharedPreferences.get(name: String, value: Set<String>) = getStringSet(name, value) ?: value
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class BooleanProperty(
|
||||||
|
private val name: String,
|
||||||
|
private val default: Boolean,
|
||||||
|
private val commit: Boolean
|
||||||
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Boolean> {
|
||||||
|
|
||||||
|
override operator fun getValue(
|
||||||
|
thisRef: PreferenceConfig,
|
||||||
|
property: KProperty<*>
|
||||||
|
): Boolean {
|
||||||
|
val prefName = name.ifBlank { property.name }
|
||||||
|
return thisRef.prefs.get(prefName, default)
|
||||||
|
}
|
||||||
|
|
||||||
|
override operator fun setValue(
|
||||||
|
thisRef: PreferenceConfig,
|
||||||
|
property: KProperty<*>,
|
||||||
|
value: Boolean
|
||||||
|
) {
|
||||||
|
val prefName = name.ifBlank { property.name }
|
||||||
|
thisRef.prefs.edit(commit) { put(prefName, value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FloatProperty(
|
||||||
|
private val name: String,
|
||||||
|
private val default: Float,
|
||||||
|
private val commit: Boolean
|
||||||
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Float> {
|
||||||
|
|
||||||
|
override operator fun getValue(
|
||||||
|
thisRef: PreferenceConfig,
|
||||||
|
property: KProperty<*>
|
||||||
|
): Float {
|
||||||
|
val prefName = name.ifBlank { property.name }
|
||||||
|
return thisRef.prefs.get(prefName, default)
|
||||||
|
}
|
||||||
|
|
||||||
|
override operator fun setValue(
|
||||||
|
thisRef: PreferenceConfig,
|
||||||
|
property: KProperty<*>,
|
||||||
|
value: Float
|
||||||
|
) {
|
||||||
|
val prefName = name.ifBlank { property.name }
|
||||||
|
thisRef.prefs.edit(commit) { put(prefName, value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IntProperty(
|
||||||
|
private val name: String,
|
||||||
|
private val default: Int,
|
||||||
|
private val commit: Boolean
|
||||||
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Int> {
|
||||||
|
|
||||||
|
override operator fun getValue(
|
||||||
|
thisRef: PreferenceConfig,
|
||||||
|
property: KProperty<*>
|
||||||
|
): Int {
|
||||||
|
val prefName = name.ifBlank { property.name }
|
||||||
|
return thisRef.prefs.get(prefName, default)
|
||||||
|
}
|
||||||
|
|
||||||
|
override operator fun setValue(
|
||||||
|
thisRef: PreferenceConfig,
|
||||||
|
property: KProperty<*>,
|
||||||
|
value: Int
|
||||||
|
) {
|
||||||
|
val prefName = name.ifBlank { property.name }
|
||||||
|
thisRef.prefs.edit(commit) { put(prefName, value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LongProperty(
|
||||||
|
private val name: String,
|
||||||
|
private val default: Long,
|
||||||
|
private val commit: Boolean
|
||||||
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Long> {
|
||||||
|
|
||||||
|
override operator fun getValue(
|
||||||
|
thisRef: PreferenceConfig,
|
||||||
|
property: KProperty<*>
|
||||||
|
): Long {
|
||||||
|
val prefName = name.ifBlank { property.name }
|
||||||
|
return thisRef.prefs.get(prefName, default)
|
||||||
|
}
|
||||||
|
|
||||||
|
override operator fun setValue(
|
||||||
|
thisRef: PreferenceConfig,
|
||||||
|
property: KProperty<*>,
|
||||||
|
value: Long
|
||||||
|
) {
|
||||||
|
val prefName = name.ifBlank { property.name }
|
||||||
|
thisRef.prefs.edit(commit) { put(prefName, value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StringProperty(
|
||||||
|
private val name: String,
|
||||||
|
private val default: String,
|
||||||
|
private val commit: Boolean
|
||||||
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, String> {
|
||||||
|
|
||||||
|
override operator fun getValue(
|
||||||
|
thisRef: PreferenceConfig,
|
||||||
|
property: KProperty<*>
|
||||||
|
): String {
|
||||||
|
val prefName = name.ifBlank { property.name }
|
||||||
|
return thisRef.prefs.get(prefName, default)
|
||||||
|
}
|
||||||
|
|
||||||
|
override operator fun setValue(
|
||||||
|
thisRef: PreferenceConfig,
|
||||||
|
property: KProperty<*>,
|
||||||
|
value: String
|
||||||
|
) {
|
||||||
|
val prefName = name.ifBlank { property.name }
|
||||||
|
thisRef.prefs.edit(commit) { put(prefName, value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StringSetProperty(
|
||||||
|
private val name: String,
|
||||||
|
private val default: Set<String>,
|
||||||
|
private val commit: Boolean
|
||||||
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Set<String>> {
|
||||||
|
|
||||||
|
override operator fun getValue(
|
||||||
|
thisRef: PreferenceConfig,
|
||||||
|
property: KProperty<*>
|
||||||
|
): Set<String> {
|
||||||
|
val prefName = name.ifBlank { property.name }
|
||||||
|
return thisRef.prefs.get(prefName, default)
|
||||||
|
}
|
||||||
|
|
||||||
|
override operator fun setValue(
|
||||||
|
thisRef: PreferenceConfig,
|
||||||
|
property: KProperty<*>,
|
||||||
|
value: Set<String>
|
||||||
|
) {
|
||||||
|
val prefName = name.ifBlank { property.name }
|
||||||
|
thisRef.prefs.edit(commit) { put(prefName, value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,44 +1,27 @@
|
|||||||
package com.topjohnwu.magisk.core.su
|
package com.topjohnwu.magisk.core.su
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Process
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.ProviderCallHandler
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.intent
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||||
|
import com.topjohnwu.magisk.core.ktx.getPackageInfo
|
||||||
|
import com.topjohnwu.magisk.core.ktx.toast
|
||||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||||
import com.topjohnwu.magisk.core.model.su.toLog
|
import com.topjohnwu.magisk.core.model.su.createSuLog
|
||||||
import com.topjohnwu.magisk.core.model.su.toPolicy
|
import kotlinx.coroutines.runBlocking
|
||||||
import com.topjohnwu.magisk.core.wrap
|
|
||||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
|
||||||
import com.topjohnwu.magisk.ktx.get
|
|
||||||
import com.topjohnwu.magisk.ktx.startActivity
|
|
||||||
import com.topjohnwu.magisk.ktx.startActivityWithRoot
|
|
||||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
object SuCallbackHandler : ProviderCallHandler {
|
object SuCallbackHandler {
|
||||||
|
|
||||||
const val REQUEST = "request"
|
const val REQUEST = "request"
|
||||||
const val LOG = "log"
|
const val LOG = "log"
|
||||||
const val NOTIFY = "notify"
|
const val NOTIFY = "notify"
|
||||||
const val TEST = "test"
|
|
||||||
|
|
||||||
override fun call(context: Context, method: String, arg: String?, extras: Bundle?): Bundle? {
|
fun run(context: Context, action: String?, data: Bundle?) {
|
||||||
invoke(context.wrap(), method, extras)
|
|
||||||
return Bundle.EMPTY
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun invoke(context: Context, action: String?, data: Bundle?) {
|
|
||||||
data ?: return
|
data ?: return
|
||||||
|
|
||||||
// Debug messages
|
// Debug messages
|
||||||
@@ -52,94 +35,65 @@ object SuCallbackHandler : ProviderCallHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
when (action) {
|
when (action) {
|
||||||
REQUEST -> handleRequest(context, data)
|
|
||||||
LOG -> handleLogging(context, data)
|
LOG -> handleLogging(context, data)
|
||||||
NOTIFY -> handleNotify(context, data)
|
NOTIFY -> handleNotify(context, data)
|
||||||
TEST -> {
|
|
||||||
val mode = data.getInt("mode", 2)
|
|
||||||
Shell.su(
|
|
||||||
"magisk --connect-mode $mode",
|
|
||||||
"magisk --use-broadcast"
|
|
||||||
).submit()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Any?.toInt(): Int? {
|
// https://android.googlesource.com/platform/frameworks/base/+/547bf5487d52b93c9fe183aa6d56459c170b17a4
|
||||||
return when (this) {
|
private fun Bundle.getIntComp(key: String, defaultValue: Int): Int {
|
||||||
is Number -> this.toInt()
|
val value = get(key) ?: return defaultValue
|
||||||
else -> null
|
return when (value) {
|
||||||
}
|
is Int -> value
|
||||||
}
|
is Long -> value.toInt()
|
||||||
|
else -> defaultValue
|
||||||
private fun handleRequest(context: Context, data: Bundle) {
|
|
||||||
val intent = context.intent<SuRequestActivity>()
|
|
||||||
.setAction(REQUEST)
|
|
||||||
.putExtras(data)
|
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
|
||||||
if (Build.VERSION.SDK_INT >= 29) {
|
|
||||||
// Android Q does not allow starting activity from background
|
|
||||||
intent.startActivityWithRoot()
|
|
||||||
} else {
|
|
||||||
intent.startActivity(context)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLogging(context: Context, data: Bundle) {
|
private fun handleLogging(context: Context, data: Bundle) {
|
||||||
val fromUid = data["from.uid"].toInt() ?: return
|
val fromUid = data.getIntComp("from.uid", -1)
|
||||||
if (fromUid == Process.myUid())
|
val notify = data.getBoolean("notify", true)
|
||||||
return
|
val policy = data.getIntComp("policy", SuPolicy.ALLOW)
|
||||||
|
val toUid = data.getIntComp("to.uid", -1)
|
||||||
|
val pid = data.getIntComp("pid", -1)
|
||||||
|
val command = data.getString("command", "")
|
||||||
|
|
||||||
val pm = context.packageManager
|
val pm = context.packageManager
|
||||||
|
|
||||||
val notify = data.getBoolean("notify", true)
|
val log = runCatching {
|
||||||
val allow = data["policy"].toInt() ?: return
|
pm.getPackageInfo(fromUid, pid)?.let {
|
||||||
|
pm.createSuLog(it, toUid, pid, command, policy)
|
||||||
val policy = runCatching { fromUid.toPolicy(pm, allow) }.getOrElse { return }
|
}
|
||||||
|
}.getOrNull() ?: createSuLog(fromUid, toUid, pid, command, policy)
|
||||||
|
|
||||||
if (notify)
|
if (notify)
|
||||||
notify(context, policy)
|
notify(context, log.action, log.appName)
|
||||||
|
|
||||||
val toUid = data["to.uid"].toInt() ?: return
|
runBlocking { ServiceLocator.logRepo.insert(log) }
|
||||||
val pid = data["pid"].toInt() ?: return
|
|
||||||
|
|
||||||
val command = data.getString("command") ?: return
|
|
||||||
val log = policy.toLog(
|
|
||||||
toUid = toUid,
|
|
||||||
fromPid = pid,
|
|
||||||
command = command
|
|
||||||
)
|
|
||||||
|
|
||||||
val logRepo = get<LogRepository>()
|
|
||||||
GlobalScope.launch {
|
|
||||||
logRepo.insert(log)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleNotify(context: Context, data: Bundle) {
|
private fun handleNotify(context: Context, data: Bundle) {
|
||||||
val fromUid = data["from.uid"].toInt() ?: return
|
val uid = data.getIntComp("from.uid", -1)
|
||||||
if (fromUid == Process.myUid())
|
val pid = data.getIntComp("pid", -1)
|
||||||
return
|
val policy = data.getIntComp("policy", SuPolicy.ALLOW)
|
||||||
|
|
||||||
val pm = context.packageManager
|
val pm = context.packageManager
|
||||||
val allow = data["policy"].toInt() ?: return
|
|
||||||
|
|
||||||
runCatching {
|
val appName = runCatching {
|
||||||
val policy = fromUid.toPolicy(pm, allow)
|
pm.getPackageInfo(uid, pid)?.applicationInfo?.getLabel(pm)
|
||||||
if (policy.policy >= 0)
|
}.getOrNull() ?: "[UID] $uid"
|
||||||
notify(context, policy)
|
|
||||||
}
|
notify(context, policy == SuPolicy.ALLOW, appName)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notify(context: Context, policy: SuPolicy) {
|
private fun notify(context: Context, granted: Boolean, appName: String) {
|
||||||
if (policy.notification && Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
|
if (Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
|
||||||
val resId = if (policy.policy == SuPolicy.ALLOW)
|
val resId = if (granted)
|
||||||
R.string.su_allow_toast
|
R.string.su_allow_toast
|
||||||
else
|
else
|
||||||
R.string.su_deny_toast
|
R.string.su_deny_toast
|
||||||
|
|
||||||
Utils.toast(context.getString(resId, policy.appName), Toast.LENGTH_SHORT)
|
context.toast(context.getString(resId, appName), Toast.LENGTH_SHORT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
package com.topjohnwu.magisk.core.su
|
package com.topjohnwu.magisk.core.su
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.LocalSocket
|
|
||||||
import android.net.LocalSocketAddress
|
|
||||||
import androidx.collection.ArrayMap
|
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
||||||
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
import com.topjohnwu.magisk.core.ktx.getPackageInfo
|
||||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||||
import com.topjohnwu.magisk.core.model.su.toPolicy
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.magisk.ktx.now
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.*
|
import java.io.DataOutputStream
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.concurrent.TimeUnit.SECONDS
|
|
||||||
|
|
||||||
class SuRequestHandler(
|
class SuRequestHandler(
|
||||||
private val pm: PackageManager,
|
val pm: PackageManager,
|
||||||
private val policyDB: PolicyDao
|
private val policyDB: PolicyDao
|
||||||
) : Closeable {
|
) {
|
||||||
|
|
||||||
private lateinit var output: DataOutputStream
|
private lateinit var output: DataOutputStream
|
||||||
lateinit var policy: SuPolicy
|
private lateinit var policy: SuPolicy
|
||||||
|
lateinit var pkgInfo: PackageInfo
|
||||||
private set
|
private set
|
||||||
|
|
||||||
// Return true to indicate undetermined policy, require user interaction
|
// Return true to indicate undetermined policy, require user interaction
|
||||||
@@ -33,10 +33,12 @@ class SuRequestHandler(
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
// Never allow com.topjohnwu.magisk (could be malware)
|
// Never allow com.topjohnwu.magisk (could be malware)
|
||||||
if (policy.packageName == BuildConfig.APPLICATION_ID)
|
if (pkgInfo.packageName == BuildConfig.APPLICATION_ID) {
|
||||||
|
Shell.cmd("(pm uninstall ${BuildConfig.APPLICATION_ID} >/dev/null 2>&1)&").exec()
|
||||||
return false
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
when (Config.suAutoReponse) {
|
when (Config.suAutoResponse) {
|
||||||
Config.Value.SU_AUTO_DENY -> {
|
Config.Value.SU_AUTO_DENY -> {
|
||||||
respond(SuPolicy.DENY, 0)
|
respond(SuPolicy.DENY, 0)
|
||||||
return false
|
return false
|
||||||
@@ -50,90 +52,60 @@ class SuRequestHandler(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun <T> Deferred<T>.timedAwait() : T? {
|
private fun close() {
|
||||||
return withTimeoutOrNull(SECONDS.toMillis(1)) {
|
|
||||||
await()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun close() {
|
|
||||||
if (::output.isInitialized)
|
if (::output.isInitialized)
|
||||||
output.close()
|
runCatching { output.close() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SuRequestError : IOException()
|
|
||||||
|
|
||||||
private suspend fun init(intent: Intent) = withContext(Dispatchers.IO) {
|
private suspend fun init(intent: Intent) = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val uid: Int
|
val fifo = intent.getStringExtra("fifo") ?: throw IOException("fifo == null")
|
||||||
if (Const.Version.atLeast_21_0()) {
|
output = DataOutputStream(FileOutputStream(fifo))
|
||||||
val name = intent.getStringExtra("fifo") ?: throw SuRequestError()
|
val uid = intent.getIntExtra("uid", -1)
|
||||||
uid = intent.getIntExtra("uid", -1).also { if (it < 0) throw SuRequestError() }
|
if (uid <= 0) {
|
||||||
output = DataOutputStream(FileOutputStream(name).buffered())
|
throw IOException("uid == $uid")
|
||||||
} else {
|
|
||||||
val name = intent.getStringExtra("socket") ?: throw SuRequestError()
|
|
||||||
val socket = LocalSocket()
|
|
||||||
socket.connect(LocalSocketAddress(name, LocalSocketAddress.Namespace.ABSTRACT))
|
|
||||||
output = DataOutputStream(BufferedOutputStream(socket.outputStream))
|
|
||||||
val input = DataInputStream(BufferedInputStream(socket.inputStream))
|
|
||||||
val map = async { input.readRequest() }.timedAwait() ?: throw SuRequestError()
|
|
||||||
uid = map["uid"]?.toIntOrNull() ?: throw SuRequestError()
|
|
||||||
}
|
}
|
||||||
policy = uid.toPolicy(pm)
|
policy = SuPolicy(uid)
|
||||||
true
|
val pid = intent.getIntExtra("pid", -1)
|
||||||
} catch (e: Exception) {
|
try {
|
||||||
when (e) {
|
pkgInfo = pm.getPackageInfo(uid, pid) ?: PackageInfo().apply {
|
||||||
is IOException, is PackageManager.NameNotFoundException -> {
|
val name = pm.getNameForUid(uid) ?: throw PackageManager.NameNotFoundException()
|
||||||
Timber.e(e)
|
// We only fill in sharedUserId and leave other fields uninitialized
|
||||||
runCatching { close() }
|
sharedUserId = name.split(":")[0]
|
||||||
false
|
|
||||||
}
|
}
|
||||||
else -> throw e // Unexpected error
|
return@withContext true
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
respond(SuPolicy.DENY, -1)
|
||||||
|
return@withContext false
|
||||||
}
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e(e)
|
||||||
|
close()
|
||||||
|
return@withContext false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun respond(action: Int, time: Int) {
|
suspend fun respond(action: Int, time: Int) {
|
||||||
val until = if (time > 0)
|
val until = if (time > 0)
|
||||||
TimeUnit.MILLISECONDS.toSeconds(now) + TimeUnit.MINUTES.toSeconds(time.toLong())
|
TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) +
|
||||||
|
TimeUnit.MINUTES.toSeconds(time.toLong())
|
||||||
else
|
else
|
||||||
time.toLong()
|
time.toLong()
|
||||||
|
|
||||||
policy.policy = action
|
policy.policy = action
|
||||||
policy.until = until
|
policy.until = until
|
||||||
policy.uid = policy.uid % 100000 + Const.USER_ID * 100000
|
|
||||||
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
output.writeInt(policy.policy)
|
output.writeInt(policy.policy)
|
||||||
output.flush()
|
output.flush()
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
} finally {
|
} finally {
|
||||||
runCatching { close() }
|
close()
|
||||||
if (until >= 0)
|
if (until >= 0)
|
||||||
policyDB.update(policy)
|
policyDB.update(policy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun DataInputStream.readRequest(): Map<String, String> {
|
|
||||||
fun readString(): String {
|
|
||||||
val len = readInt()
|
|
||||||
val buf = ByteArray(len)
|
|
||||||
readFully(buf)
|
|
||||||
return String(buf, Charsets.UTF_8)
|
|
||||||
}
|
|
||||||
val ret = ArrayMap<String, String>()
|
|
||||||
while (true) {
|
|
||||||
val name = readString()
|
|
||||||
if (name == "eof")
|
|
||||||
break
|
|
||||||
ret[name] = readString()
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
package com.topjohnwu.magisk.core.tasks
|
package com.topjohnwu.magisk.core.tasks
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.os.postDelayed
|
import androidx.core.net.toFile
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
|
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
||||||
import com.topjohnwu.magisk.core.utils.unzip
|
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
import com.topjohnwu.magisk.core.utils.unzip
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.KoinComponent
|
|
||||||
import org.koin.core.inject
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
@@ -23,63 +20,51 @@ open class FlashZip(
|
|||||||
private val mUri: Uri,
|
private val mUri: Uri,
|
||||||
private val console: MutableList<String>,
|
private val console: MutableList<String>,
|
||||||
private val logs: MutableList<String>
|
private val logs: MutableList<String>
|
||||||
): KoinComponent {
|
) {
|
||||||
|
|
||||||
val context: Context by inject()
|
private val installDir = File(AppContext.cacheDir, "flash")
|
||||||
private val installFolder = File(context.cacheDir, "flash").apply {
|
private lateinit var zipFile: File
|
||||||
if (!exists()) mkdirs()
|
|
||||||
}
|
|
||||||
private val tmpFile: File = File(installFolder, "install.zip")
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun unzipAndCheck(): Boolean {
|
|
||||||
val parentFile = tmpFile.parentFile ?: return false
|
|
||||||
tmpFile.unzip(parentFile, "META-INF/com/google/android", true)
|
|
||||||
|
|
||||||
val updaterScript = File(parentFile, "updater-script")
|
|
||||||
return Shell
|
|
||||||
.su("grep -q '#MAGISK' $updaterScript")
|
|
||||||
.exec()
|
|
||||||
.isSuccess
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun flash(): Boolean {
|
private fun flash(): Boolean {
|
||||||
console.add("- Copying zip to temp directory")
|
installDir.deleteRecursively()
|
||||||
|
installDir.mkdirs()
|
||||||
|
|
||||||
runCatching {
|
zipFile = if (mUri.scheme == "file") {
|
||||||
mUri.inputStream().writeTo(tmpFile)
|
mUri.toFile()
|
||||||
}.getOrElse {
|
} else {
|
||||||
when (it) {
|
File(installDir, "install.zip").also {
|
||||||
is FileNotFoundException -> console.add("! Invalid Uri")
|
console.add("- Copying zip to temp directory")
|
||||||
is IOException -> console.add("! Cannot copy to cache")
|
try {
|
||||||
|
mUri.inputStream().writeTo(it)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
when (e) {
|
||||||
|
is FileNotFoundException -> console.add("! Invalid Uri")
|
||||||
|
else -> console.add("! Cannot copy to cache")
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw it
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val isMagiskModule = runCatching {
|
val isValid = runCatching {
|
||||||
unzipAndCheck()
|
zipFile.unzip(installDir, "META-INF/com/google/android", true)
|
||||||
|
val script = File(installDir, "updater-script")
|
||||||
|
script.readText().contains("#MAGISK")
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
console.add("! Unzip error")
|
console.add("! Unzip error")
|
||||||
throw it
|
throw it
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isMagiskModule) {
|
if (!isValid) {
|
||||||
console.add("! This zip is not a Magisk Module!")
|
console.add("! This zip is not a Magisk module!")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
console.add("- Installing ${mUri.displayName}")
|
console.add("- Installing ${mUri.displayName}")
|
||||||
|
|
||||||
val parentFile = tmpFile.parent ?: return false
|
return Shell.cmd("sh $installDir/update-binary dummy 1 \'$zipFile\'")
|
||||||
|
.to(console, logs).exec().isSuccess
|
||||||
return Shell
|
|
||||||
.su(
|
|
||||||
"cd $parentFile",
|
|
||||||
"BOOTMODE=true sh update-binary dummy 1 $tmpFile"
|
|
||||||
)
|
|
||||||
.to(console, logs)
|
|
||||||
.exec().isSuccess
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open suspend fun exec() = withContext(Dispatchers.IO) {
|
open suspend fun exec() = withContext(Dispatchers.IO) {
|
||||||
@@ -94,25 +79,7 @@ open class FlashZip(
|
|||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
false
|
false
|
||||||
} finally {
|
} finally {
|
||||||
Shell.su("cd /", "rm -rf ${tmpFile.parent} ${Const.TMP_FOLDER_PATH}").submit()
|
Shell.cmd("cd /", "rm -rf $installDir ${Const.TMPDIR}").submit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Uninstall(
|
|
||||||
uri: Uri,
|
|
||||||
console: MutableList<String>,
|
|
||||||
log: MutableList<String>
|
|
||||||
) : FlashZip(uri, console, log) {
|
|
||||||
|
|
||||||
override suspend fun exec(): Boolean {
|
|
||||||
val success = super.exec()
|
|
||||||
if (success) {
|
|
||||||
UiThreadHandler.handler.postDelayed(3000) {
|
|
||||||
Shell.su("pm uninstall " + context.packageName).exec()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return success
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user