mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-08-16 10:57:27 +00:00
Compare commits
693 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 |
17
.github/ISSUE_TEMPLATE/bug_report.md
vendored
17
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,19 +1,18 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us improve
|
about: Create a report to help us improve
|
||||||
title: ''
|
title: ""
|
||||||
labels: ''
|
labels: ""
|
||||||
assignees: ''
|
assignees: ""
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|
||||||
## READ BEFORE OPENING ISSUES
|
## READ BEFORE OPENING ISSUES
|
||||||
|
|
||||||
All bug reports require you to **USE CANARY BUILDS**. Please include the version name and version code in the bug report.
|
All bug reports require you to **USE DEBUG BUILDS**. Please include the version name and version code in the bug report.
|
||||||
|
|
||||||
If you experience a bootloop, attach a `dmesg` (kernel logs) when the device refuse to boot. This may very likely require a custom kernel on some devices as `last_kmsg` or `pstore ramoops` are usually not enabled by default. In addition, please also upload the result of `cat /proc/mounts` when your device is working correctly **WITHOUT ROOT**.
|
If you experience a bootloop, attach a `dmesg` (kernel logs) when the device refuse to boot. This may very likely require a custom kernel on some devices as `last_kmsg` or `pstore ramoops` are usually not enabled by default. In addition, please also upload the result of `cat /proc/mounts` when your device is working correctly **WITHOUT MAGISK**.
|
||||||
|
|
||||||
If you experience issues during installation, in recovery, upload the recovery logs, or in Magisk, upload the install logs. Please also upload the `boot.img` or `recovery.img` that you are using for patching.
|
If you experience issues during installation, in recovery, upload the recovery logs, or in Magisk, upload the install logs. Please also upload the `boot.img` or `recovery.img` that you are using for patching.
|
||||||
|
|
||||||
@@ -31,7 +30,7 @@ Without following the rules above, your issue will be closed without explanation
|
|||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
Device:
|
Device:
|
||||||
Android version:
|
Android version:
|
||||||
Magisk version name:
|
Magisk version name:
|
||||||
Magisk version code:
|
Magisk version code:
|
||||||
|
19
.github/ccache.sh
vendored
19
.github/ccache.sh
vendored
@@ -1,19 +0,0 @@
|
|||||||
OS=$(uname)
|
|
||||||
CCACHE_VER=4.4
|
|
||||||
|
|
||||||
case $OS in
|
|
||||||
Darwin )
|
|
||||||
brew install ccache
|
|
||||||
ln -s $(which ccache) ./ccache
|
|
||||||
;;
|
|
||||||
Linux )
|
|
||||||
sudo apt-get install -y ccache
|
|
||||||
ln -s $(which ccache) ./ccache
|
|
||||||
;;
|
|
||||||
* )
|
|
||||||
curl -OL https://github.com/ccache/ccache/releases/download/v${CCACHE_VER}/ccache-${CCACHE_VER}-windows-64.zip
|
|
||||||
unzip -j ccache-*-windows-64.zip '*/ccache.exe'
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
mkdir ./.ccache
|
|
||||||
./ccache -o compiler_check='%compiler% -dumpmachine; %compiler% -dumpversion'
|
|
43
.github/workflows/build.yml
vendored
43
.github/workflows/build.yml
vendored
@@ -24,31 +24,44 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||||
env:
|
env:
|
||||||
NDK_CCACHE: ${{ github.workspace }}/ccache
|
NDK_CCACHE: ccache
|
||||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||||
|
CCACHE_COMPILERCHECK: "%compiler% -dumpmachine; %compiler% -dumpversion"
|
||||||
|
RUSTC_WRAPPER: sccache
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out
|
- name: Check out
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: '11'
|
distribution: 'temurin'
|
||||||
|
java-version: '17'
|
||||||
|
|
||||||
- name: Set up Python 3
|
- name: Set up Python 3
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.x'
|
python-version: '3.x'
|
||||||
|
|
||||||
- name: Set up ccache
|
- name: Set up ccache
|
||||||
run: bash .github/ccache.sh
|
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
|
- name: Cache Gradle dependencies
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
@@ -58,10 +71,9 @@ jobs:
|
|||||||
restore-keys: ${{ runner.os }}-gradle-
|
restore-keys: ${{ runner.os }}-gradle-
|
||||||
|
|
||||||
- name: Cache build cache
|
- name: Cache build cache
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ github.workspace }}/.ccache
|
|
||||||
~/.gradle/caches/build-cache-*
|
~/.gradle/caches/build-cache-*
|
||||||
key: ${{ runner.os }}-build-cache-${{ github.sha }}
|
key: ${{ runner.os }}-build-cache-${{ github.sha }}
|
||||||
restore-keys: ${{ runner.os }}-build-cache-
|
restore-keys: ${{ runner.os }}-build-cache-
|
||||||
@@ -71,13 +83,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Build release
|
- name: Build release
|
||||||
run: |
|
run: |
|
||||||
./ccache -zp
|
|
||||||
python build.py -vr all
|
python build.py -vr all
|
||||||
|
|
||||||
- name: Build debug
|
- name: Build debug
|
||||||
run: |
|
run: |
|
||||||
python build.py -v all
|
python build.py -v all
|
||||||
./ccache -s
|
|
||||||
|
|
||||||
- name: Stop gradle daemon
|
- name: Stop gradle daemon
|
||||||
run: ./gradlew --stop
|
run: ./gradlew --stop
|
||||||
@@ -85,7 +95,14 @@ jobs:
|
|||||||
# Only upload artifacts built on Linux
|
# Only upload artifacts built on Linux
|
||||||
- name: Upload build artifact
|
- name: Upload build artifact
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: ${{ github.sha }}
|
name: ${{ github.sha }}
|
||||||
path: out
|
path: out
|
||||||
|
|
||||||
|
- name: Upload mapping and native debug symbols
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ github.sha }}-symbols
|
||||||
|
path: app/build/outputs
|
||||||
|
39
.gitmodules
vendored
39
.gitmodules
vendored
@@ -1,45 +1,48 @@
|
|||||||
[submodule "selinux"]
|
[submodule "selinux"]
|
||||||
path = native/jni/external/selinux
|
path = native/src/external/selinux
|
||||||
url = https://github.com/topjohnwu/selinux.git
|
url = https://github.com/topjohnwu/selinux.git
|
||||||
[submodule "busybox"]
|
[submodule "busybox"]
|
||||||
path = native/jni/external/busybox
|
path = native/src/external/busybox
|
||||||
url = https://github.com/topjohnwu/ndk-busybox.git
|
url = https://github.com/topjohnwu/ndk-busybox.git
|
||||||
[submodule "dtc"]
|
[submodule "dtc"]
|
||||||
path = native/jni/external/dtc
|
path = native/src/external/dtc
|
||||||
url = https://github.com/dgibson/dtc.git
|
url = https://github.com/dgibson/dtc.git
|
||||||
[submodule "lz4"]
|
[submodule "lz4"]
|
||||||
path = native/jni/external/lz4
|
path = native/src/external/lz4
|
||||||
url = https://github.com/lz4/lz4.git
|
url = https://github.com/lz4/lz4.git
|
||||||
[submodule "bzip2"]
|
[submodule "bzip2"]
|
||||||
path = native/jni/external/bzip2
|
path = native/src/external/bzip2
|
||||||
url = https://github.com/nemequ/bzip2.git
|
url = https://github.com/nemequ/bzip2.git
|
||||||
[submodule "xz"]
|
[submodule "xz"]
|
||||||
path = native/jni/external/xz
|
path = native/src/external/xz
|
||||||
url = https://github.com/xz-mirror/xz.git
|
url = https://github.com/xz-mirror/xz.git
|
||||||
[submodule "nanopb"]
|
[submodule "nanopb"]
|
||||||
path = native/jni/external/nanopb
|
path = native/src/external/nanopb
|
||||||
url = https://github.com/nanopb/nanopb.git
|
url = https://github.com/nanopb/nanopb.git
|
||||||
[submodule "mincrypt"]
|
[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 "xhook"]
|
|
||||||
path = native/jni/external/xhook
|
|
||||||
url = https://github.com/iqiyi/xHook.git
|
|
||||||
[submodule "libcxx"]
|
[submodule "libcxx"]
|
||||||
path = native/jni/external/libcxx
|
path = native/src/external/libcxx
|
||||||
url = https://github.com/topjohnwu/libcxx.git
|
url = https://github.com/topjohnwu/libcxx.git
|
||||||
[submodule "zlib"]
|
[submodule "zlib"]
|
||||||
path = native/jni/external/zlib
|
path = native/src/external/zlib
|
||||||
url = https://android.googlesource.com/platform/external/zlib
|
url = https://android.googlesource.com/platform/external/zlib
|
||||||
[submodule "parallel-hashmap"]
|
[submodule "parallel-hashmap"]
|
||||||
path = native/jni/external/parallel-hashmap
|
path = native/src/external/parallel-hashmap
|
||||||
url = https://github.com/greg7mdp/parallel-hashmap.git
|
url = https://github.com/greg7mdp/parallel-hashmap.git
|
||||||
|
[submodule "zopfli"]
|
||||||
|
path = native/src/external/zopfli
|
||||||
|
url = https://github.com/google/zopfli.git
|
||||||
|
[submodule "cxx-rs"]
|
||||||
|
path = native/src/external/cxx-rs
|
||||||
|
url = https://github.com/topjohnwu/cxx.git
|
||||||
|
[submodule "lsplt"]
|
||||||
|
path = native/src/external/lsplt
|
||||||
|
url = https://github.com/LSPosed/LSPlt.git
|
||||||
[submodule "termux-elf-cleaner"]
|
[submodule "termux-elf-cleaner"]
|
||||||
path = tools/termux-elf-cleaner
|
path = tools/termux-elf-cleaner
|
||||||
url = https://github.com/termux/termux-elf-cleaner.git
|
url = https://github.com/termux/termux-elf-cleaner.git
|
||||||
[submodule "zopfli"]
|
|
||||||
path = native/jni/external/zopfli
|
|
||||||
url = https://github.com/google/zopfli.git
|
|
||||||
|
24
README.MD
24
README.MD
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
Magisk is a suite of open source software for customizing Android, supporting devices higher than Android 5.0.<br>
|
Magisk is a suite of open source software for customizing Android, supporting devices higher than Android 6.0.<br>
|
||||||
Some highlight features:
|
Some highlight features:
|
||||||
|
|
||||||
- **MagiskSU**: Provide root access for applications
|
- **MagiskSU**: Provide root access for applications
|
||||||
@@ -18,19 +18,19 @@ Some highlight features:
|
|||||||
|
|
||||||
[Github](https://github.com/topjohnwu/Magisk/) is the only source where you can get official Magisk information and downloads.
|
[Github](https://github.com/topjohnwu/Magisk/) is the only source where you can get official Magisk information and downloads.
|
||||||
|
|
||||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v23.0)
|
[](https://github.com/topjohnwu/Magisk/releases/tag/v25.2)
|
||||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v24.0)
|
[](https://github.com/topjohnwu/Magisk/releases/tag/v26.0)
|
||||||
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-debug.apk)
|
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-release.apk)
|
||||||
|
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-debug.apk)
|
||||||
|
|
||||||
## Useful Links
|
## Useful Links
|
||||||
|
|
||||||
- [Installation Instruction](https://topjohnwu.github.io/Magisk/install.html)
|
- [Installation Instruction](https://topjohnwu.github.io/Magisk/install.html)
|
||||||
- [Magisk Documentation](https://topjohnwu.github.io/Magisk/)
|
- [Magisk Documentation](https://topjohnwu.github.io/Magisk/)
|
||||||
- [Magisk Troubleshoot Wiki](https://www.didgeridoohan.com/magisk/HomePage) (by [@Didgeridoohan](https://github.com/Didgeridoohan))
|
|
||||||
|
|
||||||
## Bug Reports
|
## Bug Reports
|
||||||
|
|
||||||
**Only bug reports from Canary builds will be accepted.**
|
**Only bug reports from Debug builds will be accepted.**
|
||||||
|
|
||||||
For installation issues, upload both boot image and install logs.<br>
|
For installation issues, upload both boot image and install logs.<br>
|
||||||
For Magisk issues, upload boot logcat or dmesg.<br>
|
For Magisk issues, upload boot logcat or dmesg.<br>
|
||||||
@@ -40,7 +40,7 @@ For Magisk app crashes, record and upload the logcat when the crash occurs.
|
|||||||
|
|
||||||
- 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/Contents/Home"`
|
- macOS: `export JAVA_HOME="/Applications/Android Studio.app/Contents/jre/Contents/Home"`
|
||||||
@@ -50,9 +50,15 @@ For Magisk app crashes, record and upload the logcat when the crash occurs.
|
|||||||
- 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
|
||||||
- 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 with Android Studio. The IDE can be used for both app (Kotlin/Java) and native (C++/C) sources.
|
- To start development, open the project with Android Studio. The IDE can be used for both app (Kotlin/Java) and native sources.
|
||||||
- Optionally, set custom configs with `config.prop`. A sample `config.prop.sample` is provided.
|
- Optionally, set custom configs with `config.prop`. A sample `config.prop.sample` is provided.
|
||||||
- To sign APKs and zips with your own private keys, set signing configs in `config.prop`. For more info, check [Google's Documentation](https://developer.android.com/studio/publish/app-signing.html#generate-key).
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
7
app/.gitignore
vendored
7
app/.gitignore
vendored
@@ -3,10 +3,9 @@
|
|||||||
/local.properties
|
/local.properties
|
||||||
.idea/
|
.idea/
|
||||||
/build
|
/build
|
||||||
app/release
|
|
||||||
*.hprof
|
*.hprof
|
||||||
.externalNativeBuild/
|
.externalNativeBuild/
|
||||||
*.apk
|
*.apk
|
||||||
src/main/assets
|
src/*/assets
|
||||||
src/main/jniLibs
|
src/*/jniLibs
|
||||||
src/main/resources
|
src/*/resources
|
||||||
|
@@ -19,12 +19,17 @@ kapt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
namespace = "com.topjohnwu.magisk"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.topjohnwu.magisk"
|
applicationId = "com.topjohnwu.magisk"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
versionName = Config.version
|
versionName = Config.version
|
||||||
versionCode = Config.versionCode
|
versionCode = Config.versionCode
|
||||||
ndk.abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
ndk {
|
||||||
|
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||||
|
debugSymbolLevel = "FULL"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -37,11 +42,13 @@ android {
|
|||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
dataBinding = true
|
dataBinding = true
|
||||||
|
aidl = true
|
||||||
}
|
}
|
||||||
|
|
||||||
packagingOptions {
|
packaging {
|
||||||
resources {
|
resources {
|
||||||
excludes += "/META-INF/*"
|
excludes += "/META-INF/*"
|
||||||
|
excludes += "/META-INF/versions/**"
|
||||||
excludes += "/org/bouncycastle/**"
|
excludes += "/org/bouncycastle/**"
|
||||||
excludes += "/kotlin/**"
|
excludes += "/kotlin/**"
|
||||||
excludes += "/kotlinx/**"
|
excludes += "/kotlinx/**"
|
||||||
@@ -50,14 +57,6 @@ android {
|
|||||||
excludes += "/*.bin"
|
excludes += "/*.bin"
|
||||||
excludes += "/*.json"
|
excludes += "/*.json"
|
||||||
}
|
}
|
||||||
jniLibs {
|
|
||||||
keepDebugSymbols += "**/*.so"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "11"
|
|
||||||
freeCompilerArgs = listOf("-Xjvm-default=enable")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,55 +73,49 @@ dependencies {
|
|||||||
implementation("com.github.topjohnwu:jtar:1.0.0")
|
implementation("com.github.topjohnwu:jtar:1.0.0")
|
||||||
implementation("com.github.topjohnwu:indeterminate-checkbox:1.0.7")
|
implementation("com.github.topjohnwu:indeterminate-checkbox:1.0.7")
|
||||||
implementation("com.github.topjohnwu:lz4-java:1.7.1")
|
implementation("com.github.topjohnwu:lz4-java:1.7.1")
|
||||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
implementation("com.jakewharton.timber:timber:5.0.1")
|
||||||
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
|
implementation("org.bouncycastle:bcpkix-jdk18on:1.72")
|
||||||
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0")
|
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.3.0")
|
||||||
implementation("dev.rikka.rikkax.insets:insets:1.1.1")
|
implementation("dev.rikka.rikkax.insets:insets:1.3.0")
|
||||||
implementation("dev.rikka.rikkax.recyclerview:recyclerview-ktx:1.3.1")
|
implementation("dev.rikka.rikkax.recyclerview:recyclerview-ktx:1.3.1")
|
||||||
implementation("io.noties.markwon:core:4.6.2")
|
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 vLibsu = "3.2.1"
|
|
||||||
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
|
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
|
||||||
implementation("com.github.topjohnwu.libsu:io:${vLibsu}")
|
|
||||||
implementation("com.github.topjohnwu.libsu:service:${vLibsu}")
|
implementation("com.github.topjohnwu.libsu:service:${vLibsu}")
|
||||||
|
implementation("com.github.topjohnwu.libsu:nio:${vLibsu}")
|
||||||
|
|
||||||
val vRetrofit = "2.9.0"
|
val vRetrofit = "2.9.0"
|
||||||
implementation("com.squareup.retrofit2:retrofit:${vRetrofit}")
|
implementation("com.squareup.retrofit2:retrofit:${vRetrofit}")
|
||||||
implementation("com.squareup.retrofit2:converter-moshi:${vRetrofit}")
|
implementation("com.squareup.retrofit2:converter-moshi:${vRetrofit}")
|
||||||
implementation("com.squareup.retrofit2:converter-scalars:${vRetrofit}")
|
implementation("com.squareup.retrofit2:converter-scalars:${vRetrofit}")
|
||||||
|
|
||||||
val vOkHttp = "4.9.3"
|
val vOkHttp = "4.10.0"
|
||||||
implementation("com.squareup.okhttp3:okhttp:${vOkHttp}")
|
implementation("com.squareup.okhttp3:okhttp:${vOkHttp}")
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor:${vOkHttp}")
|
implementation("com.squareup.okhttp3:logging-interceptor:${vOkHttp}")
|
||||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:${vOkHttp}")
|
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:${vOkHttp}")
|
||||||
|
|
||||||
val vMoshi = "1.13.0"
|
val vMoshi = "1.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.4.1"
|
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 = "2.5.0-alpha01"
|
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.1.0")
|
implementation("androidx.biometric:biometric:1.1.0")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
|
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
implementation("androidx.appcompat:appcompat:1.4.1")
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||||
implementation("androidx.preference:preference:1.2.0")
|
implementation("androidx.recyclerview:recyclerview:1.3.0")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
implementation("androidx.fragment:fragment-ktx:1.5.6")
|
||||||
implementation("androidx.fragment:fragment-ktx:1.4.1")
|
|
||||||
implementation("androidx.transition:transition:1.4.1")
|
implementation("androidx.transition:transition:1.4.1")
|
||||||
implementation("androidx.core:core-ktx:1.7.0")
|
implementation("androidx.core:core-ktx:1.9.0")
|
||||||
implementation("androidx.core:core-splashscreen:1.0.0-beta01")
|
implementation("androidx.core:core-splashscreen:1.0.0")
|
||||||
implementation("com.google.android.material:material:1.5.0")
|
implementation("com.google.android.material:material:1.8.0")
|
||||||
}
|
}
|
||||||
|
44
app/proguard-rules.pro
vendored
44
app/proguard-rules.pro
vendored
@@ -1,21 +1,3 @@
|
|||||||
# Add project specific ProGuard rules here.
|
|
||||||
# By default, the flags in this file are appended to flags specified
|
|
||||||
# in /Users/topjohnwu/Library/Android/sdk/tools/proguard/proguard-android.txt
|
|
||||||
# You can edit the include path and order by changing the proguardFiles
|
|
||||||
# directive in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# Add any project specific keep options here:
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Parcelable
|
# Parcelable
|
||||||
-keepclassmembers class * implements android.os.Parcelable {
|
-keepclassmembers class * implements android.os.Parcelable {
|
||||||
public static final ** CREATOR;
|
public static final ** CREATOR;
|
||||||
@@ -26,12 +8,23 @@
|
|||||||
public static void check*(...);
|
public static void check*(...);
|
||||||
public static void throw*(...);
|
public static void throw*(...);
|
||||||
}
|
}
|
||||||
|
-assumenosideeffects class java.util.Objects {
|
||||||
|
public static ** requireNonNull(...);
|
||||||
|
}
|
||||||
|
-assumenosideeffects public class kotlin.coroutines.jvm.internal.DebugMetadataKt {
|
||||||
|
private static ** getDebugMetadataAnnotation(...) return null;
|
||||||
|
}
|
||||||
|
|
||||||
# Stub
|
# Stub
|
||||||
-keep class com.topjohnwu.magisk.core.App { <init>(java.lang.Object); }
|
-keep class com.topjohnwu.magisk.core.App { <init>(java.lang.Object); }
|
||||||
-keepclassmembers class androidx.appcompat.app.AppCompatDelegateImpl {
|
-keepclassmembers class androidx.appcompat.app.AppCompatDelegateImpl {
|
||||||
boolean mActivityHandlesUiModeChecked;
|
boolean mActivityHandlesConfigFlagsChecked;
|
||||||
boolean mActivityHandlesUiMode;
|
int mActivityHandlesConfigFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
# main
|
||||||
|
-keep,allowoptimization public class com.topjohnwu.magisk.signing.SignBoot {
|
||||||
|
public static void main(java.lang.String[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
# Strip Timber verbose and debug logging
|
# Strip Timber verbose and debug logging
|
||||||
@@ -40,6 +33,17 @@
|
|||||||
public void d(**);
|
public void d(**);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# https://github.com/square/retrofit/issues/3751#issuecomment-1192043644
|
||||||
|
# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items).
|
||||||
|
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
|
||||||
|
-keep,allowobfuscation,allowshrinking class retrofit2.Response
|
||||||
|
|
||||||
|
# With R8 full mode generic signatures are stripped for classes that are not
|
||||||
|
# kept. Suspend functions are wrapped in continuations where the type argument
|
||||||
|
# is used.
|
||||||
|
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
|
||||||
|
|
||||||
|
|
||||||
# Excessive obfuscation
|
# Excessive obfuscation
|
||||||
-repackageclasses 'a'
|
-repackageclasses 'a'
|
||||||
-allowaccessmodification
|
-allowaccessmodification
|
||||||
|
@@ -5,11 +5,5 @@ plugins {
|
|||||||
setupCommon()
|
setupCommon()
|
||||||
|
|
||||||
android {
|
android {
|
||||||
defaultConfig {
|
namespace = "com.topjohnwu.shared"
|
||||||
consumerProguardFiles("proguard-rules.pro")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
api("io.michaelrocks:paranoid-core:0.3.7")
|
|
||||||
}
|
}
|
||||||
|
21
app/shared/proguard-rules.pro
vendored
21
app/shared/proguard-rules.pro
vendored
@@ -1,21 +0,0 @@
|
|||||||
# Add project specific ProGuard rules here.
|
|
||||||
# You can control the set of applied configuration files using the
|
|
||||||
# proguardFiles setting in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
|
||||||
# debugging stack traces.
|
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
|
||||||
# hide the original source file name.
|
|
||||||
#-renamesourcefileattribute SourceFile
|
|
@@ -1,6 +1,5 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.topjohnwu.shared"
|
|
||||||
android:installLocation="internalOnly">
|
android:installLocation="internalOnly">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
@@ -9,6 +8,10 @@
|
|||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS" />
|
<uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS" />
|
||||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="29" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="29"
|
android:maxSdkVersion="29"
|
||||||
|
@@ -1,72 +0,0 @@
|
|||||||
package com.topjohnwu.magisk;
|
|
||||||
|
|
||||||
import static android.os.Build.VERSION.SDK_INT;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.AssetManager;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import io.michaelrocks.paranoid.Obfuscate;
|
|
||||||
|
|
||||||
@Obfuscate
|
|
||||||
public class DynAPK {
|
|
||||||
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 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 {
|
|
||||||
// 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; }
|
|
||||||
}
|
|
||||||
}
|
|
@@ -2,9 +2,6 @@ package com.topjohnwu.magisk;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import io.michaelrocks.paranoid.Obfuscate;
|
|
||||||
|
|
||||||
@Obfuscate
|
|
||||||
public class ProviderInstaller {
|
public class ProviderInstaller {
|
||||||
|
|
||||||
public static boolean install(Context context) {
|
public static boolean install(Context context) {
|
||||||
|
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,5 +1,6 @@
|
|||||||
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.EXTRA_STATUS;
|
||||||
import static android.content.pm.PackageInstaller.STATUS_FAILURE_INVALID;
|
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_PENDING_USER_ACTION;
|
||||||
@@ -10,47 +11,21 @@ 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.IntentFilter;
|
||||||
import android.content.pm.PackageInstaller.Session;
|
|
||||||
import android.content.pm.PackageInstaller.SessionParams;
|
import android.content.pm.PackageInstaller.SessionParams;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FilterOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import io.michaelrocks.paranoid.Obfuscate;
|
|
||||||
|
|
||||||
@Obfuscate
|
|
||||||
public final class APKInstall {
|
public final class APKInstall {
|
||||||
// @WorkerThread
|
|
||||||
public static void installapk(Context context, File apk) {
|
|
||||||
//noinspection InlinedApi
|
|
||||||
var flag = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE;
|
|
||||||
var action = APKInstall.class.getName();
|
|
||||||
var intent = new Intent(action).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);
|
|
||||||
}
|
|
||||||
try (Session session = installer.openSession(installer.createSession(params))) {
|
|
||||||
OutputStream out = session.openWrite(apk.getName(), 0, apk.length());
|
|
||||||
try (var in = new FileInputStream(apk); out) {
|
|
||||||
transfer(in, out);
|
|
||||||
}
|
|
||||||
session.commit(pending.getIntentSender());
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(APKInstall.class.getSimpleName(), "", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void transfer(InputStream in, OutputStream out) throws IOException {
|
public static void transfer(InputStream in, OutputStream out) throws IOException {
|
||||||
int size = 8192;
|
int size = 8192;
|
||||||
@@ -61,61 +36,144 @@ public final class APKInstall {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InstallReceiver register(Context context, String packageName, Runnable onSuccess) {
|
public static void registerReceiver(
|
||||||
var receiver = new InstallReceiver(context, packageName, onSuccess);
|
Context context, BroadcastReceiver receiver, IntentFilter filter) {
|
||||||
var filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
filter.addDataScheme("package");
|
// noinspection InlinedApi
|
||||||
context.registerReceiver(receiver, filter);
|
context.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED);
|
||||||
context.registerReceiver(receiver, new IntentFilter(APKInstall.class.getName()));
|
} else {
|
||||||
|
context.registerReceiver(receiver, filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
return receiver;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class InstallReceiver extends BroadcastReceiver {
|
public interface Session {
|
||||||
private final Context context;
|
// @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 String packageName;
|
||||||
private final Runnable onSuccess;
|
private final Runnable onSuccess;
|
||||||
|
private final Runnable onFailure;
|
||||||
private final CountDownLatch latch = new CountDownLatch(1);
|
private final CountDownLatch latch = new CountDownLatch(1);
|
||||||
private Intent intent = null;
|
private Intent userAction = null;
|
||||||
|
|
||||||
private InstallReceiver(Context context, String packageName, Runnable onSuccess) {
|
final String sessionId = UUID.randomUUID().toString();
|
||||||
this.context = context;
|
|
||||||
|
private InstallReceiver(String packageName, Runnable onSuccess, Runnable onFailure) {
|
||||||
this.packageName = packageName;
|
this.packageName = packageName;
|
||||||
this.onSuccess = onSuccess;
|
this.onSuccess = onSuccess;
|
||||||
|
this.onFailure = onFailure;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onReceive(Context c, Intent i) {
|
public void onReceive(Context context, Intent intent) {
|
||||||
if (Intent.ACTION_PACKAGE_ADDED.equals(i.getAction())) {
|
if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) {
|
||||||
Uri data = i.getData();
|
Uri data = intent.getData();
|
||||||
if (data == null || onSuccess == null) return;
|
if (data == null)
|
||||||
|
return;
|
||||||
String pkg = data.getSchemeSpecificPart();
|
String pkg = data.getSchemeSpecificPart();
|
||||||
if (pkg.equals(packageName)) {
|
if (pkg.equals(packageName)) {
|
||||||
onSuccess.run();
|
onSuccess(context);
|
||||||
context.unregisterReceiver(this);
|
|
||||||
}
|
}
|
||||||
return;
|
} 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();
|
||||||
}
|
}
|
||||||
int status = i.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID);
|
|
||||||
switch (status) {
|
|
||||||
case STATUS_PENDING_USER_ACTION:
|
|
||||||
intent = i.getParcelableExtra(Intent.EXTRA_INTENT);
|
|
||||||
break;
|
|
||||||
case STATUS_SUCCESS:
|
|
||||||
if (onSuccess != null) onSuccess.run();
|
|
||||||
default:
|
|
||||||
context.unregisterReceiver(this);
|
|
||||||
}
|
|
||||||
latch.countDown();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @WorkerThread @Nullable
|
private void onSuccess(Context context) {
|
||||||
|
if (onSuccess != null)
|
||||||
|
onSuccess.run();
|
||||||
|
context.getApplicationContext().unregisterReceiver(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public Intent waitIntent() {
|
public Intent waitIntent() {
|
||||||
try {
|
try {
|
||||||
//noinspection ResultOfMethodCallIgnored
|
// noinspection ResultOfMethodCallIgnored
|
||||||
latch.await(5, TimeUnit.SECONDS);
|
latch.await(5, TimeUnit.SECONDS);
|
||||||
} catch (Exception ignored) {
|
} 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 intent;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,22 +1,23 @@
|
|||||||
package com.topjohnwu.magisk.utils;
|
package com.topjohnwu.magisk.utils;
|
||||||
|
|
||||||
|
import android.os.Process;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
|
|
||||||
import dalvik.system.DexClassLoader;
|
import dalvik.system.BaseDexClassLoader;
|
||||||
|
|
||||||
public class DynamicClassLoader extends DexClassLoader {
|
public class DynamicClassLoader extends BaseDexClassLoader {
|
||||||
|
|
||||||
private static final ClassLoader base = Object.class.getClassLoader();
|
|
||||||
|
|
||||||
public DynamicClassLoader(File apk) {
|
public DynamicClassLoader(File apk) {
|
||||||
super(apk.getPath(), apk.getParent(), null, base);
|
this(apk, getSystemClassLoader());
|
||||||
}
|
}
|
||||||
|
|
||||||
public DynamicClassLoader(File apk, ClassLoader parent) {
|
public DynamicClassLoader(File apk, ClassLoader parent) {
|
||||||
super(apk.getPath(), apk.getParent(), null, parent);
|
// Set optimizedDirectory to null for RootService to bypass DexFile's security checks
|
||||||
|
super(apk.getPath(), Process.myUid() == 0 ? null : apk.getParentFile(), null, parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -28,7 +29,7 @@ public class DynamicClassLoader extends DexClassLoader {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Then check boot classpath
|
// Then check boot classpath
|
||||||
return base.loadClass(name);
|
return getSystemClassLoader().loadClass(name);
|
||||||
} catch (ClassNotFoundException ignored) {
|
} catch (ClassNotFoundException ignored) {
|
||||||
try {
|
try {
|
||||||
// Next try current dex
|
// Next try current dex
|
||||||
@@ -46,7 +47,7 @@ public class DynamicClassLoader extends DexClassLoader {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public URL getResource(String name) {
|
public URL getResource(String name) {
|
||||||
URL resource = base.getResource(name);
|
URL resource = getSystemClassLoader().getResource(name);
|
||||||
if (resource != null)
|
if (resource != null)
|
||||||
return resource;
|
return resource;
|
||||||
resource = findResource(name);
|
resource = findResource(name);
|
||||||
@@ -58,7 +59,7 @@ public class DynamicClassLoader extends DexClassLoader {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Enumeration<URL> getResources(String name) throws IOException {
|
public Enumeration<URL> getResources(String name) throws IOException {
|
||||||
return new CompoundEnumeration<>(base.getResources(name),
|
return new CompoundEnumeration<>(getSystemClassLoader().getResources(name),
|
||||||
findResources(name), getParent().getResources(name));
|
findResources(name), getParent().getResources(name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,16 +1,22 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
package="com.topjohnwu.magisk">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
<permission
|
||||||
|
android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
|
||||||
|
android:protectionLevel="signature"
|
||||||
|
tools:node="remove" />
|
||||||
|
|
||||||
|
<uses-permission
|
||||||
|
android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
|
||||||
|
tools:node="remove" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".core.App"
|
android:name=".core.App"
|
||||||
android:extractNativeLibs="true"
|
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:multiArch="true"
|
android:multiArch="true"
|
||||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
|
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning"
|
||||||
|
tools:remove="android:appComponentFactory">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
@@ -29,8 +35,8 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".ui.surequest.SuRequestActivity"
|
android:name=".ui.surequest.SuRequestActivity"
|
||||||
android:directBootAware="true"
|
android:directBootAware="true"
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
android:taskAffinity=""
|
||||||
tools:ignore="AppLinkUrlError">
|
tools:ignore="AppLinkUrlError">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
@@ -40,11 +46,11 @@
|
|||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".core.Receiver"
|
android:name=".core.Receiver"
|
||||||
android:directBootAware="true"
|
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
||||||
<action android:name="android.intent.action.UID_REMOVED" />
|
<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" />
|
||||||
@@ -56,7 +62,8 @@
|
|||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".core.download.DownloadService"
|
android:name=".core.download.DownloadService"
|
||||||
android:exported="false" />
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".core.JobService"
|
android:name=".core.JobService"
|
||||||
|
@@ -0,0 +1,9 @@
|
|||||||
|
// IRootUtils.aidl
|
||||||
|
package com.topjohnwu.magisk.core.utils;
|
||||||
|
|
||||||
|
// Declare any non-default types here with import statements
|
||||||
|
|
||||||
|
interface IRootUtils {
|
||||||
|
android.app.ActivityManager.RunningAppProcessInfo getAppProcess(int pid);
|
||||||
|
IBinder getFileSystem();
|
||||||
|
}
|
@@ -0,0 +1,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,13 +5,13 @@ import android.view.KeyEvent
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.databinding.OnRebindCallback
|
import androidx.databinding.OnRebindCallback
|
||||||
import androidx.databinding.ViewDataBinding
|
import androidx.databinding.ViewDataBinding
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
import com.topjohnwu.magisk.ktx.startAnimations
|
|
||||||
|
|
||||||
abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHolder {
|
abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHolder {
|
||||||
|
|
||||||
@@ -20,11 +20,12 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
|||||||
protected abstract val layoutRes: Int
|
protected abstract val layoutRes: Int
|
||||||
|
|
||||||
private val navigation get() = activity?.navigation
|
private val navigation get() = activity?.navigation
|
||||||
|
open val snackbarView: View? get() = null
|
||||||
open val snackbarAnchorView: View? get() = null
|
open val snackbarAnchorView: View? get() = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
startObserveEvents()
|
startObserveLiveData()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
@@ -36,9 +37,14 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
|||||||
it.setVariable(BR.viewModel, viewModel)
|
it.setVariable(BR.viewModel, viewModel)
|
||||||
it.lifecycleOwner = viewLifecycleOwner
|
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() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
activity?.supportActionBar?.subtitle = null
|
activity?.supportActionBar?.subtitle = null
|
||||||
@@ -60,6 +66,8 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
|||||||
|
|
||||||
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@BaseFragment.onPreBind(binding)
|
this@BaseFragment.onPreBind(binding)
|
||||||
@@ -70,7 +78,10 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
viewModel.requestRefresh()
|
viewModel.let {
|
||||||
|
if (it is AsyncLoadViewModel)
|
||||||
|
it.startLoading()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun onPreBind(binding: Binding) {
|
protected open fun onPreBind(binding: Binding) {
|
||||||
@@ -78,7 +89,7 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun NavDirections.navigate() {
|
fun NavDirections.navigate() {
|
||||||
navigation?.navigate(this)
|
navigation?.currentDestination?.getAction(actionId)?.let { navigation!!.navigate(this) }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,129 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.activity.result.contract.ActivityResultContract
|
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
|
||||||
import androidx.databinding.ViewDataBinding
|
|
||||||
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
|
||||||
import com.topjohnwu.magisk.R
|
|
||||||
import com.topjohnwu.magisk.core.*
|
|
||||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
|
||||||
import com.topjohnwu.magisk.ui.theme.Theme
|
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
|
||||||
import com.topjohnwu.magisk.view.Shortcuts
|
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import java.util.concurrent.CountDownLatch
|
|
||||||
|
|
||||||
abstract class BaseMainActivity<Binding : ViewDataBinding> : NavigationActivity<Binding>() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private var doPreload = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private val latch = CountDownLatch(1)
|
|
||||||
private val uninstallPkg = registerForActivityResult(UninstallPackage) { latch.countDown() }
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
setTheme(Theme.selected.themeRes)
|
|
||||||
|
|
||||||
if (isRunningAsStub && doPreload) {
|
|
||||||
// Manually apply splash theme for stub
|
|
||||||
theme.applyStyle(R.style.StubSplashTheme, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
if (!isRunningAsStub) {
|
|
||||||
val splashScreen = installSplashScreen()
|
|
||||||
splashScreen.setKeepOnScreenCondition { doPreload }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (doPreload) {
|
|
||||||
Shell.getShell(null) {
|
|
||||||
if (isRunningAsStub && !it.isRoot) {
|
|
||||||
showInvalidStateMessage()
|
|
||||||
return@getShell
|
|
||||||
}
|
|
||||||
preLoad()
|
|
||||||
runOnUiThread {
|
|
||||||
doPreload = false
|
|
||||||
if (isRunningAsStub) {
|
|
||||||
// Re-launch main activity without splash theme
|
|
||||||
relaunch()
|
|
||||||
} else {
|
|
||||||
showMainUI(savedInstanceState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showMainUI(savedInstanceState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract fun showMainUI(savedInstanceState: Bundle?)
|
|
||||||
|
|
||||||
private fun showInvalidStateMessage() {
|
|
||||||
runOnUiThread {
|
|
||||||
MagiskDialog(this).apply {
|
|
||||||
setTitle(R.string.unsupport_nonroot_stub_title)
|
|
||||||
setMessage(R.string.unsupport_nonroot_stub_msg)
|
|
||||||
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
|
||||||
text = R.string.install
|
|
||||||
onClick { HideAPK.restore(this@BaseMainActivity) }
|
|
||||||
}
|
|
||||||
setCancelable(false)
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun preLoad() {
|
|
||||||
val prevPkg = intent.getStringExtra(Const.Key.PREV_PKG)
|
|
||||||
|
|
||||||
Config.load(prevPkg)
|
|
||||||
handleRepackage(prevPkg)
|
|
||||||
Notifications.setup(this)
|
|
||||||
JobService.schedule(this)
|
|
||||||
Shortcuts.setupDynamic(this)
|
|
||||||
|
|
||||||
// Pre-fetch network services
|
|
||||||
ServiceLocator.networkService
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleRepackage(pkg: String?) {
|
|
||||||
if (packageName != APPLICATION_ID) {
|
|
||||||
runCatching {
|
|
||||||
// Hidden, remove com.topjohnwu.magisk if exist as it could be malware
|
|
||||||
packageManager.getApplicationInfo(APPLICATION_ID, 0)
|
|
||||||
Shell.su("(pm uninstall $APPLICATION_ID)& >/dev/null 2>&1").exec()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (Config.suManager.isNotEmpty())
|
|
||||||
Config.suManager = ""
|
|
||||||
pkg ?: return
|
|
||||||
if (!Shell.su("(pm uninstall $pkg)& >/dev/null 2>&1").exec().isSuccess) {
|
|
||||||
uninstallPkg.launch(pkg)
|
|
||||||
// Wait for the uninstallation to finish
|
|
||||||
latch.await()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object UninstallPackage : ActivityResultContract<String, Boolean>() {
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
override fun createIntent(context: Context, input: String): Intent {
|
|
||||||
val uri = Uri.Builder().scheme("package").opaquePart(input).build()
|
|
||||||
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, uri)
|
|
||||||
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true)
|
|
||||||
return intent
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun parseResult(resultCode: Int, intent: Intent?) = resultCode == RESULT_OK
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,84 +1,39 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
package com.topjohnwu.magisk.arch
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest.permission.*
|
||||||
import androidx.annotation.CallSuper
|
import android.annotation.SuppressLint
|
||||||
import androidx.databinding.Bindable
|
import android.os.Bundle
|
||||||
import androidx.databinding.Observable
|
|
||||||
import androidx.databinding.PropertyChangeRegistry
|
import androidx.databinding.PropertyChangeRegistry
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
import com.topjohnwu.magisk.BR
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Info
|
|
||||||
import com.topjohnwu.magisk.databinding.ObservableHost
|
import com.topjohnwu.magisk.databinding.ObservableHost
|
||||||
import com.topjohnwu.magisk.databinding.set
|
|
||||||
import com.topjohnwu.magisk.events.BackPressEvent
|
import com.topjohnwu.magisk.events.BackPressEvent
|
||||||
|
import com.topjohnwu.magisk.events.DialogBuilder
|
||||||
|
import com.topjohnwu.magisk.events.DialogEvent
|
||||||
import com.topjohnwu.magisk.events.NavigationEvent
|
import com.topjohnwu.magisk.events.NavigationEvent
|
||||||
import com.topjohnwu.magisk.events.PermissionEvent
|
import com.topjohnwu.magisk.events.PermissionEvent
|
||||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
|
|
||||||
abstract class BaseViewModel(
|
abstract class BaseViewModel : ViewModel(), ObservableHost {
|
||||||
initialState: State = State.LOADING
|
|
||||||
) : ViewModel(), ObservableHost {
|
|
||||||
|
|
||||||
override var callbacks: PropertyChangeRegistry? = null
|
override var callbacks: PropertyChangeRegistry? = null
|
||||||
|
|
||||||
enum class State {
|
private val _viewEvents = MutableLiveData<ViewEvent>()
|
||||||
LOADED, LOADING, LOADING_FAILED
|
|
||||||
}
|
|
||||||
|
|
||||||
@get:Bindable
|
|
||||||
val loading get() = state == State.LOADING
|
|
||||||
@get:Bindable
|
|
||||||
val loaded get() = state == State.LOADED
|
|
||||||
@get:Bindable
|
|
||||||
val loadFailed get() = state == State.LOADING_FAILED
|
|
||||||
|
|
||||||
val isConnected get() = Info.isConnected
|
|
||||||
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
|
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
|
||||||
|
|
||||||
var state= initialState
|
open fun onSaveState(state: Bundle) {}
|
||||||
set(value) = set(value, field, { field = it }, BR.loading, BR.loaded, BR.loadFailed)
|
open fun onRestoreState(state: Bundle) {}
|
||||||
|
open fun onNetworkChanged(network: Boolean) {}
|
||||||
private val _viewEvents = MutableLiveData<ViewEvent>()
|
|
||||||
private var runningJob: Job? = null
|
|
||||||
private val refreshCallback = object : Observable.OnPropertyChangedCallback() {
|
|
||||||
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
|
||||||
requestRefresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
isConnected.addOnPropertyChangedCallback(refreshCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** This should probably never be called manually, it's called manually via delegate. */
|
|
||||||
@Synchronized
|
|
||||||
fun requestRefresh() {
|
|
||||||
if (runningJob?.isActive == true) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
runningJob = refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun refresh(): Job? = null
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
override fun onCleared() {
|
|
||||||
isConnected.removeOnPropertyChangedCallback(refreshCallback)
|
|
||||||
super.onCleared()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
||||||
PermissionEvent(permission, callback).publish()
|
PermissionEvent(permission, callback).publish()
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun withExternalRW(crossinline 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 {
|
||||||
@@ -87,15 +42,36 @@ 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.navigate(pop: Boolean = false) {
|
fun NavDirections.navigate(pop: Boolean = false) {
|
||||||
|
@@ -20,12 +20,14 @@ abstract class NavigationActivity<Binding : ViewDataBinding> : UIActivity<Bindin
|
|||||||
val navigation: NavController get() = navHostFragment.navController
|
val navigation: NavController get() = navHostFragment.navController
|
||||||
|
|
||||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||||
return currentFragment?.onKeyEvent(event) == true || super.dispatchKeyEvent(event)
|
return if (binded && currentFragment?.onKeyEvent(event) == true) true else super.dispatchKeyEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
if (currentFragment?.onBackPressed()?.not() == true) {
|
if (binded) {
|
||||||
super.onBackPressed()
|
if (currentFragment?.onBackPressed() == false) {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,12 +5,18 @@ import android.graphics.Color
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.content.res.use
|
import androidx.core.content.res.use
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.databinding.ViewDataBinding
|
import androidx.databinding.ViewDataBinding
|
||||||
|
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||||
|
import androidx.transition.AutoTransition
|
||||||
|
import androidx.transition.TransitionManager
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||||
import rikka.insets.WindowInsetsHelper
|
import rikka.insets.WindowInsetsHelper
|
||||||
@@ -21,12 +27,13 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
|||||||
protected lateinit var binding: Binding
|
protected lateinit var binding: Binding
|
||||||
protected abstract val layoutRes: Int
|
protected abstract val layoutRes: Int
|
||||||
|
|
||||||
|
protected val binded get() = ::binding.isInitialized
|
||||||
|
|
||||||
open val snackbarView get() = binding.root
|
open val snackbarView get() = binding.root
|
||||||
open val snackbarAnchorView: View? get() = null
|
open val snackbarAnchorView: View? get() = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val theme = Config.darkTheme
|
AppCompatDelegate.setDefaultNightMode(Config.darkTheme)
|
||||||
AppCompatDelegate.setDefaultNightMode(theme)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -35,7 +42,7 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
|||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
startObserveEvents()
|
startObserveLiveData()
|
||||||
|
|
||||||
// We need to set the window background explicitly since for whatever reason it's not
|
// We need to set the window background explicitly since for whatever reason it's not
|
||||||
// propagated upstream
|
// propagated upstream
|
||||||
@@ -74,9 +81,19 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
|||||||
binding.root.rootView.accessibilityDelegate = delegate
|
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() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
viewModel.requestRefresh()
|
viewModel.let {
|
||||||
|
if (it is AsyncLoadViewModel)
|
||||||
|
it.startLoading()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEventDispatched(event: ViewEvent) = when (event) {
|
override fun onEventDispatched(event: ViewEvent) = when (event) {
|
||||||
@@ -85,3 +102,14 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
|||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ViewGroup.startAnimations() {
|
||||||
|
val transition = AutoTransition()
|
||||||
|
.setInterpolator(FastOutSlowInInterpolator())
|
||||||
|
.setDuration(400)
|
||||||
|
.excludeTarget(R.id.main_toolbar, true)
|
||||||
|
TransitionManager.beginDelayedTransition(
|
||||||
|
this,
|
||||||
|
transition
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
package com.topjohnwu.magisk.arch
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for passing events from ViewModels to Activities/Fragments
|
* Class for passing events from ViewModels to Activities/Fragments
|
||||||
@@ -9,10 +8,6 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
*/
|
*/
|
||||||
abstract class ViewEvent
|
abstract class ViewEvent
|
||||||
|
|
||||||
abstract class ViewEventWithScope: ViewEvent() {
|
|
||||||
lateinit var scope: CoroutineScope
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContextExecutor {
|
interface ContextExecutor {
|
||||||
operator fun invoke(context: Context)
|
operator fun invoke(context: Context)
|
||||||
}
|
}
|
||||||
|
@@ -1,15 +1,24 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
package com.topjohnwu.magisk.arch
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.ViewModelStoreOwner
|
||||||
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
||||||
|
import com.topjohnwu.magisk.ui.install.InstallViewModel
|
||||||
|
import com.topjohnwu.magisk.ui.log.LogViewModel
|
||||||
|
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
|
||||||
|
import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel
|
||||||
|
|
||||||
interface ViewModelHolder : LifecycleOwner {
|
interface ViewModelHolder : LifecycleOwner, ViewModelStoreOwner {
|
||||||
|
|
||||||
val viewModel: BaseViewModel
|
val viewModel: BaseViewModel
|
||||||
|
|
||||||
fun startObserveEvents() {
|
fun startObserveLiveData() {
|
||||||
viewModel.viewEvents.observe(this) {
|
viewModel.viewEvents.observe(this, this::onEventDispatched)
|
||||||
onEventDispatched(it)
|
Info.isConnected.observe(this, viewModel::onNetworkChanged)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,3 +26,24 @@ interface ViewModelHolder : LifecycleOwner {
|
|||||||
*/
|
*/
|
||||||
fun onEventDispatched(event: ViewEvent) {}
|
fun onEventDispatched(event: ViewEvent) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object VMFactory : ViewModelProvider.Factory {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return when (modelClass) {
|
||||||
|
HomeViewModel::class.java -> HomeViewModel(ServiceLocator.networkService)
|
||||||
|
LogViewModel::class.java -> LogViewModel(ServiceLocator.logRepo)
|
||||||
|
SuperuserViewModel::class.java -> SuperuserViewModel(ServiceLocator.policyDB)
|
||||||
|
InstallViewModel::class.java ->
|
||||||
|
InstallViewModel(ServiceLocator.networkService, ServiceLocator.markwon)
|
||||||
|
SuRequestViewModel::class.java ->
|
||||||
|
SuRequestViewModel(ServiceLocator.policyDB, ServiceLocator.timeoutPrefs)
|
||||||
|
else -> modelClass.newInstance()
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified VM : ViewModel> ViewModelHolder.viewModel() =
|
||||||
|
lazy(LazyThreadSafetyMode.NONE) {
|
||||||
|
ViewModelProvider(this, VMFactory)[VM::class.java]
|
||||||
|
}
|
||||||
|
@@ -1,29 +1,36 @@
|
|||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.topjohnwu.magisk.DynAPK
|
import androidx.lifecycle.ProcessLifecycleAccessor
|
||||||
import com.topjohnwu.magisk.core.utils.*
|
import com.topjohnwu.magisk.StubApk
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.core.utils.DispatcherExecutor
|
||||||
|
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||||
|
import com.topjohnwu.magisk.core.utils.ShellInit
|
||||||
|
import com.topjohnwu.magisk.core.utils.refreshLocale
|
||||||
|
import com.topjohnwu.magisk.core.utils.setConfig
|
||||||
|
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
||||||
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
import com.topjohnwu.superuser.ipc.RootService
|
import com.topjohnwu.superuser.ipc.RootService
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
open class App() : Application() {
|
open class App() : Application() {
|
||||||
|
|
||||||
constructor(o: Any) : this() {
|
constructor(o: Any) : this() {
|
||||||
val data = DynAPK.Data(o)
|
val data = StubApk.Data(o)
|
||||||
// Add the root service name mapping
|
// Add the root service name mapping
|
||||||
data.classToComponent[RootRegistry::class.java.name] = data.rootService.name
|
data.classToComponent[RootUtils::class.java.name] = data.rootService.name
|
||||||
// Send back the actual root service class
|
// Send back the actual root service class
|
||||||
data.rootService = RootRegistry::class.java
|
data.rootService = RootUtils::class.java
|
||||||
Info.stub = data
|
Info.stub = data
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,43 +44,44 @@ open class App() : Application() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(context: Context) {
|
override fun attachBaseContext(context: Context) {
|
||||||
Shell.setDefaultBuilder(Shell.Builder.create()
|
|
||||||
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
|
||||||
.setInitializers(ShellInit::class.java)
|
|
||||||
.setTimeout(2))
|
|
||||||
Shell.EXECUTOR = DispatcherExecutor(Dispatchers.IO)
|
|
||||||
|
|
||||||
// Get the actual ContextImpl
|
// Get the actual ContextImpl
|
||||||
val app: Application
|
val app: Application
|
||||||
val base: Context
|
val base: Context
|
||||||
if (context is Application) {
|
if (context is Application) {
|
||||||
app = context
|
app = context
|
||||||
base = context.baseContext
|
base = context.baseContext
|
||||||
|
AppApkPath = StubApk.current(base).path
|
||||||
} else {
|
} else {
|
||||||
app = this
|
app = this
|
||||||
base = context
|
base = context
|
||||||
|
AppApkPath = base.packageResourcePath
|
||||||
}
|
}
|
||||||
super.attachBaseContext(base)
|
super.attachBaseContext(base)
|
||||||
ServiceLocator.context = base
|
ServiceLocator.context = base
|
||||||
|
app.registerActivityLifecycleCallbacks(ActivityTracker)
|
||||||
|
|
||||||
|
Shell.setDefaultBuilder(Shell.Builder.create()
|
||||||
|
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
||||||
|
.setInitializers(ShellInit::class.java)
|
||||||
|
.setContext(base)
|
||||||
|
.setTimeout(2))
|
||||||
|
Shell.EXECUTOR = DispatcherExecutor(Dispatchers.IO)
|
||||||
|
RootUtils.bindTask = RootService.bindOrTask(
|
||||||
|
intent<RootUtils>(),
|
||||||
|
UiThreadHandler.executor,
|
||||||
|
RootUtils.Connection
|
||||||
|
)
|
||||||
|
// Pre-heat the shell ASAP
|
||||||
|
Shell.getShell(null) {}
|
||||||
|
|
||||||
refreshLocale()
|
refreshLocale()
|
||||||
AppApkPath = if (isRunningAsStub) {
|
resources.patch()
|
||||||
DynAPK.current(base).path
|
Notifications.setup()
|
||||||
} else {
|
|
||||||
base.packageResourcePath
|
|
||||||
}
|
|
||||||
|
|
||||||
base.resources.patch()
|
|
||||||
app.registerActivityLifecycleCallbacks(ActivityTracker)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
RootRegistry.bindTask = RootService.createBindTask(
|
ProcessLifecycleAccessor.init(this)
|
||||||
intent<RootRegistry>(),
|
|
||||||
UiThreadHandler.executor,
|
|
||||||
RootRegistry.Connection
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
@@ -85,20 +93,21 @@ open class App() : Application() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
|
||||||
object ActivityTracker : Application.ActivityLifecycleCallbacks {
|
object ActivityTracker : Application.ActivityLifecycleCallbacks {
|
||||||
|
|
||||||
@Volatile
|
val foreground: Activity? get() = ref.get()
|
||||||
var foreground: Activity? = null
|
|
||||||
|
|
||||||
val hasForeground get() = foreground != null
|
@Volatile
|
||||||
|
private var ref = WeakReference<Activity>(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?) {}
|
||||||
|
@@ -6,21 +6,24 @@ import android.util.Xml
|
|||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.core.repository.BoolDBPropertyNoWrite
|
||||||
|
import com.topjohnwu.magisk.core.repository.DBConfig
|
||||||
|
import com.topjohnwu.magisk.core.repository.PreferenceConfig
|
||||||
import com.topjohnwu.magisk.core.utils.refreshLocale
|
import com.topjohnwu.magisk.core.utils.refreshLocale
|
||||||
import com.topjohnwu.magisk.data.preference.PreferenceModel
|
|
||||||
import com.topjohnwu.magisk.data.repository.DBBoolSettingsNoWrite
|
|
||||||
import com.topjohnwu.magisk.data.repository.DBConfig
|
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
|
||||||
import com.topjohnwu.magisk.ui.theme.Theme
|
import com.topjohnwu.magisk.ui.theme.Theme
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
import org.xmlpull.v1.XmlPullParser
|
import org.xmlpull.v1.XmlPullParser
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
object Config : PreferenceModel, DBConfig {
|
object Config : PreferenceConfig, DBConfig {
|
||||||
|
|
||||||
override val stringDB get() = ServiceLocator.stringDB
|
override val stringDB get() = ServiceLocator.stringDB
|
||||||
override val settingsDB get() = ServiceLocator.settingsDB
|
override val settingsDB get() = ServiceLocator.settingsDB
|
||||||
override val context get() = ServiceLocator.deContext
|
override val context get() = ServiceLocator.deContext
|
||||||
|
override val coroutineScope get() = GlobalScope
|
||||||
|
|
||||||
@get:SuppressLint("ApplySharedPref")
|
@get:SuppressLint("ApplySharedPref")
|
||||||
val prefsFile: File get() {
|
val prefsFile: File get() {
|
||||||
@@ -70,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
|
||||||
@@ -106,6 +110,8 @@ 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
|
||||||
@@ -131,7 +137,15 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
var themeOrdinal by preference(Key.THEME_ORDINAL, Theme.Piplup.ordinal)
|
var themeOrdinal by preference(Key.THEME_ORDINAL, Theme.Piplup.ordinal)
|
||||||
var suReAuth by preference(Key.SU_REAUTH, false)
|
var suReAuth by preference(Key.SU_REAUTH, false)
|
||||||
var suTapjack by preference(Key.SU_TAPJACK, true)
|
var suTapjack by preference(Key.SU_TAPJACK, true)
|
||||||
var checkUpdate by preference(Key.CHECK_UPDATES, true)
|
private var checkUpdatePrefs by preference(Key.CHECK_UPDATES, true)
|
||||||
|
var checkUpdate
|
||||||
|
get() = checkUpdatePrefs
|
||||||
|
set(value) {
|
||||||
|
if (checkUpdatePrefs != value) {
|
||||||
|
checkUpdatePrefs = value
|
||||||
|
JobService.schedule(AppContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
var doh by preference(Key.DOH, false)
|
var doh by preference(Key.DOH, false)
|
||||||
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
|
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
|
||||||
|
|
||||||
@@ -149,7 +163,7 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
|
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
|
||||||
var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
|
var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
|
||||||
var zygisk by dbSettings(Key.ZYGISK, false)
|
var zygisk by dbSettings(Key.ZYGISK, false)
|
||||||
var denyList by DBBoolSettingsNoWrite(Key.DENYLIST, false)
|
var denyList by BoolDBPropertyNoWrite(Key.DENYLIST, false)
|
||||||
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
||||||
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
|
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
|
||||||
|
|
||||||
@@ -158,7 +172,7 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
fun load(pkg: String?) {
|
fun load(pkg: String?) {
|
||||||
// Only try to load prefs when fresh install and a previous package name is set
|
// Only try to load prefs when fresh install and a previous package name is set
|
||||||
if (pkg != null && prefs.all.isEmpty()) runCatching {
|
if (pkg != null && prefs.all.isEmpty()) runCatching {
|
||||||
context.contentResolver.openInputStream(Provider.PREFS_URI(pkg))?.use {
|
context.contentResolver.openInputStream(Provider.preferencesUri(pkg))?.use {
|
||||||
prefs.edit { parsePrefs(it) }
|
prefs.edit { parsePrefs(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,10 +183,11 @@ 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())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,8 +15,7 @@ object Const {
|
|||||||
else Build.SUPPORTED_32_BIT_ABIS.firstOrNull()
|
else Build.SUPPORTED_32_BIT_ABIS.firstOrNull()
|
||||||
|
|
||||||
// Paths
|
// Paths
|
||||||
lateinit var MAGISKTMP: String
|
const val MAGISK_PATH = "/data/adb/modules"
|
||||||
val MAGISK_PATH get() = "$MAGISKTMP/modules"
|
|
||||||
const val TMPDIR = "/dev/tmp"
|
const val TMPDIR = "/dev/tmp"
|
||||||
const val MAGISK_LOG = "/cache/magisk.log"
|
const val MAGISK_LOG = "/cache/magisk.log"
|
||||||
|
|
||||||
@@ -25,11 +24,11 @@ object Const {
|
|||||||
val APP_IS_CANARY get() = Version.isCanary(BuildConfig.VERSION_CODE)
|
val APP_IS_CANARY get() = Version.isCanary(BuildConfig.VERSION_CODE)
|
||||||
|
|
||||||
object Version {
|
object Version {
|
||||||
const val MIN_VERSION = "v21.0"
|
const val MIN_VERSION = "v22.0"
|
||||||
const val MIN_VERCODE = 21000
|
const val MIN_VERCODE = 22000
|
||||||
|
|
||||||
fun atLeast_21_2() = Info.env.versionCode >= 21200 || isCanary()
|
|
||||||
fun atLeast_24_0() = Info.env.versionCode >= 24000 || isCanary()
|
fun atLeast_24_0() = Info.env.versionCode >= 24000 || isCanary()
|
||||||
|
fun atLeast_25_0() = Info.env.versionCode >= 25000 || isCanary()
|
||||||
fun isCanary() = isCanary(Info.env.versionCode)
|
fun isCanary() = isCanary(Info.env.versionCode)
|
||||||
|
|
||||||
fun isCanary(ver: Int) = ver > 0 && ver % 100 != 0
|
fun isCanary(ver: Int) = ver > 0 && ver % 100 != 0
|
||||||
@@ -37,8 +36,6 @@ object Const {
|
|||||||
|
|
||||||
object ID {
|
object ID {
|
||||||
const val JOB_SERVICE_ID = 7
|
const val JOB_SERVICE_ID = 7
|
||||||
const val UPDATE_NOTIFICATION_CHANNEL = "update"
|
|
||||||
const val PROGRESS_NOTIFICATION_CHANNEL = "progress"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object Url {
|
object Url {
|
||||||
|
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
@@ -11,47 +10,51 @@ import android.content.res.AssetManager
|
|||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
import com.topjohnwu.magisk.DynAPK
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.StubApk
|
||||||
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
|
import com.topjohnwu.magisk.core.ktx.unwrap
|
||||||
import com.topjohnwu.magisk.core.utils.syncLocale
|
import com.topjohnwu.magisk.core.utils.syncLocale
|
||||||
import com.topjohnwu.magisk.di.AppContext
|
|
||||||
|
|
||||||
lateinit var AppApkPath: String
|
lateinit var AppApkPath: String
|
||||||
|
|
||||||
fun AssetManager.addAssetPath(path: String) = DynAPK.addAssetPath(this, path)
|
fun Resources.addAssetPath(path: String) = StubApk.addAssetPath(this, path)
|
||||||
|
|
||||||
fun Context.wrap(): Context = if (this is PatchedContext) this else PatchedContext(this)
|
|
||||||
|
|
||||||
private class PatchedContext(base: Context) : ContextWrapper(base) {
|
|
||||||
init { base.resources.patch() }
|
|
||||||
override fun getClassLoader() = javaClass.classLoader!!
|
|
||||||
override fun createConfigurationContext(config: Configuration) =
|
|
||||||
super.createConfigurationContext(config).wrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Resources.patch(): Resources {
|
fun Resources.patch(): Resources {
|
||||||
syncLocale()
|
|
||||||
if (isRunningAsStub)
|
if (isRunningAsStub)
|
||||||
assets.addAssetPath(AppApkPath)
|
addAssetPath(AppApkPath)
|
||||||
|
syncLocale()
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Context.patch(): Context {
|
||||||
|
unwrap().resources.patch()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapping is only necessary for ContextThemeWrapper to support configuration overrides
|
||||||
|
fun Context.wrap(): Context {
|
||||||
|
patch()
|
||||||
|
return object : ContextWrapper(this) {
|
||||||
|
override fun createConfigurationContext(config: Configuration): Context {
|
||||||
|
return super.createConfigurationContext(config).wrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun createNewResources(): Resources {
|
fun createNewResources(): Resources {
|
||||||
val asset = AssetManager::class.java.newInstance()
|
val asset = AssetManager::class.java.newInstance()
|
||||||
asset.addAssetPath(AppApkPath)
|
|
||||||
val config = Configuration(AppContext.resources.configuration)
|
val config = Configuration(AppContext.resources.configuration)
|
||||||
val metrics = DisplayMetrics()
|
val metrics = DisplayMetrics()
|
||||||
metrics.setTo(AppContext.resources.displayMetrics)
|
metrics.setTo(AppContext.resources.displayMetrics)
|
||||||
return Resources(asset, metrics, config)
|
val res = Resources(asset, metrics, config)
|
||||||
|
res.addAssetPath(AppApkPath)
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Class<*>.cmp(pkg: String) =
|
fun Class<*>.cmp(pkg: String) =
|
||||||
ComponentName(pkg, Info.stub?.classToComponent?.get(name) ?: name)
|
ComponentName(pkg, Info.stub?.classToComponent?.get(name) ?: name)
|
||||||
|
|
||||||
inline fun <reified T> Activity.redirect() = Intent(intent)
|
|
||||||
.setComponent(T::class.java.cmp(packageName))
|
|
||||||
.setFlags(0)
|
|
||||||
|
|
||||||
inline fun <reified T> Context.intent() = Intent().setComponent(T::class.java.cmp(packageName))
|
inline fun <reified T> Context.intent() = Intent().setComponent(T::class.java.cmp(packageName))
|
||||||
|
|
||||||
// Keep a reference to these resources to prevent it from
|
// Keep a reference to these resources to prevent it from
|
||||||
|
@@ -1,22 +1,21 @@
|
|||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.databinding.ObservableBoolean
|
import androidx.lifecycle.LiveData
|
||||||
import com.topjohnwu.magisk.DynAPK
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import com.topjohnwu.magisk.StubApk
|
||||||
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
|
import com.topjohnwu.magisk.core.ktx.getProperty
|
||||||
import com.topjohnwu.magisk.core.model.UpdateInfo
|
import com.topjohnwu.magisk.core.model.UpdateInfo
|
||||||
import com.topjohnwu.magisk.core.utils.net.NetworkObserver
|
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
import com.topjohnwu.magisk.core.utils.NetworkObserver
|
||||||
import com.topjohnwu.magisk.di.AppContext
|
|
||||||
import com.topjohnwu.magisk.ktx.getProperty
|
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import com.topjohnwu.superuser.ShellUtils.fastCmd
|
import com.topjohnwu.superuser.ShellUtils.fastCmd
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
|
||||||
|
|
||||||
val isRunningAsStub get() = Info.stub != null
|
val isRunningAsStub get() = Info.stub != null
|
||||||
|
|
||||||
object Info {
|
object Info {
|
||||||
|
|
||||||
var stub: DynAPK.Data? = null
|
var stub: StubApk.Data? = null
|
||||||
|
|
||||||
val EMPTY_REMOTE = UpdateInfo()
|
val EMPTY_REMOTE = UpdateInfo()
|
||||||
var remote = EMPTY_REMOTE
|
var remote = EMPTY_REMOTE
|
||||||
@@ -36,6 +35,7 @@ object Info {
|
|||||||
@JvmField var vbmeta = false
|
@JvmField var vbmeta = false
|
||||||
var crypto = ""
|
var crypto = ""
|
||||||
var noDataExec = false
|
var noDataExec = false
|
||||||
|
var isRooted = false
|
||||||
|
|
||||||
@JvmField var hasGMS = true
|
@JvmField var hasGMS = true
|
||||||
val isSamsung = Build.MANUFACTURER.equals("samsung", ignoreCase = true)
|
val isSamsung = Build.MANUFACTURER.equals("samsung", ignoreCase = true)
|
||||||
@@ -43,28 +43,39 @@ object Info {
|
|||||||
getProperty("ro.kernel.qemu", "0") == "1" ||
|
getProperty("ro.kernel.qemu", "0") == "1" ||
|
||||||
getProperty("ro.boot.qemu", "0") == "1"
|
getProperty("ro.boot.qemu", "0") == "1"
|
||||||
|
|
||||||
val isConnected by lazy {
|
val isConnected: LiveData<Boolean> by lazy {
|
||||||
ObservableBoolean(false).also { field ->
|
MutableLiveData(false).also { field ->
|
||||||
NetworkObserver.observe(AppContext) {
|
NetworkObserver.observe(AppContext) {
|
||||||
UiThreadHandler.run { field.set(it) }
|
remote = EMPTY_REMOTE
|
||||||
|
field.postValue(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadState() = Env(
|
val showSuperUser: Boolean get() {
|
||||||
fastCmd("magisk -v").split(":".toRegex())[0],
|
return env.isActive && (Const.USER_ID == 0
|
||||||
runCatching { fastCmd("magisk -V").toInt() }.getOrDefault(-1)
|
|| Config.suMultiuserMode == Config.Value.MULTIUSER_MODE_USER)
|
||||||
)
|
}
|
||||||
|
|
||||||
|
private fun loadState(): Env {
|
||||||
|
val v = fastCmd("magisk -v").split(":".toRegex())
|
||||||
|
return Env(
|
||||||
|
v[0], v.size >= 3 && v[2] == "D",
|
||||||
|
runCatching { fastCmd("magisk -V").toInt() }.getOrDefault(-1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
class Env(
|
class Env(
|
||||||
val versionString: String = "",
|
val versionString: String = "",
|
||||||
|
val isDebug: Boolean = false,
|
||||||
code: Int = -1
|
code: Int = -1
|
||||||
) {
|
) {
|
||||||
val versionCode = when {
|
val versionCode = when {
|
||||||
code < Const.Version.MIN_VERCODE -> -1
|
code < Const.Version.MIN_VERCODE -> -1
|
||||||
else -> if (Shell.rootAccess()) code else -1
|
isRooted -> code
|
||||||
|
else -> -1
|
||||||
}
|
}
|
||||||
val isUnsupported = code > 0 && code < Const.Version.MIN_VERCODE
|
val isUnsupported = code > 0 && code < Const.Version.MIN_VERCODE
|
||||||
val isActive = versionCode >= 0
|
val isActive = versionCode > 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,7 +7,7 @@ import android.content.Context
|
|||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.core.base.BaseJobService
|
import com.topjohnwu.magisk.core.base.BaseJobService
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -23,16 +23,20 @@ class JobService : BaseJobService() {
|
|||||||
override fun onStartJob(params: JobParameters): Boolean {
|
override fun onStartJob(params: JobParameters): Boolean {
|
||||||
val coroutineScope = CoroutineScope(Dispatchers.IO + job)
|
val coroutineScope = CoroutineScope(Dispatchers.IO + job)
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
svc.fetchUpdate()?.run {
|
doWork()
|
||||||
Info.remote = this
|
|
||||||
if (Info.env.isActive && BuildConfig.VERSION_CODE < magisk.versionCode)
|
|
||||||
Notifications.managerUpdate(this@JobService)
|
|
||||||
}
|
|
||||||
jobFinished(params, false)
|
jobFinished(params, false)
|
||||||
}
|
}
|
||||||
return 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 {
|
override fun onStopJob(params: JobParameters): Boolean {
|
||||||
job.cancel()
|
job.cancel()
|
||||||
return false
|
return false
|
||||||
|
@@ -1,22 +1,13 @@
|
|||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import android.content.ContentProvider
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.ProviderInfo
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
||||||
|
import com.topjohnwu.magisk.core.base.BaseProvider
|
||||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class Provider : ContentProvider() {
|
class Provider : BaseProvider() {
|
||||||
|
|
||||||
override fun attachInfo(context: Context, info: ProviderInfo) {
|
|
||||||
super.attachInfo(context.wrap(), info)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
|
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
|
||||||
SuCallbackHandler.run(context!!, method, extras)
|
SuCallbackHandler.run(context!!, method, extras)
|
||||||
@@ -25,24 +16,13 @@ class Provider : ContentProvider() {
|
|||||||
|
|
||||||
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
|
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
|
||||||
return when (uri.encodedPath ?: return null) {
|
return when (uri.encodedPath ?: return null) {
|
||||||
"/apk_file" -> ParcelFileDescriptor.open(File(context!!.packageCodePath), MODE_READ_ONLY)
|
|
||||||
"/prefs_file" -> ParcelFileDescriptor.open(Config.prefsFile, MODE_READ_ONLY)
|
"/prefs_file" -> ParcelFileDescriptor.open(Config.prefsFile, MODE_READ_ONLY)
|
||||||
else -> super.openFile(uri, mode)
|
else -> super.openFile(uri, mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun APK_URI(pkg: String) =
|
fun preferencesUri(pkg: String): Uri =
|
||||||
Uri.Builder().scheme("content").authority("$pkg.provider").path("apk_file").build()
|
|
||||||
|
|
||||||
fun PREFS_URI(pkg: String) =
|
|
||||||
Uri.Builder().scheme("content").authority("$pkg.provider").path("prefs_file").build()
|
Uri.Builder().scheme("content").authority("$pkg.provider").path("prefs_file").build()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate() = true
|
|
||||||
override fun getType(uri: Uri): String? = null
|
|
||||||
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
|
|
||||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?) = 0
|
|
||||||
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?) = 0
|
|
||||||
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? = null
|
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.ContextWrapper
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.topjohnwu.magisk.core.base.BaseReceiver
|
import com.topjohnwu.magisk.core.base.BaseReceiver
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import com.topjohnwu.magisk.view.Shortcuts
|
import com.topjohnwu.magisk.view.Shortcuts
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
@@ -25,8 +26,9 @@ open class Receiver : BaseReceiver() {
|
|||||||
return if (uid == -1) null else uid
|
return if (uid == -1) null else uid
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReceive(context: ContextWrapper, intent: Intent?) {
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
intent ?: return
|
intent ?: return
|
||||||
|
super.onReceive(context, intent)
|
||||||
|
|
||||||
fun rmPolicy(uid: Int) = GlobalScope.launch {
|
fun rmPolicy(uid: Int) = GlobalScope.launch {
|
||||||
policyDB.delete(uid)
|
policyDB.delete(uid)
|
||||||
@@ -42,9 +44,16 @@ open class Receiver : BaseReceiver() {
|
|||||||
getUid(intent)?.let { rmPolicy(it) }
|
getUid(intent)?.let { rmPolicy(it) }
|
||||||
}
|
}
|
||||||
Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
|
Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
|
||||||
getPkg(intent)?.let { Shell.su("magisk --denylist rm $it").submit() }
|
getPkg(intent)?.let { Shell.cmd("magisk --denylist rm $it").submit() }
|
||||||
}
|
}
|
||||||
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setupDynamic(context)
|
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setupDynamic(context)
|
||||||
|
Intent.ACTION_MY_PACKAGE_REPLACED -> {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val installer = context.packageManager.getInstallerPackageName(context.packageName)
|
||||||
|
if (installer == context.packageName) {
|
||||||
|
Notifications.updateDone()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,19 +1,33 @@
|
|||||||
package com.topjohnwu.magisk.core.base
|
package com.topjohnwu.magisk.core.base
|
||||||
|
|
||||||
|
import android.Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
||||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.ActivityResultCallback
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.GetContent
|
import androidx.activity.result.contract.ActivityResultContracts.GetContent
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
|
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
import com.topjohnwu.magisk.core.ktx.reflectField
|
||||||
|
import com.topjohnwu.magisk.core.ktx.toast
|
||||||
|
import com.topjohnwu.magisk.core.utils.RequestInstall
|
||||||
import com.topjohnwu.magisk.core.wrap
|
import com.topjohnwu.magisk.core.wrap
|
||||||
import com.topjohnwu.magisk.ktx.reflectField
|
|
||||||
|
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() {
|
||||||
|
|
||||||
@@ -23,16 +37,26 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
permissionCallback = null
|
permissionCallback = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private var contentCallback: ((Uri) -> Unit)? = null
|
private var installCallback: ((Boolean) -> Unit)? = null
|
||||||
|
private val requestInstall = registerForActivityResult(RequestInstall()) {
|
||||||
|
installCallback?.invoke(it)
|
||||||
|
installCallback = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private var contentCallback: ContentResultCallback? = null
|
||||||
private val getContent = registerForActivityResult(GetContent()) {
|
private val getContent = registerForActivityResult(GetContent()) {
|
||||||
if (it != null) contentCallback?.invoke(it)
|
if (it != null) contentCallback?.onActivityResult(it)
|
||||||
contentCallback = null
|
contentCallback = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun applyOverrideConfiguration(config: Configuration?) {
|
private val mReferrerField by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
// Force applying our preferred local
|
Activity::class.java.reflectField("mReferrer")
|
||||||
config?.setLocale(currentLocale)
|
}
|
||||||
super.applyOverrideConfiguration(config)
|
|
||||||
|
val realCallingPackage: String? get() {
|
||||||
|
callingPackage?.let { return it }
|
||||||
|
mReferrerField.get(this)?.let { return it as String }
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
override fun attachBaseContext(base: Context) {
|
||||||
@@ -44,25 +68,50 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
// Overwrite private members to avoid nasty "false" stack traces being logged
|
// Overwrite private members to avoid nasty "false" stack traces being logged
|
||||||
val delegate = delegate
|
val delegate = delegate
|
||||||
val clz = delegate.javaClass
|
val clz = delegate.javaClass
|
||||||
clz.reflectField("mActivityHandlesUiModeChecked").set(delegate, true)
|
clz.reflectField("mActivityHandlesConfigFlagsChecked").set(delegate, true)
|
||||||
clz.reflectField("mActivityHandlesUiMode").set(delegate, false)
|
clz.reflectField("mActivityHandlesConfigFlags").set(delegate, 0)
|
||||||
}
|
}
|
||||||
|
contentCallback = savedInstanceState?.getParcelable(CONTENT_CALLBACK_KEY)
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
contentCallback?.let {
|
||||||
|
outState.putParcelable(CONTENT_CALLBACK_KEY, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
||||||
if (permission == WRITE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= 30) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
|
||||||
// We do not need external rw on 30+
|
permission == WRITE_EXTERNAL_STORAGE) {
|
||||||
|
// We do not need external rw on R+
|
||||||
callback(true)
|
callback(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
permissionCallback = callback
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU &&
|
||||||
requestPermission.launch(permission)
|
permission == POST_NOTIFICATIONS) {
|
||||||
|
// All apps have notification permissions before T
|
||||||
|
callback(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (permission == REQUEST_INSTALL_PACKAGES) {
|
||||||
|
installCallback = callback
|
||||||
|
requestInstall.launch(Unit)
|
||||||
|
} else {
|
||||||
|
permissionCallback = callback
|
||||||
|
requestPermission.launch(permission)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getContent(type: String, callback: (Uri) -> Unit) {
|
fun getContent(type: String, callback: ContentResultCallback) {
|
||||||
contentCallback = callback
|
contentCallback = callback
|
||||||
getContent.launch(type)
|
try {
|
||||||
|
getContent.launch(type)
|
||||||
|
callback.onActivityLaunch()
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
toast(R.string.app_not_found, Toast.LENGTH_SHORT)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun recreate() {
|
override fun recreate() {
|
||||||
@@ -74,4 +123,8 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
startActivity(Intent(intent).setFlags(0))
|
startActivity(Intent(intent).setFlags(0))
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val CONTENT_CALLBACK_KEY = "content_callback"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,10 +2,10 @@ package com.topjohnwu.magisk.core.base
|
|||||||
|
|
||||||
import android.app.job.JobService
|
import android.app.job.JobService
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.topjohnwu.magisk.core.wrap
|
import com.topjohnwu.magisk.core.patch
|
||||||
|
|
||||||
abstract class BaseJobService : JobService() {
|
abstract class BaseJobService : JobService() {
|
||||||
override fun attachBaseContext(base: Context) {
|
override fun attachBaseContext(base: Context) {
|
||||||
super.attachBaseContext(base.wrap())
|
super.attachBaseContext(base.patch())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,21 @@
|
|||||||
|
package com.topjohnwu.magisk.core.base
|
||||||
|
|
||||||
|
import android.content.ContentProvider
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.ProviderInfo
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.net.Uri
|
||||||
|
import com.topjohnwu.magisk.core.patch
|
||||||
|
|
||||||
|
open class BaseProvider : ContentProvider() {
|
||||||
|
override fun attachInfo(context: Context, info: ProviderInfo) {
|
||||||
|
super.attachInfo(context.patch(), info)
|
||||||
|
}
|
||||||
|
override fun onCreate() = true
|
||||||
|
override fun getType(uri: Uri): String? = null
|
||||||
|
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
|
||||||
|
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?) = 0
|
||||||
|
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?) = 0
|
||||||
|
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? = null
|
||||||
|
}
|
@@ -2,15 +2,13 @@ package com.topjohnwu.magisk.core.base
|
|||||||
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.topjohnwu.magisk.core.wrap
|
import androidx.annotation.CallSuper
|
||||||
|
import com.topjohnwu.magisk.core.patch
|
||||||
|
|
||||||
abstract class BaseReceiver : BroadcastReceiver() {
|
abstract class BaseReceiver : BroadcastReceiver() {
|
||||||
|
@CallSuper
|
||||||
final override fun onReceive(context: Context, intent: Intent?) {
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
onReceive(context.wrap() as ContextWrapper, intent)
|
context.patch()
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun onReceive(context: ContextWrapper, intent: Intent?)
|
|
||||||
}
|
}
|
||||||
|
@@ -2,10 +2,13 @@ package com.topjohnwu.magisk.core.base
|
|||||||
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.topjohnwu.magisk.core.wrap
|
import android.content.Intent
|
||||||
|
import android.os.IBinder
|
||||||
|
import com.topjohnwu.magisk.core.patch
|
||||||
|
|
||||||
abstract class BaseService : Service() {
|
open class BaseService : Service() {
|
||||||
override fun attachBaseContext(base: Context) {
|
override fun attachBaseContext(base: Context) {
|
||||||
super.attachBaseContext(base.wrap())
|
super.attachBaseContext(base.patch())
|
||||||
}
|
}
|
||||||
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
package com.topjohnwu.magisk.data.network
|
package com.topjohnwu.magisk.core.data
|
||||||
|
|
||||||
import com.topjohnwu.magisk.core.model.BranchInfo
|
import com.topjohnwu.magisk.core.model.BranchInfo
|
||||||
import com.topjohnwu.magisk.core.model.ModuleJson
|
import com.topjohnwu.magisk.core.model.ModuleJson
|
||||||
@@ -12,15 +12,12 @@ private const val FILE = "file"
|
|||||||
|
|
||||||
interface GithubPageServices {
|
interface GithubPageServices {
|
||||||
|
|
||||||
@GET("{$FILE}")
|
@GET
|
||||||
suspend fun fetchUpdateJSON(@Path(FILE) file: String): UpdateInfo
|
suspend fun fetchUpdateJSON(@Url file: String): UpdateInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RawServices {
|
interface RawServices {
|
||||||
|
|
||||||
@GET
|
|
||||||
suspend fun fetchCustomUpdate(@Url url: String): UpdateInfo
|
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Streaming
|
@Streaming
|
||||||
suspend fun fetchFile(@Url url: String): ResponseBody
|
suspend fun fetchFile(@Url url: String): ResponseBody
|
@@ -1,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
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
package com.topjohnwu.magisk.di
|
package com.topjohnwu.magisk.core.di
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
@@ -1,25 +1,17 @@
|
|||||||
package com.topjohnwu.magisk.di
|
package com.topjohnwu.magisk.core.di
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.lifecycle.ViewModelStoreOwner
|
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
import com.topjohnwu.magisk.core.data.SuLogDatabase
|
||||||
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
|
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
||||||
import com.topjohnwu.magisk.core.magiskdb.StringDao
|
import com.topjohnwu.magisk.core.data.magiskdb.SettingsDao
|
||||||
import com.topjohnwu.magisk.data.database.SuLogDatabase
|
import com.topjohnwu.magisk.core.data.magiskdb.StringDao
|
||||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
import com.topjohnwu.magisk.core.ktx.deviceProtectedContext
|
||||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
import com.topjohnwu.magisk.core.repository.LogRepository
|
||||||
import com.topjohnwu.magisk.ktx.deviceProtectedContext
|
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||||
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
|
||||||
import com.topjohnwu.magisk.ui.install.InstallViewModel
|
|
||||||
import com.topjohnwu.magisk.ui.log.LogViewModel
|
|
||||||
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
|
|
||||||
import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel
|
|
||||||
import io.noties.markwon.Markwon
|
import io.noties.markwon.Markwon
|
||||||
import io.noties.markwon.utils.NoCopySpannableFactory
|
import io.noties.markwon.utils.NoCopySpannableFactory
|
||||||
|
|
||||||
@@ -47,30 +39,10 @@ object ServiceLocator {
|
|||||||
NetworkService(
|
NetworkService(
|
||||||
createApiService(retrofit, Const.Url.GITHUB_PAGE_URL),
|
createApiService(retrofit, Const.Url.GITHUB_PAGE_URL),
|
||||||
createApiService(retrofit, Const.Url.GITHUB_RAW_URL),
|
createApiService(retrofit, Const.Url.GITHUB_RAW_URL),
|
||||||
createApiService(retrofit, Const.Url.GITHUB_API_URL)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
object VMFactory : ViewModelProvider.Factory {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
|
||||||
return when (modelClass) {
|
|
||||||
HomeViewModel::class.java -> HomeViewModel(networkService)
|
|
||||||
LogViewModel::class.java -> LogViewModel(logRepo)
|
|
||||||
SuperuserViewModel::class.java -> SuperuserViewModel(policyDB)
|
|
||||||
InstallViewModel::class.java -> InstallViewModel(networkService)
|
|
||||||
SuRequestViewModel::class.java -> SuRequestViewModel(policyDB, timeoutPrefs)
|
|
||||||
else -> modelClass.newInstance()
|
|
||||||
} as T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified VM : ViewModel> ViewModelStoreOwner.viewModel() =
|
|
||||||
lazy(LazyThreadSafetyMode.NONE) {
|
|
||||||
ViewModelProvider(this, ServiceLocator.VMFactory)[VM::class.java]
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createSuLogDatabase(context: Context) =
|
private fun createSuLogDatabase(context: Context) =
|
||||||
Room.databaseBuilder(context, SuLogDatabase::class.java, "sulogs.db")
|
Room.databaseBuilder(context, SuLogDatabase::class.java, "sulogs.db")
|
||||||
.fallbackToDestructiveMigration()
|
.fallbackToDestructiveMigration()
|
@@ -1,79 +1,72 @@
|
|||||||
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.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 android.os.IBinder
|
import androidx.core.net.toFile
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.StubApk
|
||||||
import com.topjohnwu.magisk.core.ActivityTracker
|
import com.topjohnwu.magisk.core.ActivityTracker
|
||||||
import com.topjohnwu.magisk.core.base.BaseService
|
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.utils.ProgressInputStream
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
import com.topjohnwu.magisk.core.ktx.*
|
||||||
import com.topjohnwu.magisk.ktx.synchronized
|
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||||
import com.topjohnwu.magisk.view.Notifications.mgr
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||||
|
import com.topjohnwu.magisk.utils.APKInstall
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import okhttp3.ResponseBody
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
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
|
||||||
|
|
||||||
class DownloadService : BaseService() {
|
class DownloadService : NotificationService() {
|
||||||
|
|
||||||
private val hasNotifications get() = notifications.isNotEmpty()
|
|
||||||
private val notifications = HashMap<Int, Notification.Builder>().synchronized()
|
|
||||||
private val job = Job()
|
private val job = Job()
|
||||||
|
|
||||||
val service get() = ServiceLocator.networkService
|
|
||||||
|
|
||||||
// -- Service overrides
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? = null
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||||
intent.getParcelableExtra<Subject>(SUBJECT_KEY)?.let { doDownload(it) }
|
intent.getParcelableExtra<Subject>(SUBJECT_KEY)?.let { download(it) }
|
||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
|
||||||
super.onTaskRemoved(rootIntent)
|
|
||||||
notifications.forEach { mgr.cancel(it.key) }
|
|
||||||
notifications.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
|
||||||
job.cancel()
|
job.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Download logic
|
private fun download(subject: Subject) {
|
||||||
|
notifyUpdate(subject.notifyId)
|
||||||
private fun doDownload(subject: Subject) {
|
CoroutineScope(job + Dispatchers.IO).launch {
|
||||||
update(subject.notifyId)
|
|
||||||
val coroutineScope = CoroutineScope(job + Dispatchers.IO)
|
|
||||||
coroutineScope.launch {
|
|
||||||
try {
|
try {
|
||||||
val stream = service.fetchFile(subject.url).toProgressStream(subject)
|
val stream = service.fetchFile(subject.url).toProgressStream(subject)
|
||||||
when (subject) {
|
when (subject) {
|
||||||
is Subject.Manager -> handleAPK(subject, stream)
|
is Subject.App -> handleApp(stream, subject)
|
||||||
is Subject.Module -> stream.toModule(subject.file, assets.open("module_installer.sh"))
|
is Subject.Module -> handleModule(stream, subject.file)
|
||||||
}
|
}
|
||||||
val activity = ActivityTracker.foreground
|
val activity = ActivityTracker.foreground
|
||||||
if (activity != null && subject.autoStart) {
|
if (activity != null && subject.autoLaunch) {
|
||||||
remove(subject.notifyId)
|
notifyRemove(subject.notifyId)
|
||||||
subject.pendingIntent(activity).send()
|
subject.pendingIntent(activity)?.send()
|
||||||
} else {
|
} else {
|
||||||
notifyFinish(subject)
|
notifyFinish(subject)
|
||||||
}
|
}
|
||||||
|
subject.postDownload?.invoke()
|
||||||
if (!hasNotifications)
|
if (!hasNotifications)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -83,88 +76,112 @@ class DownloadService : BaseService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ResponseBody.toProgressStream(subject: Subject): InputStream {
|
private fun handleApp(stream: InputStream, subject: Subject.App) {
|
||||||
val max = contentLength()
|
fun writeTee(output: OutputStream) {
|
||||||
val total = max.toFloat() / 1048576
|
val uri = MediaStoreUtils.getFile("${subject.title}.apk").uri
|
||||||
val id = subject.notifyId
|
val external = uri.outputStream()
|
||||||
|
stream.copyAndClose(TeeOutputStream(external, output))
|
||||||
|
}
|
||||||
|
|
||||||
update(id) { it.setContentTitle(subject.title) }
|
if (isRunningAsStub) {
|
||||||
|
val updateApk = StubApk.update(this)
|
||||||
|
try {
|
||||||
|
// Download full APK to stub update path
|
||||||
|
writeTee(updateApk.outputStream())
|
||||||
|
|
||||||
return ProgressInputStream(byteStream()) {
|
val zf = ZipFile(updateApk)
|
||||||
val progress = it.toFloat() / 1048576
|
val prop = Properties()
|
||||||
update(id) { notification ->
|
prop.load(ByteArrayInputStream(zf.comment.toByteArray()))
|
||||||
if (max > 0) {
|
val stubVersion = prop.getProperty("stubVersion").toIntOrNull() ?: -1
|
||||||
broadcast(progress / total, subject)
|
if (Info.stub!!.version < stubVersion) {
|
||||||
notification
|
// Also upgrade stub
|
||||||
.setProgress(max.toInt(), it.toInt(), false)
|
notifyUpdate(subject.notifyId) {
|
||||||
.setContentText("%.2f / %.2f MB".format(progress, total))
|
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 {
|
} else {
|
||||||
broadcast(-1f, subject)
|
ActivityTracker.foreground?.let {
|
||||||
notification.setContentText("%.2f MB / ??".format(progress))
|
// 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 handleModule(src: InputStream, file: Uri) {
|
||||||
|
val input = ZipInputStream(src.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"))
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Notification management
|
private class TeeOutputStream(
|
||||||
|
private val o1: OutputStream,
|
||||||
private fun notifyFail(subject: Subject) = finalNotify(subject.notifyId) {
|
private val o2: OutputStream
|
||||||
broadcast(-2f, subject)
|
) : OutputStream() {
|
||||||
it.setContentText(getString(R.string.download_file_error))
|
override fun write(b: Int) {
|
||||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
o1.write(b)
|
||||||
.setOngoing(false)
|
o2.write(b)
|
||||||
}
|
}
|
||||||
|
override fun write(b: ByteArray?, off: Int, len: Int) {
|
||||||
private fun notifyFinish(subject: Subject) = finalNotify(subject.notifyId) {
|
o1.write(b, off, len)
|
||||||
broadcast(1f, subject)
|
o2.write(b, off, len)
|
||||||
it.setContentIntent(subject.pendingIntent(this))
|
}
|
||||||
.setContentTitle(subject.title)
|
override fun close() {
|
||||||
.setContentText(getString(R.string.download_complete))
|
o1.close()
|
||||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
o2.close()
|
||||||
.setProgress(0, 0, false)
|
|
||||||
.setOngoing(false)
|
|
||||||
.setAutoCancel(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
|
|
||||||
val notification = remove(id)?.also(editor) ?: return -1
|
|
||||||
val newId = Notifications.nextId()
|
|
||||||
mgr.notify(newId, notification.build())
|
|
||||||
return newId
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
mgr.notify(id, notification.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun remove(id: Int): Notification.Builder? {
|
|
||||||
val n = notifications.remove(id)?.also { updateForeground() }
|
|
||||||
mgr.cancel(id)
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateForeground() {
|
|
||||||
if (hasNotifications) {
|
|
||||||
val (id, notification) = notifications.entries.first()
|
|
||||||
startForeground(id, notification.build())
|
|
||||||
} else {
|
|
||||||
stopForeground(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val SUBJECT_KEY = "download_subject"
|
private const val SUBJECT_KEY = "subject"
|
||||||
private const val REQUEST_CODE = 1
|
private const val REQUEST_CODE = 1
|
||||||
|
|
||||||
private val progressBroadcast = MutableLiveData<Pair<Float, Subject>?>()
|
|
||||||
|
|
||||||
fun observeProgress(owner: LifecycleOwner, callback: (Float, Subject) -> Unit) {
|
fun observeProgress(owner: LifecycleOwner, callback: (Float, Subject) -> Unit) {
|
||||||
progressBroadcast.value = null
|
progressBroadcast.value = null
|
||||||
progressBroadcast.observe(owner) {
|
progressBroadcast.observe(owner) {
|
||||||
@@ -173,10 +190,6 @@ class DownloadService : BaseService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun broadcast(progress: Float, subject: Subject) {
|
|
||||||
progressBroadcast.postValue(progress to subject)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun intent(context: Context, subject: Subject) =
|
private fun intent(context: Context, subject: Subject) =
|
||||||
context.intent<DownloadService>().putExtra(SUBJECT_KEY, subject)
|
context.intent<DownloadService>().putExtra(SUBJECT_KEY, subject)
|
||||||
|
|
||||||
@@ -184,21 +197,24 @@ class DownloadService : BaseService() {
|
|||||||
fun getPendingIntent(context: Context, subject: Subject): PendingIntent {
|
fun getPendingIntent(context: Context, subject: Subject): PendingIntent {
|
||||||
val flag = FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT or FLAG_ONE_SHOT
|
val flag = FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT or FLAG_ONE_SHOT
|
||||||
val intent = intent(context, subject)
|
val intent = intent(context, subject)
|
||||||
return if (Build.VERSION.SDK_INT >= 26) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
getForegroundService(context, REQUEST_CODE, intent, flag)
|
getForegroundService(context, REQUEST_CODE, intent, flag)
|
||||||
} else {
|
} else {
|
||||||
getService(context, REQUEST_CODE, intent, flag)
|
getService(context, REQUEST_CODE, intent, flag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start(context: Context, subject: Subject) {
|
@SuppressLint("InlinedApi")
|
||||||
val app = context.applicationContext
|
fun start(activity: BaseActivity, subject: Subject) {
|
||||||
if (Build.VERSION.SDK_INT >= 26) {
|
activity.withPermission(Manifest.permission.POST_NOTIFICATIONS) {
|
||||||
app.startForegroundService(intent(app, subject))
|
// Always download regardless of notification permission status
|
||||||
} else {
|
val app = activity.applicationContext
|
||||||
app.startService(intent(app, subject))
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
app.startForegroundService(intent(app, subject))
|
||||||
|
} else {
|
||||||
|
app.startService(intent(app, subject))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,77 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.download
|
|
||||||
|
|
||||||
import android.app.AlarmManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
import androidx.core.net.toFile
|
|
||||||
import com.topjohnwu.magisk.DynAPK
|
|
||||||
import com.topjohnwu.magisk.R
|
|
||||||
import com.topjohnwu.magisk.core.ActivityTracker
|
|
||||||
import com.topjohnwu.magisk.core.Info
|
|
||||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
|
||||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
|
||||||
import com.topjohnwu.magisk.ktx.copyAndClose
|
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
private class TeeOutputStream(
|
|
||||||
private val o1: OutputStream,
|
|
||||||
private val o2: OutputStream
|
|
||||||
) : OutputStream() {
|
|
||||||
override fun write(b: Int) {
|
|
||||||
o1.write(b)
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun DownloadService.handleAPK(subject: Subject.Manager, stream: InputStream) {
|
|
||||||
fun write(output: OutputStream) {
|
|
||||||
val external = subject.externalFile.outputStream()
|
|
||||||
stream.copyAndClose(TeeOutputStream(external, output))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRunningAsStub) {
|
|
||||||
val apk = subject.file.toFile()
|
|
||||||
val id = subject.notifyId
|
|
||||||
write(DynAPK.update(this).outputStream())
|
|
||||||
if (Info.stub!!.version < subject.stub.versionCode) {
|
|
||||||
// Also upgrade stub
|
|
||||||
update(id) {
|
|
||||||
it.setProgress(0, 0, true)
|
|
||||||
.setContentTitle(getString(R.string.hide_app_title))
|
|
||||||
.setContentText("")
|
|
||||||
}
|
|
||||||
service.fetchFile(subject.stub.link).byteStream().writeTo(apk)
|
|
||||||
val patched = File(apk.parent, "patched.apk")
|
|
||||||
HideAPK.patch(this, apk, patched, packageName, applicationInfo.nonLocalizedLabel)
|
|
||||||
apk.delete()
|
|
||||||
patched.renameTo(apk)
|
|
||||||
} else {
|
|
||||||
val intent = packageManager.getLaunchIntentForPackage(packageName)
|
|
||||||
intent!!.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
//noinspection InlinedApi
|
|
||||||
val flag = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
val pending = PendingIntent.getActivity(this, id, intent, flag)
|
|
||||||
if (ActivityTracker.hasForeground) {
|
|
||||||
val alarm = getSystemService<AlarmManager>()
|
|
||||||
alarm!!.set(AlarmManager.RTC, System.currentTimeMillis() + 1000, pending)
|
|
||||||
}
|
|
||||||
stopSelf()
|
|
||||||
Runtime.getRuntime().exit(0)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
write(subject.file.outputStream())
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,38 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.download
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
|
||||||
import com.topjohnwu.magisk.ktx.forEach
|
|
||||||
import com.topjohnwu.magisk.ktx.withStreams
|
|
||||||
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")))
|
|
||||||
|
|
||||||
zin.forEach { entry ->
|
|
||||||
val path = entry.name
|
|
||||||
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
|
|
||||||
zout.putNextEntry(ZipEntry(path))
|
|
||||||
if (!entry.isDirectory) {
|
|
||||||
zin.copyTo(zout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -6,17 +6,14 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.core.net.toFile
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
|
import com.topjohnwu.magisk.core.ktx.cachedFile
|
||||||
import com.topjohnwu.magisk.core.model.MagiskJson
|
import com.topjohnwu.magisk.core.model.MagiskJson
|
||||||
import com.topjohnwu.magisk.core.model.StubJson
|
|
||||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||||
import com.topjohnwu.magisk.di.AppContext
|
|
||||||
import com.topjohnwu.magisk.ktx.cachedFile
|
|
||||||
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
||||||
import com.topjohnwu.magisk.utils.APKInstall
|
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
@@ -34,9 +31,10 @@ sealed class Subject : Parcelable {
|
|||||||
abstract val file: Uri
|
abstract val file: Uri
|
||||||
abstract val title: String
|
abstract val title: String
|
||||||
abstract val notifyId: Int
|
abstract val notifyId: Int
|
||||||
open val autoStart: Boolean get() = true
|
open val autoLaunch: Boolean get() = true
|
||||||
|
open val postDownload: (() -> Unit)? get() = null
|
||||||
|
|
||||||
abstract fun pendingIntent(context: Context): PendingIntent
|
abstract fun pendingIntent(context: Context): PendingIntent?
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class Module(
|
class Module(
|
||||||
@@ -46,7 +44,7 @@ sealed class Subject : Parcelable {
|
|||||||
) : 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 autoStart: Boolean get() = action == Action.Flash
|
override val autoLaunch: Boolean get() = action == Action.Flash
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
override val file by lazy {
|
override val file by lazy {
|
||||||
@@ -58,9 +56,8 @@ sealed class Subject : Parcelable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class Manager(
|
class App(
|
||||||
private val json: MagiskJson = Info.remote.magisk,
|
private val json: MagiskJson = Info.remote.magisk,
|
||||||
val stub: StubJson = Info.remote.stub,
|
|
||||||
override val notifyId: Int = Notifications.nextId()
|
override val notifyId: Int = Notifications.nextId()
|
||||||
) : Subject() {
|
) : Subject() {
|
||||||
override val title: String get() = "Magisk-${json.version}(${json.versionCode})"
|
override val title: String get() = "Magisk-${json.version}(${json.versionCode})"
|
||||||
@@ -71,14 +68,12 @@ sealed class Subject : Parcelable {
|
|||||||
cachedFile("manager.apk")
|
cachedFile("manager.apk")
|
||||||
}
|
}
|
||||||
|
|
||||||
val externalFile get() = MediaStoreUtils.getFile("$title.apk").uri
|
@IgnoredOnParcel
|
||||||
|
override var postDownload: (() -> Unit)? = null
|
||||||
|
|
||||||
override fun pendingIntent(context: Context): PendingIntent {
|
@IgnoredOnParcel
|
||||||
val receiver = APKInstall.register(context, null, null)
|
var intent: Intent? = null
|
||||||
APKInstall.installapk(context, file.toFile())
|
override fun pendingIntent(context: Context) = intent?.toPending(context)
|
||||||
val intent = receiver.waitIntent() ?: Intent()
|
|
||||||
return intent.toPending(context)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
|
@@ -1,58 +1,35 @@
|
|||||||
package com.topjohnwu.magisk.ktx
|
package com.topjohnwu.magisk.core.ktx
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.ComponentName
|
import android.content.*
|
||||||
import android.content.Context
|
|
||||||
import android.content.ContextWrapper
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.pm.PackageManager.*
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.content.res.Resources
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.drawable.AdaptiveIconDrawable
|
import android.graphics.drawable.AdaptiveIconDrawable
|
||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.graphics.drawable.LayerDrawable
|
import android.graphics.drawable.LayerDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Build.VERSION.SDK_INT
|
import android.os.Build.VERSION.SDK_INT
|
||||||
import android.text.PrecomputedText
|
import android.os.Process
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.ViewTreeObserver
|
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.TextView
|
import android.widget.Toast
|
||||||
import androidx.annotation.ColorRes
|
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.core.text.PrecomputedTextCompat
|
|
||||||
import androidx.core.view.isGone
|
|
||||||
import androidx.core.widget.TextViewCompat
|
|
||||||
import androidx.databinding.BindingAdapter
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.transition.AutoTransition
|
|
||||||
import androidx.transition.TransitionManager
|
|
||||||
import com.topjohnwu.magisk.R
|
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||||
import com.topjohnwu.magisk.di.AppContext
|
import com.topjohnwu.magisk.utils.APKInstall
|
||||||
import com.topjohnwu.magisk.utils.DynamicClassLoader
|
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import kotlin.Array
|
||||||
|
import kotlin.String
|
||||||
import java.lang.reflect.Array as JArray
|
import java.lang.reflect.Array as JArray
|
||||||
|
|
||||||
fun Context.rawResource(id: Int) = resources.openRawResource(id)
|
fun Context.rawResource(id: Int) = resources.openRawResource(id)
|
||||||
@@ -61,7 +38,7 @@ fun Context.getBitmap(id: Int): Bitmap {
|
|||||||
var drawable = AppCompatResources.getDrawable(this, id)!!
|
var drawable = AppCompatResources.getDrawable(this, id)!!
|
||||||
if (drawable is BitmapDrawable)
|
if (drawable is BitmapDrawable)
|
||||||
return drawable.bitmap
|
return drawable.bitmap
|
||||||
if (SDK_INT >= 26 && drawable is AdaptiveIconDrawable) {
|
if (SDK_INT >= Build.VERSION_CODES.O && drawable is AdaptiveIconDrawable) {
|
||||||
drawable = LayerDrawable(arrayOf(drawable.background, drawable.foreground))
|
drawable = LayerDrawable(arrayOf(drawable.background, drawable.foreground))
|
||||||
}
|
}
|
||||||
val bitmap = Bitmap.createBitmap(
|
val bitmap = Bitmap.createBitmap(
|
||||||
@@ -75,16 +52,14 @@ fun Context.getBitmap(id: Int): Bitmap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val Context.deviceProtectedContext: Context get() =
|
val Context.deviceProtectedContext: Context get() =
|
||||||
if (SDK_INT >= 24) {
|
if (SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
createDeviceProtectedStorageContext()
|
createDeviceProtectedStorageContext()
|
||||||
} else { this }
|
} else { this }
|
||||||
|
|
||||||
fun Intent.startActivity(context: Context) = context.startActivity(this)
|
|
||||||
|
|
||||||
fun Intent.startActivityWithRoot() {
|
fun Intent.startActivityWithRoot() {
|
||||||
val args = mutableListOf("am", "start", "--user", Const.USER_ID.toString())
|
val args = mutableListOf("am", "start", "--user", Const.USER_ID.toString())
|
||||||
val cmd = toCommand(args).joinToString(" ")
|
val cmd = toCommand(args).joinToString(" ")
|
||||||
Shell.su(cmd).submit()
|
Shell.cmd(cmd).submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Intent.toCommand(args: MutableList<String> = mutableListOf()): MutableList<String> {
|
fun Intent.toCommand(args: MutableList<String> = mutableListOf()): MutableList<String> {
|
||||||
@@ -185,16 +160,8 @@ fun Intent.toCommand(args: MutableList<String> = mutableListOf()): MutableList<S
|
|||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Intent.chooser(title: String = "Pick an app") = Intent.createChooser(this, title)
|
|
||||||
|
|
||||||
fun Context.cachedFile(name: String) = File(cacheDir, name)
|
fun Context.cachedFile(name: String) = File(cacheDir, name)
|
||||||
|
|
||||||
fun <Result> Cursor.toList(transformer: (Cursor) -> Result): List<Result> {
|
|
||||||
val out = mutableListOf<Result>()
|
|
||||||
while (moveToNext()) out.add(transformer(this))
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ApplicationInfo.getLabel(pm: PackageManager): String {
|
fun ApplicationInfo.getLabel(pm: PackageManager): String {
|
||||||
runCatching {
|
runCatching {
|
||||||
if (labelRes > 0) {
|
if (labelRes > 0) {
|
||||||
@@ -209,52 +176,13 @@ fun ApplicationInfo.getLabel(pm: PackageManager): String {
|
|||||||
return loadLabel(pm).toString()
|
return loadLabel(pm).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Intent.exists(packageManager: PackageManager) = resolveActivity(packageManager) != null
|
|
||||||
|
|
||||||
fun Context.colorCompat(@ColorRes id: Int) = try {
|
|
||||||
ContextCompat.getColor(this, id)
|
|
||||||
} catch (e: Resources.NotFoundException) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.colorStateListCompat(@ColorRes id: Int) = try {
|
|
||||||
ContextCompat.getColorStateList(this, id)
|
|
||||||
} catch (e: Resources.NotFoundException) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.drawableCompat(@DrawableRes id: Int) = AppCompatResources.getDrawable(this, id)
|
|
||||||
/**
|
|
||||||
* Pass [start] and [end] dimensions, function will return left and right
|
|
||||||
* with respect to RTL layout direction
|
|
||||||
*/
|
|
||||||
fun Context.startEndToLeftRight(start: Int, end: Int): Pair<Int, Int> {
|
|
||||||
if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL) {
|
|
||||||
return end to start
|
|
||||||
}
|
|
||||||
return start to end
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.openUrl(url: String) = Utils.openLink(this, url.toUri())
|
|
||||||
|
|
||||||
inline fun <reified T> T.createClassLoader(apk: File) =
|
|
||||||
DynamicClassLoader(apk, T::class.java.classLoader)
|
|
||||||
|
|
||||||
fun Context.unwrap(): Context {
|
fun Context.unwrap(): Context {
|
||||||
var context = this
|
var context = this
|
||||||
while (true) {
|
while (context is ContextWrapper)
|
||||||
if (context is ContextWrapper)
|
context = context.baseContext
|
||||||
context = context.baseContext
|
|
||||||
else
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.hasPermissions(vararg permissions: String) = permissions.all {
|
|
||||||
ContextCompat.checkSelfPermission(this, it) == PERMISSION_GRANTED
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Activity.hideKeyboard() {
|
fun Activity.hideKeyboard() {
|
||||||
val view = currentFocus ?: return
|
val view = currentFocus ?: return
|
||||||
getSystemService<InputMethodManager>()
|
getSystemService<InputMethodManager>()
|
||||||
@@ -262,32 +190,6 @@ fun Activity.hideKeyboard() {
|
|||||||
view.clearFocus()
|
view.clearFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Fragment.hideKeyboard() {
|
|
||||||
activity?.hideKeyboard()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun View.setOnViewReadyListener(callback: () -> Unit) = addOnGlobalLayoutListener(true, callback)
|
|
||||||
|
|
||||||
fun View.addOnGlobalLayoutListener(oneShot: Boolean = false, callback: () -> Unit) =
|
|
||||||
viewTreeObserver.addOnGlobalLayoutListener(object :
|
|
||||||
ViewTreeObserver.OnGlobalLayoutListener {
|
|
||||||
override fun onGlobalLayout() {
|
|
||||||
if (oneShot) viewTreeObserver.removeOnGlobalLayoutListener(this)
|
|
||||||
callback()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
fun ViewGroup.startAnimations() {
|
|
||||||
val transition = AutoTransition()
|
|
||||||
.setInterpolator(FastOutSlowInInterpolator())
|
|
||||||
.setDuration(400)
|
|
||||||
.excludeTarget(R.id.main_toolbar, true)
|
|
||||||
TransitionManager.beginDelayedTransition(
|
|
||||||
this,
|
|
||||||
transition
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val View.activity: Activity get() {
|
val View.activity: Activity get() {
|
||||||
var context = context
|
var context = context
|
||||||
while(true) {
|
while(true) {
|
||||||
@@ -308,3 +210,52 @@ fun getProperty(key: String, def: String): String {
|
|||||||
}
|
}
|
||||||
return def
|
return def
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
@Throws(PackageManager.NameNotFoundException::class)
|
||||||
|
fun PackageManager.getPackageInfo(uid: Int, pid: Int): PackageInfo? {
|
||||||
|
val flag = PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||||
|
val pkgs = getPackagesForUid(uid) ?: throw PackageManager.NameNotFoundException()
|
||||||
|
if (pkgs.size > 1) {
|
||||||
|
if (pid <= 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// Try to find package name from PID
|
||||||
|
val proc = RootUtils.obj?.getAppProcess(pid)
|
||||||
|
if (proc == null) {
|
||||||
|
if (uid == Process.SHELL_UID) {
|
||||||
|
// It is possible that some apps installed are sharing UID with shell.
|
||||||
|
// We will not be able to find a package from the active process list,
|
||||||
|
// because the client is forked from ADB shell, not any app process.
|
||||||
|
return getPackageInfo("com.android.shell", flag)
|
||||||
|
}
|
||||||
|
} else if (uid == proc.uid) {
|
||||||
|
return getPackageInfo(proc.pkgList[0], flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (pkgs.size == 1) {
|
||||||
|
return getPackageInfo(pkgs[0], flag)
|
||||||
|
}
|
||||||
|
throw PackageManager.NameNotFoundException()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.registerRuntimeReceiver(receiver: BroadcastReceiver, filter: IntentFilter) {
|
||||||
|
APKInstall.registerReceiver(this, receiver, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.selfLaunchIntent(): Intent {
|
||||||
|
val pm = packageManager
|
||||||
|
val intent = pm.getLaunchIntentForPackage(packageName)!!
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.toast(msg: CharSequence, duration: Int) {
|
||||||
|
UiThreadHandler.run { Toast.makeText(this, msg, duration).show() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.toast(resId: Int, duration: Int) {
|
||||||
|
UiThreadHandler.run { Toast.makeText(this, resId, duration).show() }
|
||||||
|
}
|
@@ -1,13 +1,17 @@
|
|||||||
package com.topjohnwu.magisk.ktx
|
package com.topjohnwu.magisk.core.ktx
|
||||||
|
|
||||||
import androidx.collection.SparseArrayCompat
|
import androidx.collection.SparseArrayCompat
|
||||||
import timber.log.Timber
|
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flatMapMerge
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
|
import java.text.DateFormat
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.Collections
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
@@ -45,8 +49,28 @@ fun <T> MutableSet<T>.synchronized(): MutableSet<T> = Collections.synchronizedSe
|
|||||||
|
|
||||||
fun <K, V> MutableMap<K, V>.synchronized(): MutableMap<K, V> = Collections.synchronizedMap(this)
|
fun <K, V> MutableMap<K, V>.synchronized(): MutableMap<K, V> = Collections.synchronizedMap(this)
|
||||||
|
|
||||||
fun SimpleDateFormat.parseOrNull(date: String) =
|
|
||||||
runCatching { parse(date) }.onFailure { Timber.e(it) }.getOrNull()
|
|
||||||
|
|
||||||
fun Class<*>.reflectField(name: String): Field =
|
fun Class<*>.reflectField(name: String): Field =
|
||||||
getDeclaredField(name).apply { isAccessible = true }
|
getDeclaredField(name).apply { isAccessible = true }
|
||||||
|
|
||||||
|
inline fun <T, R> Flow<T>.concurrentMap(crossinline transform: suspend (T) -> R): Flow<R> {
|
||||||
|
return flatMapMerge { value ->
|
||||||
|
flow { emit(transform(value)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Long.toTime(format: DateFormat) = format.format(this).orEmpty()
|
||||||
|
|
||||||
|
// Some devices don't allow filenames containing ":"
|
||||||
|
val timeFormatStandard by lazy {
|
||||||
|
SimpleDateFormat(
|
||||||
|
"yyyy-MM-dd'T'HH.mm.ss",
|
||||||
|
currentLocale
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val timeDateFormat: DateFormat by lazy {
|
||||||
|
DateFormat.getDateTimeInstance(
|
||||||
|
DateFormat.DEFAULT,
|
||||||
|
DateFormat.DEFAULT,
|
||||||
|
currentLocale
|
||||||
|
)
|
||||||
|
}
|
@@ -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,63 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.magiskdb
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.core.Const
|
|
||||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
|
||||||
import com.topjohnwu.magisk.core.model.su.toPolicy
|
|
||||||
import com.topjohnwu.magisk.di.AppContext
|
|
||||||
import com.topjohnwu.magisk.ktx.now
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
|
|
||||||
class PolicyDao : BaseDao() {
|
|
||||||
|
|
||||||
override val table: String = Table.POLICY
|
|
||||||
|
|
||||||
suspend fun deleteOutdated() = buildQuery<Delete> {
|
|
||||||
condition {
|
|
||||||
greaterThan("until", "0")
|
|
||||||
and {
|
|
||||||
lessThan("until", TimeUnit.MILLISECONDS.toSeconds(now).toString())
|
|
||||||
}
|
|
||||||
or {
|
|
||||||
lessThan("until", "0")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.commit()
|
|
||||||
|
|
||||||
suspend fun delete(uid: Int) = buildQuery<Delete> {
|
|
||||||
condition {
|
|
||||||
equals("uid", uid)
|
|
||||||
}
|
|
||||||
}.commit()
|
|
||||||
|
|
||||||
suspend fun fetch(uid: Int) = buildQuery<Select> {
|
|
||||||
condition {
|
|
||||||
equals("uid", uid)
|
|
||||||
}
|
|
||||||
}.query().first().toPolicyOrNull()
|
|
||||||
|
|
||||||
suspend fun update(policy: SuPolicy) = buildQuery<Replace> {
|
|
||||||
values(policy.toMap())
|
|
||||||
}.commit()
|
|
||||||
|
|
||||||
suspend fun <R: Any> fetchAll(mapper: (SuPolicy) -> R) = buildQuery<Select> {
|
|
||||||
condition {
|
|
||||||
equals("uid/100000", Const.USER_ID)
|
|
||||||
}
|
|
||||||
}.query {
|
|
||||||
it.toPolicyOrNull()?.let(mapper)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Map<String, String>.toPolicyOrNull(): SuPolicy? {
|
|
||||||
return runCatching { toPolicy(AppContext.packageManager) }.getOrElse {
|
|
||||||
Timber.w(it)
|
|
||||||
val uid = getOrElse("uid") { return null }
|
|
||||||
GlobalScope.launch { delete(uid.toInt()) }
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,161 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.magiskdb
|
|
||||||
|
|
||||||
import androidx.annotation.StringDef
|
|
||||||
import com.topjohnwu.magisk.ktx.await
|
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class Query(private val _query: String) {
|
|
||||||
val query get() = "magisk --sqlite '$_query'"
|
|
||||||
|
|
||||||
interface Builder {
|
|
||||||
val requestType: String
|
|
||||||
var table: String
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend inline fun <R : Any> query(crossinline mapper: (Map<String, String>) -> R?): List<R> =
|
|
||||||
withContext(Dispatchers.Default) {
|
|
||||||
Shell.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
|
|
||||||
|
|
||||||
}
|
|
@@ -7,7 +7,6 @@ import kotlinx.parcelize.Parcelize
|
|||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class UpdateInfo(
|
data class UpdateInfo(
|
||||||
val magisk: MagiskJson = MagiskJson(),
|
val magisk: MagiskJson = MagiskJson(),
|
||||||
val stub: StubJson = StubJson()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@@ -19,13 +18,6 @@ data class MagiskJson(
|
|||||||
val note: String = ""
|
val note: String = ""
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class StubJson(
|
|
||||||
val versionCode: Int = -1,
|
|
||||||
val link: String = ""
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class ModuleJson(
|
data class ModuleJson(
|
||||||
val version: String,
|
val version: String,
|
||||||
|
@@ -2,9 +2,9 @@ package com.topjohnwu.magisk.core.model.module
|
|||||||
|
|
||||||
import com.squareup.moshi.JsonDataException
|
import com.squareup.moshi.JsonDataException
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.io.SuFile
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@@ -26,13 +26,12 @@ data class LocalModule(
|
|||||||
var outdated = false
|
var outdated = false
|
||||||
|
|
||||||
private var updateUrl: String = ""
|
private var updateUrl: String = ""
|
||||||
private val removeFile = SuFile(path, "remove")
|
private val removeFile = RootUtils.fs.getFile(path, "remove")
|
||||||
private val disableFile = SuFile(path, "disable")
|
private val disableFile = RootUtils.fs.getFile(path, "disable")
|
||||||
private val updateFile = SuFile(path, "update")
|
private val updateFile = RootUtils.fs.getFile(path, "update")
|
||||||
private val ruleFile = SuFile(path, "sepolicy.rule")
|
private val riruFolder = RootUtils.fs.getFile(path, "riru")
|
||||||
private val riruFolder = SuFile(path, "riru")
|
private val zygiskFolder = RootUtils.fs.getFile(path, "zygisk")
|
||||||
private val zygiskFolder = SuFile(path, "zygisk")
|
private val unloaded = RootUtils.fs.getFile(zygiskFolder, "unloaded")
|
||||||
private val unloaded = SuFile(zygiskFolder, "unloaded")
|
|
||||||
|
|
||||||
val updated: Boolean get() = updateFile.exists()
|
val updated: Boolean get() = updateFile.exists()
|
||||||
val isRiru: Boolean get() = (id == "riru-core") || riruFolder.exists()
|
val isRiru: Boolean get() = (id == "riru-core") || riruFolder.exists()
|
||||||
@@ -42,19 +41,12 @@ data class LocalModule(
|
|||||||
var enable: Boolean
|
var enable: Boolean
|
||||||
get() = !disableFile.exists()
|
get() = !disableFile.exists()
|
||||||
set(enable) {
|
set(enable) {
|
||||||
val dir = "$PERSIST/$id"
|
|
||||||
if (enable) {
|
if (enable) {
|
||||||
disableFile.delete()
|
disableFile.delete()
|
||||||
if (Const.Version.atLeast_21_2())
|
Shell.cmd("copy_preinit_files").submit()
|
||||||
Shell.su("copy_sepolicy_rules").submit()
|
|
||||||
else
|
|
||||||
Shell.su("mkdir -p $dir", "cp -af $ruleFile $dir").submit()
|
|
||||||
} else {
|
} else {
|
||||||
!disableFile.createNewFile()
|
!disableFile.createNewFile()
|
||||||
if (Const.Version.atLeast_21_2())
|
Shell.cmd("copy_preinit_files").submit()
|
||||||
Shell.su("copy_sepolicy_rules").submit()
|
|
||||||
else
|
|
||||||
Shell.su("rm -rf $dir").submit()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,16 +56,10 @@ data class LocalModule(
|
|||||||
if (remove) {
|
if (remove) {
|
||||||
if (updateFile.exists()) return
|
if (updateFile.exists()) return
|
||||||
removeFile.createNewFile()
|
removeFile.createNewFile()
|
||||||
if (Const.Version.atLeast_21_2())
|
Shell.cmd("copy_preinit_files").submit()
|
||||||
Shell.su("copy_sepolicy_rules").submit()
|
|
||||||
else
|
|
||||||
Shell.su("rm -rf $PERSIST/$id").submit()
|
|
||||||
} else {
|
} else {
|
||||||
removeFile.delete()
|
removeFile.delete()
|
||||||
if (Const.Version.atLeast_21_2())
|
Shell.cmd("copy_preinit_files").submit()
|
||||||
Shell.su("copy_sepolicy_rules").submit()
|
|
||||||
else
|
|
||||||
Shell.su("cp -af $ruleFile $PERSIST/$id").submit()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +89,7 @@ data class LocalModule(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
runCatching {
|
runCatching {
|
||||||
parseProps(Shell.su("dos2unix < $path/module.prop").exec().out)
|
parseProps(Shell.cmd("dos2unix < $path/module.prop").exec().out)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id.isEmpty()) {
|
if (id.isEmpty()) {
|
||||||
@@ -136,13 +122,13 @@ data class LocalModule(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private val PERSIST get() = "${Const.MAGISKTMP}/mirror/persist/magisk"
|
fun loaded() = RootUtils.fs.getFile(Const.MAGISK_PATH).exists()
|
||||||
|
|
||||||
suspend fun installed() = withContext(Dispatchers.IO) {
|
suspend fun installed() = withContext(Dispatchers.IO) {
|
||||||
SuFile(Const.MAGISK_PATH)
|
RootUtils.fs.getFile(Const.MAGISK_PATH)
|
||||||
.listFiles()
|
.listFiles()
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
.filter { !it.isFile }
|
.filter { !it.isFile && !it.isHidden }
|
||||||
.map { LocalModule("${Const.MAGISK_PATH}/${it.name}") }
|
.map { LocalModule("${Const.MAGISK_PATH}/${it.name}") }
|
||||||
.sortedBy { it.name.lowercase(Locale.ROOT) }
|
.sortedBy { it.name.lowercase(Locale.ROOT) }
|
||||||
}
|
}
|
||||||
|
@@ -1,11 +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.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import com.topjohnwu.magisk.ktx.now
|
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||||
|
|
||||||
@Entity(tableName = "logs")
|
@Entity(tableName = "logs")
|
||||||
data class SuLog(
|
class SuLog(
|
||||||
val fromUid: Int,
|
val fromUid: Int,
|
||||||
val toUid: Int,
|
val toUid: Int,
|
||||||
val fromPid: Int,
|
val fromPid: Int,
|
||||||
@@ -13,7 +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 = now
|
val time: Long = System.currentTimeMillis()
|
||||||
) {
|
) {
|
||||||
@PrimaryKey(autoGenerate = true) var id: Int = 0
|
@PrimaryKey(autoGenerate = true) var id: Int = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun PackageManager.createSuLog(
|
||||||
|
info: PackageInfo,
|
||||||
|
toUid: Int,
|
||||||
|
fromPid: Int,
|
||||||
|
command: String,
|
||||||
|
policy: Int
|
||||||
|
): 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,85 +1,22 @@
|
|||||||
@file:SuppressLint("InlinedApi")
|
|
||||||
|
|
||||||
package com.topjohnwu.magisk.core.model.su
|
package com.topjohnwu.magisk.core.model.su
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
class SuPolicy(val uid: Int) {
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.INTERACTIVE
|
|
||||||
import com.topjohnwu.magisk.ktx.getLabel
|
|
||||||
|
|
||||||
data class SuPolicy(
|
|
||||||
val uid: Int,
|
|
||||||
val packageName: String,
|
|
||||||
val appName: String,
|
|
||||||
val icon: Drawable,
|
|
||||||
var policy: Int = INTERACTIVE,
|
|
||||||
var until: Long = -1L,
|
|
||||||
val logging: Boolean = true,
|
|
||||||
val notification: Boolean = true
|
|
||||||
) {
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val INTERACTIVE = 0
|
const val INTERACTIVE = 0
|
||||||
const val DENY = 1
|
const val DENY = 1
|
||||||
const val ALLOW = 2
|
const val ALLOW = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toLog(toUid: Int, fromPid: Int, command: String) = SuLog(
|
var policy: Int = INTERACTIVE
|
||||||
uid, toUid, fromPid, packageName, appName,
|
var until: Long = -1L
|
||||||
command, policy == ALLOW)
|
var logging: Boolean = true
|
||||||
|
var notification: Boolean = true
|
||||||
|
|
||||||
fun toMap() = mapOf(
|
fun toMap(): MutableMap<String, Any> = mutableMapOf(
|
||||||
"uid" to uid,
|
"uid" to uid,
|
||||||
"package_name" to packageName,
|
|
||||||
"policy" to policy,
|
"policy" to policy,
|
||||||
"until" to until,
|
"until" to until,
|
||||||
"logging" to logging,
|
"logging" to logging,
|
||||||
"notification" to notification
|
"notification" to notification
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(PackageManager.NameNotFoundException::class)
|
|
||||||
fun Map<String, String>.toPolicy(pm: PackageManager): SuPolicy {
|
|
||||||
val uid = get("uid")?.toIntOrNull() ?: -1
|
|
||||||
val packageName = get("package_name").orEmpty()
|
|
||||||
val info = pm.getApplicationInfo(packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES)
|
|
||||||
|
|
||||||
if (info.uid != uid)
|
|
||||||
throw PackageManager.NameNotFoundException()
|
|
||||||
|
|
||||||
return SuPolicy(
|
|
||||||
uid = uid,
|
|
||||||
packageName = packageName,
|
|
||||||
appName = info.getLabel(pm),
|
|
||||||
icon = info.loadIcon(pm),
|
|
||||||
policy = get("policy")?.toIntOrNull() ?: INTERACTIVE,
|
|
||||||
until = get("until")?.toLongOrNull() ?: -1L,
|
|
||||||
logging = get("logging")?.toIntOrNull() != 0,
|
|
||||||
notification = get("notification")?.toIntOrNull() != 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(PackageManager.NameNotFoundException::class)
|
|
||||||
fun Int.toPolicy(pm: PackageManager, policy: Int = INTERACTIVE): SuPolicy {
|
|
||||||
val pkg = pm.getPackagesForUid(this)?.firstOrNull()
|
|
||||||
?: throw PackageManager.NameNotFoundException()
|
|
||||||
val info = pm.getApplicationInfo(pkg, PackageManager.MATCH_UNINSTALLED_PACKAGES)
|
|
||||||
return SuPolicy(
|
|
||||||
uid = info.uid,
|
|
||||||
packageName = pkg,
|
|
||||||
appName = info.getLabel(pm),
|
|
||||||
icon = info.loadIcon(pm),
|
|
||||||
policy = policy
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Int.toUidPolicy(pm: PackageManager, policy: Int): SuPolicy {
|
|
||||||
return SuPolicy(
|
|
||||||
uid = this,
|
|
||||||
packageName = "[UID] $this",
|
|
||||||
appName = "[UID] $this",
|
|
||||||
icon = pm.defaultActivityIcon,
|
|
||||||
policy = policy
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
package com.topjohnwu.magisk.data.repository
|
package com.topjohnwu.magisk.core.repository
|
||||||
|
|
||||||
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
|
import com.topjohnwu.magisk.core.data.magiskdb.SettingsDao
|
||||||
import com.topjohnwu.magisk.core.magiskdb.StringDao
|
import com.topjohnwu.magisk.core.data.magiskdb.StringDao
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlin.properties.ReadWriteProperty
|
import kotlin.properties.ReadWriteProperty
|
||||||
@@ -11,26 +11,27 @@ import kotlin.reflect.KProperty
|
|||||||
interface DBConfig {
|
interface DBConfig {
|
||||||
val settingsDB: SettingsDao
|
val settingsDB: SettingsDao
|
||||||
val stringDB: StringDao
|
val stringDB: StringDao
|
||||||
|
val coroutineScope: CoroutineScope
|
||||||
|
|
||||||
fun dbSettings(
|
fun dbSettings(
|
||||||
name: String,
|
name: String,
|
||||||
default: Int
|
default: Int
|
||||||
) = DBSettingsValue(name, default)
|
) = IntDBProperty(name, default)
|
||||||
|
|
||||||
fun dbSettings(
|
fun dbSettings(
|
||||||
name: String,
|
name: String,
|
||||||
default: Boolean
|
default: Boolean
|
||||||
) = DBBoolSettings(name, default)
|
) = BoolDBProperty(name, default)
|
||||||
|
|
||||||
fun dbStrings(
|
fun dbStrings(
|
||||||
name: String,
|
name: String,
|
||||||
default: String,
|
default: String,
|
||||||
sync: Boolean = false
|
sync: Boolean = false
|
||||||
) = DBStringsValue(name, default, sync)
|
) = StringDBProperty(name, default, sync)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class DBSettingsValue(
|
class IntDBProperty(
|
||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: Int
|
private val default: Int
|
||||||
) : ReadWriteProperty<DBConfig, Int> {
|
) : ReadWriteProperty<DBConfig, Int> {
|
||||||
@@ -48,18 +49,18 @@ class DBSettingsValue(
|
|||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
this.value = value
|
this.value = value
|
||||||
}
|
}
|
||||||
GlobalScope.launch {
|
thisRef.coroutineScope.launch {
|
||||||
thisRef.settingsDB.put(name, value)
|
thisRef.settingsDB.put(name, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open class DBBoolSettings(
|
open class BoolDBProperty(
|
||||||
name: String,
|
name: String,
|
||||||
default: Boolean
|
default: Boolean
|
||||||
) : ReadWriteProperty<DBConfig, Boolean> {
|
) : ReadWriteProperty<DBConfig, Boolean> {
|
||||||
|
|
||||||
val base = DBSettingsValue(name, if (default) 1 else 0)
|
val base = IntDBProperty(name, if (default) 1 else 0)
|
||||||
|
|
||||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Boolean =
|
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Boolean =
|
||||||
base.getValue(thisRef, property) != 0
|
base.getValue(thisRef, property) != 0
|
||||||
@@ -68,10 +69,10 @@ open class DBBoolSettings(
|
|||||||
base.setValue(thisRef, property, if (value) 1 else 0)
|
base.setValue(thisRef, property, if (value) 1 else 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
class DBBoolSettingsNoWrite(
|
class BoolDBPropertyNoWrite(
|
||||||
name: String,
|
name: String,
|
||||||
default: Boolean
|
default: Boolean
|
||||||
) : DBBoolSettings(name, default) {
|
) : BoolDBProperty(name, default) {
|
||||||
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Boolean) {
|
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Boolean) {
|
||||||
synchronized(base) {
|
synchronized(base) {
|
||||||
base.value = if (value) 1 else 0
|
base.value = if (value) 1 else 0
|
||||||
@@ -79,7 +80,7 @@ class DBBoolSettingsNoWrite(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DBStringsValue(
|
class StringDBProperty(
|
||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: String,
|
private val default: String,
|
||||||
private val sync: Boolean
|
private val sync: Boolean
|
||||||
@@ -106,7 +107,7 @@ class DBStringsValue(
|
|||||||
thisRef.stringDB.delete(name)
|
thisRef.stringDB.delete(name)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
GlobalScope.launch {
|
thisRef.coroutineScope.launch {
|
||||||
thisRef.stringDB.delete(name)
|
thisRef.stringDB.delete(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +117,7 @@ class DBStringsValue(
|
|||||||
thisRef.stringDB.put(name, value)
|
thisRef.stringDB.put(name, value)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
GlobalScope.launch {
|
thisRef.coroutineScope.launch {
|
||||||
thisRef.stringDB.put(name, value)
|
thisRef.stringDB.put(name, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,10 +1,10 @@
|
|||||||
package com.topjohnwu.magisk.data.repository
|
package com.topjohnwu.magisk.core.repository
|
||||||
|
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
import com.topjohnwu.magisk.core.data.SuLogDao
|
||||||
|
import com.topjohnwu.magisk.core.ktx.await
|
||||||
import com.topjohnwu.magisk.core.model.su.SuLog
|
import com.topjohnwu.magisk.core.model.su.SuLog
|
||||||
import com.topjohnwu.magisk.data.database.SuLogDao
|
|
||||||
import com.topjohnwu.magisk.ktx.await
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
|
|
||||||
|
|
||||||
@@ -29,9 +29,9 @@ class LogRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Info.env.isActive) {
|
if (Info.env.isActive) {
|
||||||
Shell.su("cat ${Const.MAGISK_LOG} || logcat -d -s Magisk").to(list).await()
|
Shell.cmd("cat ${Const.MAGISK_LOG} || logcat -d -s Magisk").to(list).await()
|
||||||
} else {
|
} else {
|
||||||
Shell.sh("logcat -d").to(list).await()
|
Shell.cmd("logcat -d").to(list).await()
|
||||||
}
|
}
|
||||||
return list.buf.toString()
|
return list.buf.toString()
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ class LogRepository(
|
|||||||
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)
|
||||||
|
|
@@ -1,35 +1,34 @@
|
|||||||
package com.topjohnwu.magisk.data.repository
|
package com.topjohnwu.magisk.core.repository
|
||||||
|
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.Config.Value.BETA_CHANNEL
|
import com.topjohnwu.magisk.core.Config.Value.BETA_CHANNEL
|
||||||
import com.topjohnwu.magisk.core.Config.Value.CANARY_CHANNEL
|
import com.topjohnwu.magisk.core.Config.Value.CANARY_CHANNEL
|
||||||
import com.topjohnwu.magisk.core.Config.Value.CUSTOM_CHANNEL
|
import com.topjohnwu.magisk.core.Config.Value.CUSTOM_CHANNEL
|
||||||
|
import com.topjohnwu.magisk.core.Config.Value.DEBUG_CHANNEL
|
||||||
import com.topjohnwu.magisk.core.Config.Value.DEFAULT_CHANNEL
|
import com.topjohnwu.magisk.core.Config.Value.DEFAULT_CHANNEL
|
||||||
import com.topjohnwu.magisk.core.Config.Value.STABLE_CHANNEL
|
import com.topjohnwu.magisk.core.Config.Value.STABLE_CHANNEL
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.data.network.GithubApiServices
|
import com.topjohnwu.magisk.core.data.GithubPageServices
|
||||||
import com.topjohnwu.magisk.data.network.GithubPageServices
|
import com.topjohnwu.magisk.core.data.RawServices
|
||||||
import com.topjohnwu.magisk.data.network.RawServices
|
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class NetworkService(
|
class NetworkService(
|
||||||
private val pages: GithubPageServices,
|
private val pages: GithubPageServices,
|
||||||
private val raw: RawServices,
|
private val raw: RawServices
|
||||||
private val api: GithubApiServices
|
|
||||||
) {
|
) {
|
||||||
suspend fun fetchUpdate() = safe {
|
suspend fun fetchUpdate() = safe {
|
||||||
var info = when (Config.updateChannel) {
|
var info = when (Config.updateChannel) {
|
||||||
DEFAULT_CHANNEL, STABLE_CHANNEL -> fetchStableUpdate()
|
DEFAULT_CHANNEL, STABLE_CHANNEL -> fetchStableUpdate()
|
||||||
BETA_CHANNEL -> fetchBetaUpdate()
|
BETA_CHANNEL -> fetchBetaUpdate()
|
||||||
CANARY_CHANNEL -> fetchCanaryUpdate()
|
CANARY_CHANNEL -> fetchCanaryUpdate()
|
||||||
|
DEBUG_CHANNEL -> fetchDebugUpdate()
|
||||||
CUSTOM_CHANNEL -> fetchCustomUpdate(Config.customChannelUrl)
|
CUSTOM_CHANNEL -> fetchCustomUpdate(Config.customChannelUrl)
|
||||||
else -> throw IllegalArgumentException()
|
else -> throw IllegalArgumentException()
|
||||||
}
|
}
|
||||||
if (info.magisk.versionCode < Info.env.versionCode &&
|
if (info.magisk.versionCode < Info.env.versionCode &&
|
||||||
Config.updateChannel == DEFAULT_CHANNEL
|
Config.updateChannel == DEFAULT_CHANNEL) {
|
||||||
) {
|
|
||||||
Config.updateChannel = BETA_CHANNEL
|
Config.updateChannel = BETA_CHANNEL
|
||||||
info = fetchBetaUpdate()
|
info = fetchBetaUpdate()
|
||||||
}
|
}
|
||||||
@@ -40,11 +39,15 @@ class NetworkService(
|
|||||||
private suspend fun fetchStableUpdate() = pages.fetchUpdateJSON("stable.json")
|
private suspend fun fetchStableUpdate() = pages.fetchUpdateJSON("stable.json")
|
||||||
private suspend fun fetchBetaUpdate() = pages.fetchUpdateJSON("beta.json")
|
private suspend fun fetchBetaUpdate() = pages.fetchUpdateJSON("beta.json")
|
||||||
private suspend fun fetchCanaryUpdate() = pages.fetchUpdateJSON("canary.json")
|
private suspend fun fetchCanaryUpdate() = pages.fetchUpdateJSON("canary.json")
|
||||||
private suspend fun fetchCustomUpdate(url: String) = raw.fetchCustomUpdate(url)
|
private suspend fun fetchDebugUpdate() = pages.fetchUpdateJSON("debug.json")
|
||||||
|
private suspend fun fetchCustomUpdate(url: String) = pages.fetchUpdateJSON(url)
|
||||||
|
|
||||||
private inline fun <T> safe(factory: () -> T): T? {
|
private inline fun <T> safe(factory: () -> T): T? {
|
||||||
return try {
|
return try {
|
||||||
factory()
|
if (Info.isConnected.value == true)
|
||||||
|
factory()
|
||||||
|
else
|
||||||
|
null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
null
|
null
|
@@ -1,11 +1,73 @@
|
|||||||
package com.topjohnwu.magisk.data.preference
|
package com.topjohnwu.magisk.core.repository
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import kotlin.properties.ReadWriteProperty
|
import kotlin.properties.ReadWriteProperty
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
abstract class Property {
|
interface PreferenceConfig {
|
||||||
|
|
||||||
|
val context: Context
|
||||||
|
|
||||||
|
val fileName: String
|
||||||
|
get() = "${context.packageName}_preferences"
|
||||||
|
|
||||||
|
val prefs: SharedPreferences
|
||||||
|
get() = context.getSharedPreferences(fileName, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
fun preferenceStrInt(
|
||||||
|
name: String,
|
||||||
|
default: Int,
|
||||||
|
commit: Boolean = false
|
||||||
|
) = object: ReadWriteProperty<PreferenceConfig, Int> {
|
||||||
|
val base = StringProperty(name, default.toString(), commit)
|
||||||
|
override fun getValue(thisRef: PreferenceConfig, property: KProperty<*>): Int =
|
||||||
|
base.getValue(thisRef, property).toInt()
|
||||||
|
|
||||||
|
override fun setValue(thisRef: PreferenceConfig, property: KProperty<*>, value: Int) =
|
||||||
|
base.setValue(thisRef, property, value.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun preference(
|
||||||
|
name: String,
|
||||||
|
default: Boolean,
|
||||||
|
commit: Boolean = false
|
||||||
|
) = BooleanProperty(name, default, commit)
|
||||||
|
|
||||||
|
fun preference(
|
||||||
|
name: String,
|
||||||
|
default: Float,
|
||||||
|
commit: Boolean = false
|
||||||
|
) = FloatProperty(name, default, commit)
|
||||||
|
|
||||||
|
fun preference(
|
||||||
|
name: String,
|
||||||
|
default: Int,
|
||||||
|
commit: Boolean = false
|
||||||
|
) = IntProperty(name, default, commit)
|
||||||
|
|
||||||
|
fun preference(
|
||||||
|
name: String,
|
||||||
|
default: Long,
|
||||||
|
commit: Boolean = false
|
||||||
|
) = LongProperty(name, default, commit)
|
||||||
|
|
||||||
|
fun preference(
|
||||||
|
name: String,
|
||||||
|
default: String,
|
||||||
|
commit: Boolean = false
|
||||||
|
) = StringProperty(name, default, commit)
|
||||||
|
|
||||||
|
fun preference(
|
||||||
|
name: String,
|
||||||
|
default: Set<String>,
|
||||||
|
commit: Boolean = false
|
||||||
|
) = StringSetProperty(name, default, commit)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class PreferenceProperty {
|
||||||
|
|
||||||
fun SharedPreferences.Editor.put(name: String, value: Boolean) = putBoolean(name, value)
|
fun SharedPreferences.Editor.put(name: String, value: Boolean) = putBoolean(name, value)
|
||||||
fun SharedPreferences.Editor.put(name: String, value: Float) = putFloat(name, value)
|
fun SharedPreferences.Editor.put(name: String, value: Float) = putFloat(name, value)
|
||||||
@@ -27,10 +89,10 @@ class BooleanProperty(
|
|||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: Boolean,
|
private val default: Boolean,
|
||||||
private val commit: Boolean
|
private val commit: Boolean
|
||||||
) : Property(), ReadWriteProperty<PreferenceModel, Boolean> {
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Boolean> {
|
||||||
|
|
||||||
override operator fun getValue(
|
override operator fun getValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>
|
property: KProperty<*>
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val prefName = name.ifBlank { property.name }
|
val prefName = name.ifBlank { property.name }
|
||||||
@@ -38,7 +100,7 @@ class BooleanProperty(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override operator fun setValue(
|
override operator fun setValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>,
|
property: KProperty<*>,
|
||||||
value: Boolean
|
value: Boolean
|
||||||
) {
|
) {
|
||||||
@@ -51,10 +113,10 @@ class FloatProperty(
|
|||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: Float,
|
private val default: Float,
|
||||||
private val commit: Boolean
|
private val commit: Boolean
|
||||||
) : Property(), ReadWriteProperty<PreferenceModel, Float> {
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Float> {
|
||||||
|
|
||||||
override operator fun getValue(
|
override operator fun getValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>
|
property: KProperty<*>
|
||||||
): Float {
|
): Float {
|
||||||
val prefName = name.ifBlank { property.name }
|
val prefName = name.ifBlank { property.name }
|
||||||
@@ -62,7 +124,7 @@ class FloatProperty(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override operator fun setValue(
|
override operator fun setValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>,
|
property: KProperty<*>,
|
||||||
value: Float
|
value: Float
|
||||||
) {
|
) {
|
||||||
@@ -75,10 +137,10 @@ class IntProperty(
|
|||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: Int,
|
private val default: Int,
|
||||||
private val commit: Boolean
|
private val commit: Boolean
|
||||||
) : Property(), ReadWriteProperty<PreferenceModel, Int> {
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Int> {
|
||||||
|
|
||||||
override operator fun getValue(
|
override operator fun getValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>
|
property: KProperty<*>
|
||||||
): Int {
|
): Int {
|
||||||
val prefName = name.ifBlank { property.name }
|
val prefName = name.ifBlank { property.name }
|
||||||
@@ -86,7 +148,7 @@ class IntProperty(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override operator fun setValue(
|
override operator fun setValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>,
|
property: KProperty<*>,
|
||||||
value: Int
|
value: Int
|
||||||
) {
|
) {
|
||||||
@@ -99,10 +161,10 @@ class LongProperty(
|
|||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: Long,
|
private val default: Long,
|
||||||
private val commit: Boolean
|
private val commit: Boolean
|
||||||
) : Property(), ReadWriteProperty<PreferenceModel, Long> {
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Long> {
|
||||||
|
|
||||||
override operator fun getValue(
|
override operator fun getValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>
|
property: KProperty<*>
|
||||||
): Long {
|
): Long {
|
||||||
val prefName = name.ifBlank { property.name }
|
val prefName = name.ifBlank { property.name }
|
||||||
@@ -110,7 +172,7 @@ class LongProperty(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override operator fun setValue(
|
override operator fun setValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>,
|
property: KProperty<*>,
|
||||||
value: Long
|
value: Long
|
||||||
) {
|
) {
|
||||||
@@ -123,10 +185,10 @@ class StringProperty(
|
|||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: String,
|
private val default: String,
|
||||||
private val commit: Boolean
|
private val commit: Boolean
|
||||||
) : Property(), ReadWriteProperty<PreferenceModel, String> {
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, String> {
|
||||||
|
|
||||||
override operator fun getValue(
|
override operator fun getValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>
|
property: KProperty<*>
|
||||||
): String {
|
): String {
|
||||||
val prefName = name.ifBlank { property.name }
|
val prefName = name.ifBlank { property.name }
|
||||||
@@ -134,7 +196,7 @@ class StringProperty(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override operator fun setValue(
|
override operator fun setValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>,
|
property: KProperty<*>,
|
||||||
value: String
|
value: String
|
||||||
) {
|
) {
|
||||||
@@ -147,10 +209,10 @@ class StringSetProperty(
|
|||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: Set<String>,
|
private val default: Set<String>,
|
||||||
private val commit: Boolean
|
private val commit: Boolean
|
||||||
) : Property(), ReadWriteProperty<PreferenceModel, Set<String>> {
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Set<String>> {
|
||||||
|
|
||||||
override operator fun getValue(
|
override operator fun getValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>
|
property: KProperty<*>
|
||||||
): Set<String> {
|
): Set<String> {
|
||||||
val prefName = name.ifBlank { property.name }
|
val prefName = name.ifBlank { property.name }
|
||||||
@@ -158,7 +220,7 @@ class StringSetProperty(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override operator fun setValue(
|
override operator fun setValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>,
|
property: KProperty<*>,
|
||||||
value: Set<String>
|
value: Set<String>
|
||||||
) {
|
) {
|
@@ -6,13 +6,13 @@ import android.widget.Toast
|
|||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||||
|
import com.topjohnwu.magisk.core.ktx.getPackageInfo
|
||||||
|
import com.topjohnwu.magisk.core.ktx.toast
|
||||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||||
import com.topjohnwu.magisk.core.model.su.toPolicy
|
import com.topjohnwu.magisk.core.model.su.createSuLog
|
||||||
import com.topjohnwu.magisk.core.model.su.toUidPolicy
|
import kotlinx.coroutines.runBlocking
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
object SuCallbackHandler {
|
object SuCallbackHandler {
|
||||||
@@ -53,59 +53,47 @@ object SuCallbackHandler {
|
|||||||
private fun handleLogging(context: Context, data: Bundle) {
|
private fun handleLogging(context: Context, data: Bundle) {
|
||||||
val fromUid = data.getIntComp("from.uid", -1)
|
val fromUid = data.getIntComp("from.uid", -1)
|
||||||
val notify = data.getBoolean("notify", true)
|
val notify = data.getBoolean("notify", true)
|
||||||
val allow = data.getIntComp("policy", SuPolicy.ALLOW)
|
val policy = data.getIntComp("policy", SuPolicy.ALLOW)
|
||||||
|
val toUid = data.getIntComp("to.uid", -1)
|
||||||
|
val pid = data.getIntComp("pid", -1)
|
||||||
|
val command = data.getString("command", "")
|
||||||
|
|
||||||
val pm = context.packageManager
|
val pm = context.packageManager
|
||||||
|
|
||||||
val policy = runCatching {
|
val log = runCatching {
|
||||||
fromUid.toPolicy(pm, allow)
|
pm.getPackageInfo(fromUid, pid)?.let {
|
||||||
}.getOrElse {
|
pm.createSuLog(it, toUid, pid, command, policy)
|
||||||
GlobalScope.launch { ServiceLocator.policyDB.delete(fromUid) }
|
}
|
||||||
fromUid.toUidPolicy(pm, allow)
|
}.getOrNull() ?: createSuLog(fromUid, toUid, pid, command, policy)
|
||||||
}
|
|
||||||
|
|
||||||
if (notify)
|
if (notify)
|
||||||
notify(context, policy)
|
notify(context, log.action, log.appName)
|
||||||
|
|
||||||
val toUid = data.getIntComp("to.uid", -1)
|
runBlocking { ServiceLocator.logRepo.insert(log) }
|
||||||
val pid = data.getIntComp("pid", -1)
|
|
||||||
|
|
||||||
val command = data.getString("command", "")
|
|
||||||
val log = policy.toLog(
|
|
||||||
toUid = toUid,
|
|
||||||
fromPid = pid,
|
|
||||||
command = command
|
|
||||||
)
|
|
||||||
|
|
||||||
GlobalScope.launch {
|
|
||||||
ServiceLocator.logRepo.insert(log)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleNotify(context: Context, data: Bundle) {
|
private fun handleNotify(context: Context, data: Bundle) {
|
||||||
val fromUid = data.getIntComp("from.uid", -1)
|
val uid = data.getIntComp("from.uid", -1)
|
||||||
val allow = data.getIntComp("policy", SuPolicy.ALLOW)
|
val pid = data.getIntComp("pid", -1)
|
||||||
|
val policy = data.getIntComp("policy", SuPolicy.ALLOW)
|
||||||
|
|
||||||
val pm = context.packageManager
|
val pm = context.packageManager
|
||||||
|
|
||||||
|
val appName = runCatching {
|
||||||
|
pm.getPackageInfo(uid, pid)?.applicationInfo?.getLabel(pm)
|
||||||
|
}.getOrNull() ?: "[UID] $uid"
|
||||||
|
|
||||||
val policy = runCatching {
|
notify(context, policy == SuPolicy.ALLOW, appName)
|
||||||
fromUid.toPolicy(pm, allow)
|
|
||||||
}.getOrElse {
|
|
||||||
GlobalScope.launch { ServiceLocator.policyDB.delete(fromUid) }
|
|
||||||
fromUid.toUidPolicy(pm, allow)
|
|
||||||
}
|
|
||||||
notify(context, policy)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notify(context: Context, policy: SuPolicy) {
|
private fun notify(context: Context, granted: Boolean, appName: String) {
|
||||||
if (Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
|
if (Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
|
||||||
val resId = if (policy.policy == SuPolicy.ALLOW)
|
val resId = if (granted)
|
||||||
R.string.su_allow_toast
|
R.string.su_allow_toast
|
||||||
else
|
else
|
||||||
R.string.su_deny_toast
|
R.string.su_deny_toast
|
||||||
|
|
||||||
Utils.toast(context.getString(resId, policy.appName), Toast.LENGTH_SHORT)
|
context.toast(context.getString(resId, appName), Toast.LENGTH_SHORT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,32 +1,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 com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
||||||
|
import com.topjohnwu.magisk.core.ktx.getPackageInfo
|
||||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||||
import com.topjohnwu.magisk.core.model.su.toPolicy
|
|
||||||
import com.topjohnwu.magisk.ktx.now
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.Closeable
|
|
||||||
import java.io.DataOutputStream
|
import java.io.DataOutputStream
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class SuRequestHandler(
|
class SuRequestHandler(
|
||||||
private val pm: PackageManager,
|
val pm: PackageManager,
|
||||||
private val policyDB: PolicyDao
|
private val policyDB: PolicyDao
|
||||||
) : Closeable {
|
) {
|
||||||
|
|
||||||
private lateinit var output: DataOutputStream
|
private lateinit var output: 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
|
||||||
@@ -35,8 +33,8 @@ class SuRequestHandler(
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
// Never allow com.topjohnwu.magisk (could be malware)
|
// Never allow com.topjohnwu.magisk (could be malware)
|
||||||
if (policy.packageName == BuildConfig.APPLICATION_ID) {
|
if (pkgInfo.packageName == BuildConfig.APPLICATION_ID) {
|
||||||
Shell.su("(pm uninstall ${BuildConfig.APPLICATION_ID})& >/dev/null 2>&1").exec()
|
Shell.cmd("(pm uninstall ${BuildConfig.APPLICATION_ID} >/dev/null 2>&1)&").exec()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,50 +52,57 @@ class SuRequestHandler(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
private fun close() {
|
||||||
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 name = intent.getStringExtra("fifo") ?: throw SuRequestError()
|
val fifo = intent.getStringExtra("fifo") ?: throw IOException("fifo == null")
|
||||||
val uid = intent.getIntExtra("uid", -1).also { if (it < 0) throw SuRequestError() }
|
output = DataOutputStream(FileOutputStream(fifo))
|
||||||
output = DataOutputStream(FileOutputStream(name).buffered())
|
val uid = intent.getIntExtra("uid", -1)
|
||||||
policy = uid.toPolicy(pm)
|
if (uid <= 0) {
|
||||||
true
|
throw IOException("uid == $uid")
|
||||||
} catch (e: Exception) {
|
|
||||||
when (e) {
|
|
||||||
is IOException, is PackageManager.NameNotFoundException -> {
|
|
||||||
Timber.e(e)
|
|
||||||
runCatching { close() }
|
|
||||||
false
|
|
||||||
}
|
|
||||||
else -> throw e // Unexpected error
|
|
||||||
}
|
}
|
||||||
|
policy = SuPolicy(uid)
|
||||||
|
val pid = intent.getIntExtra("pid", -1)
|
||||||
|
try {
|
||||||
|
pkgInfo = pm.getPackageInfo(uid, pid) ?: PackageInfo().apply {
|
||||||
|
val name = pm.getNameForUid(uid) ?: throw PackageManager.NameNotFoundException()
|
||||||
|
// We only fill in sharedUserId and leave other fields uninitialized
|
||||||
|
sharedUserId = name.split(":")[0]
|
||||||
|
}
|
||||||
|
return@withContext true
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
respond(SuPolicy.DENY, -1)
|
||||||
|
return@withContext false
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e(e)
|
||||||
|
close()
|
||||||
|
return@withContext false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun respond(action: Int, time: Int) {
|
suspend fun respond(action: Int, time: Int) {
|
||||||
val until = if (time > 0)
|
val until = if (time > 0)
|
||||||
TimeUnit.MILLISECONDS.toSeconds(now) + TimeUnit.MINUTES.toSeconds(time.toLong())
|
TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) +
|
||||||
|
TimeUnit.MINUTES.toSeconds(time.toLong())
|
||||||
else
|
else
|
||||||
time.toLong()
|
time.toLong()
|
||||||
|
|
||||||
policy.policy = action
|
policy.policy = action
|
||||||
policy.until = until
|
policy.until = until
|
||||||
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
output.writeInt(policy.policy)
|
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)
|
||||||
}
|
}
|
||||||
|
@@ -3,11 +3,11 @@ package com.topjohnwu.magisk.core.tasks
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
|
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||||
import com.topjohnwu.magisk.core.utils.unzip
|
import com.topjohnwu.magisk.core.utils.unzip
|
||||||
import com.topjohnwu.magisk.di.AppContext
|
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -63,7 +63,7 @@ open class FlashZip(
|
|||||||
|
|
||||||
console.add("- Installing ${mUri.displayName}")
|
console.add("- Installing ${mUri.displayName}")
|
||||||
|
|
||||||
return Shell.su("sh $installDir/update-binary dummy 1 \'$zipFile\'")
|
return Shell.cmd("sh $installDir/update-binary dummy 1 \'$zipFile\'")
|
||||||
.to(console, logs).exec().isSuccess
|
.to(console, logs).exec().isSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ open class FlashZip(
|
|||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
false
|
false
|
||||||
} finally {
|
} finally {
|
||||||
Shell.su("cd /", "rm -rf $installDir ${Const.TMPDIR}").submit()
|
Shell.cmd("cd /", "rm -rf $installDir ${Const.TMPDIR}").submit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,41 +4,42 @@ import android.app.Activity
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
||||||
import com.topjohnwu.magisk.DynAPK
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.StubApk
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.Info
|
|
||||||
import com.topjohnwu.magisk.core.Provider
|
import com.topjohnwu.magisk.core.Provider
|
||||||
|
import com.topjohnwu.magisk.core.ktx.await
|
||||||
|
import com.topjohnwu.magisk.core.ktx.toast
|
||||||
|
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||||
import com.topjohnwu.magisk.core.utils.AXML
|
import com.topjohnwu.magisk.core.utils.AXML
|
||||||
import com.topjohnwu.magisk.core.utils.Keygen
|
import com.topjohnwu.magisk.core.utils.Keygen
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
|
||||||
import com.topjohnwu.magisk.signing.JarMap
|
import com.topjohnwu.magisk.signing.JarMap
|
||||||
import com.topjohnwu.magisk.signing.SignApk
|
import com.topjohnwu.magisk.signing.SignApk
|
||||||
import com.topjohnwu.magisk.utils.APKInstall
|
import com.topjohnwu.magisk.utils.APKInstall
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Runnable
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.io.OutputStream
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
|
import kotlin.random.asKotlinRandom
|
||||||
|
|
||||||
object HideAPK {
|
object HideAPK {
|
||||||
|
|
||||||
private const val ALPHA = "abcdefghijklmnopqrstuvwxyz"
|
private const val ALPHA = "abcdefghijklmnopqrstuvwxyz"
|
||||||
private const val ALPHADOTS = "$ALPHA....."
|
private const val ALPHADOTS = "$ALPHA....."
|
||||||
private const val APP_NAME = "Magisk"
|
|
||||||
private const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
private const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
||||||
|
|
||||||
// Some arbitrary limit
|
// Some arbitrary limit
|
||||||
const val MAX_LABEL_LENGTH = 32
|
const val MAX_LABEL_LENGTH = 32
|
||||||
|
const val PLACEHOLDER = "COMPONENT_PLACEHOLDER"
|
||||||
private val svc get() = ServiceLocator.networkService
|
|
||||||
|
|
||||||
private fun genPackageName(): String {
|
private fun genPackageName(): String {
|
||||||
val random = SecureRandom()
|
val random = SecureRandom()
|
||||||
@@ -63,52 +64,117 @@ object HideAPK {
|
|||||||
return builder.toString()
|
return builder.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun patch(
|
private fun classNameGenerator() = sequence {
|
||||||
|
val c1 = mutableListOf<String>()
|
||||||
|
val c2 = mutableListOf<String>()
|
||||||
|
val c3 = mutableListOf<String>()
|
||||||
|
val random = SecureRandom()
|
||||||
|
val kRandom = random.asKotlinRandom()
|
||||||
|
|
||||||
|
fun <T> chain(vararg iters: Iterable<T>) = sequence {
|
||||||
|
iters.forEach { it.forEach { v -> yield(v) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
for (a in chain('a'..'z', 'A'..'Z')) {
|
||||||
|
if (a != 'a' && a != 'A') {
|
||||||
|
c1.add("$a")
|
||||||
|
}
|
||||||
|
for (b in chain('a'..'z', 'A'..'Z', '0'..'9')) {
|
||||||
|
c2.add("$a$b")
|
||||||
|
for (c in chain('a'..'z', 'A'..'Z', '0'..'9')) {
|
||||||
|
c3.add("$a$b$c")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c1.shuffle(random)
|
||||||
|
c2.shuffle(random)
|
||||||
|
c3.shuffle(random)
|
||||||
|
|
||||||
|
fun notJavaKeyword(name: String) = when (name) {
|
||||||
|
"do", "if", "for", "int", "new", "try" -> false
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<String>.process() = asSequence().filter(::notJavaKeyword)
|
||||||
|
|
||||||
|
val names = mutableListOf<String>()
|
||||||
|
names.addAll(c1)
|
||||||
|
names.addAll(c2.process().take(30))
|
||||||
|
names.addAll(c3.process().take(30))
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val seg = 2 + random.nextInt(4)
|
||||||
|
val cls = StringBuilder()
|
||||||
|
for (i in 0 until seg) {
|
||||||
|
cls.append(names.random(kRandom))
|
||||||
|
if (i != seg - 1)
|
||||||
|
cls.append('.')
|
||||||
|
}
|
||||||
|
// Old Android does not support capitalized package names
|
||||||
|
// Check Android 7.0.0 PackageParser#buildClassName
|
||||||
|
cls[0] = cls[0].lowercaseChar()
|
||||||
|
yield(cls.toString())
|
||||||
|
}
|
||||||
|
}.distinct().iterator()
|
||||||
|
|
||||||
|
private fun patch(
|
||||||
context: Context,
|
context: Context,
|
||||||
apk: File, out: File,
|
apk: File, out: OutputStream,
|
||||||
pkg: String, label: CharSequence
|
pkg: String, label: CharSequence
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
val info = context.packageManager.getPackageArchiveInfo(apk.path, 0) ?: return false
|
||||||
|
val origLabel = info.applicationInfo.nonLocalizedLabel.toString()
|
||||||
try {
|
try {
|
||||||
val jar = JarMap.open(apk, true)
|
JarMap.open(apk, true).use { jar ->
|
||||||
val je = jar.getJarEntry(ANDROID_MANIFEST)
|
val je = jar.getJarEntry(ANDROID_MANIFEST)
|
||||||
val xml = AXML(jar.getRawData(je))
|
val xml = AXML(jar.getRawData(je))
|
||||||
|
val generator = classNameGenerator()
|
||||||
|
|
||||||
if (!xml.findAndPatch(APPLICATION_ID to pkg, APP_NAME to label.toString()))
|
if (!xml.patchStrings {
|
||||||
return false
|
for (i in it.indices) {
|
||||||
|
val s = it[i]
|
||||||
|
if (s.contains(APPLICATION_ID)) {
|
||||||
|
it[i] = s.replace(APPLICATION_ID, pkg)
|
||||||
|
} else if (s.contains(PLACEHOLDER)) {
|
||||||
|
it[i] = generator.next()
|
||||||
|
} else if (s == origLabel) {
|
||||||
|
it[i] = label.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Write apk changes
|
// Write apk changes
|
||||||
jar.getOutputStream(je).write(xml.bytes)
|
jar.getOutputStream(je).use { it.write(xml.bytes) }
|
||||||
val keys = Keygen(context)
|
val keys = Keygen()
|
||||||
SignApk.sign(keys.cert, keys.key, jar, FileOutputStream(out))
|
SignApk.sign(keys.cert, keys.key, jar, out)
|
||||||
|
return true
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchApp(activity: Activity, pkg: String) {
|
private fun launchApp(activity: Activity, pkg: String) {
|
||||||
val intent = activity.packageManager.getLaunchIntentForPackage(pkg) ?: return
|
val intent = activity.packageManager.getLaunchIntentForPackage(pkg) ?: return
|
||||||
Config.suManager = if (pkg == APPLICATION_ID) "" else pkg
|
|
||||||
val self = activity.packageName
|
val self = activity.packageName
|
||||||
val flag = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
val flag = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
activity.grantUriPermission(pkg, Provider.APK_URI(self), flag)
|
activity.grantUriPermission(pkg, Provider.preferencesUri(self), flag)
|
||||||
activity.grantUriPermission(pkg, Provider.PREFS_URI(self), flag)
|
|
||||||
intent.putExtra(Const.Key.PREV_PKG, self)
|
intent.putExtra(Const.Key.PREV_PKG, self)
|
||||||
activity.startActivity(intent)
|
activity.startActivity(intent)
|
||||||
activity.finish()
|
activity.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun patchAndHide(activity: Activity, label: String): Boolean {
|
private fun patchAndHide(activity: Activity, label: String, onFailure: Runnable): Boolean {
|
||||||
val stub = File(activity.cacheDir, "stub.apk")
|
val stub = File(activity.cacheDir, "stub.apk")
|
||||||
try {
|
try {
|
||||||
svc.fetchFile(Info.remote.stub.link).byteStream().writeTo(stub)
|
activity.assets.open("stub.apk").writeTo(stub)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
stub.createNewFile()
|
return false
|
||||||
val cmd = "\$MAGISKBIN/magiskinit -x manager ${stub.path}"
|
|
||||||
if (!Shell.su(cmd).exec().isSuccess) return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a new random package name and signature
|
// Generate a new random package name and signature
|
||||||
@@ -116,18 +182,25 @@ object HideAPK {
|
|||||||
val pkg = genPackageName()
|
val pkg = genPackageName()
|
||||||
Config.keyStoreRaw = ""
|
Config.keyStoreRaw = ""
|
||||||
|
|
||||||
if (!patch(activity, stub, repack, pkg, label))
|
if (!patch(activity, stub, FileOutputStream(repack), pkg, label))
|
||||||
return false
|
return false
|
||||||
|
|
||||||
// Install and auto launch app
|
// Install and auto launch app
|
||||||
val receiver = APKInstall.register(activity, pkg) {
|
val session = APKInstall.startSession(activity, pkg, onFailure) {
|
||||||
launchApp(activity, pkg)
|
launchApp(activity, pkg)
|
||||||
}
|
}
|
||||||
val cmd = "adb_pm_install $repack ${activity.applicationInfo.uid}"
|
|
||||||
if (!Shell.su(cmd).exec().isSuccess) {
|
Config.suManager = pkg
|
||||||
APKInstall.installapk(activity, repack)
|
val cmd = "adb_pm_install $repack $pkg"
|
||||||
receiver.waitIntent()?.let { activity.startActivity(it) }
|
if (Shell.cmd(cmd).exec().isSuccess) return true
|
||||||
|
|
||||||
|
try {
|
||||||
|
session.install(activity, repack)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e(e)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
session.waitIntent()?.let { activity.startActivity(it) } ?: return false
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,33 +212,59 @@ object HideAPK {
|
|||||||
setCancelable(false)
|
setCancelable(false)
|
||||||
show()
|
show()
|
||||||
}
|
}
|
||||||
val result = withContext(Dispatchers.IO) {
|
val onFailure = Runnable {
|
||||||
patchAndHide(activity, label)
|
|
||||||
}
|
|
||||||
if (!result) {
|
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
Utils.toast(R.string.failure, Toast.LENGTH_LONG)
|
activity.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||||
}
|
}
|
||||||
|
val success = withContext(Dispatchers.IO) {
|
||||||
|
patchAndHide(activity, label, onFailure)
|
||||||
|
}
|
||||||
|
if (!success) onFailure.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
fun restore(activity: Activity) {
|
suspend fun restore(activity: Activity) {
|
||||||
val dialog = android.app.ProgressDialog(activity).apply {
|
val dialog = android.app.ProgressDialog(activity).apply {
|
||||||
setTitle(activity.getString(R.string.restore_img_msg))
|
setTitle(activity.getString(R.string.restore_img_msg))
|
||||||
isIndeterminate = true
|
isIndeterminate = true
|
||||||
setCancelable(false)
|
setCancelable(false)
|
||||||
show()
|
show()
|
||||||
}
|
}
|
||||||
val apk = DynAPK.current(activity)
|
val onFailure = Runnable {
|
||||||
val receiver = APKInstall.register(activity, APPLICATION_ID) {
|
dialog.dismiss()
|
||||||
|
activity.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||||
|
}
|
||||||
|
val apk = StubApk.current(activity)
|
||||||
|
val session = APKInstall.startSession(activity, APPLICATION_ID, onFailure) {
|
||||||
launchApp(activity, APPLICATION_ID)
|
launchApp(activity, APPLICATION_ID)
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
val cmd = "adb_pm_install $apk ${activity.applicationInfo.uid}"
|
Config.suManager = ""
|
||||||
Shell.su(cmd).submit(Shell.EXECUTOR) { ret ->
|
val cmd = "adb_pm_install $apk $APPLICATION_ID"
|
||||||
if (ret.isSuccess) return@submit
|
if (Shell.cmd(cmd).await().isSuccess) return
|
||||||
APKInstall.installapk(activity, apk)
|
val success = withContext(Dispatchers.IO) {
|
||||||
receiver.waitIntent()?.let { activity.startActivity(it) }
|
try {
|
||||||
|
session.install(activity, apk)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e(e)
|
||||||
|
return@withContext false
|
||||||
|
}
|
||||||
|
session.waitIntent()?.let { activity.startActivity(it) } ?: return@withContext false
|
||||||
|
return@withContext true
|
||||||
}
|
}
|
||||||
|
if (!success) onFailure.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
fun upgrade(context: Context, apk: File): Intent? {
|
||||||
|
val label = context.applicationInfo.nonLocalizedLabel
|
||||||
|
val pkg = context.packageName
|
||||||
|
val session = APKInstall.startSession(context)
|
||||||
|
session.openStream(context).use {
|
||||||
|
if (!patch(context, apk, it, pkg, label)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return session.waitIntent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,25 +6,25 @@ import android.widget.Toast
|
|||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.core.os.postDelayed
|
import androidx.core.os.postDelayed
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.DynAPK
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.StubApk
|
||||||
import com.topjohnwu.magisk.core.*
|
import com.topjohnwu.magisk.core.*
|
||||||
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.core.ktx.reboot
|
||||||
|
import com.topjohnwu.magisk.core.ktx.toast
|
||||||
|
import com.topjohnwu.magisk.core.ktx.withStreams
|
||||||
|
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||||
import com.topjohnwu.magisk.ktx.reboot
|
|
||||||
import com.topjohnwu.magisk.ktx.withStreams
|
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
|
||||||
import com.topjohnwu.magisk.signing.SignBoot
|
import com.topjohnwu.magisk.signing.SignBoot
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.ShellUtils
|
import com.topjohnwu.superuser.ShellUtils
|
||||||
import com.topjohnwu.superuser.internal.NOPList
|
import com.topjohnwu.superuser.internal.NOPList
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
import com.topjohnwu.superuser.io.SuFile
|
import com.topjohnwu.superuser.nio.ExtendedFile
|
||||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
import com.topjohnwu.superuser.nio.FileSystemManager
|
||||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import net.jpountz.lz4.LZ4FrameInputStream
|
import net.jpountz.lz4.LZ4FrameInputStream
|
||||||
@@ -37,6 +37,7 @@ import java.io.*
|
|||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
@@ -45,21 +46,24 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
private val logs: MutableList<String> = NOPList.getInstance()
|
private val logs: MutableList<String> = NOPList.getInstance()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
protected var installDir = File("xxx")
|
protected lateinit var installDir: ExtendedFile
|
||||||
private lateinit var srcBoot: File
|
private lateinit var srcBoot: ExtendedFile
|
||||||
|
|
||||||
private val shell = Shell.getShell()
|
private val shell = Shell.getShell()
|
||||||
private val service get() = ServiceLocator.networkService
|
private val service get() = ServiceLocator.networkService
|
||||||
protected val context get() = ServiceLocator.deContext
|
protected val context get() = ServiceLocator.deContext
|
||||||
private val useRootDir = shell.isRoot && Info.noDataExec
|
private val useRootDir = shell.isRoot && Info.noDataExec
|
||||||
|
|
||||||
|
private val rootFS get() = RootUtils.fs
|
||||||
|
private val localFS get() = FileSystemManager.getLocal()
|
||||||
|
|
||||||
private fun findImage(): Boolean {
|
private fun findImage(): Boolean {
|
||||||
val bootPath = "find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
|
val bootPath = "RECOVERYMODE=${Config.recovery} find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
|
||||||
if (bootPath.isEmpty()) {
|
if (bootPath.isEmpty()) {
|
||||||
console.add("! Unable to detect target image")
|
console.add("! Unable to detect target image")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
srcBoot = SuFile(bootPath)
|
srcBoot = rootFS.getFile(bootPath)
|
||||||
console.add("- Target image: $bootPath")
|
console.add("- Target image: $bootPath")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -77,7 +81,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
console.add("! Unable to detect target image")
|
console.add("! Unable to detect target image")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
srcBoot = SuFile(bootPath)
|
srcBoot = rootFS.getFile(bootPath)
|
||||||
console.add("- Target image: $bootPath")
|
console.add("- Target image: $bootPath")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -86,14 +90,14 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
console.add("- Device platform: ${Const.CPU_ABI}")
|
console.add("- Device platform: ${Const.CPU_ABI}")
|
||||||
console.add("- Installing: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
console.add("- Installing: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
||||||
|
|
||||||
installDir = File(context.filesDir.parent, "install")
|
installDir = localFS.getFile(context.filesDir.parent, "install")
|
||||||
installDir.deleteRecursively()
|
installDir.deleteRecursively()
|
||||||
installDir.mkdirs()
|
installDir.mkdirs()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Extract binaries
|
// Extract binaries
|
||||||
if (isRunningAsStub) {
|
if (isRunningAsStub) {
|
||||||
val zf = ZipFile(DynAPK.current(context))
|
val zf = ZipFile(StubApk.current(context))
|
||||||
|
|
||||||
// Also extract magisk32 on non 64-bit only 64-bit devices
|
// Also extract magisk32 on non 64-bit only 64-bit devices
|
||||||
val is32lib = Const.CPU_ABI_32?.let {
|
val is32lib = Const.CPU_ABI_32?.let {
|
||||||
@@ -127,7 +131,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract scripts
|
// Extract scripts
|
||||||
for (script in listOf("util_functions.sh", "boot_patch.sh", "addon.d.sh")) {
|
for (script in listOf("util_functions.sh", "boot_patch.sh", "addon.d.sh", "stub.apk")) {
|
||||||
val dest = File(installDir, script)
|
val dest = File(installDir, script)
|
||||||
context.assets.open(script).writeTo(dest)
|
context.assets.open(script).writeTo(dest)
|
||||||
}
|
}
|
||||||
@@ -146,7 +150,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
|
|
||||||
if (useRootDir) {
|
if (useRootDir) {
|
||||||
// Move everything to tmpfs to workaround Samsung bullshit
|
// Move everything to tmpfs to workaround Samsung bullshit
|
||||||
SuFile(Const.TMPDIR).also {
|
rootFS.getFile(Const.TMPDIR).also {
|
||||||
arrayOf(
|
arrayOf(
|
||||||
"rm -rf $it",
|
"rm -rf $it",
|
||||||
"mkdir -p $it",
|
"mkdir -p $it",
|
||||||
@@ -160,14 +164,6 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optimization for SuFile I/O streams to skip an internal trial and error
|
|
||||||
private fun installDirFile(name: String): File {
|
|
||||||
return if (useRootDir)
|
|
||||||
SuFile(installDir, name)
|
|
||||||
else
|
|
||||||
File(installDir, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun InputStream.cleanPump(out: OutputStream) = withStreams(this, out) { src, _ ->
|
private fun InputStream.cleanPump(out: OutputStream) = withStreams(this, out) { src, _ ->
|
||||||
src.copyTo(out)
|
src.copyTo(out)
|
||||||
}
|
}
|
||||||
@@ -194,12 +190,13 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
|
|
||||||
while (tarIn.nextEntry?.let { entry = it } != null) {
|
while (tarIn.nextEntry?.let { entry = it } != null) {
|
||||||
if (entry.name.startsWith("boot.img") ||
|
if (entry.name.startsWith("boot.img") ||
|
||||||
|
entry.name.startsWith("init_boot.img") ||
|
||||||
(Config.recovery && entry.name.contains("recovery.img"))) {
|
(Config.recovery && entry.name.contains("recovery.img"))) {
|
||||||
val name = entry.name.replace(".lz4", "")
|
val name = entry.name.replace(".lz4", "")
|
||||||
console.add("-- Extracting: $name")
|
console.add("-- Extracting: $name")
|
||||||
|
|
||||||
val extract = installDirFile(name)
|
val extract = installDir.getChildFile(name)
|
||||||
decompressedStream().cleanPump(SuFileOutputStream.open(extract))
|
decompressedStream().cleanPump(extract.newOutputStream())
|
||||||
} else if (entry.name.contains("vbmeta.img")) {
|
} else if (entry.name.contains("vbmeta.img")) {
|
||||||
val rawData = decompressedStream().readBytes()
|
val rawData = decompressedStream().readBytes()
|
||||||
// Valid vbmeta.img should be at least 256 bytes
|
// Valid vbmeta.img should be at least 256 bytes
|
||||||
@@ -219,8 +216,9 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val boot = installDirFile("boot.img")
|
val boot = installDir.getChildFile("boot.img")
|
||||||
val recovery = installDirFile("recovery.img")
|
val initBoot = installDir.getChildFile("init_boot.img")
|
||||||
|
val recovery = installDir.getChildFile("recovery.img")
|
||||||
if (Config.recovery && recovery.exists() && boot.exists()) {
|
if (Config.recovery && recovery.exists() && boot.exists()) {
|
||||||
// Install to recovery
|
// Install to recovery
|
||||||
srcBoot = recovery
|
srcBoot = recovery
|
||||||
@@ -234,17 +232,20 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
"./magiskboot cleanup",
|
"./magiskboot cleanup",
|
||||||
"rm -f new-boot.img",
|
"rm -f new-boot.img",
|
||||||
"cd /").sh()
|
"cd /").sh()
|
||||||
SuFileInputStream.open(boot).use {
|
boot.newInputStream().use {
|
||||||
tarOut.putNextEntry(newTarEntry("boot.img", boot.length()))
|
tarOut.putNextEntry(newTarEntry("boot.img", boot.length()))
|
||||||
it.copyTo(tarOut)
|
it.copyTo(tarOut)
|
||||||
}
|
}
|
||||||
boot.delete()
|
boot.delete()
|
||||||
} else {
|
} else {
|
||||||
if (!boot.exists()) {
|
srcBoot = when {
|
||||||
console.add("! No boot image found")
|
initBoot.exists() -> initBoot
|
||||||
throw IOException()
|
boot.exists() -> boot
|
||||||
|
else -> {
|
||||||
|
console.add("! No boot image found")
|
||||||
|
throw IOException()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
srcBoot = boot
|
|
||||||
}
|
}
|
||||||
return tarOut
|
return tarOut
|
||||||
}
|
}
|
||||||
@@ -280,9 +281,9 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
processTar(src, outFile!!.uri.outputStream())
|
processTar(src, outFile!!.uri.outputStream())
|
||||||
} else {
|
} else {
|
||||||
// raw image
|
// raw image
|
||||||
srcBoot = installDirFile("boot.img")
|
srcBoot = installDir.getChildFile("boot.img")
|
||||||
console.add("- Copying image to cache")
|
console.add("- Copying image to cache")
|
||||||
src.cleanPump(SuFileOutputStream.open(srcBoot))
|
src.cleanPump(srcBoot.newOutputStream())
|
||||||
outFile = MediaStoreUtils.getFile("$filename.img", true)
|
outFile = MediaStoreUtils.getFile("$filename.img", true)
|
||||||
outFile!!.uri.outputStream()
|
outFile!!.uri.outputStream()
|
||||||
}
|
}
|
||||||
@@ -302,12 +303,18 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
|
|
||||||
// Output file
|
// Output file
|
||||||
try {
|
try {
|
||||||
val newBoot = installDirFile("new-boot.img")
|
val newBoot = installDir.getChildFile("new-boot.img")
|
||||||
if (outStream is TarOutputStream) {
|
if (outStream is TarOutputStream) {
|
||||||
val name = if (srcBoot.path.contains("recovery")) "recovery.img" else "boot.img"
|
val name = with(srcBoot.path) {
|
||||||
|
when {
|
||||||
|
contains("recovery") -> "recovery.img"
|
||||||
|
contains("init_boot") -> "init_boot.img"
|
||||||
|
else -> "boot.img"
|
||||||
|
}
|
||||||
|
}
|
||||||
outStream.putNextEntry(newTarEntry(name, newBoot.length()))
|
outStream.putNextEntry(newTarEntry(name, newBoot.length()))
|
||||||
}
|
}
|
||||||
SuFileInputStream.open(newBoot).cleanPump(outStream)
|
newBoot.newInputStream().cleanPump(outStream)
|
||||||
newBoot.delete()
|
newBoot.delete()
|
||||||
|
|
||||||
console.add("")
|
console.add("")
|
||||||
@@ -331,9 +338,9 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
|
|
||||||
private fun patchBoot(): Boolean {
|
private fun patchBoot(): Boolean {
|
||||||
var isSigned = false
|
var isSigned = false
|
||||||
if (srcBoot.let { it !is SuFile || !it.isCharacter }) {
|
if (!srcBoot.isCharacter) {
|
||||||
try {
|
try {
|
||||||
SuFileInputStream.open(srcBoot).use {
|
srcBoot.newInputStream().use {
|
||||||
if (SignBoot.verifySignature(it, null)) {
|
if (SignBoot.verifySignature(it, null)) {
|
||||||
isSigned = true
|
isSigned = true
|
||||||
console.add("- Boot image is signed with AVB 1.0")
|
console.add("- Boot image is signed with AVB 1.0")
|
||||||
@@ -346,7 +353,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val newBoot = installDirFile("new-boot.img")
|
val newBoot = installDir.getChildFile("new-boot.img")
|
||||||
if (!useRootDir) {
|
if (!useRootDir) {
|
||||||
// Create output files before hand
|
// Create output files before hand
|
||||||
newBoot.createNewFile()
|
newBoot.createNewFile()
|
||||||
@@ -359,6 +366,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
"KEEPVERITY=${Config.keepVerity} " +
|
"KEEPVERITY=${Config.keepVerity} " +
|
||||||
"PATCHVBMETAFLAG=${Config.patchVbmeta} " +
|
"PATCHVBMETAFLAG=${Config.patchVbmeta} " +
|
||||||
"RECOVERYMODE=${Config.recovery} " +
|
"RECOVERYMODE=${Config.recovery} " +
|
||||||
|
"SYSTEM_ROOT=${Info.isSAR} " +
|
||||||
"sh boot_patch.sh $srcBoot")
|
"sh boot_patch.sh $srcBoot")
|
||||||
|
|
||||||
if (!cmds.sh().isSuccess)
|
if (!cmds.sh().isSuccess)
|
||||||
@@ -370,7 +378,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
console.add("- Signing boot image with verity keys")
|
console.add("- Signing boot image with verity keys")
|
||||||
val signed = File.createTempFile("signed", ".img", context.cacheDir)
|
val signed = File.createTempFile("signed", ".img", context.cacheDir)
|
||||||
try {
|
try {
|
||||||
val src = SuFileInputStream.open(newBoot).buffered()
|
val src = newBoot.newInputStream().buffered()
|
||||||
val out = signed.outputStream().buffered()
|
val out = signed.outputStream().buffered()
|
||||||
withStreams(src, out) { _, _ ->
|
withStreams(src, out) { _, _ ->
|
||||||
SignBoot.doSignature(null, null, src, out, "/boot")
|
SignBoot.doSignature(null, null, src, out, "/boot")
|
||||||
@@ -410,11 +418,11 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
private fun String.fsh() = ShellUtils.fastCmd(shell, this)
|
private fun String.fsh() = ShellUtils.fastCmd(shell, this)
|
||||||
private fun Array<String>.fsh() = ShellUtils.fastCmd(shell, *this)
|
private fun Array<String>.fsh() = ShellUtils.fastCmd(shell, *this)
|
||||||
|
|
||||||
protected fun doPatchFile(patchFile: Uri) = extractFiles() && handleFile(patchFile)
|
protected fun patchFile(file: Uri) = extractFiles() && handleFile(file)
|
||||||
|
|
||||||
protected fun direct() = findImage() && extractFiles() && patchBoot() && flashBoot()
|
protected fun direct() = findImage() && extractFiles() && patchBoot() && flashBoot()
|
||||||
|
|
||||||
protected suspend fun secondSlot() =
|
protected fun secondSlot() =
|
||||||
findSecondary() && extractFiles() && patchBoot() && flashBoot() && postOTA()
|
findSecondary() && extractFiles() && patchBoot() && flashBoot() && postOTA()
|
||||||
|
|
||||||
protected fun fixEnv() = extractFiles() && "fix_env $installDir".sh().isSuccess
|
protected fun fixEnv() = extractFiles() && "fix_env $installDir".sh().isSuccess
|
||||||
@@ -425,20 +433,15 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
protected abstract suspend fun operations(): Boolean
|
protected abstract suspend fun operations(): Boolean
|
||||||
|
|
||||||
open suspend fun exec(): Boolean {
|
open suspend fun exec(): Boolean {
|
||||||
synchronized(Companion) {
|
if (haveActiveSession.getAndSet(true))
|
||||||
if (haveActiveSession)
|
return false
|
||||||
return false
|
|
||||||
haveActiveSession = true
|
|
||||||
}
|
|
||||||
val result = withContext(Dispatchers.IO) { operations() }
|
val result = withContext(Dispatchers.IO) { operations() }
|
||||||
synchronized(Companion) {
|
haveActiveSession.set(false)
|
||||||
haveActiveSession = false
|
|
||||||
}
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var haveActiveSession = false
|
private var haveActiveSession = AtomicBoolean(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,7 +455,7 @@ abstract class MagiskInstaller(
|
|||||||
if (success) {
|
if (success) {
|
||||||
console.add("- All done!")
|
console.add("- All done!")
|
||||||
} else {
|
} else {
|
||||||
Shell.sh("rm -rf $installDir").submit()
|
Shell.cmd("rm -rf $installDir").submit()
|
||||||
console.add("! Installation failed")
|
console.add("! Installation failed")
|
||||||
}
|
}
|
||||||
return success
|
return success
|
||||||
@@ -463,7 +466,7 @@ abstract class MagiskInstaller(
|
|||||||
console: MutableList<String>,
|
console: MutableList<String>,
|
||||||
logs: MutableList<String>
|
logs: MutableList<String>
|
||||||
) : MagiskInstaller(console, logs) {
|
) : MagiskInstaller(console, logs) {
|
||||||
override suspend fun operations() = doPatchFile(uri)
|
override suspend fun operations() = patchFile(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
class SecondSlot(
|
class SecondSlot(
|
||||||
@@ -497,7 +500,7 @@ abstract class MagiskInstaller(
|
|||||||
val success = super.exec()
|
val success = super.exec()
|
||||||
if (success) {
|
if (success) {
|
||||||
UiThreadHandler.handler.postDelayed(3000) {
|
UiThreadHandler.handler.postDelayed(3000) {
|
||||||
Shell.su("pm uninstall ${context.packageName}").exec()
|
Shell.cmd("pm uninstall ${context.packageName}").exec()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return success
|
return success
|
||||||
@@ -510,7 +513,7 @@ abstract class MagiskInstaller(
|
|||||||
override suspend fun exec(): Boolean {
|
override suspend fun exec(): Boolean {
|
||||||
val success = super.exec()
|
val success = super.exec()
|
||||||
callback()
|
callback()
|
||||||
Utils.toast(
|
context.toast(
|
||||||
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
|
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
|
||||||
Toast.LENGTH_LONG
|
Toast.LENGTH_LONG
|
||||||
)
|
)
|
||||||
|
@@ -4,7 +4,6 @@ import java.io.ByteArrayOutputStream
|
|||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder.LITTLE_ENDIAN
|
import java.nio.ByteOrder.LITTLE_ENDIAN
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class AXML(b: ByteArray) {
|
class AXML(b: ByteArray) {
|
||||||
|
|
||||||
@@ -30,7 +29,7 @@ class AXML(b: ByteArray) {
|
|||||||
* Followed by an array of uint32_t with size = number of strings
|
* Followed by an array of uint32_t with size = number of strings
|
||||||
* Each entry points to an offset into the string data
|
* Each entry points to an offset into the string data
|
||||||
*/
|
*/
|
||||||
fun findAndPatch(vararg patterns: Pair<String, String>): Boolean {
|
fun patchStrings(patchFn: (Array<String>) -> Unit): Boolean {
|
||||||
val buffer = ByteBuffer.wrap(bytes).order(LITTLE_ENDIAN)
|
val buffer = ByteBuffer.wrap(bytes).order(LITTLE_ENDIAN)
|
||||||
|
|
||||||
fun findStringPool(): Int {
|
fun findStringPool(): Int {
|
||||||
@@ -43,7 +42,6 @@ class AXML(b: ByteArray) {
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
var patch = false
|
|
||||||
val start = findStringPool()
|
val start = findStringPool()
|
||||||
if (start < 0)
|
if (start < 0)
|
||||||
return false
|
return false
|
||||||
@@ -58,34 +56,26 @@ class AXML(b: ByteArray) {
|
|||||||
val dataOff = start + intBuf.get()
|
val dataOff = start + intBuf.get()
|
||||||
intBuf.get()
|
intBuf.get()
|
||||||
|
|
||||||
val strings = ArrayList<String>(count)
|
val strList = ArrayList<String>(count)
|
||||||
// Read and patch all strings
|
// Collect all strings in the pool
|
||||||
loop@ for (i in 0 until count) {
|
for (i in 0 until count) {
|
||||||
val off = dataOff + intBuf.get()
|
val off = dataOff + intBuf.get()
|
||||||
val len = buffer.getShort(off)
|
val len = buffer.getShort(off)
|
||||||
val str = String(bytes, off + 2, len * 2, UTF_16LE)
|
strList.add(String(bytes, off + 2, len * 2, UTF_16LE))
|
||||||
for ((from, to) in patterns) {
|
|
||||||
if (str.contains(from)) {
|
|
||||||
strings.add(str.replace(from, to))
|
|
||||||
patch = true
|
|
||||||
continue@loop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
strings.add(str)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!patch)
|
val strArr = strList.toTypedArray()
|
||||||
return false
|
patchFn(strArr)
|
||||||
|
|
||||||
// Write everything before string data, will patch values later
|
// Write everything before string data, will patch values later
|
||||||
val baos = RawByteStream()
|
val baos = RawByteStream()
|
||||||
baos.write(bytes, 0, dataOff)
|
baos.write(bytes, 0, dataOff)
|
||||||
|
|
||||||
// Write string data
|
// Write string data
|
||||||
val strList = IntArray(count)
|
val offList = IntArray(count)
|
||||||
for (i in 0 until count) {
|
for (i in 0 until count) {
|
||||||
strList[i] = baos.size() - dataOff
|
offList[i] = baos.size() - dataOff
|
||||||
val str = strings[i]
|
val str = strArr[i]
|
||||||
baos.write(str.length.toShortBytes())
|
baos.write(str.length.toShortBytes())
|
||||||
baos.write(str.toByteArray(UTF_16LE))
|
baos.write(str.toByteArray(UTF_16LE))
|
||||||
// Null terminate
|
// Null terminate
|
||||||
@@ -104,7 +94,7 @@ class AXML(b: ByteArray) {
|
|||||||
// Patch index table
|
// Patch index table
|
||||||
newBuffer.position(start + STRING_INDICES_OFF)
|
newBuffer.position(start + STRING_INDICES_OFF)
|
||||||
val newIntBuf = newBuffer.asIntBuffer()
|
val newIntBuf = newBuffer.asIntBuffer()
|
||||||
strList.forEach { newIntBuf.put(it) }
|
offList.forEach { newIntBuf.put(it) }
|
||||||
|
|
||||||
// Write the rest of the chunks
|
// Write the rest of the chunks
|
||||||
val nextOff = start + size
|
val nextOff = start + size
|
||||||
|
@@ -6,7 +6,7 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.di.AppContext
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
|
|
||||||
object BiometricHelper {
|
object BiometricHelper {
|
||||||
|
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
package com.topjohnwu.magisk.core.utils
|
package com.topjohnwu.magisk.core.utils
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Runnable
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.util.concurrent.AbstractExecutorService
|
import java.util.concurrent.AbstractExecutorService
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
@@ -1,26 +1,22 @@
|
|||||||
package com.topjohnwu.magisk.core.utils
|
package com.topjohnwu.magisk.core.utils
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Base64OutputStream
|
import android.util.Base64OutputStream
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.signing.CryptoUtils.readCertificate
|
|
||||||
import com.topjohnwu.magisk.signing.CryptoUtils.readPrivateKey
|
|
||||||
import com.topjohnwu.magisk.signing.KeyData
|
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
import org.bouncycastle.asn1.x500.X500Name
|
||||||
|
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||||
|
import org.bouncycastle.cert.X509v3CertificateBuilder
|
||||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
|
|
||||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.security.KeyPairGenerator
|
import java.security.KeyPairGenerator
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.security.PrivateKey
|
import java.security.PrivateKey
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import java.util.*
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.Random
|
||||||
import java.util.zip.GZIPInputStream
|
import java.util.zip.GZIPInputStream
|
||||||
import java.util.zip.GZIPOutputStream
|
import java.util.zip.GZIPOutputStream
|
||||||
|
|
||||||
@@ -29,13 +25,11 @@ private interface CertKeyProvider {
|
|||||||
val key: PrivateKey
|
val key: PrivateKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
class Keygen : CertKeyProvider {
|
||||||
class Keygen(context: Context) : CertKeyProvider {
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ALIAS = "magisk"
|
private const val ALIAS = "magisk"
|
||||||
private val PASSWORD get() = "magisk".toCharArray()
|
private val PASSWORD get() = "magisk".toCharArray()
|
||||||
private const val TESTKEY_CERT = "61ed377e85d386a8dfee6b864bd85b0bfaa5af81"
|
|
||||||
private const val DNAME = "C=US,ST=California,L=Mountain View,O=Google Inc.,OU=Android,CN=Android"
|
private const val DNAME = "C=US,ST=California,L=Mountain View,O=Google Inc.,OU=Android,CN=Android"
|
||||||
private const val BASE64_FLAG = Base64.NO_PADDING or Base64.NO_WRAP
|
private const val BASE64_FLAG = Base64.NO_PADDING or Base64.NO_WRAP
|
||||||
}
|
}
|
||||||
@@ -43,49 +37,9 @@ class Keygen(context: Context) : CertKeyProvider {
|
|||||||
private val start = Calendar.getInstance().apply { add(Calendar.MONTH, -3) }
|
private val start = Calendar.getInstance().apply { add(Calendar.MONTH, -3) }
|
||||||
private val end = (start.clone() as Calendar).apply { add(Calendar.YEAR, 30) }
|
private val end = (start.clone() as Calendar).apply { add(Calendar.YEAR, 30) }
|
||||||
|
|
||||||
override val cert get() = provider.cert
|
private val ks = init()
|
||||||
override val key get() = provider.key
|
override val cert = ks.getCertificate(ALIAS) as X509Certificate
|
||||||
|
override val key = ks.getKey(ALIAS, PASSWORD) as PrivateKey
|
||||||
private val provider: CertKeyProvider
|
|
||||||
|
|
||||||
inner class KeyStoreProvider :
|
|
||||||
CertKeyProvider {
|
|
||||||
private val ks by lazy { init() }
|
|
||||||
override val cert by lazy { ks.getCertificate(ALIAS) as X509Certificate }
|
|
||||||
override val key by lazy { ks.getKey(
|
|
||||||
ALIAS,
|
|
||||||
PASSWORD
|
|
||||||
) as PrivateKey }
|
|
||||||
}
|
|
||||||
|
|
||||||
class TestProvider : CertKeyProvider {
|
|
||||||
override val cert by lazy {
|
|
||||||
readCertificate(ByteArrayInputStream(KeyData.testCert()))
|
|
||||||
}
|
|
||||||
override val key by lazy {
|
|
||||||
readPrivateKey(ByteArrayInputStream(KeyData.testKey()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
val pm = context.packageManager
|
|
||||||
val info = pm.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES)
|
|
||||||
val sig = info.signatures[0]
|
|
||||||
val digest = MessageDigest.getInstance("SHA1")
|
|
||||||
val chksum = digest.digest(sig.toByteArray())
|
|
||||||
|
|
||||||
val sb = StringBuilder()
|
|
||||||
for (b in chksum) {
|
|
||||||
sb.append("%02x".format(0xFF and b.toInt()))
|
|
||||||
}
|
|
||||||
|
|
||||||
provider = if (sb.toString() == TESTKEY_CERT) {
|
|
||||||
// The app was signed by the test key, continue to use it (legacy mode)
|
|
||||||
TestProvider()
|
|
||||||
} else {
|
|
||||||
KeyStoreProvider()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun init(): KeyStore {
|
private fun init(): KeyStore {
|
||||||
val raw = Config.keyStoreRaw
|
val raw = Config.keyStoreRaw
|
||||||
@@ -93,12 +47,8 @@ class Keygen(context: Context) : CertKeyProvider {
|
|||||||
if (raw.isEmpty()) {
|
if (raw.isEmpty()) {
|
||||||
ks.load(null)
|
ks.load(null)
|
||||||
} else {
|
} else {
|
||||||
GZIPInputStream(Base64.decode(raw,
|
GZIPInputStream(Base64.decode(raw, BASE64_FLAG).inputStream()).use {
|
||||||
BASE64_FLAG
|
ks.load(it, PASSWORD)
|
||||||
).inputStream()).use {
|
|
||||||
ks.load(it,
|
|
||||||
PASSWORD
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,22 +59,19 @@ class Keygen(context: Context) : CertKeyProvider {
|
|||||||
// Generate new private key and certificate
|
// Generate new private key and certificate
|
||||||
val kp = KeyPairGenerator.getInstance("RSA").apply { initialize(4096) }.genKeyPair()
|
val kp = KeyPairGenerator.getInstance("RSA").apply { initialize(4096) }.genKeyPair()
|
||||||
val dname = X500Name(DNAME)
|
val dname = X500Name(DNAME)
|
||||||
val builder = JcaX509v3CertificateBuilder(dname, BigInteger(160, Random()),
|
val builder = X509v3CertificateBuilder(
|
||||||
start.time, end.time, dname, kp.public)
|
dname, BigInteger(160, Random()),
|
||||||
|
start.time, end.time, Locale.ROOT, dname,
|
||||||
|
SubjectPublicKeyInfo.getInstance(kp.public.encoded)
|
||||||
|
)
|
||||||
val signer = JcaContentSignerBuilder("SHA1WithRSA").build(kp.private)
|
val signer = JcaContentSignerBuilder("SHA1WithRSA").build(kp.private)
|
||||||
val cert = JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
val cert = JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
||||||
|
|
||||||
// Store them into keystore
|
// Store them into keystore
|
||||||
ks.setKeyEntry(
|
ks.setKeyEntry(ALIAS, kp.private, PASSWORD, arrayOf(cert))
|
||||||
ALIAS, kp.private,
|
|
||||||
PASSWORD, arrayOf(cert))
|
|
||||||
val bytes = ByteArrayOutputStream()
|
val bytes = ByteArrayOutputStream()
|
||||||
GZIPOutputStream(Base64OutputStream(bytes,
|
GZIPOutputStream(Base64OutputStream(bytes, BASE64_FLAG)).use {
|
||||||
BASE64_FLAG
|
ks.store(it, PASSWORD)
|
||||||
)).use {
|
|
||||||
ks.store(it,
|
|
||||||
PASSWORD
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Config.keyStoreRaw = bytes.toString("UTF-8")
|
Config.keyStoreRaw = bytes.toString("UTF-8")
|
||||||
|
|
||||||
|
@@ -6,13 +6,13 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.core.ActivityTracker
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.createNewResources
|
import com.topjohnwu.magisk.core.createNewResources
|
||||||
import com.topjohnwu.magisk.di.AppContext
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
var currentLocale: Locale = Locale.getDefault()
|
var currentLocale: Locale = Locale.getDefault()
|
||||||
|
|
||||||
@@ -28,6 +28,11 @@ withContext(Dispatchers.Default) {
|
|||||||
// Create a completely new resource to prevent cross talk over active configs
|
// Create a completely new resource to prevent cross talk over active configs
|
||||||
val res = createNewResources()
|
val res = createNewResources()
|
||||||
|
|
||||||
|
fun changeLocale(locale: Locale) {
|
||||||
|
res.configuration.setLocale(locale)
|
||||||
|
res.updateConfiguration(res.configuration, res.displayMetrics)
|
||||||
|
}
|
||||||
|
|
||||||
val locales = ArrayList<String>().apply {
|
val locales = ArrayList<String>().apply {
|
||||||
// Add default locale
|
// Add default locale
|
||||||
add("en")
|
add("en")
|
||||||
@@ -41,13 +46,13 @@ withContext(Dispatchers.Default) {
|
|||||||
}.map {
|
}.map {
|
||||||
Locale.forLanguageTag(it)
|
Locale.forLanguageTag(it)
|
||||||
}.distinctBy {
|
}.distinctBy {
|
||||||
res.setLocale(it)
|
changeLocale(it)
|
||||||
res.getString(compareId)
|
res.getString(compareId)
|
||||||
}.sortedWith { a, b ->
|
}.sortedWith { a, b ->
|
||||||
a.getDisplayName(a).compareTo(b.getDisplayName(b), true)
|
a.getDisplayName(a).compareTo(b.getDisplayName(b), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.setLocale(defaultLocale)
|
changeLocale(defaultLocale)
|
||||||
val defName = res.getString(R.string.system_default)
|
val defName = res.getString(R.string.system_default)
|
||||||
|
|
||||||
val names = ArrayList<String>(locales.size + 1)
|
val names = ArrayList<String>(locales.size + 1)
|
||||||
@@ -71,11 +76,6 @@ fun Resources.setConfig(config: Configuration) {
|
|||||||
|
|
||||||
fun Resources.syncLocale() = setConfig(configuration)
|
fun Resources.syncLocale() = setConfig(configuration)
|
||||||
|
|
||||||
fun Resources.setLocale(locale: Locale) {
|
|
||||||
configuration.setLocale(locale)
|
|
||||||
updateConfiguration(configuration, displayMetrics)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refreshLocale() {
|
fun refreshLocale() {
|
||||||
val localeConfig = Config.locale
|
val localeConfig = Config.locale
|
||||||
currentLocale = when {
|
currentLocale = when {
|
||||||
@@ -84,4 +84,5 @@ fun refreshLocale() {
|
|||||||
}
|
}
|
||||||
Locale.setDefault(currentLocale)
|
Locale.setDefault(currentLocale)
|
||||||
AppContext.resources.syncLocale()
|
AppContext.resources.syncLocale()
|
||||||
|
ActivityTracker.foreground?.recreate()
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,7 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.di.AppContext
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -87,7 +87,7 @@ object MediaStoreUtils {
|
|||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun getFile(displayName: String, skipQuery: Boolean = false): UriFile {
|
fun getFile(displayName: String, skipQuery: Boolean = false): UriFile {
|
||||||
if (Build.VERSION.SDK_INT < 30) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||||
// Fallback to file based I/O pre Android 11
|
// Fallback to file based I/O pre Android 11
|
||||||
val parent = File(Environment.getExternalStorageDirectory(), relativePath)
|
val parent = File(Environment.getExternalStorageDirectory(), relativePath)
|
||||||
parent.mkdirs()
|
parent.mkdirs()
|
||||||
|
@@ -0,0 +1,79 @@
|
|||||||
|
package com.topjohnwu.magisk.core.utils
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.Network
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.net.NetworkRequest
|
||||||
|
import android.os.PowerManager
|
||||||
|
import androidx.collection.ArraySet
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
|
import com.topjohnwu.magisk.core.ktx.registerRuntimeReceiver
|
||||||
|
|
||||||
|
typealias ConnectionCallback = (Boolean) -> Unit
|
||||||
|
|
||||||
|
class NetworkObserver(
|
||||||
|
context: Context,
|
||||||
|
private val callback: ConnectionCallback
|
||||||
|
): DefaultLifecycleObserver {
|
||||||
|
private val manager = context.getSystemService<ConnectivityManager>()!!
|
||||||
|
|
||||||
|
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||||
|
private val activeList = ArraySet<Network>()
|
||||||
|
|
||||||
|
override fun onAvailable(network: Network) {
|
||||||
|
activeList.add(network)
|
||||||
|
callback(true)
|
||||||
|
}
|
||||||
|
override fun onLost(network: Network) {
|
||||||
|
activeList.remove(network)
|
||||||
|
callback(!activeList.isEmpty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val receiver = object : BroadcastReceiver() {
|
||||||
|
private fun Context.isIdleMode(): Boolean {
|
||||||
|
val pwm = getSystemService<PowerManager>() ?: return true
|
||||||
|
val isIgnoringOptimizations = pwm.isIgnoringBatteryOptimizations(packageName)
|
||||||
|
return pwm.isDeviceIdleMode && !isIgnoringOptimizations
|
||||||
|
}
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if (context.isIdleMode()) {
|
||||||
|
callback(false)
|
||||||
|
} else {
|
||||||
|
postCurrentState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
val request = NetworkRequest.Builder()
|
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||||
|
.build()
|
||||||
|
manager.registerNetworkCallback(request, networkCallback)
|
||||||
|
val filter = IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)
|
||||||
|
context.applicationContext.registerRuntimeReceiver(receiver, filter)
|
||||||
|
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart(owner: LifecycleOwner) {
|
||||||
|
postCurrentState()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun postCurrentState() {
|
||||||
|
callback(manager.getNetworkCapabilities(manager.activeNetwork)
|
||||||
|
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) ?: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun observe(context: Context, callback: ConnectionCallback): NetworkObserver {
|
||||||
|
return NetworkObserver(context, callback).apply { postCurrentState() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,34 @@
|
|||||||
|
package com.topjohnwu.magisk.core.utils
|
||||||
|
|
||||||
|
import android.annotation.TargetApi
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
|
|
||||||
|
class RequestInstall : ActivityResultContract<Unit, Boolean>() {
|
||||||
|
|
||||||
|
@TargetApi(26)
|
||||||
|
override fun createIntent(context: Context, input: Unit): Intent {
|
||||||
|
// This will only be called on API 26+
|
||||||
|
return Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
|
||||||
|
.setData(Uri.parse("package:${context.packageName}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parseResult(resultCode: Int, intent: Intent?) =
|
||||||
|
resultCode == Activity.RESULT_OK
|
||||||
|
|
||||||
|
override fun getSynchronousResult(
|
||||||
|
context: Context,
|
||||||
|
input: Unit
|
||||||
|
): SynchronousResult<Boolean>? {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
|
||||||
|
return SynchronousResult(true)
|
||||||
|
if (context.packageManager.canRequestPackageInstalls())
|
||||||
|
return SynchronousResult(true)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
@@ -1,55 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.utils
|
|
||||||
|
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.ServiceConnection
|
|
||||||
import android.os.Binder
|
|
||||||
import android.os.IBinder
|
|
||||||
import com.topjohnwu.superuser.ipc.RootService
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.concurrent.CountDownLatch
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
class RootRegistry(stub: Any?) : RootService() {
|
|
||||||
|
|
||||||
constructor() : this(null)
|
|
||||||
|
|
||||||
private val className: String? = stub?.javaClass?.name
|
|
||||||
|
|
||||||
init {
|
|
||||||
// Always log full stack trace with Timber
|
|
||||||
Timber.plant(Timber.DebugTree())
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler { _, e ->
|
|
||||||
Timber.e(e)
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder {
|
|
||||||
// TODO: PLACEHOLDER
|
|
||||||
return Binder()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getComponentName(): ComponentName {
|
|
||||||
return ComponentName(packageName, className ?: javaClass.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: PLACEHOLDER
|
|
||||||
object Connection : CountDownLatch(1), ServiceConnection {
|
|
||||||
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
|
||||||
Timber.d("onServiceConnected")
|
|
||||||
countDown()
|
|
||||||
}
|
|
||||||
override fun onNullBinding(name: ComponentName) {
|
|
||||||
Timber.d("onServiceConnected")
|
|
||||||
countDown()
|
|
||||||
}
|
|
||||||
override fun onServiceDisconnected(name: ComponentName) {
|
|
||||||
bind(Intent().setComponent(name), this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
var bindTask: Runnable? = null
|
|
||||||
}
|
|
||||||
}
|
|
139
app/src/main/java/com/topjohnwu/magisk/core/utils/RootUtils.kt
Normal file
139
app/src/main/java/com/topjohnwu/magisk/core/utils/RootUtils.kt
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package com.topjohnwu.magisk.core.utils
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.system.Os
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import com.topjohnwu.superuser.ShellUtils
|
||||||
|
import com.topjohnwu.superuser.ipc.RootService
|
||||||
|
import com.topjohnwu.superuser.nio.FileSystemManager
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.util.concurrent.locks.AbstractQueuedSynchronizer
|
||||||
|
|
||||||
|
class RootUtils(stub: Any?) : RootService() {
|
||||||
|
|
||||||
|
private val className: String = stub?.javaClass?.name ?: javaClass.name
|
||||||
|
private lateinit var am: ActivityManager
|
||||||
|
|
||||||
|
constructor() : this(null) {
|
||||||
|
Timber.plant(object : Timber.DebugTree() {
|
||||||
|
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||||
|
super.log(priority, "Magisk", message, t)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
am = getSystemService()!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getComponentName(): ComponentName {
|
||||||
|
return ComponentName(packageName, className)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent): IBinder {
|
||||||
|
return object : IRootUtils.Stub() {
|
||||||
|
override fun getAppProcess(pid: Int) = safe(null) { getAppProcessImpl(pid) }
|
||||||
|
override fun getFileSystem(): IBinder = FileSystemManager.getService()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <T> safe(default: T, block: () -> T): T {
|
||||||
|
return try {
|
||||||
|
block()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// The process died unexpectedly
|
||||||
|
Timber.e(e)
|
||||||
|
default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAppProcessImpl(_pid: Int): ActivityManager.RunningAppProcessInfo? {
|
||||||
|
val procList = am.runningAppProcesses
|
||||||
|
var pid = _pid
|
||||||
|
while (pid > 1) {
|
||||||
|
val proc = procList.find { it.pid == pid }
|
||||||
|
if (proc != null)
|
||||||
|
return proc
|
||||||
|
|
||||||
|
// Stop find when root process
|
||||||
|
if (Os.stat("/proc/$pid").st_uid == 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find PPID
|
||||||
|
File("/proc/$pid/status").useLines {
|
||||||
|
val line = it.find { l -> l.startsWith("PPid:") } ?: return null
|
||||||
|
pid = line.substring(5).trim().toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
object Connection : AbstractQueuedSynchronizer(), ServiceConnection {
|
||||||
|
init {
|
||||||
|
state = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||||
|
Timber.d("onServiceConnected")
|
||||||
|
IRootUtils.Stub.asInterface(service).let {
|
||||||
|
obj = it
|
||||||
|
fs = FileSystemManager.getRemote(it.fileSystem)
|
||||||
|
}
|
||||||
|
releaseShared(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName) {
|
||||||
|
state = 1
|
||||||
|
obj = null
|
||||||
|
bind(Intent().setComponent(name), this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun tryAcquireShared(acquires: Int) = if (state == 0) 1 else -1
|
||||||
|
|
||||||
|
override fun tryReleaseShared(releases: Int): Boolean {
|
||||||
|
// Decrement count; signal when transition to zero
|
||||||
|
while (true) {
|
||||||
|
val c = state
|
||||||
|
if (c == 0)
|
||||||
|
return false
|
||||||
|
val n = c - 1
|
||||||
|
if (compareAndSetState(c, n))
|
||||||
|
return n == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun await() {
|
||||||
|
if (!Info.isRooted)
|
||||||
|
return
|
||||||
|
if (!ShellUtils.onMainThread()) {
|
||||||
|
acquireSharedInterruptibly(1)
|
||||||
|
} else if (state != 0) {
|
||||||
|
throw IllegalStateException("Cannot await on the main thread")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
var bindTask: Shell.Task? = null
|
||||||
|
var fs: FileSystemManager = FileSystemManager.getLocal()
|
||||||
|
get() {
|
||||||
|
Connection.await()
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
var obj: IRootUtils? = null
|
||||||
|
get() {
|
||||||
|
Connection.await()
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
}
|
||||||
|
}
|
@@ -1,16 +1,16 @@
|
|||||||
package com.topjohnwu.magisk.core.utils
|
package com.topjohnwu.magisk.core.utils
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.topjohnwu.magisk.DynAPK
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.StubApk
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
import com.topjohnwu.magisk.ktx.cachedFile
|
import com.topjohnwu.magisk.core.ktx.cachedFile
|
||||||
import com.topjohnwu.magisk.ktx.deviceProtectedContext
|
import com.topjohnwu.magisk.core.ktx.deviceProtectedContext
|
||||||
import com.topjohnwu.magisk.ktx.rawResource
|
import com.topjohnwu.magisk.core.ktx.rawResource
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.ShellUtils
|
import com.topjohnwu.superuser.ShellUtils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -19,8 +19,9 @@ import java.util.jar.JarFile
|
|||||||
class ShellInit : Shell.Initializer() {
|
class ShellInit : Shell.Initializer() {
|
||||||
override fun onInit(context: Context, shell: Shell): Boolean {
|
override fun onInit(context: Context, shell: Shell): Boolean {
|
||||||
if (shell.isRoot) {
|
if (shell.isRoot) {
|
||||||
RootRegistry.bindTask?.run()
|
Info.isRooted = true
|
||||||
RootRegistry.bindTask = null
|
RootUtils.bindTask?.let { shell.execTask(it) }
|
||||||
|
RootUtils.bindTask = null
|
||||||
}
|
}
|
||||||
shell.newJob().apply {
|
shell.newJob().apply {
|
||||||
add("export ASH_STANDALONE=1")
|
add("export ASH_STANDALONE=1")
|
||||||
@@ -29,7 +30,7 @@ class ShellInit : Shell.Initializer() {
|
|||||||
if (isRunningAsStub) {
|
if (isRunningAsStub) {
|
||||||
if (!shell.isRoot)
|
if (!shell.isRoot)
|
||||||
return true
|
return true
|
||||||
val jar = JarFile(DynAPK.current(context))
|
val jar = JarFile(StubApk.current(context))
|
||||||
val bb = jar.getJarEntry("lib/${Const.CPU_ABI}/libbusybox.so")
|
val bb = jar.getJarEntry("lib/${Const.CPU_ABI}/libbusybox.so")
|
||||||
localBB = context.deviceProtectedContext.cachedFile("busybox")
|
localBB = context.deviceProtectedContext.cachedFile("busybox")
|
||||||
localBB.delete()
|
localBB.delete()
|
||||||
@@ -40,20 +41,20 @@ class ShellInit : Shell.Initializer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (shell.isRoot) {
|
if (shell.isRoot) {
|
||||||
add("export MAGISKTMP=\$(magisk --path)/.magisk")
|
add("export MAGISKTMP=\$(magisk --path)")
|
||||||
// Test if we can properly execute stuff in /data
|
// Test if we can properly execute stuff in /data
|
||||||
Info.noDataExec = !shell.newJob().add("$localBB true").exec().isSuccess
|
Info.noDataExec = !shell.newJob().add("$localBB sh -c \"$localBB true\"").exec().isSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Info.noDataExec) {
|
if (Info.noDataExec) {
|
||||||
// Copy it out of /data to workaround Samsung bullshit
|
// Copy it out of /data to workaround Samsung bullshit
|
||||||
add(
|
add(
|
||||||
"if [ -x \$MAGISKTMP/busybox/busybox ]; then",
|
"if [ -x \$MAGISKTMP/.magisk/busybox/busybox ]; then",
|
||||||
" cp -af $localBB \$MAGISKTMP/busybox/busybox",
|
" cp -af $localBB \$MAGISKTMP/.magisk/busybox/busybox",
|
||||||
" exec \$MAGISKTMP/busybox/busybox sh",
|
" exec \$MAGISKTMP/.magisk/busybox/busybox sh",
|
||||||
"else",
|
"else",
|
||||||
" cp -af $localBB /dev/.busybox",
|
" cp -af $localBB /dev/busybox",
|
||||||
" exec /dev/.busybox sh",
|
" exec /dev/busybox sh",
|
||||||
"fi"
|
"fi"
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -72,7 +73,6 @@ class ShellInit : Shell.Initializer() {
|
|||||||
fun getVar(name: String) = fastCmd("echo \$$name")
|
fun getVar(name: String) = fastCmd("echo \$$name")
|
||||||
fun getBool(name: String) = getVar(name).toBoolean()
|
fun getBool(name: String) = getVar(name).toBoolean()
|
||||||
|
|
||||||
Const.MAGISKTMP = getVar("MAGISKTMP")
|
|
||||||
Info.isSAR = getBool("SYSTEM_ROOT")
|
Info.isSAR = getBool("SYSTEM_ROOT")
|
||||||
Info.ramdisk = getBool("RAMDISKEXIST")
|
Info.ramdisk = getBool("RAMDISKEXIST")
|
||||||
Info.vbmeta = getBool("VBMETAEXIST")
|
Info.vbmeta = getBool("VBMETAEXIST")
|
||||||
|
@@ -1,7 +1,5 @@
|
|||||||
package com.topjohnwu.magisk.core.utils
|
package com.topjohnwu.magisk.core.utils
|
||||||
|
|
||||||
import com.topjohnwu.superuser.io.SuFile
|
|
||||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@@ -31,12 +29,12 @@ fun InputStream.unzip(folder: File, path: String, junkPath: Boolean) {
|
|||||||
else
|
else
|
||||||
entry.name
|
entry.name
|
||||||
|
|
||||||
var dest = File(folder, name)
|
val dest = File(folder, name)
|
||||||
if (!dest.parentFile!!.exists() && !dest.parentFile!!.mkdirs()) {
|
dest.parentFile!!.let {
|
||||||
dest = SuFile(folder, name)
|
if (!it.exists())
|
||||||
dest.parentFile!!.mkdirs()
|
it.mkdirs()
|
||||||
}
|
}
|
||||||
SuFileOutputStream.open(dest).use { out -> zin.copyTo(out) }
|
dest.outputStream().use { out -> zin.copyTo(out) }
|
||||||
}
|
}
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
throw IOException(e)
|
throw IOException(e)
|
||||||
|
@@ -1,49 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.utils.net
|
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.net.Network
|
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import android.net.NetworkRequest
|
|
||||||
import androidx.collection.ArraySet
|
|
||||||
|
|
||||||
@TargetApi(21)
|
|
||||||
open class LollipopNetworkObserver(
|
|
||||||
context: Context,
|
|
||||||
callback: ConnectionCallback
|
|
||||||
): NetworkObserver(context, callback) {
|
|
||||||
|
|
||||||
private val networkCallback = NetCallback()
|
|
||||||
|
|
||||||
init {
|
|
||||||
val request = NetworkRequest.Builder()
|
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
|
||||||
.build()
|
|
||||||
manager.registerNetworkCallback(request, networkCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
override fun getCurrentState() {
|
|
||||||
callback(manager.activeNetworkInfo?.isConnected ?: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun stopObserving() {
|
|
||||||
manager.unregisterNetworkCallback(networkCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class NetCallback : ConnectivityManager.NetworkCallback() {
|
|
||||||
|
|
||||||
private val activeList = ArraySet<Network>()
|
|
||||||
|
|
||||||
override fun onAvailable(network: Network) {
|
|
||||||
activeList.add(network)
|
|
||||||
callback(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLost(network: Network) {
|
|
||||||
activeList.remove(network)
|
|
||||||
callback(!activeList.isEmpty())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,53 +0,0 @@
|
|||||||
@file:Suppress("DEPRECATION")
|
|
||||||
|
|
||||||
package com.topjohnwu.magisk.core.utils.net
|
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import android.os.PowerManager
|
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
|
|
||||||
@TargetApi(23)
|
|
||||||
class MarshmallowNetworkObserver(
|
|
||||||
context: Context,
|
|
||||||
callback: ConnectionCallback
|
|
||||||
): LollipopNetworkObserver(context, callback) {
|
|
||||||
|
|
||||||
private val receiver = IdleBroadcastReceiver()
|
|
||||||
|
|
||||||
init {
|
|
||||||
val filter = IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)
|
|
||||||
app.registerReceiver(receiver, filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun stopObserving() {
|
|
||||||
super.stopObserving()
|
|
||||||
app.unregisterReceiver(receiver)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCurrentState() {
|
|
||||||
callback(manager.getNetworkCapabilities(manager.activeNetwork)
|
|
||||||
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class IdleBroadcastReceiver: BroadcastReceiver() {
|
|
||||||
|
|
||||||
private fun Context.isIdleMode(): Boolean {
|
|
||||||
val pwm = getSystemService<PowerManager>() ?: return true
|
|
||||||
val isIgnoringOptimizations = pwm.isIgnoringBatteryOptimizations(packageName)
|
|
||||||
return pwm.isDeviceIdleMode && !isIgnoringOptimizations
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
if (context.isIdleMode()) {
|
|
||||||
callback(false)
|
|
||||||
} else {
|
|
||||||
getCurrentState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,29 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.utils.net
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
|
|
||||||
typealias ConnectionCallback = (Boolean) -> Unit
|
|
||||||
|
|
||||||
abstract class NetworkObserver(
|
|
||||||
context: Context,
|
|
||||||
protected val callback: ConnectionCallback
|
|
||||||
) {
|
|
||||||
|
|
||||||
protected val app: Context = context.applicationContext
|
|
||||||
protected val manager = context.getSystemService<ConnectivityManager>()!!
|
|
||||||
|
|
||||||
protected abstract fun stopObserving()
|
|
||||||
protected abstract fun getCurrentState()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun observe(context: Context, callback: ConnectionCallback): NetworkObserver {
|
|
||||||
val observer: NetworkObserver = if (Build.VERSION.SDK_INT >= 23)
|
|
||||||
MarshmallowNetworkObserver(context, callback)
|
|
||||||
else LollipopNetworkObserver(context, callback)
|
|
||||||
return observer.apply { getCurrentState() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,67 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.data.preference
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import kotlin.properties.ReadWriteProperty
|
|
||||||
import kotlin.reflect.KProperty
|
|
||||||
|
|
||||||
interface PreferenceModel {
|
|
||||||
|
|
||||||
val context: Context
|
|
||||||
|
|
||||||
val fileName: String
|
|
||||||
get() = "${context.packageName}_preferences"
|
|
||||||
|
|
||||||
val prefs: SharedPreferences
|
|
||||||
get() = context.getSharedPreferences(fileName, Context.MODE_PRIVATE)
|
|
||||||
|
|
||||||
fun preferenceStrInt(
|
|
||||||
name: String,
|
|
||||||
default: Int,
|
|
||||||
commit: Boolean = false
|
|
||||||
) = object: ReadWriteProperty<PreferenceModel, Int> {
|
|
||||||
val base = StringProperty(name, default.toString(), commit)
|
|
||||||
override fun getValue(thisRef: PreferenceModel, property: KProperty<*>): Int =
|
|
||||||
base.getValue(thisRef, property).toInt()
|
|
||||||
|
|
||||||
override fun setValue(thisRef: PreferenceModel, property: KProperty<*>, value: Int) =
|
|
||||||
base.setValue(thisRef, property, value.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun preference(
|
|
||||||
name: String,
|
|
||||||
default: Boolean,
|
|
||||||
commit: Boolean = false
|
|
||||||
) = BooleanProperty(name, default, commit)
|
|
||||||
|
|
||||||
fun preference(
|
|
||||||
name: String,
|
|
||||||
default: Float,
|
|
||||||
commit: Boolean = false
|
|
||||||
) = FloatProperty(name, default, commit)
|
|
||||||
|
|
||||||
fun preference(
|
|
||||||
name: String,
|
|
||||||
default: Int,
|
|
||||||
commit: Boolean = false
|
|
||||||
) = IntProperty(name, default, commit)
|
|
||||||
|
|
||||||
fun preference(
|
|
||||||
name: String,
|
|
||||||
default: Long,
|
|
||||||
commit: Boolean = false
|
|
||||||
) = LongProperty(name, default, commit)
|
|
||||||
|
|
||||||
fun preference(
|
|
||||||
name: String,
|
|
||||||
default: String,
|
|
||||||
commit: Boolean = false
|
|
||||||
) = StringProperty(name, default, commit)
|
|
||||||
|
|
||||||
fun preference(
|
|
||||||
name: String,
|
|
||||||
default: Set<String>,
|
|
||||||
commit: Boolean = false
|
|
||||||
) = StringSetProperty(name, default, commit)
|
|
||||||
|
|
||||||
}
|
|
@@ -1,13 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.databinding
|
|
||||||
|
|
||||||
import androidx.databinding.ViewDataBinding
|
|
||||||
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
|
|
||||||
|
|
||||||
open class BindingBoundAdapter : BindingRecyclerViewAdapter<RvItem>() {
|
|
||||||
|
|
||||||
override fun onBindBinding(binding: ViewDataBinding, variableId: Int, layoutRes: Int, position: Int, item: RvItem) {
|
|
||||||
super.onBindBinding(binding, variableId, layoutRes, position, item)
|
|
||||||
|
|
||||||
item.onBindingBound(binding)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -8,10 +8,7 @@ import android.text.Spanned
|
|||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Button
|
import android.widget.*
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.ProgressBar
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
@@ -29,7 +26,8 @@ import com.google.android.material.card.MaterialCardView
|
|||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.utils.TextHolder
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
import com.topjohnwu.widget.IndeterminateCheckBox
|
import com.topjohnwu.widget.IndeterminateCheckBox
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -289,3 +287,13 @@ fun TextView.setTextColorAttr(attr: Int) {
|
|||||||
context.theme.resolveAttribute(attr, tv, true)
|
context.theme.resolveAttribute(attr, tv, true)
|
||||||
setTextColor(tv.data)
|
setTextColor(tv.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@BindingAdapter("android:text")
|
||||||
|
fun TextView.setText(text: TextHolder) {
|
||||||
|
this.text = text.getText(context.resources)
|
||||||
|
}
|
||||||
|
|
||||||
|
@BindingAdapter("items", "layout")
|
||||||
|
fun Spinner.setAdapter(items: Array<Any>, layoutRes: Int) {
|
||||||
|
adapter = ArrayAdapter(context, layoutRes, items)
|
||||||
|
}
|
||||||
|
@@ -1,38 +1,53 @@
|
|||||||
package com.topjohnwu.magisk.databinding
|
package com.topjohnwu.magisk.databinding
|
||||||
|
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.databinding.ListChangeRegistry
|
import androidx.databinding.ListChangeRegistry
|
||||||
import androidx.databinding.ObservableList
|
import androidx.databinding.ObservableList
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListUpdateCallback
|
import androidx.recyclerview.widget.ListUpdateCallback
|
||||||
import java.util.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlin.collections.ArrayList
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.util.AbstractList
|
||||||
|
|
||||||
/**
|
// Only expose the immutable List types
|
||||||
* @param callback The callback that controls the behavior of the DiffObservableList.
|
interface DiffList<T : DiffItem<*>> : List<T> {
|
||||||
* @param detectMoves True if DiffUtil should try to detect moved items, false otherwise.
|
fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult
|
||||||
*/
|
|
||||||
open class DiffObservableList<T>(
|
|
||||||
private val callback: Callback<T>,
|
|
||||||
private val detectMoves: Boolean = true
|
|
||||||
) : AbstractList<T>(), ObservableList<T> {
|
|
||||||
|
|
||||||
protected var list: MutableList<T> = ArrayList()
|
@MainThread
|
||||||
|
fun update(newItems: List<T>, diffResult: DiffUtil.DiffResult)
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
suspend fun update(newItems: List<T>)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilterList<T : DiffItem<*>> : List<T> {
|
||||||
|
fun filter(filter: (T) -> Boolean)
|
||||||
|
|
||||||
|
@MainThread
|
||||||
|
fun set(newItems: List<T>)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T : DiffItem<*>> diffList(): DiffList<T> = DiffObservableList()
|
||||||
|
|
||||||
|
fun <T : DiffItem<*>> filterList(scope: CoroutineScope): FilterList<T> =
|
||||||
|
FilterableDiffObservableList(scope)
|
||||||
|
|
||||||
|
private open class DiffObservableList<T : DiffItem<*>>
|
||||||
|
: AbstractList<T>(), ObservableList<T>, DiffList<T>, ListUpdateCallback {
|
||||||
|
|
||||||
|
protected var list: List<T> = emptyList()
|
||||||
private val listeners = ListChangeRegistry()
|
private val listeners = ListChangeRegistry()
|
||||||
protected val listCallback = ObservableListUpdateCallback()
|
|
||||||
|
|
||||||
override val size: Int get() = list.size
|
override val size: Int get() = list.size
|
||||||
|
|
||||||
/**
|
override fun get(index: Int) = list[index]
|
||||||
* Calculates the list of update operations that can convert this list into the given one.
|
|
||||||
*
|
override fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult {
|
||||||
* @param newItems The items that this list will be set to.
|
return doCalculateDiff(list, newItems)
|
||||||
* @return A DiffResult that contains the information about the edit sequence to covert this
|
|
||||||
* list into the given one.
|
|
||||||
*/
|
|
||||||
fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult {
|
|
||||||
val frozenList = ArrayList(list)
|
|
||||||
return doCalculateDiff(frozenList, newItems)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun doCalculateDiff(oldItems: List<T>, newItems: List<T>): DiffUtil.DiffResult {
|
protected fun doCalculateDiff(oldItems: List<T>, newItems: List<T>): DiffUtil.DiffResult {
|
||||||
@@ -41,47 +56,34 @@ open class DiffObservableList<T>(
|
|||||||
|
|
||||||
override fun getNewListSize() = newItems.size
|
override fun getNewListSize() = newItems.size
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
val oldItem = oldItems[oldItemPosition]
|
val oldItem = oldItems[oldItemPosition]
|
||||||
val newItem = newItems[newItemPosition]
|
val newItem = newItems[newItemPosition]
|
||||||
return callback.areItemsTheSame(oldItem, newItem)
|
return (oldItem as DiffItem<Any>).itemSameAs(newItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
val oldItem = oldItems[oldItemPosition]
|
val oldItem = oldItems[oldItemPosition]
|
||||||
val newItem = newItems[newItemPosition]
|
val newItem = newItems[newItemPosition]
|
||||||
return callback.areContentsTheSame(oldItem, newItem)
|
return (oldItem as DiffItem<Any>).contentSameAs(newItem)
|
||||||
}
|
}
|
||||||
}, detectMoves)
|
}, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the contents of this list to the given one using the DiffResults to dispatch change
|
|
||||||
* notifications.
|
|
||||||
*
|
|
||||||
* @param newItems The items to set this list to.
|
|
||||||
* @param diffResult The diff results to dispatch change notifications.
|
|
||||||
*/
|
|
||||||
@MainThread
|
@MainThread
|
||||||
fun update(newItems: List<T>, diffResult: DiffUtil.DiffResult) {
|
override fun update(newItems: List<T>, diffResult: DiffUtil.DiffResult) {
|
||||||
list = newItems.toMutableList()
|
list = ArrayList(newItems)
|
||||||
diffResult.dispatchUpdatesTo(listCallback)
|
diffResult.dispatchUpdatesTo(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@WorkerThread
|
||||||
* Sets this list to the given items. This is a convenience method for calling [ ][.calculateDiff] followed by [.update].
|
override suspend fun update(newItems: List<T>) {
|
||||||
*
|
val diffResult = calculateDiff(newItems)
|
||||||
*
|
withContext(Dispatchers.Main) {
|
||||||
* **Warning!** If the lists are large this operation may be too slow for the main thread. In
|
update(newItems, diffResult)
|
||||||
* that case, you should call [.calculateDiff] on a background thread and then
|
}
|
||||||
* [.update] on the main thread.
|
|
||||||
*
|
|
||||||
* @param newItems The items to set this list to.
|
|
||||||
*/
|
|
||||||
@MainThread
|
|
||||||
fun update(newItems: List<T>) {
|
|
||||||
val diffResult = doCalculateDiff(list, newItems)
|
|
||||||
update(newItems, diffResult)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addOnListChangedCallback(listener: ObservableList.OnListChangedCallback<out ObservableList<T>>) {
|
override fun addOnListChangedCallback(listener: ObservableList.OnListChangedCallback<out ObservableList<T>>) {
|
||||||
@@ -92,113 +94,63 @@ open class DiffObservableList<T>(
|
|||||||
listeners.remove(listener)
|
listeners.remove(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun get(index: Int) = list[index]
|
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||||
|
listeners.notifyChanged(this, position, count)
|
||||||
override fun add(index: Int, element: T) {
|
|
||||||
list.add(index, element)
|
|
||||||
notifyAdd(index, 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addAll(elements: Collection<T>) = addAll(size, elements)
|
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||||
|
listeners.notifyMoved(this, fromPosition, toPosition, 1)
|
||||||
override fun addAll(index: Int, elements: Collection<T>): Boolean {
|
|
||||||
val added = list.addAll(index, elements)
|
|
||||||
if (added) {
|
|
||||||
notifyAdd(index, elements.size)
|
|
||||||
}
|
|
||||||
return added
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clear() {
|
override fun onInserted(position: Int, count: Int) {
|
||||||
val oldSize = size
|
modCount += 1
|
||||||
list.clear()
|
listeners.notifyInserted(this, position, count)
|
||||||
if (oldSize != 0) {
|
|
||||||
notifyRemove(0, oldSize)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun remove(element: T): Boolean {
|
override fun onRemoved(position: Int, count: Int) {
|
||||||
val index = indexOf(element)
|
modCount += 1
|
||||||
return if (index >= 0) {
|
listeners.notifyRemoved(this, position, count)
|
||||||
removeAt(index)
|
}
|
||||||
true
|
}
|
||||||
} else {
|
|
||||||
false
|
private class FilterableDiffObservableList<T : DiffItem<*>>(
|
||||||
}
|
private val scope: CoroutineScope
|
||||||
}
|
) : DiffObservableList<T>(), FilterList<T> {
|
||||||
|
|
||||||
override fun removeAt(index: Int): T {
|
private var sublist: List<T> = emptyList()
|
||||||
val element = list.removeAt(index)
|
private var job: Job? = null
|
||||||
notifyRemove(index, 1)
|
private var lastFilter: ((T) -> Boolean)? = null
|
||||||
return element
|
|
||||||
}
|
// ---
|
||||||
|
|
||||||
override fun set(index: Int, element: T): T {
|
override fun filter(filter: (T) -> Boolean) {
|
||||||
val old = list.set(index, element)
|
lastFilter = filter
|
||||||
listeners.notifyChanged(this, index, 1)
|
job?.cancel()
|
||||||
return old
|
job = scope.launch(Dispatchers.Default) {
|
||||||
}
|
val oldList = sublist
|
||||||
|
val newList = list.filter(filter)
|
||||||
private fun notifyAdd(start: Int, count: Int) {
|
val diff = doCalculateDiff(oldList, newList)
|
||||||
listeners.notifyInserted(this, start, count)
|
withContext(Dispatchers.Main) {
|
||||||
}
|
sublist = newList
|
||||||
|
diff.dispatchUpdatesTo(this@FilterableDiffObservableList)
|
||||||
private fun notifyRemove(start: Int, count: Int) {
|
}
|
||||||
listeners.notifyRemoved(this, start, count)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ---
|
||||||
* A Callback class used by DiffUtil while calculating the diff between two lists.
|
|
||||||
*/
|
override fun get(index: Int): T {
|
||||||
interface Callback<T> {
|
return sublist[index]
|
||||||
/**
|
}
|
||||||
* Called by the DiffUtil to decide whether two object represent the same Item.
|
|
||||||
*
|
override val size: Int
|
||||||
*
|
get() = sublist.size
|
||||||
* For example, if your items have unique ids, this method should check their id equality.
|
|
||||||
*
|
@MainThread
|
||||||
* @param oldItem The old item.
|
override fun set(newItems: List<T>) {
|
||||||
* @param newItem The new item.
|
onRemoved(0, sublist.size)
|
||||||
* @return True if the two items represent the same object or false if they are different.
|
list = newItems
|
||||||
*/
|
sublist = emptyList()
|
||||||
fun areItemsTheSame(oldItem: T, newItem: T): Boolean
|
lastFilter?.let { filter(it) }
|
||||||
|
|
||||||
/**
|
|
||||||
* Called by the DiffUtil when it wants to check whether two items have the same data.
|
|
||||||
* DiffUtil uses this information to detect if the contents of an item has changed.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* DiffUtil uses this method to check equality instead of [Object.equals] so
|
|
||||||
* that you can change its behavior depending on your UI.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* This method is called only if [.areItemsTheSame] returns `true` for
|
|
||||||
* these items.
|
|
||||||
*
|
|
||||||
* @param oldItem The old item.
|
|
||||||
* @param newItem The new item which replaces the old item.
|
|
||||||
* @return True if the contents of the items are the same or false if they are different.
|
|
||||||
*/
|
|
||||||
fun areContentsTheSame(oldItem: T, newItem: T): Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class ObservableListUpdateCallback : ListUpdateCallback {
|
|
||||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
|
||||||
listeners.notifyChanged(this@DiffObservableList, position, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
|
||||||
listeners.notifyMoved(this@DiffObservableList, fromPosition, toPosition, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInserted(position: Int, count: Int) {
|
|
||||||
modCount += 1
|
|
||||||
listeners.notifyInserted(this@DiffObservableList, position, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRemoved(position: Int, count: Int) {
|
|
||||||
modCount += 1
|
|
||||||
listeners.notifyRemoved(this@DiffObservableList, position, count)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,85 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.databinding
|
|
||||||
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.HandlerThread
|
|
||||||
import android.os.Looper
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class FilterableDiffObservableList<T>(
|
|
||||||
callback: Callback<T>
|
|
||||||
) : DiffObservableList<T>(callback) {
|
|
||||||
|
|
||||||
var filter: ((T) -> Boolean)? = null
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
queueUpdate()
|
|
||||||
}
|
|
||||||
@Volatile
|
|
||||||
private var sublist: MutableList<T> = super.list
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
private val ui by lazy { Handler(Looper.getMainLooper()) }
|
|
||||||
private val handler = Handler(HandlerThread("List${hashCode()}").apply { start() }.looper)
|
|
||||||
private val updater = Runnable {
|
|
||||||
val filter = filter ?: { true }
|
|
||||||
val newList = super.list.filter(filter)
|
|
||||||
val diff = synchronized(this) { doCalculateDiff(sublist, newList) }
|
|
||||||
ui.post {
|
|
||||||
sublist = Collections.synchronizedList(newList)
|
|
||||||
diff.dispatchUpdatesTo(listCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun queueUpdate() {
|
|
||||||
handler.removeCallbacks(updater)
|
|
||||||
handler.post(updater)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasFilter() = filter != null
|
|
||||||
|
|
||||||
fun filter(switch: (T) -> Boolean) {
|
|
||||||
filter = switch
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reset() {
|
|
||||||
filter = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
override fun get(index: Int): T {
|
|
||||||
return sublist.get(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun add(element: T): Boolean {
|
|
||||||
return sublist.add(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun add(index: Int, element: T) {
|
|
||||||
sublist.add(index, element)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addAll(elements: Collection<T>): Boolean {
|
|
||||||
return sublist.addAll(elements)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addAll(index: Int, elements: Collection<T>): Boolean {
|
|
||||||
return sublist.addAll(index, elements)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun remove(element: T): Boolean {
|
|
||||||
return sublist.remove(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun removeAt(index: Int): T {
|
|
||||||
return sublist.removeAt(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun set(index: Int, element: T): T {
|
|
||||||
return sublist.set(index, element)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val size: Int
|
|
||||||
get() = sublist.size
|
|
||||||
}
|
|
@@ -1,35 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.databinding
|
|
||||||
|
|
||||||
import androidx.databinding.ViewDataBinding
|
|
||||||
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
|
|
||||||
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
|
||||||
import me.tatarka.bindingcollectionadapter2.OnItemBind
|
|
||||||
|
|
||||||
fun <T : AnyDiffRvItem> diffListOf() =
|
|
||||||
DiffObservableList(DiffRvItem.callback<T>())
|
|
||||||
|
|
||||||
fun <T : AnyDiffRvItem> diffListOf(newItems: List<T>) =
|
|
||||||
DiffObservableList(DiffRvItem.callback<T>()).also { it.update(newItems) }
|
|
||||||
|
|
||||||
fun <T : AnyDiffRvItem> filterableListOf() =
|
|
||||||
FilterableDiffObservableList(DiffRvItem.callback<T>())
|
|
||||||
|
|
||||||
fun <T : RvItem> adapterOf() = object : BindingRecyclerViewAdapter<T>() {
|
|
||||||
override fun onBindBinding(
|
|
||||||
binding: ViewDataBinding,
|
|
||||||
variableId: Int,
|
|
||||||
layoutRes: Int,
|
|
||||||
position: Int,
|
|
||||||
item: T
|
|
||||||
) {
|
|
||||||
super.onBindBinding(binding, variableId, layoutRes, position, item)
|
|
||||||
item.onBindingBound(binding)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <T : RvItem> itemBindingOf(
|
|
||||||
crossinline body: (ItemBinding<*>) -> Unit = {}
|
|
||||||
) = OnItemBind<T> { itemBinding, _, item ->
|
|
||||||
item.bind(itemBinding)
|
|
||||||
body(itemBinding)
|
|
||||||
}
|
|
@@ -0,0 +1,162 @@
|
|||||||
|
package com.topjohnwu.magisk.databinding
|
||||||
|
|
||||||
|
import androidx.databinding.ListChangeRegistry
|
||||||
|
import androidx.databinding.ObservableList
|
||||||
|
import androidx.databinding.ObservableList.OnListChangedCallback
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
class MergeObservableList<T> : AbstractList<T>(), ObservableList<T> {
|
||||||
|
|
||||||
|
private val lists: MutableList<List<T>> = mutableListOf()
|
||||||
|
private val listeners = ListChangeRegistry()
|
||||||
|
private val callback = Callback<T>()
|
||||||
|
|
||||||
|
override fun addOnListChangedCallback(callback: OnListChangedCallback<out ObservableList<T>>) {
|
||||||
|
listeners.add(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeOnListChangedCallback(callback: OnListChangedCallback<out ObservableList<T>>) {
|
||||||
|
listeners.remove(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun get(index: Int): T {
|
||||||
|
if (index < 0)
|
||||||
|
throw IndexOutOfBoundsException()
|
||||||
|
var idx = index
|
||||||
|
for (list in lists) {
|
||||||
|
val size = list.size
|
||||||
|
if (idx < size) {
|
||||||
|
return list[idx]
|
||||||
|
}
|
||||||
|
idx -= size
|
||||||
|
}
|
||||||
|
throw IndexOutOfBoundsException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override val size: Int
|
||||||
|
get() = lists.fold(0) { i, it -> i + it.size }
|
||||||
|
|
||||||
|
|
||||||
|
fun insertItem(obj: T): MergeObservableList<T> {
|
||||||
|
val idx = size
|
||||||
|
lists.add(listOf(obj))
|
||||||
|
++modCount
|
||||||
|
listeners.notifyInserted(this, idx, 1)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun insertList(list: List<T>): MergeObservableList<T> {
|
||||||
|
val idx = size
|
||||||
|
lists.add(list)
|
||||||
|
++modCount
|
||||||
|
(list as? ObservableList<T>)?.addOnListChangedCallback(callback)
|
||||||
|
if (list.isNotEmpty())
|
||||||
|
listeners.notifyInserted(this, idx, list.size)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeItem(obj: T): Boolean {
|
||||||
|
var idx = 0
|
||||||
|
for ((i, list) in lists.withIndex()) {
|
||||||
|
if (list !is ObservableList<*>) {
|
||||||
|
if (obj == list[0]) {
|
||||||
|
lists.removeAt(i)
|
||||||
|
++modCount
|
||||||
|
listeners.notifyRemoved(this, idx, 1)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
idx += list.size
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeList(listToRemove: List<T>): Boolean {
|
||||||
|
var idx = 0
|
||||||
|
for ((i, list) in lists.withIndex()) {
|
||||||
|
if (listToRemove === list) {
|
||||||
|
(list as? ObservableList<T>)?.removeOnListChangedCallback(callback)
|
||||||
|
lists.removeAt(i)
|
||||||
|
++modCount
|
||||||
|
listeners.notifyRemoved(this, idx, list.size)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
idx += list.size
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clear() {
|
||||||
|
val sz = size
|
||||||
|
for (list in lists) {
|
||||||
|
if (list is ObservableList) {
|
||||||
|
list.removeOnListChangedCallback(callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
++modCount
|
||||||
|
lists.clear()
|
||||||
|
if (sz > 0)
|
||||||
|
listeners.notifyRemoved(this, 0, sz)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun subIndexToIndex(subList: List<*>, index: Int): Int {
|
||||||
|
if (index < 0)
|
||||||
|
throw IndexOutOfBoundsException()
|
||||||
|
var idx = 0
|
||||||
|
for (list in lists) {
|
||||||
|
if (subList === list) {
|
||||||
|
return idx + index
|
||||||
|
}
|
||||||
|
idx += list.size
|
||||||
|
}
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class Callback<T> : OnListChangedCallback<ObservableList<T>>() {
|
||||||
|
override fun onChanged(sender: ObservableList<T>) {
|
||||||
|
++modCount
|
||||||
|
listeners.notifyChanged(this@MergeObservableList)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeChanged(
|
||||||
|
sender: ObservableList<T>,
|
||||||
|
positionStart: Int,
|
||||||
|
itemCount: Int
|
||||||
|
) {
|
||||||
|
listeners.notifyChanged(this@MergeObservableList,
|
||||||
|
subIndexToIndex(sender, positionStart), itemCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeInserted(
|
||||||
|
sender: ObservableList<T>,
|
||||||
|
positionStart: Int,
|
||||||
|
itemCount: Int
|
||||||
|
) {
|
||||||
|
++modCount
|
||||||
|
listeners.notifyInserted(this@MergeObservableList,
|
||||||
|
subIndexToIndex(sender, positionStart), itemCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeMoved(
|
||||||
|
sender: ObservableList<T>,
|
||||||
|
fromPosition: Int,
|
||||||
|
toPosition: Int,
|
||||||
|
itemCount: Int
|
||||||
|
) {
|
||||||
|
val idx = subIndexToIndex(sender, 0)
|
||||||
|
listeners.notifyMoved(this@MergeObservableList,
|
||||||
|
idx + fromPosition, idx + toPosition, itemCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeRemoved(
|
||||||
|
sender: ObservableList<T>,
|
||||||
|
positionStart: Int,
|
||||||
|
itemCount: Int
|
||||||
|
) {
|
||||||
|
++modCount
|
||||||
|
listeners.notifyRemoved(this@MergeObservableList,
|
||||||
|
subIndexToIndex(sender, positionStart), itemCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,89 +1,35 @@
|
|||||||
package com.topjohnwu.magisk.databinding
|
package com.topjohnwu.magisk.databinding
|
||||||
|
|
||||||
import androidx.annotation.CallSuper
|
|
||||||
import androidx.databinding.PropertyChangeRegistry
|
import androidx.databinding.PropertyChangeRegistry
|
||||||
import androidx.databinding.ViewDataBinding
|
import androidx.databinding.ViewDataBinding
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.topjohnwu.magisk.BR
|
|
||||||
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
|
||||||
|
|
||||||
abstract class RvItem {
|
abstract class RvItem {
|
||||||
|
|
||||||
abstract val layoutRes: Int
|
abstract val layoutRes: Int
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
open fun bind(binding: ItemBinding<*>) {
|
|
||||||
binding.set(BR.item, layoutRes)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This callback is useful if you want to manipulate your views directly.
|
|
||||||
* If you want to use this callback, you must set [me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter]
|
|
||||||
* on your RecyclerView and call it from there. You can use [BindingBoundAdapter] for your convenience.
|
|
||||||
*/
|
|
||||||
open fun onBindingBound(binding: ViewDataBinding) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RvContainer<E> {
|
|
||||||
val item: E
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ComparableRv<T> : Comparable<T> {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
fun comparableEqual(o: Any?) =
|
|
||||||
o != null && o::class == this::class && compareTo(o as T) == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class DiffRvItem<T> : RvItem() {
|
|
||||||
|
|
||||||
// Defer to contentSameAs by default
|
|
||||||
open fun itemSameAs(other: T) = true
|
|
||||||
|
|
||||||
open fun contentSameAs(other: T) =
|
|
||||||
when (this) {
|
|
||||||
is RvContainer<*> -> item == (other as RvContainer<*>).item
|
|
||||||
is ComparableRv<*> -> comparableEqual(other)
|
|
||||||
else -> this == other
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val callback = object : DiffObservableList.Callback<DiffRvItem<Any>> {
|
|
||||||
override fun areItemsTheSame(
|
|
||||||
oldItem: DiffRvItem<Any>,
|
|
||||||
newItem: DiffRvItem<Any>
|
|
||||||
): Boolean {
|
|
||||||
return oldItem::class == newItem::class && oldItem.itemSameAs(newItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(
|
|
||||||
oldItem: DiffRvItem<Any>,
|
|
||||||
newItem: DiffRvItem<Any>
|
|
||||||
): Boolean {
|
|
||||||
return oldItem.contentSameAs(newItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
fun <T : AnyDiffRvItem> callback() = callback as DiffObservableList.Callback<T>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
typealias AnyDiffRvItem = DiffRvItem<*>
|
|
||||||
|
|
||||||
abstract class ObservableDiffRvItem<T> : DiffRvItem<T>(), ObservableHost {
|
|
||||||
override var callbacks: PropertyChangeRegistry? = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class ObservableRvItem : RvItem(), ObservableHost {
|
abstract class ObservableRvItem : RvItem(), ObservableHost {
|
||||||
override var callbacks: PropertyChangeRegistry? = null
|
override var callbacks: PropertyChangeRegistry? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
interface ItemWrapper<E> {
|
||||||
* This item addresses issues where enclosing recycler has to be invalidated or generally
|
val item: E
|
||||||
* manipulated with. This shouldn't be however necessary for 99.9% of use-cases. Refrain from using
|
}
|
||||||
* this item as it provides virtually no additional functionality. Stick with ComparableRvItem.
|
|
||||||
* */
|
interface ViewAwareItem {
|
||||||
|
fun onBind(binding: ViewDataBinding, recyclerView: RecyclerView)
|
||||||
interface LenientRvItem {
|
}
|
||||||
fun onBindingBound(binding: ViewDataBinding, recyclerView: RecyclerView)
|
|
||||||
|
interface DiffItem<T : Any> {
|
||||||
|
|
||||||
|
fun itemSameAs(other: T): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
return when (this) {
|
||||||
|
is ItemWrapper<*> -> item == (other as ItemWrapper<*>).item
|
||||||
|
is Comparable<*> -> compareValues(this, other as Comparable<*>) == 0
|
||||||
|
else -> this == other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun contentSameAs(other: T) = true
|
||||||
}
|
}
|
||||||
|
@@ -1,35 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.databinding
|
|
||||||
|
|
||||||
import androidx.databinding.ViewDataBinding
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
|
|
||||||
|
|
||||||
class RvBindingAdapter<T : RvItem> : BindingRecyclerViewAdapter<T>() {
|
|
||||||
|
|
||||||
private var recyclerView: RecyclerView? = null
|
|
||||||
|
|
||||||
override fun onBindBinding(
|
|
||||||
binding: ViewDataBinding,
|
|
||||||
variableId: Int,
|
|
||||||
layoutRes: Int,
|
|
||||||
position: Int,
|
|
||||||
item: T
|
|
||||||
) {
|
|
||||||
super.onBindBinding(binding, variableId, layoutRes, position, item)
|
|
||||||
|
|
||||||
when (item) {
|
|
||||||
is LenientRvItem -> {
|
|
||||||
val recycler = recyclerView ?: return
|
|
||||||
item.onBindingBound(binding)
|
|
||||||
item.onBindingBound(binding, recycler)
|
|
||||||
}
|
|
||||||
else -> item.onBindingBound(binding)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
|
||||||
super.onAttachedToRecyclerView(recyclerView)
|
|
||||||
this.recyclerView = recyclerView
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -0,0 +1,121 @@
|
|||||||
|
package com.topjohnwu.magisk.databinding
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.util.SparseArray
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.databinding.BindingAdapter
|
||||||
|
import androidx.databinding.DataBindingUtil
|
||||||
|
import androidx.databinding.ObservableList
|
||||||
|
import androidx.databinding.ObservableList.OnListChangedCallback
|
||||||
|
import androidx.databinding.ViewDataBinding
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.topjohnwu.magisk.BR
|
||||||
|
|
||||||
|
class RvItemAdapter<T: RvItem>(
|
||||||
|
val items: List<T>,
|
||||||
|
val extraBindings: SparseArray<*>?
|
||||||
|
) : RecyclerView.Adapter<RvItemAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
private var lifecycleOwner: LifecycleOwner? = null
|
||||||
|
private var recyclerView: RecyclerView? = null
|
||||||
|
private val observer by lazy(LazyThreadSafetyMode.NONE) { ListObserver<T>() }
|
||||||
|
|
||||||
|
override fun onAttachedToRecyclerView(rv: RecyclerView) {
|
||||||
|
lifecycleOwner = rv.findViewTreeLifecycleOwner()
|
||||||
|
recyclerView = rv
|
||||||
|
if (items is ObservableList)
|
||||||
|
items.addOnListChangedCallback(observer)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromRecyclerView(rv: RecyclerView) {
|
||||||
|
lifecycleOwner = null
|
||||||
|
recyclerView = null
|
||||||
|
if (items is ObservableList)
|
||||||
|
items.removeOnListChangedCallback(observer)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, layoutRes: Int): ViewHolder {
|
||||||
|
val inflator = LayoutInflater.from(parent.context)
|
||||||
|
return ViewHolder(DataBindingUtil.inflate(inflator, layoutRes, parent, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val item = items[position]
|
||||||
|
holder.binding.setVariable(BR.item, item)
|
||||||
|
extraBindings?.let {
|
||||||
|
for (i in 0 until it.size()) {
|
||||||
|
holder.binding.setVariable(it.keyAt(i), it.valueAt(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
holder.binding.lifecycleOwner = lifecycleOwner
|
||||||
|
holder.binding.executePendingBindings()
|
||||||
|
recyclerView?.let {
|
||||||
|
if (item is ViewAwareItem)
|
||||||
|
item.onBind(holder.binding, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = items.size
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int) = items[position].layoutRes
|
||||||
|
|
||||||
|
class ViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
|
inner class ListObserver<T: RvItem> : OnListChangedCallback<ObservableList<T>>() {
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
override fun onChanged(sender: ObservableList<T>) {
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeChanged(
|
||||||
|
sender: ObservableList<T>,
|
||||||
|
positionStart: Int,
|
||||||
|
itemCount: Int
|
||||||
|
) {
|
||||||
|
notifyItemRangeChanged(positionStart, itemCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeInserted(
|
||||||
|
sender: ObservableList<T>?,
|
||||||
|
positionStart: Int,
|
||||||
|
itemCount: Int
|
||||||
|
) {
|
||||||
|
notifyItemRangeInserted(positionStart, itemCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeMoved(
|
||||||
|
sender: ObservableList<T>?,
|
||||||
|
fromPosition: Int,
|
||||||
|
toPosition: Int,
|
||||||
|
itemCount: Int
|
||||||
|
) {
|
||||||
|
for (i in 0 until itemCount) {
|
||||||
|
notifyItemMoved(fromPosition + i, toPosition + i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeRemoved(
|
||||||
|
sender: ObservableList<T>?,
|
||||||
|
positionStart: Int,
|
||||||
|
itemCount: Int
|
||||||
|
) {
|
||||||
|
notifyItemRangeRemoved(positionStart, itemCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun bindExtra(body: (SparseArray<Any?>) -> Unit) = SparseArray<Any?>().also(body)
|
||||||
|
|
||||||
|
@BindingAdapter("items", "extraBindings", requireAll = false)
|
||||||
|
fun <T: RvItem> RecyclerView.setAdapter(items: List<T>?, extraBindings: SparseArray<*>?) {
|
||||||
|
if (items != null) {
|
||||||
|
val rva = (adapter as? RvItemAdapter<*>)
|
||||||
|
if (rva == null || rva.items !== items || rva.extraBindings !== extraBindings) {
|
||||||
|
adapter = RvItemAdapter(items, extraBindings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,12 +1,14 @@
|
|||||||
package com.topjohnwu.magisk.events.dialog
|
package com.topjohnwu.magisk.dialog
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.arch.UIActivity
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
|
import com.topjohnwu.magisk.events.DialogBuilder
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
|
|
||||||
class DarkThemeDialog : DialogEvent() {
|
class DarkThemeDialog : DialogBuilder {
|
||||||
|
|
||||||
override fun build(dialog: MagiskDialog) {
|
override fun build(dialog: MagiskDialog) {
|
||||||
val activity = dialog.ownerActivity!!
|
val activity = dialog.ownerActivity!!
|
||||||
@@ -33,6 +35,6 @@ class DarkThemeDialog : DialogEvent() {
|
|||||||
|
|
||||||
private fun selectTheme(mode: Int, activity: Activity) {
|
private fun selectTheme(mode: Int, activity: Activity) {
|
||||||
Config.darkTheme = mode
|
Config.darkTheme = mode
|
||||||
activity.recreate()
|
(activity as UIActivity<*>).delegate.localNightMode = mode
|
||||||
}
|
}
|
||||||
}
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user