mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-08-14 12:40:56 +00:00
Compare commits
1257 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ba7cb47383 | ||
![]() |
48d417f9af | ||
![]() |
df4db6bf6b | ||
![]() |
b8ef491bc7 | ||
![]() |
ea1ebb8d00 | ||
![]() |
91b6d2852a | ||
![]() |
d7cd1b37f8 | ||
![]() |
160ff7bb07 | ||
![]() |
31142180cb | ||
![]() |
38b0fa04a8 | ||
![]() |
29817245ba | ||
![]() |
925fe6f152 | ||
![]() |
93fd574b75 | ||
![]() |
0de88bcbb9 | ||
![]() |
0b70bd2b60 | ||
![]() |
84ecba4629 | ||
![]() |
f7142e69b6 | ||
![]() |
ed7e560849 | ||
![]() |
47e50e8511 | ||
![]() |
72f6770d61 | ||
![]() |
7da35e5468 | ||
![]() |
7768274b2f | ||
![]() |
33f006655d | ||
![]() |
612b51d48f | ||
![]() |
8101f3f67d | ||
![]() |
e3c8d723e3 | ||
![]() |
4579825758 | ||
![]() |
ef91c33f55 | ||
![]() |
511d5993df | ||
![]() |
9f4958e869 | ||
![]() |
c07775f5e3 | ||
![]() |
e261579e72 | ||
![]() |
cf54cad3ce | ||
![]() |
a0998009c1 | ||
![]() |
d6fdbfe9b7 | ||
![]() |
07228279a3 | ||
![]() |
6877ef790f | ||
![]() |
a3809648dd | ||
![]() |
df15606b00 | ||
![]() |
4dc0d13688 | ||
![]() |
541fa5cb1f | ||
![]() |
ab9442d4ae | ||
![]() |
f5c099e9a7 | ||
![]() |
9582379e1b | ||
![]() |
db9a4b31f9 | ||
![]() |
409cb06ea0 | ||
![]() |
88d917b662 | ||
![]() |
faf077b494 | ||
![]() |
ee1f45aa91 | ||
![]() |
915fd3020b | ||
![]() |
642788abec | ||
![]() |
3cd11dd9a0 | ||
![]() |
bf2c5ce368 | ||
![]() |
65c510a211 | ||
![]() |
6fbc38d764 | ||
![]() |
200bf993d8 | ||
![]() |
38af82e152 | ||
![]() |
fc05f377fb | ||
![]() |
5c0e86383c | ||
![]() |
64f5ff5475 | ||
![]() |
758777111a | ||
![]() |
b90e0430f8 | ||
![]() |
0ce7da1bf6 | ||
![]() |
e6464c5c7f | ||
![]() |
c6b3f06b95 | ||
![]() |
581419b6a3 | ||
![]() |
696ab677be | ||
![]() |
0d229dac3b | ||
![]() |
3b8ea599f0 | ||
![]() |
3e70a61e33 | ||
![]() |
76f35d02b7 | ||
![]() |
356b417a04 | ||
![]() |
56147a80b5 | ||
![]() |
91728991d7 | ||
![]() |
0f7e59d288 | ||
![]() |
f33028c645 | ||
![]() |
f9149ad433 | ||
![]() |
0d7474cc88 | ||
![]() |
1e7e06d1cc | ||
![]() |
8453282fa6 | ||
![]() |
40f971d18a | ||
![]() |
ce7cb1eeae | ||
![]() |
d2701616da | ||
![]() |
10eb159e1b | ||
![]() |
36897ceb19 | ||
![]() |
9a8274130b | ||
![]() |
c8d050c3e3 | ||
![]() |
a46cd63c9d | ||
![]() |
e9e6eaf079 | ||
![]() |
cb5897af93 | ||
![]() |
d701d6eb82 | ||
![]() |
470ebb54e2 | ||
![]() |
632cab398e | ||
![]() |
189c4cc9d8 | ||
![]() |
70d5e2dee8 | ||
![]() |
c586106e51 | ||
![]() |
ffa85a616a | ||
![]() |
e5ea3e4a43 | ||
![]() |
0492e63862 | ||
![]() |
9952387356 | ||
![]() |
d7653e6e42 | ||
![]() |
e9fc40d285 | ||
![]() |
740559e3bc | ||
![]() |
9471577b3b | ||
![]() |
e85d5e54e2 | ||
![]() |
5fb071d80b | ||
![]() |
022151fefd | ||
![]() |
3b8d2fe8b7 | ||
![]() |
d51d549a28 | ||
![]() |
b5ac24f239 | ||
![]() |
3ca99005f8 | ||
![]() |
0b9f2921d2 | ||
![]() |
389501ad0c | ||
![]() |
082e4eb05c | ||
![]() |
47f885a566 | ||
![]() |
bc964b8588 | ||
![]() |
b57b3313e4 | ||
![]() |
f185cefa11 | ||
![]() |
9d256e02d7 | ||
![]() |
086c64c0be | ||
![]() |
798fe57025 | ||
![]() |
a03f744648 | ||
![]() |
64f35744c4 | ||
![]() |
b512528148 | ||
![]() |
fdfa037dca | ||
![]() |
db4ef1443d | ||
![]() |
810468c279 | ||
![]() |
8146d0830d | ||
![]() |
7e946b040c | ||
![]() |
97d24a7d4d | ||
![]() |
f8bea66313 | ||
![]() |
dd9129017f | ||
![]() |
cbe3602cb7 | ||
![]() |
1d831d65f3 | ||
![]() |
c35d020731 | ||
![]() |
c18db555a4 | ||
![]() |
373092af16 | ||
![]() |
1a2e157cda | ||
![]() |
b3bc1a3907 | ||
![]() |
4dd8d75cc0 | ||
![]() |
e5f50bb7e0 | ||
![]() |
45d5b4bea6 | ||
![]() |
ed58cf953a | ||
![]() |
ec26bc5ab7 | ||
![]() |
84e4bd3d41 | ||
![]() |
0ecfb63cd6 | ||
![]() |
ebdd6ec40c | ||
![]() |
0586760347 | ||
![]() |
d535f244ad | ||
![]() |
613d46824d | ||
![]() |
041355f182 | ||
![]() |
6977dc082f | ||
![]() |
d3dffe8165 | ||
![]() |
6812f9d202 | ||
![]() |
555e7cc907 | ||
![]() |
6180558068 | ||
![]() |
cf589f8c64 | ||
![]() |
e864919c0b | ||
![]() |
c72d83b637 | ||
![]() |
f2d2f28e23 | ||
![]() |
a7435dad6d | ||
![]() |
793f0b605c | ||
![]() |
5b56ca7ffc | ||
![]() |
5c988510b3 | ||
![]() |
290624844b | ||
![]() |
497efc9f5e | ||
![]() |
19d76b635c | ||
![]() |
4875def31c | ||
![]() |
155c0e3609 | ||
![]() |
00ea15dc19 | ||
![]() |
f04c4cb78a | ||
![]() |
6e4777692e | ||
![]() |
4638fdf2d7 | ||
![]() |
0783d385d5 | ||
![]() |
cf918e7df8 | ||
![]() |
1ba9faf35b | ||
![]() |
6e48294f2a | ||
![]() |
dea607b148 | ||
![]() |
e938e717b0 | ||
![]() |
2eed09ef1b | ||
![]() |
8a6b3644be | ||
![]() |
1d89fe503b | ||
![]() |
788db036fd | ||
![]() |
c38c473e11 | ||
![]() |
aef1f8f701 | ||
![]() |
83f9767254 | ||
![]() |
3e0352eee6 | ||
![]() |
28faff6425 | ||
![]() |
d0112f989c | ||
![]() |
9c4c310f46 | ||
![]() |
7bf7bfb9c6 | ||
![]() |
fbe776db0b | ||
![]() |
1e2de1bb14 | ||
![]() |
e395c9442f | ||
![]() |
30286f0ea5 | ||
![]() |
60ee742855 | ||
![]() |
a913ede48f | ||
![]() |
9592583783 | ||
![]() |
ad49d3ad26 | ||
![]() |
21ee73c2a3 | ||
![]() |
f5d0cc9f32 | ||
![]() |
b90c65370e | ||
![]() |
88920e0546 | ||
![]() |
d27773de03 | ||
![]() |
8abdaeb044 | ||
![]() |
9682d2f84a | ||
![]() |
a86b9e81e9 | ||
![]() |
a8bb7c68a3 | ||
![]() |
bdad29adab | ||
![]() |
fadcfe5f7a | ||
![]() |
fbd83b5ff3 | ||
![]() |
c351174fa4 | ||
![]() |
cc4f99fe28 | ||
![]() |
b2a9b88fe5 | ||
![]() |
da06e0ec76 | ||
![]() |
851ee81486 | ||
![]() |
0dc9f5c324 | ||
![]() |
36513c2301 | ||
![]() |
3a10597aed | ||
![]() |
2291be5d26 | ||
![]() |
345c3ef15e | ||
![]() |
c1dad11cb3 | ||
![]() |
12b219e7b2 | ||
![]() |
f8b48cf18d | ||
![]() |
12a9792c7d | ||
![]() |
ba55e2bc32 | ||
![]() |
c5e5b70e08 | ||
![]() |
327b186240 | ||
![]() |
5c1417e276 | ||
![]() |
0a2c99f1dc | ||
![]() |
836bfbdd02 | ||
![]() |
b13a35057a | ||
![]() |
c3e77b1ec1 | ||
![]() |
fb60bea659 | ||
![]() |
b2ddba4cbf | ||
![]() |
053251d566 | ||
![]() |
cf161a5dd9 | ||
![]() |
e4bcdbd0c4 | ||
![]() |
cae43b26f4 | ||
![]() |
b95cf9b9a3 | ||
![]() |
e6f443cb24 | ||
![]() |
087ccd69c9 | ||
![]() |
7532477a2f | ||
![]() |
433ae89e53 | ||
![]() |
de853a2651 | ||
![]() |
47c3045980 | ||
![]() |
dd50c19ba3 | ||
![]() |
707d7b3342 | ||
![]() |
ba1a2fbce4 | ||
![]() |
84f1e78660 | ||
![]() |
3490ba0a56 | ||
![]() |
1449486958 | ||
![]() |
9094cf7ce3 | ||
![]() |
df0a5b59f8 | ||
![]() |
0827044caf | ||
![]() |
342ae7c8cd | ||
![]() |
fc690b9f02 | ||
![]() |
22c9d836e0 | ||
![]() |
984997e73b | ||
![]() |
b39f407596 | ||
![]() |
615ad0cc5a | ||
![]() |
0b41cd8564 | ||
![]() |
7db523071d | ||
![]() |
974ee58b9c | ||
![]() |
1e88f2c382 | ||
![]() |
0bdcfcaaf5 | ||
![]() |
5f9c78d04f | ||
![]() |
afa178fdec | ||
![]() |
3a0e3c98f7 | ||
![]() |
fafa92d44b | ||
![]() |
fcedd06e72 | ||
![]() |
6a2acbe929 | ||
![]() |
4cfff40475 | ||
![]() |
904948dc7d | ||
![]() |
7342509b2e | ||
![]() |
ed837ba26f | ||
![]() |
13262fdb18 | ||
![]() |
baf18a8762 | ||
![]() |
c0b56b927f | ||
![]() |
242e64d72f | ||
![]() |
2262af728e | ||
![]() |
ea9947081f | ||
![]() |
e04f943980 | ||
![]() |
b38e940088 | ||
![]() |
bc0bb92f7a | ||
![]() |
8737be2623 | ||
![]() |
eb929160b3 | ||
![]() |
b8b0f257db | ||
![]() |
67b5f39df2 | ||
![]() |
7e9b3f1a60 | ||
![]() |
bce777d7c6 | ||
![]() |
465aaeff82 | ||
![]() |
40c64d50d5 | ||
![]() |
15bd2da824 | ||
![]() |
bd438ca288 | ||
![]() |
e0d02a61a9 | ||
![]() |
b3328a0ec2 | ||
![]() |
3c2041933f | ||
![]() |
e88b1cc443 | ||
![]() |
71b05b18a0 | ||
![]() |
b07b528e2a | ||
![]() |
1aeb6315ff | ||
![]() |
1b4a3d2d9f | ||
![]() |
3049a81c3b | ||
![]() |
2db1e5cb74 | ||
![]() |
78c64d39ec | ||
![]() |
46ba726232 | ||
![]() |
eb26e62889 | ||
![]() |
7f667fed18 | ||
![]() |
b2cb2b8b75 | ||
![]() |
d19f65ce4a | ||
![]() |
025b060506 | ||
![]() |
7fa2625a03 | ||
![]() |
33d62d7f21 | ||
![]() |
b336655a79 | ||
![]() |
3beffd84d6 | ||
![]() |
02761f5f35 | ||
![]() |
3b9f7885e0 | ||
![]() |
7668e45890 | ||
![]() |
695c8bc5d0 | ||
![]() |
06c42d05c3 | ||
![]() |
404104208f | ||
![]() |
b4d0ad9713 | ||
![]() |
89b1fa341b | ||
![]() |
3bda7cb26b | ||
![]() |
4f4f54a059 | ||
![]() |
12fda29280 | ||
![]() |
af060b3132 | ||
![]() |
8c500709e4 | ||
![]() |
490e6a6f23 | ||
![]() |
08177c3dd8 | ||
![]() |
d22b9c26b6 | ||
![]() |
85a350b6c8 | ||
![]() |
eae4eff92f | ||
![]() |
848be8f806 | ||
![]() |
4bb8ad19cf | ||
![]() |
c79b79b37e | ||
![]() |
8a03c366b8 | ||
![]() |
37677f389c | ||
![]() |
3e275b7dba | ||
![]() |
11b7076a43 | ||
![]() |
291c718ba2 | ||
![]() |
fcd6071c57 | ||
![]() |
476b61c4c9 | ||
![]() |
8cc5f096a2 | ||
![]() |
474d65207e | ||
![]() |
03428329ef | ||
![]() |
2692234b8c | ||
![]() |
bfb5d7e5ac | ||
![]() |
8c818e707f | ||
![]() |
3efea47ca8 | ||
![]() |
8d21988656 | ||
![]() |
89da45f9ac | ||
![]() |
34a0a00e3c | ||
![]() |
dec1094a59 | ||
![]() |
02e323133d | ||
![]() |
cb96b536a2 | ||
![]() |
627b40799c | ||
![]() |
73c4b21285 | ||
![]() |
78d7c45be3 | ||
![]() |
72edbfc455 | ||
![]() |
276535dad6 | ||
![]() |
e373e59661 | ||
![]() |
ac5ecf222e | ||
![]() |
a20594ed48 | ||
![]() |
cb59cc92a3 | ||
![]() |
34bb18448c | ||
![]() |
01253f050a | ||
![]() |
cc7e47bbb6 | ||
![]() |
5bee1c56a9 | ||
![]() |
474cc7d56d | ||
![]() |
bffdedddb4 | ||
![]() |
fd72f658c0 | ||
![]() |
42606162b2 | ||
![]() |
e82bc1b7bc | ||
![]() |
4f0e1c6c61 | ||
![]() |
550f6aff7e | ||
![]() |
67c50d7504 | ||
![]() |
94f0c61619 | ||
![]() |
8a86b30fd1 | ||
![]() |
d3b5cf82d8 | ||
![]() |
d26d804cc2 | ||
![]() |
4f9a25ee89 | ||
![]() |
6379108a75 | ||
![]() |
bb9ce0e897 | ||
![]() |
fbeaad077f | ||
![]() |
8918113a31 | ||
![]() |
c5385b5b4c | ||
![]() |
35475e1d25 | ||
![]() |
fb2c292f35 | ||
![]() |
afc3fb10c7 | ||
![]() |
0a239c2fef | ||
![]() |
f5342a09d3 | ||
![]() |
f72de687c5 | ||
![]() |
d6fb9868bf | ||
![]() |
9aff1a57d3 | ||
![]() |
7681fde4d0 | ||
![]() |
d3b7b41927 | ||
![]() |
833269fd0a | ||
![]() |
332c1a6c59 | ||
![]() |
0f1f43057e | ||
![]() |
784a7a7f24 | ||
![]() |
8e34baa59f | ||
![]() |
2926772bba | ||
![]() |
da159e4655 | ||
![]() |
a7f4496db7 | ||
![]() |
f972f02fff | ||
![]() |
1c77e26c05 | ||
![]() |
59c5363933 | ||
![]() |
b744bb0a5a | ||
![]() |
0f140b408c | ||
![]() |
7f6a6016d6 | ||
![]() |
44ed0a3279 | ||
![]() |
9964e1bb8e | ||
![]() |
8b8f725499 | ||
![]() |
bab856bce2 | ||
![]() |
711799b194 | ||
![]() |
3d285b91c6 | ||
![]() |
1dc531930d | ||
![]() |
3d3345acac | ||
![]() |
2105cacce3 | ||
![]() |
9d1d1710eb | ||
![]() |
c69dcf3e20 | ||
![]() |
eec5b37da1 | ||
![]() |
b29f0ca4d1 | ||
![]() |
576efbdc1b | ||
![]() |
a7f0510a3e | ||
![]() |
2ef088cb60 | ||
![]() |
7c320b6fc4 | ||
![]() |
e1bda4ee8b | ||
![]() |
5a4c82b860 | ||
![]() |
9b297b752e | ||
![]() |
1d6ba58ccd | ||
![]() |
1542447822 | ||
![]() |
a6f0aff659 | ||
![]() |
54930024f5 | ||
![]() |
c5f2f63458 | ||
![]() |
b2b81a5d0f | ||
![]() |
265dca3723 | ||
![]() |
171ddab32b | ||
![]() |
2aee0b0be0 | ||
![]() |
817cdf7113 | ||
![]() |
495e734428 | ||
![]() |
82120cf47f | ||
![]() |
027a5695f2 | ||
![]() |
d6d82edff5 | ||
![]() |
a12eb3fc6f | ||
![]() |
6c84574366 | ||
![]() |
1a38f25bd9 | ||
![]() |
ad40e53349 | ||
![]() |
a2ddf362d8 | ||
![]() |
65eca31635 | ||
![]() |
8b0b4a2c39 | ||
![]() |
bc5cbe9fba | ||
![]() |
f83f92d3fa | ||
![]() |
c0216c0653 | ||
![]() |
61de63a518 | ||
![]() |
d952cc2327 | ||
![]() |
19fd4dd89c | ||
![]() |
f941f5c0b0 | ||
![]() |
c7cad7e4aa | ||
![]() |
1c8988d3f7 | ||
![]() |
70a3dbe2b0 | ||
![]() |
efbb3ab25f | ||
![]() |
46447f7cfd | ||
![]() |
a6e62e07a2 | ||
![]() |
b1d25e0503 | ||
![]() |
25c557248c | ||
![]() |
b0e7c65504 | ||
![]() |
b18b044b63 | ||
![]() |
8f5f8db717 | ||
![]() |
016e28383b | ||
![]() |
f1427e9279 | ||
![]() |
169e9ab5ad | ||
![]() |
472cde29b8 | ||
![]() |
73525d19e9 | ||
![]() |
26618f8d73 | ||
![]() |
6f7c13b814 | ||
![]() |
e7d668502c | ||
![]() |
6fd357962f | ||
![]() |
0c9feedb37 | ||
![]() |
dad52724db | ||
![]() |
14ba002cbc | ||
![]() |
d48e9d5d72 | ||
![]() |
7da97489cc | ||
![]() |
a9f11b28c8 | ||
![]() |
b31d986c8d | ||
![]() |
2dad751889 | ||
![]() |
c85b1c56af | ||
![]() |
6dd34aec47 | ||
![]() |
4cd154675f | ||
![]() |
24e2c3a5e9 | ||
![]() |
064523ef25 | ||
![]() |
85f293a44e | ||
![]() |
8e412bee5f | ||
![]() |
7d5555f82e | ||
![]() |
6720725d27 | ||
![]() |
fe5c65d798 | ||
![]() |
253f3cf1ba | ||
![]() |
d8d72f92b3 | ||
![]() |
a30f5b175f | ||
![]() |
8277896ca1 | ||
![]() |
493068c073 | ||
![]() |
f4299fbea8 | ||
![]() |
10ce11d671 | ||
![]() |
db2e48b49f | ||
![]() |
5e089451af | ||
![]() |
6aa22267f4 | ||
![]() |
0f34457a10 | ||
![]() |
34c65e13bc | ||
![]() |
17a77e2577 | ||
![]() |
0f219e5ae6 | ||
![]() |
353c3c7d81 | ||
![]() |
0a89edf3b0 | ||
![]() |
e7155837d7 | ||
![]() |
f76c020dd7 | ||
![]() |
722fba7805 | ||
![]() |
86551909fc | ||
![]() |
588e94c11d | ||
![]() |
31e003bda5 | ||
![]() |
490e4d3180 | ||
![]() |
dc9f69bab0 | ||
![]() |
fdf04f77f2 | ||
![]() |
9e66310c28 | ||
![]() |
93c422dce6 | ||
![]() |
7d6eebdae3 | ||
![]() |
f11bb609c9 | ||
![]() |
5e87483f34 | ||
![]() |
f7aa451591 | ||
![]() |
321d11c2c6 | ||
![]() |
b910a92731 | ||
![]() |
ee447bc4ce | ||
![]() |
31153e4366 | ||
![]() |
7693024c29 | ||
![]() |
9628700a2f | ||
![]() |
38576173cb | ||
![]() |
19a769c12e | ||
![]() |
3c1db7d2f7 | ||
![]() |
626507093a | ||
![]() |
ee7d297ca8 | ||
![]() |
a70c0174e1 | ||
![]() |
da707afa3f | ||
![]() |
a41597431c | ||
![]() |
d0b817381e | ||
![]() |
60a2e9b5dc | ||
![]() |
df3a37b0a3 | ||
![]() |
5f4718cd13 | ||
![]() |
3cc5cb3123 | ||
![]() |
8a2872afa4 | ||
![]() |
85941c4729 | ||
![]() |
588b3d14a3 | ||
![]() |
815efa7791 | ||
![]() |
97a691ce2f | ||
![]() |
82eeefb544 | ||
![]() |
9d948f2c2b | ||
![]() |
f6061ba00e | ||
![]() |
9e3afcfe7a | ||
![]() |
0b87108174 | ||
![]() |
7fc7809cfc | ||
![]() |
c30be20e49 | ||
![]() |
25c64db0a1 | ||
![]() |
676e9c6593 | ||
![]() |
d459859361 | ||
![]() |
2be0cef446 | ||
![]() |
294db93fde | ||
![]() |
21f2f86cb8 | ||
![]() |
04576ca828 | ||
![]() |
067cb0cd9d | ||
![]() |
7f971f7173 | ||
![]() |
5c7b59524d | ||
![]() |
5133e5910e | ||
![]() |
1512c350df | ||
![]() |
a5fc7891a6 | ||
![]() |
3eb9633231 | ||
![]() |
ac67b48247 | ||
![]() |
81b65ea646 | ||
![]() |
45c1f6bc27 | ||
![]() |
0d31e5c8b1 | ||
![]() |
6378abf454 | ||
![]() |
f8fcaadb5b | ||
![]() |
0b5fd3ee76 | ||
![]() |
d010cb7e42 | ||
![]() |
71136d7347 | ||
![]() |
a18c552ddf | ||
![]() |
17fb8f2298 | ||
![]() |
fbfc4e72ca | ||
![]() |
d2e171eabc | ||
![]() |
e50094af80 | ||
![]() |
93edf72993 | ||
![]() |
a230d63cf9 | ||
![]() |
9656878ef3 | ||
![]() |
7ded7de39a | ||
![]() |
0f74e89b44 | ||
![]() |
953c40b083 | ||
![]() |
2bb39bee2f | ||
![]() |
ce2ca5446a | ||
![]() |
8a014ff786 | ||
![]() |
271b0287d8 | ||
![]() |
96a8a2a8b8 | ||
![]() |
dc09ec7598 | ||
![]() |
27fb0474d5 | ||
![]() |
7f0a87742a | ||
![]() |
47e236788c | ||
![]() |
75306f658f | ||
![]() |
325d9a0b86 | ||
![]() |
236ad57608 | ||
![]() |
6d03798314 | ||
![]() |
c954a4f7bc | ||
![]() |
ba588d1097 | ||
![]() |
44f7c9a545 | ||
![]() |
b910db322b | ||
![]() |
c44a942fb7 | ||
![]() |
d713ad3499 | ||
![]() |
ddf40df649 | ||
![]() |
7c6d85221d | ||
![]() |
b66b82a6e9 | ||
![]() |
c44b85ea87 | ||
![]() |
a02493fbaa | ||
![]() |
9c27d691dd | ||
![]() |
fcbf56e93a | ||
![]() |
a539ffb188 | ||
![]() |
512f533a80 | ||
![]() |
96ef9cdbee | ||
![]() |
935bd01f59 | ||
![]() |
eeb5d669f6 | ||
![]() |
28fcbbcf7b | ||
![]() |
0f4326151f | ||
![]() |
e0e27774ad | ||
![]() |
1223b48b2c | ||
![]() |
d8338f0b48 | ||
![]() |
38019f7f42 | ||
![]() |
78daa2eb62 | ||
![]() |
40eda05a30 | ||
![]() |
9f9de8c43b | ||
![]() |
23978ef4d2 | ||
![]() |
3b4cb23112 | ||
![]() |
974cb1167f | ||
![]() |
6ccbc272c6 | ||
![]() |
0eb28c3265 | ||
![]() |
2daa131fb2 | ||
![]() |
51247d36c5 | ||
![]() |
a910c8ccd8 | ||
![]() |
43bda2d4a4 | ||
![]() |
c7033dd757 | ||
![]() |
5673a9bace | ||
![]() |
34ff764515 | ||
![]() |
1b3a009da7 | ||
![]() |
a49002bb2c | ||
![]() |
7342fc2307 | ||
![]() |
9867a3bd60 | ||
![]() |
5ffb9eaa5b | ||
![]() |
37fa227fb5 | ||
![]() |
9dd272b357 | ||
![]() |
277298feae | ||
![]() |
ff24bc0b68 | ||
![]() |
b05b688267 | ||
![]() |
f3d7f85063 | ||
![]() |
de969a9dab | ||
![]() |
59fd38bbf8 | ||
![]() |
06dc6df270 | ||
![]() |
ff8460b361 | ||
![]() |
674d272eaa | ||
![]() |
c3e00c279d | ||
![]() |
175d920c94 | ||
![]() |
700c51f95c | ||
![]() |
04920883ea | ||
![]() |
659914afbe | ||
![]() |
ee06aed94b | ||
![]() |
af1f5d5ab2 | ||
![]() |
5e44b0b9d5 | ||
![]() |
23c1a1dab8 | ||
![]() |
f5d054b93c | ||
![]() |
d25ae5e0a9 | ||
![]() |
c42a51dcbb | ||
![]() |
da3fd92b31 | ||
![]() |
4a45ba3c14 | ||
![]() |
4292ddd0ae | ||
![]() |
4a68fd65b6 | ||
![]() |
0e33632e79 | ||
![]() |
a9b20dae33 | ||
![]() |
dbc8bed234 | ||
![]() |
f8b4190a11 | ||
![]() |
479972e3ae | ||
![]() |
3ea28b0afb | ||
![]() |
2b3cc28966 | ||
![]() |
751642b39a | ||
![]() |
d6c2c821a4 | ||
![]() |
dfc65b95f7 | ||
![]() |
b45d922463 | ||
![]() |
e595937740 | ||
![]() |
72eb584e65 | ||
![]() |
f87ee3fcf9 | ||
![]() |
e0927cd763 | ||
![]() |
8999a57f06 | ||
![]() |
8024089bde | ||
![]() |
5e01f785ae | ||
![]() |
d35d1b8860 | ||
![]() |
88027f2151 | ||
![]() |
21099eabfa | ||
![]() |
cd41e7108b | ||
![]() |
abbd2e6b72 | ||
![]() |
6da566faff | ||
![]() |
df7a866617 | ||
![]() |
1cc8f13d54 | ||
![]() |
086ce63c6c | ||
![]() |
f1dcecc6cf | ||
![]() |
fe1ce08a6c | ||
![]() |
1d64ddb7f5 | ||
![]() |
823b121cc7 | ||
![]() |
149d35c687 | ||
![]() |
3a18e68751 | ||
![]() |
6afcc83955 | ||
![]() |
277d8773f2 | ||
![]() |
f161cf8b0a | ||
![]() |
dc62ae95a6 | ||
![]() |
f4ecc315d0 | ||
![]() |
cb2a1e57fe | ||
![]() |
1396faf433 | ||
![]() |
dc8d2ae683 | ||
![]() |
191c7c50b6 | ||
![]() |
c6725b0518 | ||
![]() |
4820a6e01c | ||
![]() |
57a9b5bc0c | ||
![]() |
8c224da5d5 | ||
![]() |
14e49f3c80 | ||
![]() |
cc8f1adca3 | ||
![]() |
5b7ddbbb01 | ||
![]() |
6352fbb3b2 | ||
![]() |
122e2f7a8e | ||
![]() |
b4e1585e2b | ||
![]() |
d3f49334e2 | ||
![]() |
c4356171b3 | ||
![]() |
5c5625911d | ||
![]() |
6a10cc9c55 | ||
![]() |
6b317f918e | ||
![]() |
08b528dc4f | ||
![]() |
fc886a5a47 | ||
![]() |
0cb90e2e55 | ||
![]() |
64113a69b4 | ||
![]() |
544bb7459c | ||
![]() |
578a50b464 | ||
![]() |
3d4081d0af | ||
![]() |
b763b81f56 | ||
![]() |
947dae4900 | ||
![]() |
a5830599c4 | ||
![]() |
debd1d7d54 | ||
![]() |
cba0d04000 | ||
![]() |
695e7e6da0 | ||
![]() |
4cd4bfa1d7 | ||
![]() |
16b400964b | ||
![]() |
cf2d02c0dd | ||
![]() |
0fcd0de0d1 | ||
![]() |
748a35774f | ||
![]() |
a52a3e38ed | ||
![]() |
ee0cef06a6 | ||
![]() |
0e5a113a0c | ||
![]() |
a1ccd44013 | ||
![]() |
4d91e50d6d | ||
![]() |
120668c7bc | ||
![]() |
d81ccde569 | ||
![]() |
e8581b4adb | ||
![]() |
19906575a3 | ||
![]() |
9329094a4e | ||
![]() |
b44f5122fd | ||
![]() |
17981730a4 | ||
![]() |
53de6da26c | ||
![]() |
3e30ccdeee | ||
![]() |
baaaf7d5de | ||
![]() |
45d8d139a9 | ||
![]() |
fe644e10d0 | ||
![]() |
f383d11d10 | ||
![]() |
ef1b928532 | ||
![]() |
6e46d394b1 | ||
![]() |
f109038d12 | ||
![]() |
e31e687602 | ||
![]() |
86bfb22d4c | ||
![]() |
3f057367e3 | ||
![]() |
3d7ed5820e | ||
![]() |
0118f2efa7 | ||
![]() |
15312e4709 | ||
![]() |
bf1568a73a | ||
![]() |
13a2520ea5 | ||
![]() |
f53238f206 | ||
![]() |
9375748d9b | ||
![]() |
201df54e79 | ||
![]() |
0b54fe477b | ||
![]() |
4119e6669e | ||
![]() |
d33e5226b3 | ||
![]() |
d73f39c706 | ||
![]() |
087b451e17 | ||
![]() |
86481c74ff | ||
![]() |
5b937fb1fa | ||
![]() |
ff828116bc | ||
![]() |
ee39616a8b | ||
![]() |
cdb53ca049 | ||
![]() |
8cf475f708 | ||
![]() |
0cb449e1d6 | ||
![]() |
e6adb7abca | ||
![]() |
cfad7dd317 | ||
![]() |
dd35224f92 | ||
![]() |
1283590eeb | ||
![]() |
dca3fe396f | ||
![]() |
8d87eae11b | ||
![]() |
fd7eaacae0 | ||
![]() |
fba33cbbe9 | ||
![]() |
950ffcd790 | ||
![]() |
c178299013 | ||
![]() |
5d17c1f588 | ||
![]() |
a75c00d94e | ||
![]() |
cd19517414 | ||
![]() |
155f39aab5 | ||
![]() |
4514d0b467 | ||
![]() |
6f4a938a31 | ||
![]() |
1303ea95dd | ||
![]() |
727fe1bd15 | ||
![]() |
64ebc977e9 | ||
![]() |
e89c50d934 | ||
![]() |
c859ddfb8f | ||
![]() |
a6126c5eda | ||
![]() |
85d9bd9106 | ||
![]() |
39e9622205 | ||
![]() |
021994c9f3 | ||
![]() |
2e7ce2a769 | ||
![]() |
84f0ff2fad | ||
![]() |
e6561e5f84 | ||
![]() |
5fa452aa74 | ||
![]() |
2225ccb146 | ||
![]() |
5aafc78847 | ||
![]() |
0d03833cff | ||
![]() |
a797d5d396 | ||
![]() |
f2494374f8 | ||
![]() |
48395ba860 | ||
![]() |
5ba5f5f94e | ||
![]() |
42ce6fd334 | ||
![]() |
f5c3ee3ae1 | ||
![]() |
3c7ece1605 | ||
![]() |
870efc49ea | ||
![]() |
085ede6d93 | ||
![]() |
4ef19d17da | ||
![]() |
223913c30a | ||
![]() |
010e4de4e1 | ||
![]() |
41134466ed | ||
![]() |
8f07747452 | ||
![]() |
eb5ce5be1e | ||
![]() |
71d855e836 | ||
![]() |
33b7ab593c | ||
![]() |
8706d834b4 | ||
![]() |
7cfab33ebb | ||
![]() |
1ababc8c7f | ||
![]() |
1f75e63c37 | ||
![]() |
cb3f9b9740 | ||
![]() |
9784353223 | ||
![]() |
7d93ca5c73 | ||
![]() |
ac20063e86 | ||
![]() |
debaec32af | ||
![]() |
0e9b71e7a9 | ||
![]() |
85f5ff3c14 | ||
![]() |
3d81f167ea | ||
![]() |
fb70a2e52d | ||
![]() |
460e85a1b5 | ||
![]() |
539b64bd57 | ||
![]() |
90e38a06a2 | ||
![]() |
09ab910630 | ||
![]() |
c15f80b33f | ||
![]() |
b2e6ba3c4a | ||
![]() |
b16f696b0e | ||
![]() |
9adfb382e8 | ||
![]() |
44368383f4 | ||
![]() |
d1ff7e0ffe | ||
![]() |
42e7db8d13 | ||
![]() |
0c17ea5755 | ||
![]() |
cdaff5b39c | ||
![]() |
2b1b970e78 | ||
![]() |
0aebc0a8e3 | ||
![]() |
c3a89f589e | ||
![]() |
971cd73fb3 | ||
![]() |
1947860d61 | ||
![]() |
55aaa421e8 | ||
![]() |
a8932706d8 | ||
![]() |
a97972aac0 | ||
![]() |
094c3d559a | ||
![]() |
6fb032b3c2 | ||
![]() |
8ca188f4d4 | ||
![]() |
746a1d8d59 | ||
![]() |
63c5e00d86 | ||
![]() |
9d2e5d6665 | ||
![]() |
f6045bf8b5 | ||
![]() |
e83f40d5c5 | ||
![]() |
e5118418b2 | ||
![]() |
7cd814d917 | ||
![]() |
78282c1a49 | ||
![]() |
fd4214ccf3 | ||
![]() |
0785945635 | ||
![]() |
967bdeae7b | ||
![]() |
452db51669 | ||
![]() |
5875ced367 | ||
![]() |
fbac6bcfd0 | ||
![]() |
0dcd3ece9d | ||
![]() |
224fff89e3 | ||
![]() |
22e73644f9 | ||
![]() |
6a0f6ab319 | ||
![]() |
88a394836f | ||
![]() |
f822c1c2e4 | ||
![]() |
1d16d980b3 | ||
![]() |
501b18f986 | ||
![]() |
21ed759e53 | ||
![]() |
8d50dfd93c | ||
![]() |
51e40dd98c | ||
![]() |
b2048379af | ||
![]() |
011539f6f1 | ||
![]() |
5457c3803f | ||
![]() |
b3d777bb6c | ||
![]() |
12e00c3054 | ||
![]() |
40b683111c | ||
![]() |
9542ca773f | ||
![]() |
8af832a496 | ||
![]() |
6836130fda | ||
![]() |
724893879f | ||
![]() |
736729f5ef | ||
![]() |
aa47966347 | ||
![]() |
d64d12afe8 | ||
![]() |
1f8df419c4 | ||
![]() |
7ba8202af5 | ||
![]() |
d7b691cf59 | ||
![]() |
7058d5e4cd | ||
![]() |
52fd508fea | ||
![]() |
41045b62dc | ||
![]() |
188ea2644a | ||
![]() |
4c8f357978 | ||
![]() |
4bb2fd6ba6 | ||
![]() |
33c9f74508 | ||
![]() |
f53fe67372 | ||
![]() |
51ff724691 | ||
![]() |
291bf93f9d | ||
![]() |
5fcd629f16 | ||
![]() |
ab90901793 | ||
![]() |
4f206fd918 | ||
![]() |
7233285437 | ||
![]() |
8e348a11c2 | ||
![]() |
085ea6d0a1 | ||
![]() |
aaf88b1895 | ||
![]() |
4f4a9412a3 | ||
![]() |
a92e039363 | ||
![]() |
33aa4ca4b7 | ||
![]() |
05658cafc7 | ||
![]() |
ff3710de66 | ||
![]() |
db8dd9f186 | ||
![]() |
e8b73ba6d1 | ||
![]() |
f1112fdf37 | ||
![]() |
a48c4f9e05 | ||
![]() |
19a521d2e9 | ||
![]() |
dd6e55ac31 | ||
![]() |
b1e63f0f14 | ||
![]() |
b0e49a4cc8 | ||
![]() |
1e94517a72 | ||
![]() |
98f60216ac | ||
![]() |
e29b712108 | ||
![]() |
a462435f2f | ||
![]() |
911b8273fe | ||
![]() |
09935e591a | ||
![]() |
4a212dba35 | ||
![]() |
aac9e85e04 | ||
![]() |
bb67a837d3 | ||
![]() |
6cde695194 | ||
![]() |
a1a1ac0bbb | ||
![]() |
9ec8bc2166 | ||
![]() |
28cd6a75e7 | ||
![]() |
4cc7aced15 | ||
![]() |
1058aeb04f | ||
![]() |
cfec0db947 | ||
![]() |
120bd6cd68 | ||
![]() |
9aef06d1b8 | ||
![]() |
e6e9dd751c | ||
![]() |
5dd677756f | ||
![]() |
b77c590910 | ||
![]() |
7e5f2822ae | ||
![]() |
12bbc7fd6b | ||
![]() |
bf9ac8252b | ||
![]() |
4a3f5dc619 | ||
![]() |
ca156befbd | ||
![]() |
4db41e2ac4 | ||
![]() |
982a43fce1 | ||
![]() |
dd76a74e1c | ||
![]() |
70cb52b2c7 | ||
![]() |
5c7f69acaa | ||
![]() |
f1d9015e5f | ||
![]() |
e8d900c58e | ||
![]() |
a6241ae912 | ||
![]() |
4a697ca2ec | ||
![]() |
58bec7f2c9 | ||
![]() |
213f84985c | ||
![]() |
074b1f8c61 | ||
![]() |
326eee8c83 | ||
![]() |
00bff4912e | ||
![]() |
0ce1720516 | ||
![]() |
ee407472cf | ||
![]() |
f341f3b2dd | ||
![]() |
8513946e09 | ||
![]() |
8ebd9c8927 | ||
![]() |
1d54c5144e | ||
![]() |
e40d4318fa | ||
![]() |
7756e10779 | ||
![]() |
3e58d502d0 | ||
![]() |
1c8846dc57 | ||
![]() |
2f320c7239 | ||
![]() |
e799918ab6 | ||
![]() |
86c4928e0f | ||
![]() |
0293eb5c51 | ||
![]() |
1ee75b6aa6 | ||
![]() |
4b30b224b5 | ||
![]() |
16b232d2a3 | ||
![]() |
3f3b1f5b1d | ||
![]() |
cec017b7bf | ||
![]() |
3123cc1059 | ||
![]() |
caa9df86bc | ||
![]() |
f417389a7a | ||
![]() |
662a5c8ea6 | ||
![]() |
7edfbfb764 | ||
![]() |
c1602d2554 | ||
![]() |
9f8d4e1022 | ||
![]() |
d1dfda405f | ||
![]() |
28efded624 | ||
![]() |
06c86ee267 | ||
![]() |
5892780871 | ||
![]() |
4fcdcd9a8a | ||
![]() |
80d834fb55 | ||
![]() |
4122ebe18f | ||
![]() |
7d87777bf8 | ||
![]() |
4a73d634e0 | ||
![]() |
373dc10a40 | ||
![]() |
ed43ec8ea2 | ||
![]() |
7918fc3528 | ||
![]() |
bf58205b0a | ||
![]() |
c0d1ce96d1 | ||
![]() |
b31d3802eb | ||
![]() |
be1228c3b4 | ||
![]() |
15c94c6b34 | ||
![]() |
202d23426a | ||
![]() |
fc26de48b2 | ||
![]() |
76c88913f9 | ||
![]() |
a3a1aed723 | ||
![]() |
81aa56f60f | ||
![]() |
73bb850209 | ||
![]() |
8dfec12330 | ||
![]() |
ae24397793 | ||
![]() |
3b0f888407 | ||
![]() |
845d1e02b0 | ||
![]() |
5d357bc41f | ||
![]() |
6a54672b13 | ||
![]() |
3d9a15df44 | ||
![]() |
449c7fda2f | ||
![]() |
8b7b05da68 | ||
![]() |
92400ebcab | ||
![]() |
23d3e56967 | ||
![]() |
6785dc4967 | ||
![]() |
dad20f6a2d | ||
![]() |
bb15671046 | ||
![]() |
21984fac8b | ||
![]() |
f392afe87f | ||
![]() |
6a243ec7bc | ||
![]() |
8cd3b603df | ||
![]() |
6e1aefe6d8 | ||
![]() |
1c90b6eca3 | ||
![]() |
c33cf9f878 | ||
![]() |
27cb40eec9 | ||
![]() |
c06081b75d | ||
![]() |
a7eec2f0a0 | ||
![]() |
4fd0fe3194 | ||
![]() |
cc74593ddd | ||
![]() |
fdb7c5dba1 | ||
![]() |
77470c7cfa | ||
![]() |
f0a734fdab | ||
![]() |
75405b2b25 | ||
![]() |
90ed4b3c49 | ||
![]() |
290a17a764 | ||
![]() |
aaabd836e4 | ||
![]() |
076e5cea3b | ||
![]() |
8515971ccf | ||
![]() |
d86fb033ea | ||
![]() |
99d7d8ddbc | ||
![]() |
df78fd2d41 | ||
![]() |
dabe6267b9 | ||
![]() |
0119ebddbe | ||
![]() |
3216ef9f47 | ||
![]() |
b79d1bcded | ||
![]() |
17e234f9d5 | ||
![]() |
ea1f75f80e | ||
![]() |
8c40db5730 | ||
![]() |
6fe03d2795 | ||
![]() |
c595a87ccf | ||
![]() |
fac07c3913 | ||
![]() |
c63fdbbc6b | ||
![]() |
2ff5d9606b | ||
![]() |
ed43452c1a | ||
![]() |
8f28d4028f | ||
![]() |
b54543b18c | ||
![]() |
966d6593ca | ||
![]() |
ad95b1c9d1 | ||
![]() |
3bfa38c60a | ||
![]() |
0bdbcad8be | ||
![]() |
80855e89ec | ||
![]() |
0850401dc4 | ||
![]() |
337fda2023 | ||
![]() |
64f238191e | ||
![]() |
eb169cb133 | ||
![]() |
80cd85b061 | ||
![]() |
89275270f3 | ||
![]() |
e7339ba619 | ||
![]() |
d9ad7d522c | ||
![]() |
92789c3113 | ||
![]() |
c1c677e161 | ||
![]() |
2fe917ff82 | ||
![]() |
0e6c205732 | ||
![]() |
125ae0a173 | ||
![]() |
0245e13591 | ||
![]() |
d546733287 | ||
![]() |
c275326d59 | ||
![]() |
d4561507b8 | ||
![]() |
ef0e22cc41 | ||
![]() |
62db65bf18 | ||
![]() |
d5371f752c | ||
![]() |
a5f5e94115 | ||
![]() |
2624706c69 | ||
![]() |
d39d885ec2 | ||
![]() |
d83c744725 | ||
![]() |
843995cdb9 | ||
![]() |
9491ba77e0 | ||
![]() |
58a449d437 | ||
![]() |
7f55e0f05b | ||
![]() |
67c3f40adb | ||
![]() |
ff7a0ba599 | ||
![]() |
b152c63102 | ||
![]() |
415ff23be5 | ||
![]() |
b0d6de783e | ||
![]() |
ac28e6e5ca | ||
![]() |
4f9e8d2e8a | ||
![]() |
21be2f46f3 | ||
![]() |
a6e7680212 | ||
![]() |
e79e744e08 | ||
![]() |
7abdac72a4 | ||
![]() |
90d85eaf7d | ||
![]() |
e65f9740fb | ||
![]() |
7538f89b56 | ||
![]() |
7c755a3991 | ||
![]() |
10e903c9fc | ||
![]() |
b018124226 | ||
![]() |
a9350f50c9 | ||
![]() |
ed7babcbf1 | ||
![]() |
61ebc335c4 | ||
![]() |
0167bd76f1 | ||
![]() |
79d704008b | ||
![]() |
0a703585b0 | ||
![]() |
5d632d0d90 | ||
![]() |
4eecaea601 | ||
![]() |
63055818ec | ||
![]() |
0beb08b687 | ||
![]() |
b27801a27c | ||
![]() |
a0cfce7cbc | ||
![]() |
8b7144c986 | ||
![]() |
d3f5f5ee59 | ||
![]() |
a2a3c7f438 | ||
![]() |
4496f82d5b | ||
![]() |
09d531557d | ||
![]() |
7fee82f731 | ||
![]() |
475054c48a | ||
![]() |
a743d05751 | ||
![]() |
d1ed502e03 | ||
![]() |
37744c7ab6 | ||
![]() |
bbc9e60a12 | ||
![]() |
6c975ecc4c | ||
![]() |
23e8a4ce4b | ||
![]() |
50134a2f9b | ||
![]() |
628b37c4fa | ||
![]() |
1b4ae70a43 | ||
![]() |
b25c49725f | ||
![]() |
b245782c7e | ||
![]() |
a9f32baae0 | ||
![]() |
e7ef71865d | ||
![]() |
88c4f72b37 | ||
![]() |
abbcdf91a5 | ||
![]() |
b876df6e21 | ||
![]() |
4bb81f35d7 | ||
![]() |
ff20267b3f | ||
![]() |
2c9586d811 | ||
![]() |
2813d2031a | ||
![]() |
065051a360 | ||
![]() |
db218407b0 | ||
![]() |
d52210dd90 | ||
![]() |
f3cd9a096a | ||
![]() |
e426090a18 | ||
![]() |
cbe64fd559 | ||
![]() |
63ea7a70bd | ||
![]() |
fb0998f7a2 | ||
![]() |
a9b00dd537 | ||
![]() |
52eb059515 | ||
![]() |
7640246255 | ||
![]() |
52c83b2916 | ||
![]() |
d9cded0fc9 | ||
![]() |
750c42caf1 | ||
![]() |
bbf650c6cf | ||
![]() |
a25dace7e0 | ||
![]() |
14ff22fbcd | ||
![]() |
07eb7dda2d | ||
![]() |
d4058175b4 | ||
![]() |
2de984ae24 | ||
![]() |
761a8bf2a9 | ||
![]() |
6df7006b36 | ||
![]() |
aceb3ee863 | ||
![]() |
11d716a3c8 | ||
![]() |
7cc8c014eb | ||
![]() |
f21241d944 | ||
![]() |
a181fa0652 | ||
![]() |
3f748b4d2a | ||
![]() |
683450f9c6 | ||
![]() |
adbd47a36c | ||
![]() |
ce693aa5e9 | ||
![]() |
ad80804461 | ||
![]() |
2d55632430 | ||
![]() |
e81f00ef1a | ||
![]() |
93fb0e3d74 | ||
![]() |
71ce0de606 | ||
![]() |
0407062c1d | ||
![]() |
cda14af208 | ||
![]() |
258f170cd7 | ||
![]() |
f76015d714 | ||
![]() |
7e5e14163c | ||
![]() |
bcd1064e94 | ||
![]() |
8a8441c875 | ||
![]() |
15aa813416 | ||
![]() |
605faccffd | ||
![]() |
79f2d08c81 | ||
![]() |
0568ae5391 | ||
![]() |
5330dda9f8 | ||
![]() |
ebab126579 | ||
![]() |
0e5417a13e | ||
![]() |
9a968e0584 | ||
![]() |
ffec64d209 | ||
![]() |
f332746188 | ||
![]() |
b2fa5b551e | ||
![]() |
36e83edddc | ||
![]() |
6b045eadef | ||
![]() |
147264822c | ||
![]() |
36e4ccd800 | ||
![]() |
796c16237d | ||
![]() |
861ad9881c | ||
![]() |
3101c538e9 | ||
![]() |
42adc7382f | ||
![]() |
9bb4dfad13 | ||
![]() |
bd00ae8ede | ||
![]() |
f309522268 | ||
![]() |
0efaddff23 | ||
![]() |
94ba7cb0c5 | ||
![]() |
2d58c725e0 | ||
![]() |
e035523eb8 | ||
![]() |
bea5308ab7 | ||
![]() |
f006a85fec | ||
![]() |
ea93013ebc |
7
.gitattributes
vendored
7
.gitattributes
vendored
@@ -11,9 +11,14 @@
|
||||
*.bat text eol=crlf
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
chromeos/** binary
|
||||
tools/** binary
|
||||
*.jar binary
|
||||
*.exe binary
|
||||
*.apk binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.ttf binary
|
||||
|
||||
# Help GitHub detect languages
|
||||
native/jni/external/** linguist-vendored
|
||||
native/jni/systemproperties/** linguist-language=C++
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -2,8 +2,8 @@ out
|
||||
*.zip
|
||||
*.jks
|
||||
*.apk
|
||||
config.prop
|
||||
update.sh
|
||||
/config.prop
|
||||
/update.sh
|
||||
|
||||
# Built binaries
|
||||
native/out
|
||||
|
9
.gitmodules
vendored
9
.gitmodules
vendored
@@ -19,3 +19,12 @@
|
||||
[submodule "nanopb"]
|
||||
path = native/jni/external/nanopb
|
||||
url = https://github.com/nanopb/nanopb.git
|
||||
[submodule "mincrypt"]
|
||||
path = native/jni/external/mincrypt
|
||||
url = https://github.com/topjohnwu/mincrypt.git
|
||||
[submodule "pcre"]
|
||||
path = native/jni/external/pcre
|
||||
url = https://android.googlesource.com/platform/external/pcre
|
||||
[submodule "termux-elf-cleaner"]
|
||||
path = tools/termux-elf-cleaner
|
||||
url = https://github.com/termux/termux-elf-cleaner.git
|
||||
|
37
README.MD
37
README.MD
@@ -6,11 +6,18 @@
|
||||
|
||||
Magisk is a suite of open source tools for customizing Android, supporting devices higher than Android 4.2 (API 17). It covers the fundamental parts for Android customization: root, boot scripts, SELinux patches, AVB2.0 / dm-verity / forceencrypt removals etc.
|
||||
|
||||
Furthermore, Magisk provides a **Systemless Interface** to alter the system (or vendor) arbitrarily while the actual partitions stay completely intact. With its systemless nature along with several other hacks, Magisk can hide modifications from nearly any system integrity verifications used in banking apps, corporation monitoring apps, game cheat detections, and most importantly [Google's SafetyNet API](https://developer.android.com/training/safetynet/index.html).
|
||||
Furthermore, Magisk provides a **Systemless Interface** to alter the system (or vendor) arbitrarily while the actual partitions stay completely intact. With its systemless nature along with several other hacks, Magisk can almost perfectly hide modifications within userspace. Note that since 2020.3, the CTS check of [Google's SafetyNet API](https://developer.android.com/training/safetynet/index.html) will **NOT** pass.
|
||||
|
||||
## Bug Reports
|
||||
|
||||
**Make sure to install the latest [Canary Build](https://forum.xda-developers.com/apps/magisk/dev-magisk-canary-channel-bleeding-edge-t3839337) before reporting any bugs!** **DO NOT** report bugs that is already fixed upstream. Follow the instructions in the [Canary Channel XDA Thread](https://forum.xda-developers.com/apps/magisk/dev-magisk-canary-channel-bleeding-edge-t3839337), and report a bug either by opening an issue on GitHub or directly in the thread.
|
||||
**Only reports using debug canary builds will be accepted.** \
|
||||
Access canary builds by upgrading to either canary Magisk Manager:
|
||||
- [Canary Manager (Release)](https://raw.githubusercontent.com/topjohnwu/magisk_files/canary/app-release.apk)
|
||||
- [Canary Manager (Debug)](https://raw.githubusercontent.com/topjohnwu/magisk_files/canary/app-debug.apk)
|
||||
|
||||
For installation issues, upload both boot image and install logs. \
|
||||
For Magisk issues, upload boot logcat or dmesg. \
|
||||
For Magisk Manager crashes, record and upload the logcat when the crash occurs.
|
||||
|
||||
## Building Environment Requirements
|
||||
|
||||
@@ -30,34 +37,12 @@ Furthermore, Magisk provides a **Systemless Interface** to alter the system (or
|
||||
|
||||
## Translations
|
||||
|
||||
Default string resources for Magisk Manager are scattered throughout
|
||||
Default string resources for Magisk Manager and its stub APK are located here:
|
||||
|
||||
- `app/src/main/res/values/strings.xml`
|
||||
- `stub/src/main/res/values/strings.xml`
|
||||
- `shared/src/main/res/values/strings.xml`
|
||||
|
||||
Translate each and place them in the respective locations (`<module>/src/main/res/values-<lang>/strings.xml`).
|
||||
|
||||
## Signature Verification
|
||||
|
||||
Official release zips and APKs are signed with my personal private key. You can verify the key certificate to make sure the binaries you downloaded are not manipulated in anyway.
|
||||
|
||||
``` bash
|
||||
# Use the keytool command from JDK to print certificates
|
||||
keytool -printcert -jarfile <APK or Magisk zip>
|
||||
|
||||
# The output should contain the following signature
|
||||
Owner: CN=John Wu, L=Taipei, C=TW
|
||||
Issuer: CN=John Wu, L=Taipei, C=TW
|
||||
Serial number: 50514879
|
||||
Valid from: Sun Aug 14 13:23:44 EDT 2016 until: Tue Jul 21 13:23:44 EDT 2116
|
||||
Certificate fingerprints:
|
||||
MD5: CE:DA:68:C1:E1:74:71:0A:EF:58:89:7D:AE:6E:AB:4F
|
||||
SHA1: DC:0F:2B:61:CB:D7:E9:D3:DB:BE:06:0B:2B:87:0D:46:BB:06:02:11
|
||||
SHA256: B4:CB:83:B4:DA:D9:9F:99:7D:BE:87:2F:01:3A:A1:6C:14:EE:C4:1D:16:70:21:F3:71:F7:E1:33:0F:27:3E:E6
|
||||
Signature algorithm name: SHA256withRSA
|
||||
Version: 3
|
||||
```
|
||||
Translate each and place them in the respective locations (`[module]/src/main/res/values-[lang]/strings.xml`).
|
||||
|
||||
## License
|
||||
|
||||
|
123
app/build.gradle
123
app/build.gradle
@@ -1,6 +1,6 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
kapt {
|
||||
@@ -16,51 +16,120 @@ android {
|
||||
defaultConfig {
|
||||
applicationId 'com.topjohnwu.magisk'
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
versionName configProps['appVersion']
|
||||
versionCode configProps['appVersionCode'] as Integer
|
||||
multiDexEnabled true
|
||||
versionName props['appVersion']
|
||||
versionCode props['appVersionCode'] as Integer
|
||||
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
argument('butterknife.debuggable', 'false')
|
||||
arguments = ["room.incremental":"true"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
|
||||
'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
dataBinding {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude '/META-INF/**'
|
||||
exclude '/androidsupportmultidexversion.txt'
|
||||
exclude '/org/bouncycastle/**'
|
||||
exclude '/kotlin/**'
|
||||
exclude '/kotlinx/**'
|
||||
exclude '/okhttp3/**'
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
implementation project(':net')
|
||||
implementation project(':shared')
|
||||
implementation project(':signing')
|
||||
|
||||
implementation 'com.github.topjohnwu:jtar:1.0.0'
|
||||
implementation 'net.sourceforge.streamsupport:android-retrostreams:1.7.0'
|
||||
implementation 'com.github.sevar83:indeterminate-checkbox:1.0.5'
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
implementation 'com.ncapdevi:frag-nav:3.2.0'
|
||||
implementation 'com.github.pwittchen:reactivenetwork-rx2:3.0.6'
|
||||
|
||||
def markwonVersion = '3.0.0'
|
||||
implementation "ru.noties.markwon:core:${markwonVersion}"
|
||||
implementation "ru.noties.markwon:html:${markwonVersion}"
|
||||
implementation "ru.noties.markwon:image-svg:${markwonVersion}"
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.2.18'
|
||||
implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||
|
||||
def androidXVersion = "1.0.0"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||
implementation "androidx.preference:preference:${androidXVersion}"
|
||||
implementation "androidx.recyclerview:recyclerview:${androidXVersion}"
|
||||
implementation "androidx.cardview:cardview:${androidXVersion}"
|
||||
implementation "com.google.android.material:material:${androidXVersion}"
|
||||
implementation 'androidx.work:work-runtime:2.0.1'
|
||||
implementation 'androidx.transition:transition:1.1.0-beta01'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:${vKotlin}"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${vKotlin}"
|
||||
|
||||
def libsuVersion = '2.5.0'
|
||||
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
|
||||
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
|
||||
def vBAdapt = '3.1.1'
|
||||
def bindingAdapter = 'me.tatarka.bindingcollectionadapter2:bindingcollectionadapter'
|
||||
implementation "${bindingAdapter}:${vBAdapt}"
|
||||
implementation "${bindingAdapter}-recyclerview:${vBAdapt}"
|
||||
|
||||
def butterKnifeVersion = '10.1.0'
|
||||
implementation "com.jakewharton:butterknife-runtime:${butterKnifeVersion}"
|
||||
kapt "com.jakewharton:butterknife-compiler:${butterKnifeVersion}"
|
||||
def vMarkwon = '4.2.1'
|
||||
implementation "io.noties.markwon:core:${vMarkwon}"
|
||||
implementation "io.noties.markwon:html:${vMarkwon}"
|
||||
implementation "io.noties.markwon:image:${vMarkwon}"
|
||||
implementation 'com.caverock:androidsvg:1.4'
|
||||
|
||||
def vLibsu = '2.5.1'
|
||||
implementation "com.github.topjohnwu.libsu:core:${vLibsu}"
|
||||
implementation "com.github.topjohnwu.libsu:io:${vLibsu}"
|
||||
|
||||
def vKoin = '2.0.1'
|
||||
implementation "org.koin:koin-core:${vKoin}"
|
||||
implementation "org.koin:koin-android:${vKoin}"
|
||||
implementation "org.koin:koin-androidx-viewmodel:${vKoin}"
|
||||
|
||||
def vRetrofit = '2.7.1'
|
||||
implementation "com.squareup.retrofit2:retrofit:${vRetrofit}"
|
||||
implementation "com.squareup.retrofit2:converter-moshi:${vRetrofit}"
|
||||
implementation "com.squareup.retrofit2:converter-scalars:${vRetrofit}"
|
||||
implementation "com.squareup.retrofit2:adapter-rxjava2:${vRetrofit}"
|
||||
|
||||
def vOkHttp = '3.12.10'
|
||||
implementation("com.squareup.okhttp3:okhttp:${vOkHttp}") {
|
||||
force = true
|
||||
}
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:${vOkHttp}"
|
||||
|
||||
def vMoshi = '1.10.0-SNAPSHOT'
|
||||
implementation "com.squareup.moshi:moshi:${vMoshi}"
|
||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}"
|
||||
|
||||
def vRoom = '2.2.4'
|
||||
implementation "androidx.room:room-runtime:${vRoom}"
|
||||
implementation "androidx.room:room-rxjava2:${vRoom}"
|
||||
kapt "androidx.room:room-compiler:${vRoom}"
|
||||
|
||||
def vNav = '2.2.1'
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:${vNav}"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:${vNav}"
|
||||
|
||||
implementation 'androidx.biometric:biometric:1.0.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-beta01'
|
||||
implementation 'androidx.browser:browser:1.2.0'
|
||||
implementation 'androidx.preference:preference:1.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.2.2'
|
||||
implementation 'androidx.work:work-runtime:2.3.3'
|
||||
implementation 'androidx.transition:transition:1.3.1'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.core:core-ktx:1.2.0'
|
||||
implementation 'com.google.android.material:material:1.2.0-alpha03'
|
||||
}
|
||||
|
44
app/proguard-rules.pro
vendored
44
app/proguard-rules.pro
vendored
@@ -16,38 +16,36 @@
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# BouncyCastle
|
||||
-keep,allowoptimization class org.bouncycastle.jcajce.provider.asymmetric.rsa.**SHA1** { *; }
|
||||
-keep,allowoptimization class org.bouncycastle.jcajce.provider.asymmetric.RSA** { *; }
|
||||
-keep,allowoptimization class org.bouncycastle.jcajce.provider.digest.SHA1** { *; }
|
||||
-dontwarn javax.naming.**
|
||||
# Kotlin
|
||||
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
||||
public static void checkExpressionValueIsNotNull(...);
|
||||
public static void checkNotNullExpressionValue(...);
|
||||
public static void checkReturnedValueIsNotNull(...);
|
||||
public static void checkFieldIsNotNull(...);
|
||||
public static void checkParameterIsNotNull(...);
|
||||
}
|
||||
|
||||
# Stubs
|
||||
-keep class a.* { *; }
|
||||
|
||||
# Snet
|
||||
-keepclassmembers class com.topjohnwu.magisk.utils.ISafetyNetHelper { *; }
|
||||
-keep,allowobfuscation interface com.topjohnwu.magisk.utils.ISafetyNetHelper$Callback
|
||||
-keepclassmembers class * implements com.topjohnwu.magisk.utils.ISafetyNetHelper$Callback {
|
||||
-keepclassmembers class com.topjohnwu.magisk.core.utils.SafetyNetHelper { *; }
|
||||
-keep,allowobfuscation interface com.topjohnwu.magisk.core.utils.SafetyNetHelper$Callback
|
||||
-keepclassmembers class * implements com.topjohnwu.magisk.core.utils.SafetyNetHelper$Callback {
|
||||
void onResponse(int);
|
||||
}
|
||||
|
||||
# DelegateWorker
|
||||
-keep,allowobfuscation class * extends com.topjohnwu.magisk.model.worker.DelegateWorker
|
||||
# Fragments
|
||||
-keep,allowobfuscation class * extends androidx.fragment.app.Fragment
|
||||
|
||||
# BootSigner
|
||||
-keepclassmembers class com.topjohnwu.signing.BootSigner { *; }
|
||||
|
||||
# SVG
|
||||
-dontwarn com.caverock.androidsvg.SVGAndroidRenderer
|
||||
|
||||
# RetroStreams
|
||||
-dontwarn java9.**
|
||||
|
||||
# Strip logging
|
||||
-assumenosideeffects class com.topjohnwu.magisk.utils.Logger {
|
||||
public *** debug(...);
|
||||
# Strip Timber verbose and debug logging
|
||||
-assumenosideeffects class timber.log.Timber.Tree {
|
||||
public void v(**);
|
||||
public void d(**);
|
||||
}
|
||||
|
||||
# Excessive obfuscation
|
||||
-repackageclasses 'a'
|
||||
-repackageclasses
|
||||
-allowaccessmodification
|
||||
|
||||
# QOL
|
||||
|
5
app/res-ids.txt
Normal file
5
app/res-ids.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
com.topjohnwu.magisk:color/xxxxxxxx = 0x7f010000
|
||||
com.topjohnwu.magisk:drawable/xxxxxxxx = 0x7f020000
|
||||
com.topjohnwu.magisk:string/xxxxxxxx = 0x7f030000
|
||||
com.topjohnwu.magisk:style/xxxxxxxx = 0x7f040000
|
||||
com.topjohnwu.magisk:xml/xxxxxxxx = 0x7f050000
|
@@ -3,56 +3,55 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.topjohnwu.magisk">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:name="a.e"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:allowBackup="true"
|
||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
|
||||
|
||||
<!-- Activities -->
|
||||
|
||||
<activity
|
||||
android:name="a.b"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:exported="true" />
|
||||
<!-- Splash -->
|
||||
<activity
|
||||
android:name="a.c"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:exported="true"
|
||||
android:theme="@style/SplashTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="a.f"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:screenOrientation="nosensor"
|
||||
android:theme="@style/AppTheme.NoDrawer" />
|
||||
|
||||
<!-- Main -->
|
||||
<activity android:name="a.b" />
|
||||
|
||||
<!-- Flashing -->
|
||||
<activity android:name="a.f" />
|
||||
|
||||
<!-- Superuser -->
|
||||
|
||||
<activity
|
||||
android:name="a.m"
|
||||
android:exported="false"
|
||||
android:directBootAware="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:theme="@style/SuRequest" />
|
||||
android:exported="false"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
tools:ignore="AppLinkUrlError">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Receiver -->
|
||||
|
||||
<receiver
|
||||
android:name="a.h"
|
||||
android:directBootAware="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.REBOOT" />
|
||||
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
@@ -63,8 +62,7 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- Service -->
|
||||
|
||||
<!-- DownloadService -->
|
||||
<service android:name="a.j" />
|
||||
|
||||
<!-- Hardcode GMS version -->
|
||||
@@ -72,6 +70,22 @@
|
||||
android:name="com.google.android.gms.version"
|
||||
android:value="12451000" />
|
||||
|
||||
<!-- Initialize WorkManager on-demand -->
|
||||
<provider
|
||||
android:name="androidx.work.impl.WorkManagerInitializer"
|
||||
android:authorities="${applicationId}.workmanager-init"
|
||||
tools:node="remove" />
|
||||
|
||||
<!-- We don't invalidate Room -->
|
||||
<service
|
||||
android:name="androidx.room.MultiInstanceInvalidationService"
|
||||
tools:node="remove"/>
|
||||
|
||||
<!-- We don't use Device Credentials -->
|
||||
<activity
|
||||
android:name="androidx.biometric.DeviceCredentialHandlerActivity"
|
||||
tools:node="remove" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
@@ -1,13 +1,10 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.utils.PatchAPK;
|
||||
import com.topjohnwu.signing.BootSigner;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
public class a {
|
||||
|
||||
@Keep
|
||||
public class a extends BootSigner {
|
||||
public static boolean patchAPK(String in, String out, String pkg) {
|
||||
return PatchAPK.patch(in, out, pkg);
|
||||
public static void main(String[] args) throws Exception {
|
||||
BootSigner.main(args);
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.ui.MainActivity;
|
||||
|
||||
public class b extends MainActivity {
|
||||
/* stub */
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.ui.SplashActivity;
|
||||
|
||||
public class c extends SplashActivity {
|
||||
/* stub */
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.App;
|
||||
|
||||
public class e extends App {
|
||||
/* stub */
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.ui.flash.FlashActivity;
|
||||
|
||||
public class f extends FlashActivity {
|
||||
/* stub */
|
||||
}
|
@@ -1,15 +0,0 @@
|
||||
package a;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.topjohnwu.magisk.model.update.UpdateCheckService;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
public class g extends w<UpdateCheckService> {
|
||||
/* Stub */
|
||||
public g(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
||||
super(context, workerParams);
|
||||
}
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.model.receiver.GeneralReceiver;
|
||||
|
||||
public class h extends GeneralReceiver {
|
||||
/* stub */
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.model.download.DownloadModuleService;
|
||||
|
||||
public class j extends DownloadModuleService {
|
||||
/* stub */
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity;
|
||||
|
||||
public class m extends SuRequestActivity {
|
||||
/* stub */
|
||||
}
|
26
app/src/main/java/a/stubs.kt
Normal file
26
app/src/main/java/a/stubs.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
package a
|
||||
|
||||
import com.topjohnwu.magisk.core.App
|
||||
import com.topjohnwu.magisk.core.GeneralReceiver
|
||||
import com.topjohnwu.magisk.core.SplashActivity
|
||||
import com.topjohnwu.magisk.core.download.DownloadService
|
||||
import com.topjohnwu.magisk.legacy.flash.FlashActivity
|
||||
import com.topjohnwu.magisk.legacy.surequest.SuRequestActivity
|
||||
import com.topjohnwu.magisk.ui.MainActivity
|
||||
|
||||
class b : MainActivity()
|
||||
|
||||
class c : SplashActivity()
|
||||
|
||||
class e : App {
|
||||
constructor() : super()
|
||||
constructor(o: Any) : super(o)
|
||||
}
|
||||
|
||||
class f : FlashActivity()
|
||||
|
||||
class h : GeneralReceiver()
|
||||
|
||||
class j : DownloadService()
|
||||
|
||||
class m : SuRequestActivity()
|
@@ -1,42 +0,0 @@
|
||||
package a;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.topjohnwu.magisk.model.worker.DelegateWorker;
|
||||
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
public abstract class w<T extends DelegateWorker> extends Worker {
|
||||
|
||||
/* Wrapper class to workaround Proguard -keep class * extends Worker */
|
||||
|
||||
private T base;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
w(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
||||
super(context, workerParams);
|
||||
try {
|
||||
base = ((Class<T>) ((ParameterizedType) getClass().getGenericSuperclass())
|
||||
.getActualTypeArguments()[0]).newInstance();
|
||||
base.setActualWorker(this);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Result doWork() {
|
||||
if (base == null)
|
||||
return Result.failure();
|
||||
return base.doWork();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopped() {
|
||||
if (base != null)
|
||||
base.onStopped();
|
||||
}
|
||||
}
|
@@ -1,101 +0,0 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
|
||||
import com.topjohnwu.magisk.data.database.MagiskDB;
|
||||
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper;
|
||||
import com.topjohnwu.magisk.ui.base.BaseActivity;
|
||||
import com.topjohnwu.magisk.utils.LocaleManager;
|
||||
import com.topjohnwu.magisk.utils.RootUtils;
|
||||
import com.topjohnwu.net.Networking;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
public class App extends Application implements Application.ActivityLifecycleCallbacks {
|
||||
|
||||
public static App self;
|
||||
public static Context deContext;
|
||||
public static ThreadPoolExecutor THREAD_POOL;
|
||||
|
||||
// Global resources
|
||||
public SharedPreferences prefs;
|
||||
public MagiskDB mDB;
|
||||
public RepoDatabaseHelper repoDB;
|
||||
private volatile BaseActivity foreground;
|
||||
|
||||
static {
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
|
||||
Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER | Shell.FLAG_USE_MAGISK_BUSYBOX);
|
||||
Shell.Config.verboseLogging(BuildConfig.DEBUG);
|
||||
Shell.Config.addInitializers(RootUtils.class);
|
||||
Shell.Config.setTimeout(2);
|
||||
THREAD_POOL = (ThreadPoolExecutor) AsyncTask.THREAD_POOL_EXECUTOR;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(base);
|
||||
self = this;
|
||||
deContext = base;
|
||||
registerActivityLifecycleCallbacks(this);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
deContext = base.createDeviceProtectedStorageContext();
|
||||
deContext.moveSharedPreferencesFrom(base,
|
||||
PreferenceManager.getDefaultSharedPreferencesName(base));
|
||||
}
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(deContext);
|
||||
mDB = new MagiskDB(base);
|
||||
|
||||
Networking.init(base);
|
||||
LocaleManager.setLocale(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
LocaleManager.setLocale(this);
|
||||
}
|
||||
|
||||
public static BaseActivity foreground() {
|
||||
return self.foreground;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle bundle) {}
|
||||
|
||||
@Override
|
||||
public void onActivityStarted(@NonNull Activity activity) {}
|
||||
|
||||
@Override
|
||||
public synchronized void onActivityResumed(@NonNull Activity activity) {
|
||||
foreground = (BaseActivity) activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onActivityPaused(@NonNull Activity activity) {
|
||||
foreground = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStopped(@NonNull Activity activity) {}
|
||||
|
||||
@Override
|
||||
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle bundle) {}
|
||||
|
||||
@Override
|
||||
public void onActivityDestroyed(@NonNull Activity activity) {}
|
||||
}
|
@@ -1,31 +0,0 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import com.topjohnwu.magisk.model.download.DownloadModuleService;
|
||||
import com.topjohnwu.magisk.model.receiver.GeneralReceiver;
|
||||
import com.topjohnwu.magisk.model.update.UpdateCheckService;
|
||||
import com.topjohnwu.magisk.ui.MainActivity;
|
||||
import com.topjohnwu.magisk.ui.SplashActivity;
|
||||
import com.topjohnwu.magisk.ui.flash.FlashActivity;
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ClassMap {
|
||||
private static Map<Class, Class> classMap = new HashMap<>();
|
||||
|
||||
static {
|
||||
classMap.put(App.class, a.e.class);
|
||||
classMap.put(MainActivity.class, a.b.class);
|
||||
classMap.put(SplashActivity.class, a.c.class);
|
||||
classMap.put(FlashActivity.class, a.f.class);
|
||||
classMap.put(UpdateCheckService.class, a.g.class);
|
||||
classMap.put(GeneralReceiver.class, a.h.class);
|
||||
classMap.put(DownloadModuleService.class, a.j.class);
|
||||
classMap.put(SuRequestActivity.class, a.m.class);
|
||||
}
|
||||
|
||||
public static <T> Class<T> get(Class c) {
|
||||
return classMap.get(c);
|
||||
}
|
||||
}
|
@@ -1,396 +0,0 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Xml;
|
||||
|
||||
import androidx.collection.ArrayMap;
|
||||
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.ShellUtils;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
public class Config {
|
||||
|
||||
// Current status
|
||||
public static String magiskVersionString;
|
||||
public static int magiskVersionCode = -1;
|
||||
private static boolean magiskHide;
|
||||
|
||||
// Update Info
|
||||
public static String remoteMagiskVersionString;
|
||||
public static int remoteMagiskVersionCode = -1;
|
||||
public static String magiskLink;
|
||||
public static String magiskNoteLink;
|
||||
public static String magiskMD5;
|
||||
public static String remoteManagerVersionString;
|
||||
public static int remoteManagerVersionCode = -1;
|
||||
public static String managerLink;
|
||||
public static String managerNoteLink;
|
||||
public static String uninstallerLink;
|
||||
|
||||
// Install flags
|
||||
public static boolean keepVerity = false;
|
||||
public static boolean keepEnc = false;
|
||||
public static boolean recovery = false;
|
||||
|
||||
public static int suLogTimeout = 14;
|
||||
|
||||
public static class Key {
|
||||
// su configs
|
||||
public static final String ROOT_ACCESS = "root_access";
|
||||
public static final String SU_MULTIUSER_MODE = "multiuser_mode";
|
||||
public static final String SU_MNT_NS = "mnt_ns";
|
||||
public static final String SU_MANAGER = "requester";
|
||||
public static final String SU_REQUEST_TIMEOUT = "su_request_timeout";
|
||||
public static final String SU_AUTO_RESPONSE = "su_auto_response";
|
||||
public static final String SU_NOTIFICATION = "su_notification";
|
||||
public static final String SU_REAUTH = "su_reauth";
|
||||
public static final String SU_FINGERPRINT = "su_fingerprint";
|
||||
|
||||
// prefs
|
||||
public static final String CHECK_UPDATES = "check_update";
|
||||
public static final String UPDATE_CHANNEL = "update_channel";
|
||||
public static final String CUSTOM_CHANNEL = "custom_channel";
|
||||
public static final String LOCALE = "locale";
|
||||
public static final String DARK_THEME = "dark_theme";
|
||||
public static final String ETAG_KEY = "ETag";
|
||||
public static final String REPO_ORDER = "repo_order";
|
||||
public static final String SHOW_SYSTEM_APP = "show_system";
|
||||
|
||||
// system state
|
||||
public static final String UPDATE_SERVICE_VER = "update_service_version";
|
||||
public static final String MAGISKHIDE = "magiskhide";
|
||||
public static final String COREONLY = "disable";
|
||||
}
|
||||
|
||||
public static class Value {
|
||||
public static final int DEFAULT_CHANNEL = -1;
|
||||
public static final int STABLE_CHANNEL = 0;
|
||||
public static final int BETA_CHANNEL = 1;
|
||||
public static final int CUSTOM_CHANNEL = 2;
|
||||
public static final int CANARY_CHANNEL = 3;
|
||||
public static final int CANARY_DEBUG_CHANNEL = 4;
|
||||
public static final int ROOT_ACCESS_DISABLED = 0;
|
||||
public static final int ROOT_ACCESS_APPS_ONLY = 1;
|
||||
public static final int ROOT_ACCESS_ADB_ONLY = 2;
|
||||
public static final int ROOT_ACCESS_APPS_AND_ADB = 3;
|
||||
public static final int MULTIUSER_MODE_OWNER_ONLY = 0;
|
||||
public static final int MULTIUSER_MODE_OWNER_MANAGED = 1;
|
||||
public static final int MULTIUSER_MODE_USER = 2;
|
||||
public static final int NAMESPACE_MODE_GLOBAL = 0;
|
||||
public static final int NAMESPACE_MODE_REQUESTER = 1;
|
||||
public static final int NAMESPACE_MODE_ISOLATE = 2;
|
||||
public static final int NO_NOTIFICATION = 0;
|
||||
public static final int NOTIFICATION_TOAST = 1;
|
||||
public static final int SU_PROMPT = 0;
|
||||
public static final int SU_AUTO_DENY = 1;
|
||||
public static final int SU_AUTO_ALLOW = 2;
|
||||
public static final int[] TIMEOUT_LIST = {0, -1, 10, 20, 30, 60};
|
||||
public static final int ORDER_NAME = 0;
|
||||
public static final int ORDER_DATE = 1;
|
||||
}
|
||||
|
||||
public static void loadMagiskInfo() {
|
||||
try {
|
||||
magiskVersionString = ShellUtils.fastCmd("magisk -v").split(":")[0];
|
||||
magiskVersionCode = Integer.parseInt(ShellUtils.fastCmd("magisk -V"));
|
||||
magiskHide = Shell.su("magiskhide --status").exec().isSuccess();
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
public static void export() {
|
||||
// Flush prefs to disk
|
||||
App app = App.self;
|
||||
app.prefs.edit().commit();
|
||||
File xml = new File(App.deContext.getFilesDir().getParent() + "/shared_prefs",
|
||||
app.getPackageName() + "_preferences.xml");
|
||||
Shell.su(Utils.fmt("cat %s > /data/adb/%s", xml, Const.MANAGER_CONFIGS)).exec();
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
SharedPreferences pref = App.self.prefs;
|
||||
SharedPreferences.Editor editor = pref.edit();
|
||||
File config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS);
|
||||
if (config.exists()) {
|
||||
try {
|
||||
SuFileInputStream is = new SuFileInputStream(config);
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||
parser.setInput(is, "UTF-8");
|
||||
parser.nextTag();
|
||||
parser.require(XmlPullParser.START_TAG, null, "map");
|
||||
while (parser.next() != XmlPullParser.END_TAG) {
|
||||
if (parser.getEventType() != XmlPullParser.START_TAG)
|
||||
continue;
|
||||
String key = parser.getAttributeValue(null, "name");
|
||||
String value = parser.getAttributeValue(null, "value");
|
||||
switch (parser.getName()) {
|
||||
case "string":
|
||||
parser.require(XmlPullParser.START_TAG, null, "string");
|
||||
editor.putString(key, parser.nextText());
|
||||
parser.require(XmlPullParser.END_TAG, null, "string");
|
||||
break;
|
||||
case "boolean":
|
||||
parser.require(XmlPullParser.START_TAG, null, "boolean");
|
||||
editor.putBoolean(key, Boolean.parseBoolean(value));
|
||||
parser.nextTag();
|
||||
parser.require(XmlPullParser.END_TAG, null, "boolean");
|
||||
break;
|
||||
case "int":
|
||||
parser.require(XmlPullParser.START_TAG, null, "int");
|
||||
editor.putInt(key, Integer.parseInt(value));
|
||||
parser.nextTag();
|
||||
parser.require(XmlPullParser.END_TAG, null, "int");
|
||||
break;
|
||||
case "long":
|
||||
parser.require(XmlPullParser.START_TAG, null, "long");
|
||||
editor.putLong(key, Long.parseLong(value));
|
||||
parser.nextTag();
|
||||
parser.require(XmlPullParser.END_TAG, null, "long");
|
||||
break;
|
||||
case "float":
|
||||
parser.require(XmlPullParser.START_TAG, null, "int");
|
||||
editor.putFloat(key, Float.parseFloat(value));
|
||||
parser.nextTag();
|
||||
parser.require(XmlPullParser.END_TAG, null, "int");
|
||||
break;
|
||||
default:
|
||||
parser.next();
|
||||
}
|
||||
}
|
||||
} catch (IOException | XmlPullParserException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
editor.remove(Key.ETAG_KEY);
|
||||
editor.apply();
|
||||
editor = pref.edit();
|
||||
config.delete();
|
||||
}
|
||||
|
||||
// Set defaults if not set
|
||||
setDefs(pref, editor);
|
||||
|
||||
// These settings are from actual device state
|
||||
editor.putBoolean(Key.MAGISKHIDE, magiskHide)
|
||||
.putBoolean(Key.COREONLY, Const.MAGISK_DISABLE_FILE.exists())
|
||||
.putInt(Key.UPDATE_SERVICE_VER, Const.UPDATE_SERVICE_VER)
|
||||
.apply();
|
||||
}
|
||||
|
||||
private static final int PREF_INT = 0;
|
||||
private static final int PREF_STR_INT = 1;
|
||||
private static final int PREF_BOOL = 2;
|
||||
private static final int PREF_STR = 3;
|
||||
private static final int DB_INT = 4;
|
||||
private static final int DB_BOOL = 5;
|
||||
private static final int DB_STR = 6;
|
||||
|
||||
private static int getConfigType(String key) {
|
||||
switch (key) {
|
||||
case Key.REPO_ORDER:
|
||||
return PREF_INT;
|
||||
|
||||
case Key.SU_REQUEST_TIMEOUT:
|
||||
case Key.SU_AUTO_RESPONSE:
|
||||
case Key.SU_NOTIFICATION:
|
||||
case Key.UPDATE_CHANNEL:
|
||||
return PREF_STR_INT;
|
||||
|
||||
case Key.DARK_THEME:
|
||||
case Key.SU_REAUTH:
|
||||
case Key.CHECK_UPDATES:
|
||||
case Key.MAGISKHIDE:
|
||||
case Key.COREONLY:
|
||||
case Key.SHOW_SYSTEM_APP:
|
||||
return PREF_BOOL;
|
||||
|
||||
case Key.CUSTOM_CHANNEL:
|
||||
case Key.LOCALE:
|
||||
case Key.ETAG_KEY:
|
||||
return PREF_STR;
|
||||
|
||||
case Key.ROOT_ACCESS:
|
||||
case Key.SU_MNT_NS:
|
||||
case Key.SU_MULTIUSER_MODE:
|
||||
return DB_INT;
|
||||
|
||||
case Key.SU_FINGERPRINT:
|
||||
return DB_BOOL;
|
||||
|
||||
case Key.SU_MANAGER:
|
||||
return DB_STR;
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T get(String key) {
|
||||
App app = App.self;
|
||||
switch (getConfigType(key)) {
|
||||
case PREF_INT:
|
||||
return (T) (Integer) app.prefs.getInt(key, getDef(key));
|
||||
case PREF_STR_INT:
|
||||
return (T) (Integer) Utils.getPrefsInt(app.prefs, key, getDef(key));
|
||||
case PREF_BOOL:
|
||||
return (T) (Boolean) app.prefs.getBoolean(key, getDef(key));
|
||||
case PREF_STR:
|
||||
return (T) app.prefs.getString(key, getDef(key));
|
||||
case DB_INT:
|
||||
return (T) (Integer) app.mDB.getSettings(key, getDef(key));
|
||||
case DB_BOOL:
|
||||
return (T) (Boolean) (app.mDB.getSettings(key, getDef(key) ? 1 : 0) != 0);
|
||||
case DB_STR:
|
||||
return (T) app.mDB.getStrings(key, getDef(key));
|
||||
}
|
||||
/* Will never get here (IllegalArgumentException in getConfigType) */
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void set(String key, Object val) {
|
||||
App app = App.self;
|
||||
switch (getConfigType(key)) {
|
||||
case PREF_INT:
|
||||
app.prefs.edit().putInt(key, (int) val).apply();
|
||||
break;
|
||||
case PREF_STR_INT:
|
||||
app.prefs.edit().putString(key, String.valueOf(val)).apply();
|
||||
break;
|
||||
case PREF_BOOL:
|
||||
app.prefs.edit().putBoolean(key, (boolean) val).apply();
|
||||
break;
|
||||
case PREF_STR:
|
||||
app.prefs.edit().putString(key, (String) val).apply();
|
||||
break;
|
||||
case DB_INT:
|
||||
app.mDB.setSettings(key, (int) val);
|
||||
break;
|
||||
case DB_BOOL:
|
||||
app.mDB.setSettings(key, (boolean) val ? 1 : 0);
|
||||
break;
|
||||
case DB_STR:
|
||||
app.mDB.setStrings(key, (String) val);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void remove(String key) {
|
||||
App app = App.self;
|
||||
switch (getConfigType(key)) {
|
||||
case PREF_INT:
|
||||
case PREF_STR_INT:
|
||||
case PREF_BOOL:
|
||||
case PREF_STR:
|
||||
app.prefs.edit().remove(key).apply();
|
||||
break;
|
||||
case DB_BOOL:
|
||||
case DB_INT:
|
||||
app.mDB.rmSettings(key);
|
||||
break;
|
||||
case DB_STR:
|
||||
app.mDB.setStrings(key, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static ArrayMap<String, Object> defs = new ArrayMap<>();
|
||||
|
||||
static {
|
||||
/* Set default configurations */
|
||||
|
||||
// prefs int
|
||||
defs.put(Key.REPO_ORDER, Value.ORDER_DATE);
|
||||
|
||||
// prefs string int
|
||||
defs.put(Key.SU_REQUEST_TIMEOUT, 10);
|
||||
defs.put(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT);
|
||||
defs.put(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST);
|
||||
defs.put(Key.UPDATE_CHANNEL, Utils.isCanary() ?
|
||||
Value.CANARY_DEBUG_CHANNEL : Value.DEFAULT_CHANNEL);
|
||||
|
||||
// prefs bool
|
||||
defs.put(Key.CHECK_UPDATES, true);
|
||||
defs.put(Key.DARK_THEME, true);
|
||||
//defs.put(Key.SU_REAUTH, false);
|
||||
//defs.put(Key.SHOW_SYSTEM_APP, false);
|
||||
|
||||
// prefs string
|
||||
defs.put(Key.CUSTOM_CHANNEL, "");
|
||||
defs.put(Key.LOCALE, "");
|
||||
//defs.put(Key.ETAG_KEY, null);
|
||||
|
||||
// db int
|
||||
defs.put(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB);
|
||||
defs.put(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER);
|
||||
defs.put(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY);
|
||||
|
||||
// db bool
|
||||
//defs.put(Key.SU_FINGERPRINT, false);
|
||||
|
||||
// db strings
|
||||
//defs.put(Key.SU_MANAGER, null);
|
||||
}
|
||||
|
||||
private static <T> T getDef(String key) {
|
||||
Object val = defs.get(key);
|
||||
switch (getConfigType(key)) {
|
||||
case PREF_INT:
|
||||
case DB_INT:
|
||||
case PREF_STR_INT:
|
||||
return val != null ? (T) val : (T) (Integer) 0;
|
||||
case DB_BOOL:
|
||||
case PREF_BOOL:
|
||||
return val != null ? (T) val : (T) (Boolean) false;
|
||||
case DB_STR:
|
||||
case PREF_STR:
|
||||
return (T) val;
|
||||
}
|
||||
/* Will never get here (IllegalArgumentException in getConfigType) */
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void setDefs(SharedPreferences pref, SharedPreferences.Editor editor) {
|
||||
App app = App.self;
|
||||
for (String key : defs.keySet()) {
|
||||
int type = getConfigType(key);
|
||||
switch (type) {
|
||||
case DB_INT:
|
||||
editor.putString(key, String.valueOf(
|
||||
app.mDB.getSettings(key, (Integer) defs.get(key))));
|
||||
continue;
|
||||
case DB_STR:
|
||||
editor.putString(key, app.mDB.getStrings(key, (String) defs.get(key)));
|
||||
continue;
|
||||
case DB_BOOL:
|
||||
int bs = app.mDB.getSettings(key, -1);
|
||||
editor.putBoolean(key, bs < 0 ? (Boolean) defs.get(key) : bs != 0);
|
||||
continue;
|
||||
}
|
||||
if (pref.contains(key))
|
||||
continue;
|
||||
switch (type) {
|
||||
case PREF_INT:
|
||||
editor.putInt(key, (Integer) defs.get(key));
|
||||
break;
|
||||
case PREF_STR_INT:
|
||||
editor.putString(key, String.valueOf(defs.get(key)));
|
||||
break;
|
||||
case PREF_STR:
|
||||
editor.putString(key, (String) defs.get(key));
|
||||
break;
|
||||
case PREF_BOOL:
|
||||
editor.putBoolean(key, (Boolean) defs.get(key));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,99 +0,0 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import android.os.Environment;
|
||||
import android.os.Process;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class Const {
|
||||
|
||||
public static final String DEBUG_TAG = "MagiskManager";
|
||||
|
||||
// APK content
|
||||
public static final String ANDROID_MANIFEST = "AndroidManifest.xml";
|
||||
|
||||
public static final String SU_KEYSTORE_KEY = "su_key";
|
||||
|
||||
// Paths
|
||||
public static final String MAGISK_PATH = "/sbin/.magisk/img";
|
||||
public static final File EXTERNAL_PATH;
|
||||
public static File MAGISK_DISABLE_FILE;
|
||||
|
||||
static {
|
||||
MAGISK_DISABLE_FILE = new File("xxx");
|
||||
EXTERNAL_PATH = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||
EXTERNAL_PATH.mkdirs();
|
||||
}
|
||||
|
||||
public static final String TMP_FOLDER_PATH = "/dev/tmp";
|
||||
public static final String MAGISK_LOG = "/cache/magisk.log";
|
||||
public static final String MANAGER_CONFIGS = ".tmp.magisk.config";
|
||||
|
||||
// Versions
|
||||
public static final int UPDATE_SERVICE_VER = 1;
|
||||
public static final int SNET_EXT_VER = 12;
|
||||
|
||||
public static final int USER_ID = Process.myUid() / 100000;
|
||||
|
||||
public static final class MAGISK_VER {
|
||||
public static final int MIN_SUPPORT = 18000;
|
||||
}
|
||||
|
||||
public static class ID {
|
||||
public static final int FETCH_ZIP = 2;
|
||||
public static final int SELECT_BOOT = 3;
|
||||
|
||||
// notifications
|
||||
public static final int MAGISK_UPDATE_NOTIFICATION_ID = 4;
|
||||
public static final int APK_UPDATE_NOTIFICATION_ID = 5;
|
||||
public static final int DTBO_NOTIFICATION_ID = 7;
|
||||
public static final int HIDE_MANAGER_NOTIFICATION_ID = 8;
|
||||
public static final String UPDATE_NOTIFICATION_CHANNEL = "update";
|
||||
public static final String PROGRESS_NOTIFICATION_CHANNEL = "progress";
|
||||
public static final String CHECK_MAGISK_UPDATE_WORKER_ID = "magisk_update";
|
||||
}
|
||||
|
||||
public static class Url {
|
||||
private static String getRaw(String where, String name) {
|
||||
return String.format("https://raw.githubusercontent.com/topjohnwu/magisk_files/%s/%s", where, name);
|
||||
}
|
||||
public static final String STABLE_URL = getRaw("master", "stable.json");
|
||||
public static final String BETA_URL = getRaw("master", "beta.json");
|
||||
public static final String CANARY_URL = getRaw("master", "canary_builds/release.json");
|
||||
public static final String CANARY_DEBUG_URL = getRaw("master", "canary_builds/canary.json");
|
||||
public static final String REPO_URL = "https://api.github.com/users/Magisk-Modules-Repo/repos?per_page=100&sort=pushed&page=%d";
|
||||
public static final String FILE_URL = "https://raw.githubusercontent.com/Magisk-Modules-Repo/%s/master/%s";
|
||||
public static final String ZIP_URL = "https://github.com/Magisk-Modules-Repo/%s/archive/master.zip";
|
||||
public static final String MODULE_INSTALLER = "https://raw.githubusercontent.com/topjohnwu/Magisk/master/scripts/module_installer.sh";
|
||||
public static final String PAYPAL_URL = "https://www.paypal.me/topjohnwu";
|
||||
public static final String PATREON_URL = "https://www.patreon.com/topjohnwu";
|
||||
public static final String TWITTER_URL = "https://twitter.com/topjohnwu";
|
||||
public static final String XDA_THREAD = "http://forum.xda-developers.com/showthread.php?t=3432382";
|
||||
public static final String SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk";
|
||||
public static final String SNET_URL = getRaw("b66b1a914978e5f4c4bbfd74a59f4ad371bac107", "snet.apk");
|
||||
public static final String BOOTCTL_URL = getRaw("9c5dfc1b8245c0b5b524901ef0ff0f8335757b77", "bootctl");
|
||||
}
|
||||
|
||||
public static class Key {
|
||||
// others
|
||||
public static final String LINK_KEY = "Link";
|
||||
public static final String IF_NONE_MATCH = "If-None-Match";
|
||||
// intents
|
||||
public static final String OPEN_SECTION = "section";
|
||||
public static final String INTENT_SET_NAME = "filename";
|
||||
public static final String INTENT_SET_LINK = "link";
|
||||
public static final String FLASH_ACTION = "action";
|
||||
public static final String BROADCAST_MANAGER_UPDATE = "manager_update";
|
||||
public static final String BROADCAST_REBOOT = "reboot";
|
||||
}
|
||||
|
||||
public static class Value {
|
||||
public static final String FLASH_ZIP = "flash";
|
||||
public static final String PATCH_FILE = "patch";
|
||||
public static final String FLASH_MAGISK = "magisk";
|
||||
public static final String FLASH_INACTIVE_SLOT = "slot";
|
||||
public static final String UNINSTALL = "uninstall";
|
||||
}
|
||||
|
||||
|
||||
}
|
85
app/src/main/java/com/topjohnwu/magisk/core/App.kt
Normal file
85
app/src/main/java/com/topjohnwu/magisk/core/App.kt
Normal file
@@ -0,0 +1,85 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.multidex.MultiDex
|
||||
import androidx.work.WorkManager
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
import com.topjohnwu.magisk.FileProvider
|
||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
||||
import com.topjohnwu.magisk.core.utils.RootInit
|
||||
import com.topjohnwu.magisk.core.utils.updateConfig
|
||||
import com.topjohnwu.magisk.di.ActivityTracker
|
||||
import com.topjohnwu.magisk.di.koinModules
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.unwrap
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.context.startKoin
|
||||
import timber.log.Timber
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
open class App() : Application() {
|
||||
|
||||
constructor(o: Any) : this() {
|
||||
Info.stub = DynAPK.load(o)
|
||||
}
|
||||
|
||||
init {
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER or Shell.FLAG_USE_MAGISK_BUSYBOX)
|
||||
Shell.Config.verboseLogging(BuildConfig.DEBUG)
|
||||
Shell.Config.addInitializers(RootInit::class.java)
|
||||
Shell.Config.setTimeout(2)
|
||||
FileProvider.callHandler = SuCallbackHandler
|
||||
|
||||
// Always log full stack trace with Timber
|
||||
Timber.plant(Timber.DebugTree())
|
||||
Thread.setDefaultUncaughtExceptionHandler { _, e ->
|
||||
Timber.e(e)
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
// Basic setup
|
||||
if (BuildConfig.DEBUG)
|
||||
MultiDex.install(base)
|
||||
|
||||
// Some context magic
|
||||
val app: Application
|
||||
val impl: Context
|
||||
if (base is Application) {
|
||||
app = base
|
||||
impl = base.baseContext
|
||||
} else {
|
||||
app = this
|
||||
impl = base
|
||||
}
|
||||
val wrapped = impl.wrap()
|
||||
super.attachBaseContext(wrapped)
|
||||
|
||||
// Normal startup
|
||||
startKoin {
|
||||
androidContext(wrapped)
|
||||
modules(koinModules)
|
||||
}
|
||||
ResMgr.init(impl)
|
||||
app.registerActivityLifecycleCallbacks(get<ActivityTracker>())
|
||||
WorkManager.initialize(impl.wrapJob(), androidx.work.Configuration.Builder().build())
|
||||
}
|
||||
|
||||
// This is required as some platforms expect ContextImpl
|
||||
override fun getBaseContext(): Context {
|
||||
return super.getBaseContext().unwrap()
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
resources.updateConfig(newConfig)
|
||||
if (!isRunningAsStub)
|
||||
super.onConfigurationChanged(newConfig)
|
||||
}
|
||||
}
|
244
app/src/main/java/com/topjohnwu/magisk/core/Config.kt
Normal file
244
app/src/main/java/com/topjohnwu/magisk/core/Config.kt
Normal file
@@ -0,0 +1,244 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Environment
|
||||
import android.util.Xml
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.edit
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
|
||||
import com.topjohnwu.magisk.core.magiskdb.StringDao
|
||||
import com.topjohnwu.magisk.core.utils.BiometricHelper
|
||||
import com.topjohnwu.magisk.core.utils.Utils
|
||||
import com.topjohnwu.magisk.data.repository.DBConfig
|
||||
import com.topjohnwu.magisk.di.Protected
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.magisk.model.preference.PreferenceModel
|
||||
import com.topjohnwu.magisk.ui.theme.Theme
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import java.io.File
|
||||
|
||||
object Config : PreferenceModel, DBConfig {
|
||||
|
||||
override val stringDao: StringDao by inject()
|
||||
override val settingsDao: SettingsDao by inject()
|
||||
override val context: Context by inject(Protected)
|
||||
|
||||
object Key {
|
||||
// db configs
|
||||
const val ROOT_ACCESS = "root_access"
|
||||
const val SU_MULTIUSER_MODE = "multiuser_mode"
|
||||
const val SU_MNT_NS = "mnt_ns"
|
||||
const val SU_BIOMETRIC = "su_biometric"
|
||||
const val SU_MANAGER = "requester"
|
||||
const val KEYSTORE = "keystore"
|
||||
|
||||
// prefs
|
||||
const val SU_REQUEST_TIMEOUT = "su_request_timeout"
|
||||
const val SU_AUTO_RESPONSE = "su_auto_response"
|
||||
const val SU_NOTIFICATION = "su_notification"
|
||||
const val SU_REAUTH = "su_reauth"
|
||||
const val CHECK_UPDATES = "check_update"
|
||||
const val UPDATE_CHANNEL = "update_channel"
|
||||
const val CUSTOM_CHANNEL = "custom_channel"
|
||||
const val LOCALE = "locale"
|
||||
const val DARK_THEME = "dark_theme"
|
||||
const val DARK_THEME_EXTENDED = "dark_theme_extended"
|
||||
const val REPO_ORDER = "repo_order"
|
||||
const val SHOW_SYSTEM_APP = "show_system"
|
||||
const val DOWNLOAD_PATH = "download_path"
|
||||
const val REDESIGN = "redesign"
|
||||
const val SAFETY = "safety_notice"
|
||||
const val THEME_ORDINAL = "theme_ordinal"
|
||||
const val BOOT_ID = "boot_id"
|
||||
const val LIST_SPAN_COUNT = "column_count"
|
||||
|
||||
// system state
|
||||
const val MAGISKHIDE = "magiskhide"
|
||||
const val COREONLY = "disable"
|
||||
}
|
||||
|
||||
object Value {
|
||||
// Update channels
|
||||
const val DEFAULT_CHANNEL = -1
|
||||
const val STABLE_CHANNEL = 0
|
||||
const val BETA_CHANNEL = 1
|
||||
const val CUSTOM_CHANNEL = 2
|
||||
const val CANARY_CHANNEL = 3
|
||||
const val CANARY_DEBUG_CHANNEL = 4
|
||||
|
||||
// root access mode
|
||||
const val ROOT_ACCESS_DISABLED = 0
|
||||
const val ROOT_ACCESS_APPS_ONLY = 1
|
||||
const val ROOT_ACCESS_ADB_ONLY = 2
|
||||
const val ROOT_ACCESS_APPS_AND_ADB = 3
|
||||
|
||||
// su multiuser
|
||||
const val MULTIUSER_MODE_OWNER_ONLY = 0
|
||||
const val MULTIUSER_MODE_OWNER_MANAGED = 1
|
||||
const val MULTIUSER_MODE_USER = 2
|
||||
|
||||
// su mnt ns
|
||||
const val NAMESPACE_MODE_GLOBAL = 0
|
||||
const val NAMESPACE_MODE_REQUESTER = 1
|
||||
const val NAMESPACE_MODE_ISOLATE = 2
|
||||
|
||||
// su notification
|
||||
const val NO_NOTIFICATION = 0
|
||||
const val NOTIFICATION_TOAST = 1
|
||||
|
||||
// su auto response
|
||||
const val SU_PROMPT = 0
|
||||
const val SU_AUTO_DENY = 1
|
||||
const val SU_AUTO_ALLOW = 2
|
||||
|
||||
// su timeout
|
||||
val TIMEOUT_LIST = intArrayOf(0, -1, 10, 20, 30, 60)
|
||||
|
||||
// repo order
|
||||
const val ORDER_NAME = 0
|
||||
const val ORDER_DATE = 1
|
||||
}
|
||||
|
||||
private val defaultChannel =
|
||||
if (isCanaryVersion) {
|
||||
if (BuildConfig.DEBUG)
|
||||
Value.CANARY_DEBUG_CHANNEL
|
||||
else
|
||||
Value.CANARY_CHANNEL
|
||||
} else Value.DEFAULT_CHANNEL
|
||||
|
||||
var bootId by preference(Key.BOOT_ID, "")
|
||||
|
||||
var downloadPath by preference(Key.DOWNLOAD_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||
var repoOrder by preference(Key.REPO_ORDER, Value.ORDER_DATE)
|
||||
|
||||
var suDefaultTimeout by preferenceStrInt(Key.SU_REQUEST_TIMEOUT, 10)
|
||||
var suAutoReponse by preferenceStrInt(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT)
|
||||
var suNotification by preferenceStrInt(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST)
|
||||
var updateChannel by preferenceStrInt(Key.UPDATE_CHANNEL, defaultChannel)
|
||||
|
||||
var safetyNotice by preference(Key.SAFETY, true)
|
||||
var darkThemeExtended by preference(
|
||||
Key.DARK_THEME_EXTENDED,
|
||||
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
)
|
||||
var themeOrdinal by preference(Key.THEME_ORDINAL, Theme.Piplup.ordinal)
|
||||
var suReAuth by preference(Key.SU_REAUTH, false)
|
||||
var checkUpdate by preference(Key.CHECK_UPDATES, true)
|
||||
var magiskHide by preference(Key.MAGISKHIDE, true)
|
||||
@JvmStatic
|
||||
var coreOnly by preference(Key.COREONLY, false)
|
||||
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
|
||||
@JvmStatic
|
||||
var listSpanCount by preference(Key.LIST_SPAN_COUNT, 1)
|
||||
|
||||
var customChannelUrl by preference(Key.CUSTOM_CHANNEL, "")
|
||||
var locale by preference(Key.LOCALE, "")
|
||||
|
||||
var rootMode by dbSettings(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB)
|
||||
var suMntNamespaceMode by dbSettings(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER)
|
||||
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
|
||||
var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
|
||||
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
||||
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
|
||||
|
||||
// Always return a path in external storage where we can write
|
||||
val downloadDirectory get() =
|
||||
Utils.ensureDownloadPath(downloadPath) ?: get<Context>().getExternalFilesDir(null)!!
|
||||
|
||||
private const val SU_FINGERPRINT = "su_fingerprint"
|
||||
|
||||
fun initialize() = prefs.also {
|
||||
if (it.getBoolean(SU_FINGERPRINT, false)) {
|
||||
suBiometric = true
|
||||
}
|
||||
}.edit {
|
||||
parsePrefs(this)
|
||||
|
||||
// Legacy stuff
|
||||
remove(SU_FINGERPRINT)
|
||||
|
||||
// Get actual state
|
||||
putBoolean(Key.COREONLY, Const.MAGISK_DISABLE_FILE.exists())
|
||||
|
||||
// Write database configs
|
||||
putString(Key.ROOT_ACCESS, rootMode.toString())
|
||||
putString(Key.SU_MNT_NS, suMntNamespaceMode.toString())
|
||||
putString(Key.SU_MULTIUSER_MODE, suMultiuserMode.toString())
|
||||
putBoolean(Key.SU_BIOMETRIC, BiometricHelper.isEnabled)
|
||||
}.also {
|
||||
if (!prefs.contains(Key.UPDATE_CHANNEL))
|
||||
prefs.edit().putString(Key.UPDATE_CHANNEL, defaultChannel.toString()).apply()
|
||||
}
|
||||
|
||||
private fun parsePrefs(editor: SharedPreferences.Editor) = editor.apply {
|
||||
val config = SuFile.open("/data/adb",
|
||||
Const.MANAGER_CONFIGS
|
||||
)
|
||||
if (config.exists()) runCatching {
|
||||
val input = SuFileInputStream(config)
|
||||
val parser = Xml.newPullParser()
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
|
||||
parser.setInput(input, "UTF-8")
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.START_TAG, null, "map")
|
||||
while (parser.next() != XmlPullParser.END_TAG) {
|
||||
if (parser.eventType != XmlPullParser.START_TAG)
|
||||
continue
|
||||
val key: String = parser.getAttributeValue(null, "name")
|
||||
fun value() = parser.getAttributeValue(null, "value")!!
|
||||
when (parser.name) {
|
||||
"string" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "string")
|
||||
putString(key, parser.nextText())
|
||||
parser.require(XmlPullParser.END_TAG, null, "string")
|
||||
}
|
||||
"boolean" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "boolean")
|
||||
putBoolean(key, value().toBoolean())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "boolean")
|
||||
}
|
||||
"int" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "int")
|
||||
putInt(key, value().toInt())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "int")
|
||||
}
|
||||
"long" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "long")
|
||||
putLong(key, value().toLong())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "long")
|
||||
}
|
||||
"float" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "int")
|
||||
putFloat(key, value().toFloat())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "int")
|
||||
}
|
||||
else -> parser.next()
|
||||
}
|
||||
}
|
||||
config.delete()
|
||||
}
|
||||
}
|
||||
|
||||
fun export() {
|
||||
// Flush prefs to disk
|
||||
prefs.edit().commit()
|
||||
val context = get<Context>(Protected)
|
||||
val xml = File(
|
||||
"${context.filesDir.parent}/shared_prefs",
|
||||
"${context.packageName}_preferences.xml"
|
||||
)
|
||||
Shell.su("cat $xml > /data/adb/${Const.MANAGER_CONFIGS}").exec()
|
||||
}
|
||||
|
||||
}
|
82
app/src/main/java/com/topjohnwu/magisk/core/Const.kt
Normal file
82
app/src/main/java/com/topjohnwu/magisk/core/Const.kt
Normal file
@@ -0,0 +1,82 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.os.Process
|
||||
import java.io.File
|
||||
|
||||
object Const {
|
||||
|
||||
// Paths
|
||||
const val MAGISK_PATH = "/sbin/.magisk/modules"
|
||||
var MAGISK_DISABLE_FILE = File("xxx")
|
||||
const val TMP_FOLDER_PATH = "/dev/tmp"
|
||||
const val MAGISK_LOG = "/cache/magisk.log"
|
||||
|
||||
// Versions
|
||||
const val SNET_EXT_VER = 13
|
||||
const val SNET_REVISION = "a6c47f86f10b310358afa9dbe837037dd5d561df"
|
||||
const val BOOTCTL_REVISION = "a6c47f86f10b310358afa9dbe837037dd5d561df"
|
||||
|
||||
// Misc
|
||||
const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
||||
const val MAGISK_INSTALL_LOG_FILENAME = "magisk_install_log_%s.log"
|
||||
const val MANAGER_CONFIGS = ".tmp.magisk.config"
|
||||
val USER_ID = Process.myUid() / 100000
|
||||
|
||||
object Version {
|
||||
const val MIN_VERSION = "v19.0"
|
||||
const val MIN_VERCODE = 19000
|
||||
const val CONNECT_MODE = 20100
|
||||
const val PROVIDER_CONNECT = 20102
|
||||
}
|
||||
|
||||
object ID {
|
||||
const val FETCH_ZIP = 2
|
||||
const val SELECT_BOOT = 3
|
||||
|
||||
// notifications
|
||||
const val MAGISK_UPDATE_NOTIFICATION_ID = 4
|
||||
const val APK_UPDATE_NOTIFICATION_ID = 5
|
||||
const val DTBO_NOTIFICATION_ID = 7
|
||||
const val HIDE_MANAGER_NOTIFICATION_ID = 8
|
||||
const val UPDATE_NOTIFICATION_CHANNEL = "update"
|
||||
const val PROGRESS_NOTIFICATION_CHANNEL = "progress"
|
||||
const val CHECK_MAGISK_UPDATE_WORKER_ID = "magisk_update"
|
||||
}
|
||||
|
||||
object Url {
|
||||
const val ZIP_URL = "https://github.com/Magisk-Modules-Repo/%s/archive/master.zip"
|
||||
const val PAYPAL_URL = "https://www.paypal.me/topjohnwu"
|
||||
const val PATREON_URL = "https://www.patreon.com/topjohnwu"
|
||||
const val TWITTER_URL = "https://twitter.com/topjohnwu"
|
||||
const val XDA_THREAD = "http://forum.xda-developers.com/showthread.php?t=3432382"
|
||||
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
|
||||
|
||||
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com/"
|
||||
const val GITHUB_API_URL = "https://api.github.com/users/Magisk-Modules-Repo/"
|
||||
}
|
||||
|
||||
object Key {
|
||||
// others
|
||||
const val LINK_KEY = "Link"
|
||||
const val IF_NONE_MATCH = "If-None-Match"
|
||||
const val ETAG_KEY = "ETag"
|
||||
// intents
|
||||
const val OPEN_SECTION = "section"
|
||||
const val OPEN_SETTINGS = "settings"
|
||||
const val INTENT_SET_APP = "app_json"
|
||||
const val FLASH_ACTION = "action"
|
||||
const val FLASH_DATA = "additional_data"
|
||||
const val DISMISS_ID = "dismiss_id"
|
||||
const val BROADCAST_MANAGER_UPDATE = "manager_update"
|
||||
const val BROADCAST_REBOOT = "reboot"
|
||||
}
|
||||
|
||||
object Value {
|
||||
const val FLASH_ZIP = "flash"
|
||||
const val PATCH_FILE = "patch"
|
||||
const val FLASH_MAGISK = "magisk"
|
||||
const val FLASH_INACTIVE_SLOT = "slot"
|
||||
const val UNINSTALL = "uninstall"
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import com.topjohnwu.magisk.core.base.BaseReceiver
|
||||
import com.topjohnwu.magisk.core.download.DownloadService
|
||||
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.model.ManagerJson
|
||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
||||
import com.topjohnwu.magisk.core.view.Shortcuts
|
||||
import com.topjohnwu.magisk.extensions.reboot
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import org.koin.core.inject
|
||||
|
||||
open class GeneralReceiver : BaseReceiver() {
|
||||
|
||||
private val policyDB: PolicyDao by inject()
|
||||
|
||||
private fun getPkg(intent: Intent): String {
|
||||
return intent.data?.encodedSchemeSpecificPart.orEmpty()
|
||||
}
|
||||
|
||||
override fun onReceive(context: ContextWrapper, intent: Intent?) {
|
||||
intent ?: return
|
||||
|
||||
when (intent.action ?: return) {
|
||||
Intent.ACTION_REBOOT -> {
|
||||
SuCallbackHandler(context, intent.getStringExtra("action"), intent.extras)
|
||||
}
|
||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
||||
// This will only work pre-O
|
||||
if (Config.suReAuth)
|
||||
policyDB.delete(getPkg(intent)).blockingGet()
|
||||
}
|
||||
Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
|
||||
val pkg = getPkg(intent)
|
||||
policyDB.delete(pkg).blockingGet()
|
||||
Shell.su("magiskhide --rm $pkg").submit()
|
||||
}
|
||||
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setup(context)
|
||||
Const.Key.BROADCAST_MANAGER_UPDATE -> {
|
||||
intent.getParcelableExtra<ManagerJson>(Const.Key.INTENT_SET_APP)?.let {
|
||||
Info.remote = Info.remote.copy(app = it)
|
||||
}
|
||||
DownloadService(context) {
|
||||
subject = DownloadSubject.Manager(Configuration.APK.Upgrade)
|
||||
}
|
||||
}
|
||||
Const.Key.BROADCAST_REBOOT -> reboot()
|
||||
}
|
||||
}
|
||||
}
|
170
app/src/main/java/com/topjohnwu/magisk/core/Hacks.kt
Normal file
170
app/src/main/java/com/topjohnwu/magisk/core/Hacks.kt
Normal file
@@ -0,0 +1,170 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.job.JobInfo
|
||||
import android.app.job.JobScheduler
|
||||
import android.app.job.JobWorkItem
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.content.res.AssetManager
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
import com.topjohnwu.magisk.ProcessPhoenix
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.download.DownloadService
|
||||
import com.topjohnwu.magisk.core.utils.refreshLocale
|
||||
import com.topjohnwu.magisk.core.utils.updateConfig
|
||||
import com.topjohnwu.magisk.extensions.forceGetDeclaredField
|
||||
import com.topjohnwu.magisk.legacy.flash.FlashActivity
|
||||
import com.topjohnwu.magisk.legacy.surequest.SuRequestActivity
|
||||
import com.topjohnwu.magisk.ui.MainActivity
|
||||
|
||||
fun AssetManager.addAssetPath(path: String) {
|
||||
DynAPK.addAssetPath(this, path)
|
||||
}
|
||||
|
||||
fun Context.wrap(global: Boolean = true): Context =
|
||||
if (global) GlobalResContext(this) else ResContext(this)
|
||||
|
||||
fun Context.wrapJob(): Context = object : GlobalResContext(this) {
|
||||
|
||||
override fun getApplicationContext(): Context {
|
||||
return this
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
override fun getSystemService(name: String): Any? {
|
||||
return if (!isRunningAsStub) super.getSystemService(name) else
|
||||
when (name) {
|
||||
Context.JOB_SCHEDULER_SERVICE ->
|
||||
JobSchedulerWrapper(super.getSystemService(name) as JobScheduler)
|
||||
else -> super.getSystemService(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Class<*>.cmp(pkg: String): ComponentName {
|
||||
val name = ClassMap[this].name
|
||||
return ComponentName(pkg, Info.stub?.classToComponent?.get(name) ?: name)
|
||||
}
|
||||
|
||||
inline fun <reified T> Context.intent() = Intent().setComponent(T::class.java.cmp(packageName))
|
||||
|
||||
private open class GlobalResContext(base: Context) : ContextWrapper(base) {
|
||||
open val mRes: Resources get() = ResMgr.resource
|
||||
|
||||
override fun getResources(): Resources {
|
||||
return mRes
|
||||
}
|
||||
|
||||
override fun getClassLoader(): ClassLoader {
|
||||
return javaClass.classLoader!!
|
||||
}
|
||||
|
||||
override fun createConfigurationContext(config: Configuration): Context {
|
||||
return ResContext(super.createConfigurationContext(config))
|
||||
}
|
||||
}
|
||||
|
||||
private class ResContext(base: Context) : GlobalResContext(base) {
|
||||
override val mRes by lazy { base.resources.patch() }
|
||||
|
||||
private fun Resources.patch(): Resources {
|
||||
updateConfig()
|
||||
if (isRunningAsStub)
|
||||
assets.addAssetPath(ResMgr.apk)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
object ResMgr {
|
||||
|
||||
lateinit var resource: Resources
|
||||
lateinit var apk: String
|
||||
|
||||
fun init(context: Context) {
|
||||
resource = context.resources
|
||||
refreshLocale()
|
||||
if (isRunningAsStub) {
|
||||
apk = DynAPK.current(context).path
|
||||
resource.assets.addAssetPath(apk)
|
||||
} else {
|
||||
apk = context.packageResourcePath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(28)
|
||||
private class JobSchedulerWrapper(private val base: JobScheduler) : JobScheduler() {
|
||||
|
||||
override fun schedule(job: JobInfo): Int {
|
||||
return base.schedule(job.patch())
|
||||
}
|
||||
|
||||
override fun enqueue(job: JobInfo, work: JobWorkItem): Int {
|
||||
return base.enqueue(job.patch(), work)
|
||||
}
|
||||
|
||||
override fun cancel(jobId: Int) {
|
||||
base.cancel(jobId)
|
||||
}
|
||||
|
||||
override fun cancelAll() {
|
||||
base.cancelAll()
|
||||
}
|
||||
|
||||
override fun getAllPendingJobs(): List<JobInfo> {
|
||||
return base.allPendingJobs
|
||||
}
|
||||
|
||||
override fun getPendingJob(jobId: Int): JobInfo? {
|
||||
return base.getPendingJob(jobId)
|
||||
}
|
||||
|
||||
private fun JobInfo.patch(): JobInfo {
|
||||
// We need to swap out the service of JobInfo
|
||||
val name = service.className
|
||||
val component = ComponentName(
|
||||
service.packageName,
|
||||
Info.stub!!.classToComponent[name] ?: name
|
||||
)
|
||||
|
||||
javaClass.forceGetDeclaredField("service")?.set(this, component)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
object ClassMap {
|
||||
|
||||
private val map = mapOf(
|
||||
App::class.java to a.e::class.java,
|
||||
MainActivity::class.java to a.b::class.java,
|
||||
SplashActivity::class.java to a.c::class.java,
|
||||
FlashActivity::class.java to a.f::class.java,
|
||||
GeneralReceiver::class.java to a.h::class.java,
|
||||
DownloadService::class.java to a.j::class.java,
|
||||
SuRequestActivity::class.java to a.m::class.java,
|
||||
ProcessPhoenix::class.java to a.r::class.java
|
||||
)
|
||||
|
||||
operator fun get(c: Class<*>) = map.getOrElse(c) { c }
|
||||
}
|
||||
|
||||
/*
|
||||
* Keep a reference to these resources to prevent it from
|
||||
* being removed when running "remove unused resources" */
|
||||
val shouldKeepResources = listOf(
|
||||
/* TODO: The following strings should be used somewhere */
|
||||
R.string.no_apps_found,
|
||||
R.string.no_info_provided,
|
||||
R.string.release_notes,
|
||||
R.string.settings_download_path_error,
|
||||
R.string.invalid_update_channel,
|
||||
R.string.update_available
|
||||
)
|
87
app/src/main/java/com/topjohnwu/magisk/core/Info.kt
Normal file
87
app/src/main/java/com/topjohnwu/magisk/core/Info.kt
Normal file
@@ -0,0 +1,87 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
import com.topjohnwu.magisk.core.model.UpdateInfo
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.utils.CachedValue
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils.fastCmd
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
|
||||
val isRunningAsStub get() = Info.stub != null
|
||||
val isCanaryVersion = !BuildConfig.VERSION_NAME.contains(".")
|
||||
|
||||
object Info {
|
||||
|
||||
val envRef = CachedValue { loadState() }
|
||||
|
||||
@JvmStatic
|
||||
val env by envRef // Local
|
||||
var remote = UpdateInfo() // Remote
|
||||
@JvmStatic
|
||||
var stub: DynAPK.Data? = null // Stub
|
||||
|
||||
// Toggle-able options
|
||||
@JvmStatic var keepVerity = false
|
||||
@JvmStatic var keepEnc = false
|
||||
@JvmStatic var recovery = false
|
||||
|
||||
// Immutable device state
|
||||
@JvmStatic var isSAR = false
|
||||
@JvmStatic var isAB = false
|
||||
@JvmStatic var ramdisk = false
|
||||
|
||||
val isConnected by lazy {
|
||||
KObservableField(false).also { field ->
|
||||
ReactiveNetwork.observeNetworkConnectivity(get())
|
||||
.subscribeK {
|
||||
field.value = it.available()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val isNewReboot by lazy {
|
||||
try {
|
||||
FileInputStream("/proc/sys/kernel/random/boot_id").bufferedReader().use {
|
||||
val id = it.readLine()
|
||||
if (id != Config.bootId) {
|
||||
Config.bootId = id
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadState() = Env(
|
||||
fastCmd("magisk -v").split(":".toRegex())[0],
|
||||
runCatching { fastCmd("magisk -V").toInt() }.getOrDefault(-1),
|
||||
Shell.su("magiskhide --status").exec().isSuccess
|
||||
)
|
||||
|
||||
class Env(
|
||||
val magiskVersionString: String = "",
|
||||
code: Int = -1,
|
||||
hide: Boolean = false
|
||||
) {
|
||||
val magiskHide get() = Config.magiskHide
|
||||
val magiskVersionCode = when (code) {
|
||||
in Int.MIN_VALUE..Const.Version.MIN_VERCODE -> -1
|
||||
else -> if (Shell.rootAccess()) code else -1
|
||||
}
|
||||
val isUnsupported = code > 0 && code < Const.Version.MIN_VERCODE
|
||||
val isActive = magiskVersionCode >= 0
|
||||
|
||||
init {
|
||||
Config.magiskHide = hide
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,64 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.core.tasks.patchDTB
|
||||
import com.topjohnwu.magisk.core.utils.Utils
|
||||
import com.topjohnwu.magisk.core.view.Notifications
|
||||
import com.topjohnwu.magisk.core.view.Shortcuts
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.model.navigation.Navigation
|
||||
import com.topjohnwu.superuser.Shell
|
||||
|
||||
open class SplashActivity : Activity() {
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base.wrap())
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Shell.getShell { Shell.EXECUTOR.execute(this::initAndStart) }
|
||||
}
|
||||
|
||||
private fun handleRepackage() {
|
||||
val pkg = Config.suManager
|
||||
if (Config.suManager.isNotEmpty() && packageName == BuildConfig.APPLICATION_ID) {
|
||||
Config.suManager = ""
|
||||
Shell.su("(pm uninstall $pkg)& >/dev/null 2>&1").exec()
|
||||
}
|
||||
if (pkg == packageName) {
|
||||
runCatching {
|
||||
// We are the manager, remove com.topjohnwu.magisk as it could be malware
|
||||
packageManager.getApplicationInfo(BuildConfig.APPLICATION_ID, 0)
|
||||
Shell.su("(pm uninstall ${BuildConfig.APPLICATION_ID})& >/dev/null 2>&1").exec()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initAndStart() {
|
||||
Config.initialize()
|
||||
handleRepackage()
|
||||
Notifications.setup(this)
|
||||
Utils.scheduleUpdateCheck(this)
|
||||
Shortcuts.setup(this)
|
||||
|
||||
// Patch DTB partitions if needed
|
||||
patchDTB(this)
|
||||
|
||||
// Pre-fetch network stuffs
|
||||
get<GithubRawServices>()
|
||||
|
||||
DONE = true
|
||||
Navigation.start(intent, this)
|
||||
finish()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
var DONE = false
|
||||
}
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.core.view.Notifications
|
||||
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.superuser.Shell
|
||||
|
||||
class UpdateCheckService(context: Context, workerParams: WorkerParameters)
|
||||
: Worker(context, workerParams) {
|
||||
|
||||
private val magiskRepo: MagiskRepository by inject()
|
||||
|
||||
override fun doWork(): Result {
|
||||
// Make sure shell initializer was ran
|
||||
Shell.getShell()
|
||||
return runCatching {
|
||||
magiskRepo.fetchUpdate().blockingGet()
|
||||
if (BuildConfig.VERSION_CODE < Info.remote.app.versionCode)
|
||||
Notifications.managerUpdate(applicationContext)
|
||||
else if (Info.env.magiskVersionCode < Info.remote.magisk.versionCode)
|
||||
Notifications.magiskUpdate(applicationContext)
|
||||
Result.success()
|
||||
}.getOrElse {
|
||||
Result.failure()
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,92 @@
|
||||
package com.topjohnwu.magisk.core.base
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import com.topjohnwu.magisk.extensions.set
|
||||
import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder
|
||||
import kotlin.random.Random
|
||||
|
||||
typealias RequestCallback = BaseActivity.(Int, Intent?) -> Unit
|
||||
|
||||
abstract class BaseActivity : AppCompatActivity() {
|
||||
|
||||
private val resultCallbacks by lazy { SparseArrayCompat<RequestCallback>() }
|
||||
|
||||
override fun applyOverrideConfiguration(config: Configuration?) {
|
||||
// Force applying our preferred local
|
||||
config?.setLocale(currentLocale)
|
||||
super.applyOverrideConfiguration(config)
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base.wrap(false))
|
||||
}
|
||||
|
||||
fun withPermissions(vararg permissions: String, builder: PermissionRequestBuilder.() -> Unit) {
|
||||
val request = PermissionRequestBuilder().apply(builder).build()
|
||||
val ungranted = permissions.filter {
|
||||
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
if (ungranted.isEmpty()) {
|
||||
request.onSuccess()
|
||||
} else {
|
||||
val requestCode = Random.nextInt(256, 512)
|
||||
resultCallbacks[requestCode] = { result, _ ->
|
||||
if (result > 0)
|
||||
request.onSuccess()
|
||||
else
|
||||
request.onFailure()
|
||||
}
|
||||
ActivityCompat.requestPermissions(this, ungranted.toTypedArray(), requestCode)
|
||||
}
|
||||
}
|
||||
|
||||
fun withExternalRW(builder: PermissionRequestBuilder.() -> Unit) {
|
||||
withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, builder = builder)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
var success = true
|
||||
for (res in grantResults) {
|
||||
if (res != PackageManager.PERMISSION_GRANTED) {
|
||||
success = false
|
||||
break
|
||||
}
|
||||
}
|
||||
resultCallbacks[requestCode]?.also {
|
||||
resultCallbacks.remove(requestCode)
|
||||
it(this@BaseActivity, if (success) 1 else -1, null)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
resultCallbacks[requestCode]?.also {
|
||||
resultCallbacks.remove(requestCode)
|
||||
it(this@BaseActivity, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
fun startActivityForResult(intent: Intent, requestCode: Int, listener: RequestCallback) {
|
||||
resultCallbacks[requestCode] = listener
|
||||
startActivityForResult(intent, requestCode)
|
||||
}
|
||||
|
||||
override fun recreate() {
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package com.topjohnwu.magisk.core.base
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import org.koin.core.KoinComponent
|
||||
|
||||
abstract class BaseReceiver : BroadcastReceiver(), KoinComponent {
|
||||
|
||||
final override fun onReceive(context: Context, intent: Intent?) {
|
||||
onReceive(context.wrap() as ContextWrapper, intent)
|
||||
}
|
||||
|
||||
abstract fun onReceive(context: ContextWrapper, intent: Intent?)
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
package com.topjohnwu.magisk.core.base
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import org.koin.core.KoinComponent
|
||||
|
||||
abstract class BaseService : Service(), KoinComponent {
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base.wrap())
|
||||
}
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
package com.topjohnwu.magisk.core.base
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Network
|
||||
import android.net.Uri
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.work.Data
|
||||
import androidx.work.ListenableWorker
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import java.util.*
|
||||
|
||||
abstract class BaseWorkerWrapper {
|
||||
|
||||
private lateinit var worker: ListenableWorker
|
||||
|
||||
val applicationContext: Context
|
||||
get() = worker.applicationContext
|
||||
|
||||
val id: UUID
|
||||
get() = worker.id
|
||||
|
||||
val inputData: Data
|
||||
get() = worker.inputData
|
||||
|
||||
val tags: Set<String>
|
||||
get() = worker.tags
|
||||
|
||||
val triggeredContentUris: List<Uri>
|
||||
@RequiresApi(24)
|
||||
get() = worker.triggeredContentUris
|
||||
|
||||
val triggeredContentAuthorities: List<String>
|
||||
@RequiresApi(24)
|
||||
get() = worker.triggeredContentAuthorities
|
||||
|
||||
val network: Network?
|
||||
@RequiresApi(28)
|
||||
get() = worker.network
|
||||
|
||||
val runAttemptCount: Int
|
||||
get() = worker.runAttemptCount
|
||||
|
||||
val isStopped: Boolean
|
||||
get() = worker.isStopped
|
||||
|
||||
abstract fun doWork(): ListenableWorker.Result
|
||||
|
||||
fun onStopped() {}
|
||||
|
||||
fun attachWorker(w: ListenableWorker) {
|
||||
worker = w
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun startWork(): ListenableFuture<ListenableWorker.Result> {
|
||||
return worker.startWork()
|
||||
}
|
||||
}
|
@@ -0,0 +1,164 @@
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.webkit.MimeTypeMap
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.intent
|
||||
import com.topjohnwu.magisk.core.tasks.EnvFixTask
|
||||
import com.topjohnwu.magisk.extensions.chooser
|
||||
import com.topjohnwu.magisk.extensions.exists
|
||||
import com.topjohnwu.magisk.extensions.provide
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.legacy.flash.FlashActivity
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.*
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.Flash.Secondary
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import io.reactivex.Completable
|
||||
import org.koin.core.get
|
||||
import java.io.File
|
||||
import kotlin.random.Random.Default.nextInt
|
||||
|
||||
/* More of a facade for [RemoteFileService], but whatever... */
|
||||
@SuppressLint("Registered")
|
||||
open class DownloadService : RemoteFileService() {
|
||||
|
||||
private val context get() = this
|
||||
private val File.type
|
||||
get() = MimeTypeMap.getSingleton()
|
||||
.getMimeTypeFromExtension(extension)
|
||||
?: "resource/folder"
|
||||
|
||||
override fun onFinished(subject: DownloadSubject, id: Int) = when (subject) {
|
||||
is Magisk -> onFinishedInternal(subject, id)
|
||||
is Module -> onFinishedInternal(subject, id)
|
||||
is Manager -> onFinishedInternal(subject, id)
|
||||
}
|
||||
|
||||
private fun onFinishedInternal(
|
||||
subject: Magisk,
|
||||
id: Int
|
||||
) = when (val conf = subject.configuration) {
|
||||
Uninstall -> FlashActivity.uninstall(this, subject.file, id)
|
||||
EnvFix -> { remove(id); EnvFixTask(subject.file).exec() }
|
||||
is Patch -> FlashActivity.patch(this, subject.file, conf.fileUri, id)
|
||||
is Flash -> FlashActivity.flash(this, subject.file, conf is Secondary, id)
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
private fun onFinishedInternal(
|
||||
subject: Module,
|
||||
id: Int
|
||||
) = when (subject.configuration) {
|
||||
is Flash -> FlashActivity.install(this, subject.file, id)
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
private fun onFinishedInternal(
|
||||
subject: Manager,
|
||||
id: Int
|
||||
) {
|
||||
Completable.fromAction {
|
||||
handleAPK(subject)
|
||||
}.subscribeK {
|
||||
remove(id)
|
||||
when (subject.configuration) {
|
||||
is APK.Upgrade -> APKInstall.install(this, subject.file)
|
||||
is APK.Restore -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
override fun Notification.Builder.addActions(subject: DownloadSubject)
|
||||
= when (subject) {
|
||||
is Magisk -> addActionsInternal(subject)
|
||||
is Module -> addActionsInternal(subject)
|
||||
is Manager -> addActionsInternal(subject)
|
||||
}
|
||||
|
||||
private fun Notification.Builder.addActionsInternal(subject: Magisk)
|
||||
= when (val conf = subject.configuration) {
|
||||
Download -> this.apply {
|
||||
fileIntent(subject.file.parentFile!!)
|
||||
.takeIf { it.exists(get()) }
|
||||
?.let { addAction(0, R.string.download_open_parent, it.chooser()) }
|
||||
fileIntent(subject.file)
|
||||
.takeIf { it.exists(get()) }
|
||||
?.let { addAction(0, R.string.download_open_self, it.chooser()) }
|
||||
}
|
||||
Uninstall -> setContentIntent(FlashActivity.uninstallIntent(context, subject.file))
|
||||
is Flash -> setContentIntent(FlashActivity.flashIntent(context, subject.file, conf is Secondary))
|
||||
is Patch -> setContentIntent(FlashActivity.patchIntent(context, subject.file, conf.fileUri))
|
||||
else -> this
|
||||
}
|
||||
|
||||
private fun Notification.Builder.addActionsInternal(subject: Module)
|
||||
= when (subject.configuration) {
|
||||
Download -> this.apply {
|
||||
fileIntent(subject.file.parentFile!!)
|
||||
.takeIf { it.exists(get()) }
|
||||
?.let { addAction(0, R.string.download_open_parent, it.chooser()) }
|
||||
fileIntent(subject.file)
|
||||
.takeIf { it.exists(get()) }
|
||||
?.let { addAction(0, R.string.download_open_self, it.chooser()) }
|
||||
}
|
||||
is Flash -> setContentIntent(FlashActivity.installIntent(context, subject.file))
|
||||
else -> this
|
||||
}
|
||||
|
||||
private fun Notification.Builder.addActionsInternal(subject: Manager)
|
||||
= when (subject.configuration) {
|
||||
APK.Upgrade -> setContentIntent(APKInstall.installIntent(context, subject.file))
|
||||
else -> this
|
||||
}
|
||||
|
||||
@Suppress("ReplaceSingleLineLet")
|
||||
private fun Notification.Builder.setContentIntent(intent: Intent) =
|
||||
setContentIntent(
|
||||
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
||||
)
|
||||
|
||||
@Suppress("ReplaceSingleLineLet")
|
||||
private fun Notification.Builder.addAction(icon: Int, title: Int, intent: Intent) =
|
||||
addAction(icon, getString(title),
|
||||
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
||||
)
|
||||
|
||||
// ---
|
||||
|
||||
private fun fileIntent(file: File): Intent {
|
||||
return Intent(Intent.ACTION_VIEW)
|
||||
.setDataAndType(file.provide(this), file.type)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
|
||||
class Builder {
|
||||
lateinit var subject: DownloadSubject
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
inline operator fun invoke(context: Context, argBuilder: Builder.() -> Unit) {
|
||||
val app = context.applicationContext
|
||||
val builder = Builder().apply(argBuilder)
|
||||
val intent = app.intent<DownloadService>().putExtra(ARG_URL, builder.subject)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
app.startForegroundService(intent)
|
||||
} else {
|
||||
app.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,72 @@
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
import com.topjohnwu.magisk.ProcessPhoenix
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.intent
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.utils.PatchAPK
|
||||
import com.topjohnwu.magisk.extensions.writeTo
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.APK.Restore
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.APK.Upgrade
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import java.io.File
|
||||
|
||||
private fun RemoteFileService.patch(apk: File, id: Int) {
|
||||
if (packageName == BuildConfig.APPLICATION_ID)
|
||||
return
|
||||
|
||||
update(id) {
|
||||
it.setProgress(0, 0, true)
|
||||
.setProgress(0, 0, true)
|
||||
.setContentTitle(getString(R.string.hide_manager_title))
|
||||
.setContentText("")
|
||||
}
|
||||
val patched = File(apk.parent, "patched.apk")
|
||||
PatchAPK.patch(apk.path, patched.path, packageName, applicationInfo.nonLocalizedLabel)
|
||||
apk.delete()
|
||||
patched.renameTo(apk)
|
||||
}
|
||||
|
||||
private fun RemoteFileService.upgrade(apk: File, id: Int) {
|
||||
if (isRunningAsStub) {
|
||||
// Move to upgrade location
|
||||
apk.copyTo(DynAPK.update(this), overwrite = true)
|
||||
apk.delete()
|
||||
if (Info.stub!!.version < Info.remote.stub.versionCode) {
|
||||
// We also want to upgrade stub
|
||||
service.fetchFile(Info.remote.stub.link).blockingGet().byteStream().use {
|
||||
it.writeTo(apk)
|
||||
}
|
||||
patch(apk, id)
|
||||
} else {
|
||||
// Simply relaunch the app
|
||||
ProcessPhoenix.triggerRebirth(this, intent<ProcessPhoenix>())
|
||||
}
|
||||
} else {
|
||||
patch(apk, id)
|
||||
}
|
||||
}
|
||||
|
||||
private fun RemoteFileService.restore(apk: File, id: Int) {
|
||||
update(id) {
|
||||
it.setProgress(0, 0, true)
|
||||
.setProgress(0, 0, true)
|
||||
.setContentTitle(getString(R.string.restore_img_msg))
|
||||
.setContentText("")
|
||||
}
|
||||
Config.export()
|
||||
// Make it world readable
|
||||
apk.setReadable(true, false)
|
||||
Shell.su("pm install $apk && pm uninstall $packageName").exec()
|
||||
}
|
||||
|
||||
fun RemoteFileService.handleAPK(subject: DownloadSubject.Manager) =
|
||||
when (subject.configuration) {
|
||||
is Upgrade -> upgrade(subject.file, subject.hashCode())
|
||||
is Restore -> restore(subject.file, subject.hashCode())
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import com.topjohnwu.magisk.extensions.withStreams
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
fun InputStream.toModule(file: File, installer: InputStream) {
|
||||
|
||||
val input = ZipInputStream(buffered())
|
||||
val output = ZipOutputStream(file.outputStream().buffered())
|
||||
|
||||
withStreams(input, output) { zin, zout ->
|
||||
zout.putNextEntry(ZipEntry("META-INF/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
|
||||
installer.copyTo(zout)
|
||||
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
|
||||
zout.write("#MAGISK\n".toByteArray(charset("UTF-8")))
|
||||
|
||||
var off = -1
|
||||
var entry: ZipEntry? = zin.nextEntry
|
||||
while (entry != null) {
|
||||
if (off < 0) {
|
||||
off = entry.name.indexOf('/') + 1
|
||||
}
|
||||
|
||||
val path = entry.name.substring(off)
|
||||
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
|
||||
zout.putNextEntry(ZipEntry(path))
|
||||
if (!entry.isDirectory) {
|
||||
zin.copyTo(zout)
|
||||
}
|
||||
}
|
||||
|
||||
entry = zin.nextEntry
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,86 @@
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import com.topjohnwu.magisk.core.base.BaseService
|
||||
import com.topjohnwu.magisk.core.view.Notifications
|
||||
import org.koin.core.KoinComponent
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
import kotlin.random.Random.Default.nextInt
|
||||
|
||||
abstract class NotificationService : BaseService(), KoinComponent {
|
||||
|
||||
private val hasNotifications get() = notifications.isNotEmpty()
|
||||
|
||||
private val notifications = Collections.synchronizedMap(HashMap<Int, Notification.Builder>())
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
notifications.forEach { cancel(it.key) }
|
||||
notifications.clear()
|
||||
}
|
||||
|
||||
abstract fun createNotification(): Notification.Builder
|
||||
|
||||
// --
|
||||
|
||||
fun update(
|
||||
id: Int,
|
||||
body: (Notification.Builder) -> Unit = {}
|
||||
) {
|
||||
val wasEmpty = notifications.isEmpty()
|
||||
val notification = notifications.getOrPut(id, ::createNotification).also(body)
|
||||
if (wasEmpty)
|
||||
updateForeground()
|
||||
else
|
||||
notify(id, notification.build())
|
||||
}
|
||||
|
||||
protected fun lastNotify(
|
||||
id: Int,
|
||||
editBody: (Notification.Builder) -> Notification.Builder? = { null }
|
||||
) : Int {
|
||||
val currentNotification = remove(id)?.run(editBody)
|
||||
|
||||
var newId = -1
|
||||
currentNotification?.let {
|
||||
newId = nextInt(Int.MAX_VALUE)
|
||||
notify(newId, it.build())
|
||||
}
|
||||
|
||||
if (!hasNotifications) {
|
||||
stopSelf()
|
||||
}
|
||||
return newId
|
||||
}
|
||||
|
||||
protected fun remove(id: Int) = notifications.remove(id).also {
|
||||
cancel(id)
|
||||
updateForeground()
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
private fun notify(id: Int, notification: Notification) {
|
||||
Notifications.mgr.notify(id, notification)
|
||||
}
|
||||
|
||||
private fun cancel(id: Int) {
|
||||
Notifications.mgr.cancel(id)
|
||||
}
|
||||
|
||||
private fun updateForeground() {
|
||||
if (hasNotifications) {
|
||||
val first = notifications.entries.first()
|
||||
startForeground(first.key, first.value.build())
|
||||
} else {
|
||||
stopForeground(true)
|
||||
}
|
||||
}
|
||||
|
||||
// --
|
||||
|
||||
override fun onBind(p0: Intent?): IBinder? = null
|
||||
}
|
@@ -0,0 +1,135 @@
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Notification
|
||||
import android.content.Intent
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
||||
import com.topjohnwu.magisk.core.view.Notifications
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.di.NullActivity
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.extensions.writeTo
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Magisk
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Module
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import io.reactivex.Completable
|
||||
import okhttp3.ResponseBody
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.core.KoinComponent
|
||||
import timber.log.Timber
|
||||
import java.io.InputStream
|
||||
|
||||
abstract class RemoteFileService : NotificationService() {
|
||||
|
||||
val service: GithubRawServices by inject()
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
intent?.getParcelableExtra<DownloadSubject>(ARG_URL)?.let { start(it) }
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
override fun createNotification() = Notifications.progress(this, "")
|
||||
|
||||
// ---
|
||||
|
||||
private fun start(subject: DownloadSubject) = checkExisting(subject)
|
||||
.onErrorResumeNext { download(subject) }
|
||||
.subscribeK(onError = {
|
||||
Timber.e(it)
|
||||
failNotify(subject)
|
||||
}) {
|
||||
val newId = finishNotify(subject)
|
||||
if (get<Activity>() !is NullActivity) {
|
||||
onFinished(subject, newId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkExisting(subject: DownloadSubject) = Completable.fromAction {
|
||||
check(subject is Magisk) { "Download cache is disabled" }
|
||||
check(subject.file.exists() &&
|
||||
ShellUtils.checkSum("MD5", subject.file, subject.magisk.md5)) {
|
||||
"The given file does not match checksum"
|
||||
}
|
||||
}
|
||||
|
||||
private fun download(subject: DownloadSubject) = service.fetchFile(subject.url)
|
||||
.map { it.toProgressStream(subject) }
|
||||
.flatMapCompletable { stream ->
|
||||
when (subject) {
|
||||
is Module -> service.fetchInstaller()
|
||||
.doOnSuccess { stream.toModule(subject.file, it.byteStream()) }
|
||||
.ignoreElement()
|
||||
else -> Completable.fromAction { stream.writeTo(subject.file) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun ResponseBody.toProgressStream(subject: DownloadSubject): InputStream {
|
||||
val maxRaw = contentLength()
|
||||
val max = maxRaw / 1_000_000f
|
||||
val id = subject.hashCode()
|
||||
|
||||
update(id) { it.setContentTitle(subject.title) }
|
||||
|
||||
return ProgressInputStream(byteStream()) {
|
||||
val progress = it / 1_000_000f
|
||||
update(id) { notification ->
|
||||
if (maxRaw > 0) {
|
||||
send(progress / max, subject)
|
||||
notification
|
||||
.setProgress(maxRaw.toInt(), it.toInt(), false)
|
||||
.setContentText("%.2f / %.2f MB".format(progress, max))
|
||||
} else {
|
||||
send(-1f, subject)
|
||||
notification.setContentText("%.2f MB / ??".format(progress))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun failNotify(subject: DownloadSubject) = lastNotify(subject.hashCode()) {
|
||||
send(0f, subject)
|
||||
it.setContentText(getString(R.string.download_file_error))
|
||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||
.setOngoing(false)
|
||||
}
|
||||
|
||||
private fun finishNotify(subject: DownloadSubject) = lastNotify(subject.hashCode()) {
|
||||
send(1f, subject)
|
||||
it.addActions(subject)
|
||||
.setContentText(getString(R.string.download_complete))
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setProgress(0, 0, false)
|
||||
.setOngoing(false)
|
||||
.setAutoCancel(true)
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
|
||||
@Throws(Throwable::class)
|
||||
protected abstract fun onFinished(subject: DownloadSubject, id: Int)
|
||||
|
||||
protected abstract fun Notification.Builder.addActions(subject: DownloadSubject)
|
||||
: Notification.Builder
|
||||
|
||||
companion object : KoinComponent {
|
||||
const val ARG_URL = "arg_url"
|
||||
|
||||
private val internalProgressBroadcast = MutableLiveData<Pair<Float, DownloadSubject>>()
|
||||
val progressBroadcast: LiveData<Pair<Float, DownloadSubject>> get() = internalProgressBroadcast
|
||||
|
||||
fun send(progress: Float, subject: DownloadSubject) {
|
||||
internalProgressBroadcast.postValue(progress to subject)
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
internalProgressBroadcast.value = null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
import androidx.annotation.StringDef
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Single
|
||||
|
||||
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> query(builder: Builder.() -> Unit = {}) =
|
||||
Builder::class.java.newInstance()
|
||||
.apply { table = this@BaseDao.table }
|
||||
.apply(builder)
|
||||
.toString()
|
||||
.let { Query(it) }
|
||||
.query()
|
||||
|
||||
}
|
||||
|
||||
fun Query.query() = query.su()
|
||||
|
||||
private fun String.suRaw() = Single.fromCallable { Shell.su(this).exec().out }
|
||||
private fun String.su() = suRaw().map { it.toMap() }
|
||||
|
||||
private fun List<String>.toMap() = map { it.split(Regex("\\|")) }
|
||||
.map { it.toMapInternal() }
|
||||
|
||||
private fun List<String>.toMapInternal() = map { it.split("=", limit = 2) }
|
||||
.filter { it.size == 2 }
|
||||
.map { Pair(it[0], it[1]) }
|
||||
.toMap()
|
@@ -0,0 +1,74 @@
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.model.MagiskPolicy
|
||||
import com.topjohnwu.magisk.core.model.toMap
|
||||
import com.topjohnwu.magisk.core.model.toPolicy
|
||||
import com.topjohnwu.magisk.extensions.now
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
class PolicyDao(
|
||||
private val context: Context
|
||||
) : BaseDao() {
|
||||
|
||||
override val table: String = Table.POLICY
|
||||
|
||||
fun deleteOutdated(
|
||||
nowSeconds: Long = TimeUnit.MILLISECONDS.toSeconds(now)
|
||||
) = query<Delete> {
|
||||
condition {
|
||||
greaterThan("until", "0")
|
||||
and {
|
||||
lessThan("until", nowSeconds.toString())
|
||||
}
|
||||
or {
|
||||
lessThan("until", "0")
|
||||
}
|
||||
}
|
||||
}.ignoreElement()
|
||||
|
||||
fun delete(packageName: String) = query<Delete> {
|
||||
condition {
|
||||
equals("package_name", packageName)
|
||||
}
|
||||
}.ignoreElement()
|
||||
|
||||
fun delete(uid: Int) = query<Delete> {
|
||||
condition {
|
||||
equals("uid", uid)
|
||||
}
|
||||
}.ignoreElement()
|
||||
|
||||
fun fetch(uid: Int) = query<Select> {
|
||||
condition {
|
||||
equals("uid", uid)
|
||||
}
|
||||
}.map { it.first().toPolicySafe() }
|
||||
|
||||
fun update(policy: MagiskPolicy) = query<Replace> {
|
||||
values(policy.toMap())
|
||||
}.ignoreElement()
|
||||
|
||||
fun fetchAll() = query<Select> {
|
||||
condition {
|
||||
equals("uid/100000", Const.USER_ID)
|
||||
}
|
||||
}.map { it.mapNotNull { it.toPolicySafe() } }
|
||||
|
||||
|
||||
private fun Map<String, String>.toPolicySafe(): MagiskPolicy? {
|
||||
return runCatching { toPolicy(context.packageManager) }.getOrElse {
|
||||
Timber.e(it)
|
||||
if (it is PackageManager.NameNotFoundException) {
|
||||
val uid = getOrElse("uid") { null } ?: return null
|
||||
delete(uid).subscribe()
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
137
app/src/main/java/com/topjohnwu/magisk/core/magiskdb/Query.kt
Normal file
137
app/src/main/java/com/topjohnwu/magisk/core/magiskdb/Query.kt
Normal file
@@ -0,0 +1,137 @@
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
import androidx.annotation.StringDef
|
||||
|
||||
class Query(private val _query: String) {
|
||||
val query get() = "magisk --sqlite '$_query'"
|
||||
|
||||
interface Builder {
|
||||
val requestType: String
|
||||
var table: String
|
||||
}
|
||||
}
|
||||
|
||||
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
|
@@ -0,0 +1,20 @@
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
class SettingsDao : BaseDao() {
|
||||
|
||||
override val table = Table.SETTINGS
|
||||
|
||||
fun delete(key: String) = query<Delete> {
|
||||
condition { equals("key", key) }
|
||||
}.ignoreElement()
|
||||
|
||||
fun put(key: String, value: Int) = query<Replace> {
|
||||
values("key" to key, "value" to value)
|
||||
}.ignoreElement()
|
||||
|
||||
fun fetch(key: String, default: Int = -1) = query<Select> {
|
||||
fields("value")
|
||||
condition { equals("key", key) }
|
||||
}.map { it.firstOrNull()?.values?.firstOrNull()?.toIntOrNull() ?: default }
|
||||
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
class StringDao : BaseDao() {
|
||||
|
||||
override val table = Table.STRINGS
|
||||
|
||||
fun delete(key: String) = query<Delete> {
|
||||
condition { equals("key", key) }
|
||||
}.ignoreElement()
|
||||
|
||||
fun put(key: String, value: String) = query<Replace> {
|
||||
values("key" to key, "value" to value)
|
||||
}.ignoreElement()
|
||||
|
||||
fun fetch(key: String, default: String = "") = query<Select> {
|
||||
fields("value")
|
||||
condition { equals("key", key) }
|
||||
}.map { it.firstOrNull()?.values?.firstOrNull() ?: default }
|
||||
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
package com.topjohnwu.magisk.core.model
|
||||
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.core.model.MagiskPolicy.Companion.INTERACTIVE
|
||||
import com.topjohnwu.magisk.extensions.getLabel
|
||||
|
||||
|
||||
data class MagiskPolicy(
|
||||
var uid: Int,
|
||||
val packageName: String,
|
||||
val appName: String,
|
||||
var policy: Int = INTERACTIVE,
|
||||
var until: Long = -1L,
|
||||
val logging: Boolean = true,
|
||||
val notification: Boolean = true,
|
||||
val applicationInfo: ApplicationInfo
|
||||
) {
|
||||
|
||||
companion object {
|
||||
const val INTERACTIVE = 0
|
||||
const val DENY = 1
|
||||
const val ALLOW = 2
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun MagiskPolicy.toMap() = mapOf(
|
||||
"uid" to uid,
|
||||
"package_name" to packageName,
|
||||
"policy" to policy,
|
||||
"until" to until,
|
||||
"logging" to logging,
|
||||
"notification" to notification
|
||||
)
|
||||
|
||||
@Throws(PackageManager.NameNotFoundException::class)
|
||||
fun Map<String, String>.toPolicy(pm: PackageManager): MagiskPolicy {
|
||||
val uid = get("uid")?.toIntOrNull() ?: -1
|
||||
val packageName = get("package_name").orEmpty()
|
||||
val info = pm.getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES)
|
||||
|
||||
if (info.uid != uid)
|
||||
throw PackageManager.NameNotFoundException()
|
||||
|
||||
return MagiskPolicy(
|
||||
uid = uid,
|
||||
packageName = packageName,
|
||||
policy = get("policy")?.toIntOrNull() ?: INTERACTIVE,
|
||||
until = get("until")?.toLongOrNull() ?: -1L,
|
||||
logging = get("logging")?.toIntOrNull() != 0,
|
||||
notification = get("notification")?.toIntOrNull() != 0,
|
||||
applicationInfo = info,
|
||||
appName = info.getLabel(pm)
|
||||
)
|
||||
}
|
||||
|
||||
@Throws(PackageManager.NameNotFoundException::class)
|
||||
fun Int.toPolicy(pm: PackageManager, policy: Int = INTERACTIVE): MagiskPolicy {
|
||||
val pkg = pm.getPackagesForUid(this)?.firstOrNull()
|
||||
?: throw PackageManager.NameNotFoundException()
|
||||
val info = pm.getApplicationInfo(pkg, PackageManager.GET_UNINSTALLED_PACKAGES)
|
||||
return MagiskPolicy(
|
||||
uid = info.uid,
|
||||
packageName = pkg,
|
||||
policy = policy,
|
||||
applicationInfo = info,
|
||||
appName = info.getLabel(pm)
|
||||
)
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
package com.topjohnwu.magisk.core.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class UpdateInfo(
|
||||
val app: ManagerJson = ManagerJson(),
|
||||
val uninstaller: UninstallerJson = UninstallerJson(),
|
||||
val magisk: MagiskJson = MagiskJson(),
|
||||
val stub: StubJson = StubJson()
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class UninstallerJson(
|
||||
val link: String = ""
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MagiskJson(
|
||||
val version: String = "",
|
||||
val versionCode: Int = -1,
|
||||
val link: String = "",
|
||||
val note: String = "",
|
||||
val md5: String = ""
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ManagerJson(
|
||||
val version: String = "",
|
||||
val versionCode: Int = -1,
|
||||
val link: String = "",
|
||||
val note: String = ""
|
||||
) : Parcelable
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StubJson(
|
||||
val versionCode: Int = -1,
|
||||
val link: String = ""
|
||||
)
|
@@ -0,0 +1,41 @@
|
||||
package com.topjohnwu.magisk.core.model.module
|
||||
|
||||
abstract class BaseModule : Comparable<BaseModule> {
|
||||
abstract var id: String
|
||||
protected set
|
||||
abstract var name: String
|
||||
protected set
|
||||
abstract var author: String
|
||||
protected set
|
||||
abstract var version: String
|
||||
protected set
|
||||
abstract var versionCode: Int
|
||||
protected set
|
||||
abstract var description: String
|
||||
protected set
|
||||
|
||||
@Throws(NumberFormatException::class)
|
||||
protected fun parseProps(props: List<String>) {
|
||||
for (line in props) {
|
||||
val prop = line.split("=".toRegex(), 2).map { it.trim() }
|
||||
if (prop.size != 2)
|
||||
continue
|
||||
|
||||
val key = prop[0]
|
||||
val value = prop[1]
|
||||
if (key.isEmpty() || key[0] == '#')
|
||||
continue
|
||||
|
||||
when (key) {
|
||||
"id" -> id = value
|
||||
"name" -> name = value
|
||||
"version" -> version = value
|
||||
"versionCode" -> versionCode = value.toInt()
|
||||
"author" -> author = value
|
||||
"description" -> description = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override operator fun compareTo(other: BaseModule) = name.compareTo(other.name, true)
|
||||
}
|
@@ -0,0 +1,79 @@
|
||||
package com.topjohnwu.magisk.core.model.module
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
|
||||
class Module(path: String) : BaseModule() {
|
||||
override var id: String = ""
|
||||
override var name: String = ""
|
||||
override var author: String = ""
|
||||
override var version: String = ""
|
||||
override var versionCode: Int = -1
|
||||
override var description: String = ""
|
||||
|
||||
private val removeFile = SuFile(path, "remove")
|
||||
private val disableFile = SuFile(path, "disable")
|
||||
private val updateFile = SuFile(path, "update")
|
||||
private val ruleFile = SuFile(path, "sepolicy.rule")
|
||||
|
||||
val updated: Boolean = updateFile.exists()
|
||||
|
||||
var enable: Boolean = !disableFile.exists()
|
||||
set(enable) {
|
||||
val dir = "$PERSIST/$id"
|
||||
field = if (enable) {
|
||||
Shell.su("mkdir -p $dir", "cp -af $ruleFile $dir").submit()
|
||||
disableFile.delete()
|
||||
} else {
|
||||
Shell.su("rm -rf $dir").submit()
|
||||
!disableFile.createNewFile()
|
||||
}
|
||||
}
|
||||
|
||||
var remove: Boolean = removeFile.exists()
|
||||
set(remove) {
|
||||
field = if (remove) {
|
||||
Shell.su("rm -rf $PERSIST/$id").submit()
|
||||
removeFile.createNewFile()
|
||||
} else {
|
||||
Shell.su("cp -af $ruleFile $PERSIST/$id").submit()
|
||||
!removeFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
runCatching {
|
||||
parseProps(Shell.su("dos2unix < $path/module.prop").exec().out)
|
||||
}
|
||||
|
||||
if (id.isEmpty()) {
|
||||
val sep = path.lastIndexOf('/')
|
||||
id = path.substring(sep + 1)
|
||||
}
|
||||
|
||||
if (name.isEmpty()) {
|
||||
name = id
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val PERSIST = "/sbin/.magisk/mirror/persist/magisk"
|
||||
|
||||
@WorkerThread
|
||||
fun loadModules(): List<Module> {
|
||||
val moduleList = mutableListOf<Module>()
|
||||
val path = SuFile(Const.MAGISK_PATH)
|
||||
val modules =
|
||||
path.listFiles { _, name -> name != "lost+found" && name != ".core" }.orEmpty()
|
||||
for (file in modules) {
|
||||
if (file.isFile) continue
|
||||
val module = Module(Const.MAGISK_PATH + "/" + file.name)
|
||||
moduleList.add(module)
|
||||
}
|
||||
return moduleList.sortedBy { it.name.toLowerCase() }
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
package com.topjohnwu.magisk.core.model.module
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.data.repository.StringRepository
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.legalFilename
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
||||
@Entity(tableName = "repos")
|
||||
@Parcelize
|
||||
data class Repo(
|
||||
@PrimaryKey override var id: String,
|
||||
override var name: String,
|
||||
override var author: String,
|
||||
override var version: String,
|
||||
override var versionCode: Int,
|
||||
override var description: String,
|
||||
var last_update: Long
|
||||
) : BaseModule(), Parcelable {
|
||||
|
||||
private val stringRepo: StringRepository get() = get()
|
||||
|
||||
val lastUpdate get() = Date(last_update)
|
||||
|
||||
val lastUpdateString: String get() = dateFormat.format(lastUpdate)
|
||||
|
||||
val downloadFilename: String get() = "$name-$version($versionCode).zip".legalFilename()
|
||||
|
||||
val readme get() = stringRepo.getReadme(this)
|
||||
|
||||
val zipUrl: String get() = Const.Url.ZIP_URL.format(id)
|
||||
|
||||
constructor(id: String) : this(id, "", "", "", -1, "", 0)
|
||||
|
||||
@Throws(IllegalRepoException::class)
|
||||
fun update() {
|
||||
val props = runCatching {
|
||||
stringRepo.getMetadata(this).blockingGet()
|
||||
.orEmpty().split("\\n".toRegex()).dropLastWhile { it.isEmpty() }
|
||||
}.getOrElse {
|
||||
throw IllegalRepoException("Repo [$id] module.prop download error: " + it.message)
|
||||
}
|
||||
|
||||
props.runCatching {
|
||||
parseProps(this)
|
||||
}.onFailure {
|
||||
throw IllegalRepoException("Repo [$id] parse error: " + it.message)
|
||||
}
|
||||
|
||||
if (versionCode < 0) {
|
||||
throw IllegalRepoException("Repo [$id] does not contain versionCode")
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IllegalRepoException::class)
|
||||
fun update(lastUpdate: Date) {
|
||||
last_update = lastUpdate.time
|
||||
update()
|
||||
}
|
||||
|
||||
class IllegalRepoException(message: String) : Exception(message)
|
||||
|
||||
companion object {
|
||||
val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)!!
|
||||
}
|
||||
}
|
@@ -0,0 +1,141 @@
|
||||
package com.topjohnwu.magisk.core.su
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Process
|
||||
import android.widget.Toast
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.ProviderCallHandler
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.intent
|
||||
import com.topjohnwu.magisk.core.model.MagiskPolicy
|
||||
import com.topjohnwu.magisk.core.model.toPolicy
|
||||
import com.topjohnwu.magisk.core.utils.Utils
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.startActivity
|
||||
import com.topjohnwu.magisk.extensions.startActivityWithRoot
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.legacy.surequest.SuRequestActivity
|
||||
import com.topjohnwu.magisk.model.entity.toLog
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import timber.log.Timber
|
||||
|
||||
object SuCallbackHandler : ProviderCallHandler {
|
||||
|
||||
const val REQUEST = "request"
|
||||
const val LOG = "log"
|
||||
const val NOTIFY = "notify"
|
||||
const val TEST = "test"
|
||||
|
||||
override fun call(context: Context, method: String, arg: String?, extras: Bundle?): Bundle? {
|
||||
invoke(context.wrap(), method, extras)
|
||||
return Bundle.EMPTY
|
||||
}
|
||||
|
||||
operator fun invoke(context: Context, action: String?, data: Bundle?) {
|
||||
data ?: return
|
||||
|
||||
// Debug messages
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.d(action)
|
||||
data.let { bundle ->
|
||||
bundle.keySet().forEach {
|
||||
Timber.d("[%s]=[%s]", it, bundle[it])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (action) {
|
||||
REQUEST -> {
|
||||
val intent = context.intent<SuRequestActivity>()
|
||||
.setAction(action)
|
||||
.putExtras(data)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
// Android Q does not allow starting activity from background
|
||||
intent.startActivityWithRoot()
|
||||
} else {
|
||||
intent.startActivity(context)
|
||||
}
|
||||
}
|
||||
LOG -> handleLogs(context, data)
|
||||
NOTIFY -> handleNotify(context, data)
|
||||
TEST -> {
|
||||
val mode = data.getInt("mode", 2)
|
||||
Shell.su(
|
||||
"magisk --connect-mode $mode",
|
||||
"magisk --use-broadcast"
|
||||
).submit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Any?.toInt(): Int? {
|
||||
return when (this) {
|
||||
is Int -> this
|
||||
is Long -> this.toInt()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLogs(context: Context, data: Bundle) {
|
||||
val fromUid = data["from.uid"].toInt() ?: return
|
||||
if (fromUid == Process.myUid())
|
||||
return
|
||||
|
||||
val pm = context.packageManager
|
||||
|
||||
val notify = data.getBoolean("notify", true)
|
||||
val allow = data["policy"].toInt() ?: return
|
||||
|
||||
val policy = runCatching { fromUid.toPolicy(pm, allow) }.getOrElse { return }
|
||||
|
||||
if (notify)
|
||||
notify(context, policy)
|
||||
|
||||
val toUid = data["to.uid"].toInt() ?: return
|
||||
val pid = data["pid"].toInt() ?: return
|
||||
|
||||
val command = data.getString("command") ?: return
|
||||
val log = policy.toLog(
|
||||
toUid = toUid,
|
||||
fromPid = pid,
|
||||
command = command
|
||||
)
|
||||
|
||||
val logRepo = get<LogRepository>()
|
||||
logRepo.insert(log).subscribeK(onError = { Timber.e(it) })
|
||||
}
|
||||
|
||||
private fun handleNotify(context: Context, data: Bundle) {
|
||||
val fromUid = data["from.uid"].toInt() ?: return
|
||||
if (fromUid == Process.myUid())
|
||||
return
|
||||
|
||||
val pm = context.packageManager
|
||||
val allow = data["policy"].toInt() ?: return
|
||||
|
||||
runCatching {
|
||||
val policy = fromUid.toPolicy(pm, allow)
|
||||
if (policy.policy >= 0)
|
||||
notify(context, policy)
|
||||
}
|
||||
}
|
||||
|
||||
private fun notify(context: Context, policy: MagiskPolicy) {
|
||||
if (policy.notification && Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
|
||||
val resId = if (policy.policy == MagiskPolicy.ALLOW)
|
||||
R.string.su_allow_toast
|
||||
else
|
||||
R.string.su_deny_toast
|
||||
|
||||
Utils.toast(context.getString(resId, policy.appName), Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
package com.topjohnwu.magisk.core.su
|
||||
|
||||
import android.net.LocalSocket
|
||||
import android.net.LocalSocketAddress
|
||||
import androidx.collection.ArrayMap
|
||||
import timber.log.Timber
|
||||
import java.io.*
|
||||
|
||||
abstract class SuConnector @Throws(IOException::class)
|
||||
protected constructor(name: String) {
|
||||
|
||||
private val socket: LocalSocket = LocalSocket()
|
||||
protected var out: DataOutputStream
|
||||
protected var input: DataInputStream
|
||||
|
||||
init {
|
||||
socket.connect(LocalSocketAddress(name, LocalSocketAddress.Namespace.ABSTRACT))
|
||||
out = DataOutputStream(BufferedOutputStream(socket.outputStream))
|
||||
input = DataInputStream(BufferedInputStream(socket.inputStream))
|
||||
}
|
||||
|
||||
private fun readString(): String {
|
||||
val len = input.readInt()
|
||||
val buf = ByteArray(len)
|
||||
input.readFully(buf)
|
||||
return String(buf, Charsets.UTF_8)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun readRequest(): Map<String, String> {
|
||||
val ret = ArrayMap<String, String>()
|
||||
while (true) {
|
||||
val name = readString()
|
||||
if (name == "eof")
|
||||
break
|
||||
ret[name] = readString()
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
fun response() {
|
||||
runCatching {
|
||||
onResponse()
|
||||
out.flush()
|
||||
}.onFailure { Timber.e(it) }
|
||||
|
||||
runCatching {
|
||||
input.close()
|
||||
out.close()
|
||||
socket.close()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
protected abstract fun onResponse()
|
||||
|
||||
}
|
@@ -0,0 +1,103 @@
|
||||
package com.topjohnwu.magisk.core.su
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.CountDownTimer
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.model.MagiskPolicy
|
||||
import com.topjohnwu.magisk.core.model.toPolicy
|
||||
import com.topjohnwu.magisk.extensions.now
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
abstract class SuRequestHandler(
|
||||
private val packageManager: PackageManager,
|
||||
private val policyDB: PolicyDao
|
||||
) {
|
||||
protected var timer: CountDownTimer = object : CountDownTimer(
|
||||
TimeUnit.MINUTES.toMillis(1), TimeUnit.MINUTES.toMillis(1)) {
|
||||
override fun onFinish() {
|
||||
respond(MagiskPolicy.DENY, 0)
|
||||
}
|
||||
override fun onTick(remains: Long) {}
|
||||
}
|
||||
set(value) {
|
||||
field.cancel()
|
||||
field = value
|
||||
field.start()
|
||||
}
|
||||
|
||||
protected lateinit var policy: MagiskPolicy
|
||||
|
||||
private val cleanupTasks = mutableListOf<() -> Unit>()
|
||||
private lateinit var connector: SuConnector
|
||||
|
||||
abstract fun onStart()
|
||||
abstract fun onRespond()
|
||||
|
||||
fun start(intent: Intent): Boolean {
|
||||
val socketName = intent.getStringExtra("socket") ?: return false
|
||||
|
||||
try {
|
||||
connector = object : SuConnector(socketName) {
|
||||
override fun onResponse() {
|
||||
out.writeInt(policy.policy)
|
||||
}
|
||||
}
|
||||
val map = connector.readRequest()
|
||||
val uid = map["uid"]?.toIntOrNull() ?: return false
|
||||
policy = uid.toPolicy(packageManager)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
// Never allow com.topjohnwu.magisk (could be malware)
|
||||
if (policy.packageName == BuildConfig.APPLICATION_ID)
|
||||
return false
|
||||
|
||||
when (Config.suAutoReponse) {
|
||||
Config.Value.SU_AUTO_DENY -> {
|
||||
respond(MagiskPolicy.DENY, 0)
|
||||
return true
|
||||
}
|
||||
Config.Value.SU_AUTO_ALLOW -> {
|
||||
respond(MagiskPolicy.ALLOW, 0)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
timer.start()
|
||||
cleanupTasks.add {
|
||||
timer.cancel()
|
||||
}
|
||||
|
||||
onStart()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun respond() {
|
||||
connector.response()
|
||||
cleanupTasks.forEach { it() }
|
||||
onRespond()
|
||||
}
|
||||
|
||||
fun respond(action: Int, time: Int) {
|
||||
val until = if (time > 0)
|
||||
TimeUnit.MILLISECONDS.toSeconds(now) + TimeUnit.MINUTES.toSeconds(time.toLong())
|
||||
else
|
||||
time.toLong()
|
||||
|
||||
policy.policy = action
|
||||
policy.until = until
|
||||
policy.uid = policy.uid % 100000 + Const.USER_ID * 100000
|
||||
|
||||
if (until >= 0)
|
||||
policyDB.update(policy).blockingAwait()
|
||||
|
||||
respond()
|
||||
}
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.base.BaseReceiver
|
||||
import com.topjohnwu.magisk.core.view.Notifications
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import timber.log.Timber
|
||||
|
||||
private const val DTB_PATCH_RESULT = "dtb_result"
|
||||
private const val DTB_PATCH_ACTION = "com.topjohnwu.magisk.DTBO_PATCH"
|
||||
|
||||
private class DTBPatchReceiver : BaseReceiver() {
|
||||
override fun onReceive(context: ContextWrapper, intent: Intent?) {
|
||||
intent?.also {
|
||||
val result = it.getIntExtra(DTB_PATCH_RESULT, 1)
|
||||
Timber.d("result=[$result]")
|
||||
if (result == 0)
|
||||
Notifications.dtboPatched(context)
|
||||
}
|
||||
context.unregisterReceiver(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun patchDTB(context: Context) {
|
||||
if (Info.isNewReboot) {
|
||||
val c = context.applicationContext
|
||||
c.registerReceiver(DTBPatchReceiver(), IntentFilter(DTB_PATCH_ACTION))
|
||||
val broadcastCmd = "am broadcast --user ${Const.USER_ID} -p ${c.packageName} " +
|
||||
"-a $DTB_PATCH_ACTION --ei $DTB_PATCH_RESULT \$result"
|
||||
Shell.su("mm_patch_dtb '$broadcastCmd'").submit()
|
||||
}
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import androidx.annotation.MainThread
|
||||
|
||||
interface FlashResultListener {
|
||||
|
||||
@MainThread
|
||||
fun onResult(success: Boolean)
|
||||
|
||||
}
|
@@ -0,0 +1,97 @@
|
||||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.utils.unzip
|
||||
import com.topjohnwu.magisk.extensions.fileName
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.magisk.extensions.readUri
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Single
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
|
||||
abstract class FlashZip(
|
||||
private val mUri: Uri,
|
||||
private val console: MutableList<String>,
|
||||
private val logs: MutableList<String>
|
||||
) : FlashResultListener {
|
||||
|
||||
private val context: Context by inject()
|
||||
private val installFolder = File(context.cacheDir, "flash").apply {
|
||||
if (!exists()) mkdirs()
|
||||
}
|
||||
private val tmpFile: File = File(installFolder, "install.zip")
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun unzipAndCheck(): Boolean {
|
||||
val parentFile = tmpFile.parentFile ?: return false
|
||||
tmpFile.unzip(parentFile, "META-INF/com/google/android", true)
|
||||
|
||||
val updaterScript = File(parentFile, "updater-script")
|
||||
return Shell
|
||||
.su("grep -q '#MAGISK' $updaterScript")
|
||||
.exec()
|
||||
.isSuccess
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun flash(): Boolean {
|
||||
console.add("- Copying zip to temp directory")
|
||||
|
||||
runCatching {
|
||||
context.readUri(mUri).use { input ->
|
||||
tmpFile.outputStream().use { out -> input.copyTo(out) }
|
||||
}
|
||||
}.getOrElse {
|
||||
when (it) {
|
||||
is FileNotFoundException -> console.add("! Invalid Uri")
|
||||
is IOException -> console.add("! Cannot copy to cache")
|
||||
}
|
||||
throw it
|
||||
}
|
||||
|
||||
val isMagiskModule = runCatching {
|
||||
unzipAndCheck()
|
||||
}.getOrElse {
|
||||
console.add("! Unzip error")
|
||||
throw it
|
||||
}
|
||||
|
||||
if (!isMagiskModule) {
|
||||
console.add("! This zip is not a Magisk Module!")
|
||||
return false
|
||||
}
|
||||
|
||||
console.add("- Installing ${mUri.fileName}")
|
||||
|
||||
val parentFile = tmpFile.parent ?: return false
|
||||
|
||||
return Shell
|
||||
.su(
|
||||
"cd $parentFile",
|
||||
"BOOTMODE=true sh update-binary dummy 1 $tmpFile"
|
||||
)
|
||||
.to(console, logs)
|
||||
.exec().isSuccess
|
||||
}
|
||||
|
||||
fun exec() = Single
|
||||
.fromCallable {
|
||||
runCatching {
|
||||
flash()
|
||||
}.getOrElse {
|
||||
it.printStackTrace()
|
||||
false
|
||||
}.apply {
|
||||
Shell.su("cd /", "rm -rf ${tmpFile.parent} ${Const.TMP_FOLDER_PATH}")
|
||||
.submit()
|
||||
}
|
||||
}
|
||||
.subscribeK(onError = { onResult(false) }) { onResult(it) }
|
||||
.let { Unit } // ignores result disposable
|
||||
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.os.postDelayed
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
|
||||
sealed class Flashing(
|
||||
uri: Uri,
|
||||
private val console: MutableList<String>,
|
||||
log: MutableList<String>,
|
||||
private val resultListener: FlashResultListener
|
||||
) : FlashZip(uri, console, log) {
|
||||
|
||||
override fun onResult(success: Boolean) {
|
||||
if (!success) {
|
||||
console.add("! Installation failed")
|
||||
}
|
||||
|
||||
resultListener.onResult(success)
|
||||
}
|
||||
|
||||
class Install(
|
||||
uri: Uri,
|
||||
console: MutableList<String>,
|
||||
log: MutableList<String>,
|
||||
resultListener: FlashResultListener
|
||||
) : Flashing(uri, console, log, resultListener)
|
||||
|
||||
class Uninstall(
|
||||
uri: Uri,
|
||||
console: MutableList<String>,
|
||||
log: MutableList<String>,
|
||||
resultListener: FlashResultListener
|
||||
) : Flashing(uri, console, log, resultListener) {
|
||||
|
||||
private val context: Context by inject()
|
||||
|
||||
override fun onResult(success: Boolean) {
|
||||
if (success) {
|
||||
UiThreadHandler.handler.postDelayed(3000) {
|
||||
Shell.su("pm uninstall " + context.packageName).exec()
|
||||
}
|
||||
}
|
||||
super.onResult(success)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,370 @@
|
||||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.text.TextUtils
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.net.toUri
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.di.Protected
|
||||
import com.topjohnwu.magisk.extensions.*
|
||||
import com.topjohnwu.signing.SignBoot
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import com.topjohnwu.superuser.internal.NOPList
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
||||
import io.reactivex.Single
|
||||
import org.kamranzafar.jtar.TarEntry
|
||||
import org.kamranzafar.jtar.TarHeader
|
||||
import org.kamranzafar.jtar.TarInputStream
|
||||
import org.kamranzafar.jtar.TarOutputStream
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
abstract class MagiskInstallImpl : FlashResultListener {
|
||||
|
||||
protected lateinit var installDir: File
|
||||
private lateinit var srcBoot: String
|
||||
private lateinit var destFile: File
|
||||
private lateinit var zipUri: Uri
|
||||
|
||||
private val console: MutableList<String>
|
||||
private val logs: MutableList<String>
|
||||
private var tarOut: TarOutputStream? = null
|
||||
|
||||
private val service: GithubRawServices by inject()
|
||||
protected val context: Context by inject()
|
||||
|
||||
protected constructor() {
|
||||
console = NOPList.getInstance()
|
||||
logs = NOPList.getInstance()
|
||||
}
|
||||
|
||||
constructor(zip: Uri, out: MutableList<String>, err: MutableList<String>) {
|
||||
console = out
|
||||
logs = err
|
||||
zipUri = zip
|
||||
installDir = File(get<Context>(Protected).filesDir.parent, "install")
|
||||
"rm -rf $installDir".sh()
|
||||
installDir.mkdirs()
|
||||
}
|
||||
|
||||
private fun findImage(): Boolean {
|
||||
srcBoot = "find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
|
||||
if (srcBoot.isEmpty()) {
|
||||
console.add("! Unable to detect target image")
|
||||
return false
|
||||
}
|
||||
console.add("- Target image: $srcBoot")
|
||||
return true
|
||||
}
|
||||
|
||||
private fun findSecondaryImage(): Boolean {
|
||||
val slot = "echo \$SLOT".fsh()
|
||||
val target = if (slot == "_a") "_b" else "_a"
|
||||
console.add("- Target slot: $target")
|
||||
srcBoot = arrayOf(
|
||||
"SLOT=$target",
|
||||
"find_boot_image",
|
||||
"SLOT=$slot",
|
||||
"echo \"\$BOOTIMAGE\"").fsh()
|
||||
if (srcBoot.isEmpty()) {
|
||||
console.add("! Unable to detect target image")
|
||||
return false
|
||||
}
|
||||
console.add("- Target image: $srcBoot")
|
||||
return true
|
||||
}
|
||||
|
||||
private fun extractZip(): Boolean {
|
||||
val arch: String
|
||||
arch = if (Build.VERSION.SDK_INT >= 21) {
|
||||
val abis = listOf(*Build.SUPPORTED_ABIS)
|
||||
if (abis.contains("x86")) "x86" else "arm"
|
||||
} else {
|
||||
if (TextUtils.equals(Build.CPU_ABI, "x86")) "x86" else "arm"
|
||||
}
|
||||
|
||||
console.add("- Device platform: " + Build.CPU_ABI)
|
||||
|
||||
try {
|
||||
ZipInputStream(context.readUri(zipUri).buffered()).use { zi ->
|
||||
lateinit var ze: ZipEntry
|
||||
while (zi.nextEntry?.let { ze = it } != null) {
|
||||
if (ze.isDirectory)
|
||||
continue
|
||||
var name: String? = null
|
||||
val names = arrayOf("$arch/", "common/", "META-INF/com/google/android/update-binary")
|
||||
for (n in names) {
|
||||
ze.name.run {
|
||||
if (startsWith(n)) {
|
||||
name = substring(lastIndexOf('/') + 1)
|
||||
}
|
||||
}
|
||||
name ?: continue
|
||||
break
|
||||
}
|
||||
if (name == null && ze.name.startsWith("chromeos/"))
|
||||
name = ze.name
|
||||
if (name == null)
|
||||
continue
|
||||
val dest = if (installDir is SuFile)
|
||||
SuFile(installDir, name)
|
||||
else
|
||||
File(installDir, name)
|
||||
dest.parentFile!!.mkdirs()
|
||||
SuFileOutputStream(dest).use { zi.copyTo(it) }
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
console.add("! Cannot unzip zip")
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
val init64 = SuFile.open(installDir, "magiskinit64")
|
||||
if (Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_64_BIT_ABIS.isNotEmpty()) {
|
||||
init64.renameTo(SuFile.open(installDir, "magiskinit"))
|
||||
} else {
|
||||
init64.delete()
|
||||
}
|
||||
"cd $installDir; chmod 755 *".sh()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun newEntry(name: String, size: Long): TarEntry {
|
||||
console.add("-- Writing: $name")
|
||||
return TarEntry(TarHeader.createHeader(name, size, 0, false, 420 /* 0644 */))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun handleTar(input: InputStream) {
|
||||
console.add("- Processing tar file")
|
||||
var vbmeta = false
|
||||
val tarOut = TarOutputStream(destFile)
|
||||
this.tarOut = tarOut
|
||||
TarInputStream(input).use { tarIn ->
|
||||
lateinit var entry: TarEntry
|
||||
while (tarIn.nextEntry?.let { entry = it } != null) {
|
||||
if (entry.name.contains("boot.img") || entry.name.contains("recovery.img")) {
|
||||
val name = entry.name
|
||||
console.add("-- Extracting: $name")
|
||||
val extract = File(installDir, name)
|
||||
FileOutputStream(extract).use { tarIn.copyTo(it) }
|
||||
if (name.contains(".lz4")) {
|
||||
console.add("-- Decompressing: $name")
|
||||
"./magiskboot decompress $extract".sh()
|
||||
}
|
||||
} else if (entry.name.contains("vbmeta.img")) {
|
||||
vbmeta = true
|
||||
val buf = ByteBuffer.allocate(256)
|
||||
buf.put("AVB0".toByteArray()) // magic
|
||||
buf.putInt(1) // required_libavb_version_major
|
||||
buf.putInt(120, 2) // flags
|
||||
buf.position(128) // release_string
|
||||
buf.put("avbtool 1.1.0".toByteArray())
|
||||
tarOut.putNextEntry(newEntry("vbmeta.img", 256))
|
||||
tarOut.write(buf.array())
|
||||
} else {
|
||||
console.add("-- Writing: " + entry.name)
|
||||
tarOut.putNextEntry(entry)
|
||||
tarIn.copyTo(tarOut)
|
||||
}
|
||||
}
|
||||
val boot = SuFile.open(installDir, "boot.img")
|
||||
val recovery = SuFile.open(installDir, "recovery.img")
|
||||
if (vbmeta && recovery.exists() && boot.exists()) {
|
||||
// Install Magisk to recovery
|
||||
srcBoot = recovery.path
|
||||
// Repack boot image to prevent restore
|
||||
arrayOf(
|
||||
"./magiskboot unpack boot.img",
|
||||
"./magiskboot repack boot.img",
|
||||
"./magiskboot cleanup",
|
||||
"mv new-boot.img boot.img").sh()
|
||||
SuFileInputStream(boot).use {
|
||||
tarOut.putNextEntry(newEntry("boot.img", boot.length()))
|
||||
it.copyTo(tarOut)
|
||||
}
|
||||
boot.delete()
|
||||
} else {
|
||||
if (!boot.exists()) {
|
||||
console.add("! No boot image found")
|
||||
throw IOException()
|
||||
}
|
||||
srcBoot = boot.path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFile(uri: Uri): Boolean {
|
||||
try {
|
||||
context.readUri(uri).buffered().use {
|
||||
it.mark(500)
|
||||
val magic = ByteArray(5)
|
||||
if (it.skip(257) != 257L || it.read(magic) != magic.size) {
|
||||
console.add("! Invalid file")
|
||||
return false
|
||||
}
|
||||
it.reset()
|
||||
if (magic.contentEquals("ustar".toByteArray())) {
|
||||
destFile = File(Config.downloadDirectory, "magisk_patched.tar")
|
||||
handleTar(it)
|
||||
} else {
|
||||
// Raw image
|
||||
srcBoot = File(installDir, "boot.img").path
|
||||
destFile = File(Config.downloadDirectory, "magisk_patched.img")
|
||||
console.add("- Copying image to cache")
|
||||
FileOutputStream(srcBoot).use { out -> it.copyTo(out) }
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
console.add("! Process error")
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun patchBoot(): Boolean {
|
||||
var isSigned = false
|
||||
try {
|
||||
SuFileInputStream(srcBoot).use {
|
||||
isSigned = SignBoot.verifySignature(it, null)
|
||||
if (isSigned) {
|
||||
console.add("- Boot image is signed with AVB 1.0")
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
console.add("! Unable to check signature")
|
||||
return false
|
||||
}
|
||||
|
||||
if (!("KEEPFORCEENCRYPT=${Info.keepEnc} KEEPVERITY=${Info.keepVerity} " +
|
||||
"RECOVERYMODE=${Info.recovery} sh update-binary " +
|
||||
"sh boot_patch.sh $srcBoot").sh().isSuccess) {
|
||||
return false
|
||||
}
|
||||
|
||||
val job = Shell.sh(
|
||||
"./magiskboot cleanup",
|
||||
"mv bin/busybox busybox",
|
||||
"rm -rf magisk.apk bin boot.img update-binary",
|
||||
"cd /")
|
||||
|
||||
val patched = File(installDir, "new-boot.img")
|
||||
if (isSigned) {
|
||||
console.add("- Signing boot image with verity keys")
|
||||
val signed = File(installDir, "signed.img")
|
||||
try {
|
||||
withStreams(SuFileInputStream(patched), signed.outputStream().buffered()) {
|
||||
input, out -> SignBoot.doSignature("/boot", input, out, null, null)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
console.add("! Unable to sign image")
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
job.add("mv -f $signed $patched")
|
||||
}
|
||||
job.exec()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun flashBoot(): Boolean {
|
||||
if (!"direct_install $installDir $srcBoot".sh().isSuccess)
|
||||
return false
|
||||
arrayOf(
|
||||
"(KEEPVERITY=${Info.keepVerity} patch_dtb_partitions)",
|
||||
"run_migrations"
|
||||
).sh()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun storeBoot(): Boolean {
|
||||
val patched = SuFile.open(installDir, "new-boot.img")
|
||||
try {
|
||||
val os = tarOut?.let {
|
||||
it.putNextEntry(newEntry(
|
||||
if (srcBoot.contains("recovery")) "recovery.img" else "boot.img",
|
||||
patched.length()))
|
||||
tarOut = null
|
||||
it
|
||||
} ?: destFile.outputStream()
|
||||
patched.suInputStream().use { it.copyTo(os); os.close() }
|
||||
} catch (e: IOException) {
|
||||
console.add("! Failed to output to $destFile")
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
patched.delete()
|
||||
console.add("")
|
||||
console.add("****************************")
|
||||
console.add(" Output file is placed in ")
|
||||
console.add(" $destFile ")
|
||||
console.add("****************************")
|
||||
return true
|
||||
}
|
||||
|
||||
private fun postOTA(): Boolean {
|
||||
val bootctl = SuFile("/data/adb/bootctl")
|
||||
try {
|
||||
withStreams(service.fetchBootctl().blockingGet().byteStream(), bootctl.suOutputStream()) {
|
||||
input, out -> input.copyTo(out)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
console.add("! Unable to download bootctl")
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
"post_ota ${bootctl.parent}".sh()
|
||||
|
||||
console.add("***************************************")
|
||||
console.add(" Next reboot will boot to second slot!")
|
||||
console.add("***************************************")
|
||||
return true
|
||||
}
|
||||
|
||||
private fun String.sh() = Shell.sh(this).to(console, logs).exec()
|
||||
private fun Array<String>.sh() = Shell.sh(*this).to(console, logs).exec()
|
||||
private fun String.fsh() = ShellUtils.fastCmd(this)
|
||||
private fun Array<String>.fsh() = ShellUtils.fastCmd(*this)
|
||||
|
||||
protected fun doPatchFile(patchFile: Uri) =
|
||||
extractZip() && handleFile(patchFile) && patchBoot() && storeBoot()
|
||||
|
||||
protected fun direct() = findImage() && extractZip() && patchBoot() && flashBoot()
|
||||
|
||||
protected fun secondSlot() =
|
||||
findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA()
|
||||
|
||||
protected fun fixEnv(zip: File): Boolean {
|
||||
installDir = SuFile("/data/adb/magisk")
|
||||
Shell.su("rm -rf /data/adb/magisk/*").exec()
|
||||
zipUri = zip.toUri()
|
||||
return extractZip() && Shell.su("fix_env").exec().isSuccess
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
protected abstract fun operations(): Boolean
|
||||
|
||||
fun exec() {
|
||||
Single.fromCallable { operations() }.subscribeK { onResult(it) }
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.core.os.postDelayed
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.utils.Utils
|
||||
import com.topjohnwu.magisk.extensions.reboot
|
||||
import com.topjohnwu.magisk.model.events.dialog.EnvFixDialog
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import java.io.File
|
||||
|
||||
sealed class MagiskInstaller(
|
||||
file: Uri,
|
||||
private val console: MutableList<String>,
|
||||
logs: MutableList<String>,
|
||||
private val resultListener: FlashResultListener
|
||||
) : MagiskInstallImpl(file, console, logs) {
|
||||
|
||||
override fun onResult(success: Boolean) {
|
||||
if (success) {
|
||||
console.add("- All done!")
|
||||
} else {
|
||||
Shell.sh("rm -rf $installDir").submit()
|
||||
console.add("! Installation failed")
|
||||
}
|
||||
resultListener.onResult(success)
|
||||
}
|
||||
|
||||
class Patch(
|
||||
file: Uri,
|
||||
private val uri: Uri,
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>,
|
||||
resultListener: FlashResultListener
|
||||
) : MagiskInstaller(file, console, logs, resultListener) {
|
||||
override fun operations() = doPatchFile(uri)
|
||||
}
|
||||
|
||||
class SecondSlot(
|
||||
file: Uri,
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>,
|
||||
resultListener: FlashResultListener
|
||||
) : MagiskInstaller(file, console, logs, resultListener) {
|
||||
override fun operations() = secondSlot()
|
||||
}
|
||||
|
||||
class Direct(
|
||||
file: Uri,
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>,
|
||||
resultListener: FlashResultListener
|
||||
) : MagiskInstaller(file, console, logs, resultListener) {
|
||||
override fun operations() = direct()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class EnvFixTask(
|
||||
private val zip: File
|
||||
) : MagiskInstallImpl() {
|
||||
override fun operations() = fixEnv(zip)
|
||||
|
||||
override fun onResult(success: Boolean) {
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(Intent(EnvFixDialog.DISMISS))
|
||||
Utils.toast(
|
||||
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
if (success)
|
||||
UiThreadHandler.handler.postDelayed(5000) { reboot() }
|
||||
}
|
||||
}
|
101
app/src/main/java/com/topjohnwu/magisk/core/tasks/RepoUpdater.kt
Normal file
101
app/src/main/java/com/topjohnwu/magisk/core/tasks/RepoUpdater.kt
Normal file
@@ -0,0 +1,101 @@
|
||||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.model.module.Repo
|
||||
import com.topjohnwu.magisk.data.database.RepoDao
|
||||
import com.topjohnwu.magisk.data.network.GithubApiServices
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.rxkotlin.toFlowable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
import java.net.HttpURLConnection
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.collections.HashSet
|
||||
|
||||
class RepoUpdater(
|
||||
private val api: GithubApiServices,
|
||||
private val repoDB: RepoDao
|
||||
) {
|
||||
private fun loadRepos(repos: List<GithubRepoInfo>, cached: MutableSet<String>) =
|
||||
repos.toFlowable().parallel().runOn(Schedulers.io()).map {
|
||||
// Skip submission
|
||||
if (it.id == "submission")
|
||||
return@map
|
||||
val repo = repoDB.getRepo(it.id)?.apply { cached.remove(it.id) } ?: Repo(it.id)
|
||||
repo.runCatching {
|
||||
update(it.pushDate)
|
||||
repoDB.addRepo(this)
|
||||
}.getOrElse(Timber::e)
|
||||
}.sequential()
|
||||
|
||||
private fun loadPage(
|
||||
cached: MutableSet<String>,
|
||||
page: Int = 1,
|
||||
etag: String = ""
|
||||
): Flowable<Unit> = api.fetchRepos(page, etag).flatMap {
|
||||
it.error()?.also { throw it }
|
||||
it.response()?.run {
|
||||
if (code() == HttpURLConnection.HTTP_NOT_MODIFIED)
|
||||
return@run Flowable.error<Unit>(CachedException())
|
||||
|
||||
if (page == 1)
|
||||
repoDB.etagKey = headers()[Const.Key.ETAG_KEY].orEmpty().trimEtag()
|
||||
|
||||
val flow = loadRepos(body()!!, cached)
|
||||
if (headers()[Const.Key.LINK_KEY].orEmpty().contains("next")) {
|
||||
flow.mergeWith(loadPage(cached, page + 1))
|
||||
} else {
|
||||
flow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun forcedReload(cached: MutableSet<String>) =
|
||||
cached.toFlowable().parallel().runOn(Schedulers.io()).map {
|
||||
runCatching {
|
||||
Repo(it).update()
|
||||
}.getOrElse(Timber::e)
|
||||
}.sequential()
|
||||
|
||||
private fun String.trimEtag() = substring(indexOf('\"'), lastIndexOf('\"') + 1)
|
||||
|
||||
@Suppress("RedundantLambdaArrow")
|
||||
operator fun invoke(forced: Boolean) : Completable {
|
||||
return Flowable
|
||||
.fromCallable { Collections.synchronizedSet(HashSet(repoDB.repoIDList)) }
|
||||
.flatMap { cached ->
|
||||
loadPage(cached, etag = repoDB.etagKey).doOnComplete {
|
||||
repoDB.removeRepos(cached)
|
||||
}.onErrorResumeNext { it: Throwable ->
|
||||
if (it is CachedException) {
|
||||
if (forced)
|
||||
return@onErrorResumeNext forcedReload(cached)
|
||||
} else {
|
||||
Timber.e(it)
|
||||
}
|
||||
Flowable.empty()
|
||||
}
|
||||
}.ignoreElements()
|
||||
}
|
||||
|
||||
class CachedException : Exception()
|
||||
}
|
||||
|
||||
private val dateFormat: SimpleDateFormat =
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GithubRepoInfo(
|
||||
val name: String,
|
||||
val pushed_at: String
|
||||
) {
|
||||
val id get() = name
|
||||
|
||||
@Transient
|
||||
val pushDate = dateFormat.parse(pushed_at)!!
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.get
|
||||
|
||||
object BiometricHelper: KoinComponent {
|
||||
|
||||
private val mgr by lazy { BiometricManager.from(get()) }
|
||||
|
||||
val isSupported get() = when (mgr.canAuthenticate()) {
|
||||
BiometricManager.BIOMETRIC_SUCCESS -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
val isEnabled: Boolean get() {
|
||||
val enabled = Config.suBiometric
|
||||
if (enabled && !isSupported) {
|
||||
Config.suBiometric = false
|
||||
return false
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
|
||||
fun authenticate(
|
||||
activity: FragmentActivity,
|
||||
onError: () -> Unit = {},
|
||||
onSuccess: () -> Unit): BiometricPrompt {
|
||||
val prompt = BiometricPrompt(activity,
|
||||
ContextCompat.getMainExecutor(activity),
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
onError()
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
onError()
|
||||
}
|
||||
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
)
|
||||
val info = BiometricPrompt.PromptInfo.Builder()
|
||||
.setConfirmationRequired(true)
|
||||
.setDeviceCredentialAllowed(false)
|
||||
.setTitle(activity.getString(R.string.authenticate))
|
||||
.setNegativeButtonText(activity.getString(android.R.string.cancel))
|
||||
.build()
|
||||
prompt.authenticate(info)
|
||||
return prompt
|
||||
}
|
||||
|
||||
}
|
142
app/src/main/java/com/topjohnwu/magisk/core/utils/Keygen.kt
Normal file
142
app/src/main/java/com/topjohnwu/magisk/core/utils/Keygen.kt
Normal file
@@ -0,0 +1,142 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Base64
|
||||
import android.util.Base64OutputStream
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.utils.PatchAPK.ALPHANUM
|
||||
import com.topjohnwu.signing.CryptoUtils.readCertificate
|
||||
import com.topjohnwu.signing.CryptoUtils.readPrivateKey
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.math.BigInteger
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.KeyStore
|
||||
import java.security.MessageDigest
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
import java.util.zip.GZIPInputStream
|
||||
import java.util.zip.GZIPOutputStream
|
||||
|
||||
private interface CertKeyProvider {
|
||||
val cert: X509Certificate
|
||||
val key: PrivateKey
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
class Keygen(context: Context) : CertKeyProvider {
|
||||
|
||||
companion object {
|
||||
private const val ALIAS = "magisk"
|
||||
private val PASSWORD get() = "magisk".toCharArray()
|
||||
private const val TESTKEY_CERT = "61ed377e85d386a8dfee6b864bd85b0bfaa5af81"
|
||||
private const val BASE64_FLAG = Base64.NO_PADDING or Base64.NO_WRAP
|
||||
}
|
||||
|
||||
private val start = Calendar.getInstance()
|
||||
private val end = Calendar.getInstance().apply { add(Calendar.YEAR, 30) }
|
||||
|
||||
override val cert get() = provider.cert
|
||||
override val key get() = provider.key
|
||||
|
||||
private val provider: CertKeyProvider
|
||||
|
||||
inner class KeyStoreProvider :
|
||||
CertKeyProvider {
|
||||
private val ks by lazy { init() }
|
||||
override val cert by lazy { ks.getCertificate(ALIAS) as X509Certificate }
|
||||
override val key by lazy { ks.getKey(
|
||||
ALIAS,
|
||||
PASSWORD
|
||||
) as PrivateKey }
|
||||
}
|
||||
|
||||
class TestProvider : CertKeyProvider {
|
||||
override val cert by lazy {
|
||||
readCertificate(javaClass.getResourceAsStream("/keys/testkey.x509.pem"))
|
||||
}
|
||||
override val key by lazy {
|
||||
readPrivateKey(javaClass.getResourceAsStream("/keys/testkey.pk8"))
|
||||
}
|
||||
}
|
||||
|
||||
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 randomString(): String {
|
||||
val rand = kotlin.random.Random.Default
|
||||
val len = rand.nextInt(5, 10)
|
||||
val sb = StringBuilder(len)
|
||||
for (i in 0..len) {
|
||||
val idx = rand.nextInt(ALPHANUM.length)
|
||||
sb.append(ALPHANUM[idx])
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun init(): KeyStore {
|
||||
val raw = Config.keyStoreRaw
|
||||
val ks = KeyStore.getInstance("PKCS12")
|
||||
if (raw.isEmpty()) {
|
||||
ks.load(null)
|
||||
} else {
|
||||
GZIPInputStream(Base64.decode(raw,
|
||||
BASE64_FLAG
|
||||
).inputStream()).use {
|
||||
ks.load(it,
|
||||
PASSWORD
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Keys already exist
|
||||
if (ks.containsAlias(ALIAS))
|
||||
return ks
|
||||
|
||||
// Generate new private key and certificate
|
||||
val kp = KeyPairGenerator.getInstance("RSA").apply { initialize(4096) }.genKeyPair()
|
||||
val dname = X500Name("CN=${randomString()}")
|
||||
val builder = JcaX509v3CertificateBuilder(dname, BigInteger(160, Random()),
|
||||
start.time, end.time, dname, kp.public)
|
||||
val signer = JcaContentSignerBuilder("SHA1WithRSA").build(kp.private)
|
||||
val cert = JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
||||
|
||||
// Store them into keystore
|
||||
ks.setKeyEntry(
|
||||
ALIAS, kp.private,
|
||||
PASSWORD, arrayOf(cert))
|
||||
val bytes = ByteArrayOutputStream()
|
||||
GZIPOutputStream(Base64OutputStream(bytes,
|
||||
BASE64_FLAG
|
||||
)).use {
|
||||
ks.store(it,
|
||||
PASSWORD
|
||||
)
|
||||
}
|
||||
Config.keyStoreRaw = bytes.toString("UTF-8")
|
||||
|
||||
return ks
|
||||
}
|
||||
}
|
88
app/src/main/java/com/topjohnwu/magisk/core/utils/Locales.kt
Normal file
88
app/src/main/java/com/topjohnwu/magisk/core/utils/Locales.kt
Normal file
@@ -0,0 +1,88 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.AssetManager
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.util.DisplayMetrics
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.ResMgr
|
||||
import com.topjohnwu.magisk.core.addAssetPath
|
||||
import com.topjohnwu.magisk.extensions.langTagToLocale
|
||||
import com.topjohnwu.magisk.extensions.toLangTag
|
||||
import io.reactivex.Single
|
||||
import java.util.*
|
||||
import kotlin.Comparator
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
var currentLocale: Locale = Locale.getDefault()
|
||||
|
||||
@SuppressLint("ConstantLocale")
|
||||
val defaultLocale: Locale = Locale.getDefault()
|
||||
|
||||
val availableLocales = Single.fromCallable {
|
||||
val compareId = R.string.app_changelog
|
||||
|
||||
// Create a completely new resource to prevent cross talk over app's configs
|
||||
val asset = AssetManager::class.java.newInstance().apply { addAssetPath(ResMgr.apk) }
|
||||
val config = Configuration(ResMgr.resource.configuration)
|
||||
val metrics = DisplayMetrics().apply { setTo(ResMgr.resource.displayMetrics) }
|
||||
val res = Resources(asset, metrics, config)
|
||||
|
||||
val locales = ArrayList<String>().apply {
|
||||
// Add default locale
|
||||
add("en")
|
||||
|
||||
// Add some special locales
|
||||
add("zh-TW")
|
||||
add("pt-BR")
|
||||
|
||||
// Then add all supported locales
|
||||
addAll(res.assets.locales)
|
||||
}.map {
|
||||
it.langTagToLocale()
|
||||
}.distinctBy {
|
||||
config.setLocale(it)
|
||||
res.updateConfiguration(config, metrics)
|
||||
res.getString(compareId)
|
||||
}.sortedWith(Comparator { a, b ->
|
||||
a.getDisplayName(a).compareTo(b.getDisplayName(b), true)
|
||||
})
|
||||
|
||||
config.setLocale(defaultLocale)
|
||||
res.updateConfiguration(config, metrics)
|
||||
val defName = res.getString(R.string.system_default)
|
||||
|
||||
Pair(locales, defName)
|
||||
}.map { (locales, defName) ->
|
||||
val names = ArrayList<String>(locales.size + 1)
|
||||
val values = ArrayList<String>(locales.size + 1)
|
||||
|
||||
names.add(defName)
|
||||
values.add("")
|
||||
|
||||
locales.forEach { locale ->
|
||||
names.add(locale.getDisplayName(locale))
|
||||
values.add(locale.toLangTag())
|
||||
}
|
||||
|
||||
Pair(names.toTypedArray(), values.toTypedArray())
|
||||
}.cache()!!
|
||||
|
||||
fun Resources.updateConfig(config: Configuration = configuration) {
|
||||
config.setLocale(currentLocale)
|
||||
updateConfiguration(config, displayMetrics)
|
||||
}
|
||||
|
||||
fun refreshLocale() {
|
||||
val localeConfig = Config.locale
|
||||
currentLocale = when {
|
||||
localeConfig.isEmpty() -> defaultLocale
|
||||
else -> localeConfig.langTagToLocale()
|
||||
}
|
||||
Locale.setDefault(currentLocale)
|
||||
ResMgr.resource.updateConfig()
|
||||
}
|
155
app/src/main/java/com/topjohnwu/magisk/core/utils/PatchAPK.kt
Normal file
155
app/src/main/java/com/topjohnwu/magisk/core/utils/PatchAPK.kt
Normal file
@@ -0,0 +1,155 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.widget.Toast
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.view.Notifications
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.extensions.writeTo
|
||||
import com.topjohnwu.signing.JarMap
|
||||
import com.topjohnwu.signing.SignAPK
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Single
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.security.SecureRandom
|
||||
|
||||
object PatchAPK {
|
||||
|
||||
private const val ALPHA = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
private const val DIGITS = "0123456789"
|
||||
const val ALPHANUM = ALPHA + DIGITS
|
||||
private const val ALPHANUMDOTS = "$ALPHANUM............"
|
||||
|
||||
private const val APP_ID = "com.topjohnwu.magisk"
|
||||
private const val APP_NAME = "Magisk Manager"
|
||||
|
||||
private fun genPackageName(prefix: String, length: Int): CharSequence {
|
||||
val builder = StringBuilder(length)
|
||||
builder.append(prefix)
|
||||
val len = length - prefix.length
|
||||
val random = SecureRandom()
|
||||
var next: Char
|
||||
var prev = prefix[prefix.length - 1]
|
||||
for (i in 0 until len) {
|
||||
next = if (prev == '.' || i == len - 1) {
|
||||
ALPHA[random.nextInt(ALPHA.length)]
|
||||
} else {
|
||||
ALPHANUMDOTS[random.nextInt(ALPHANUMDOTS.length)]
|
||||
}
|
||||
builder.append(next)
|
||||
prev = next
|
||||
}
|
||||
return builder
|
||||
}
|
||||
|
||||
private fun findAndPatch(xml: ByteArray, from: CharSequence, to: CharSequence): Boolean {
|
||||
if (to.length > from.length)
|
||||
return false
|
||||
val buf = ByteBuffer.wrap(xml).order(ByteOrder.LITTLE_ENDIAN).asCharBuffer()
|
||||
val offList = mutableListOf<Int>()
|
||||
var i = 0
|
||||
loop@ while (i < buf.length - from.length) {
|
||||
for (j in from.indices) {
|
||||
if (buf.get(i + j) != from[j]) {
|
||||
++i
|
||||
continue@loop
|
||||
}
|
||||
}
|
||||
offList.add(i)
|
||||
i += from.length
|
||||
}
|
||||
if (offList.isEmpty())
|
||||
return false
|
||||
|
||||
val toBuf = to.toString().toCharArray().copyOf(from.length)
|
||||
for (off in offList) {
|
||||
buf.position(off)
|
||||
buf.put(toBuf)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun patch(apk: String, out: String, pkg: CharSequence, label: CharSequence): Boolean {
|
||||
try {
|
||||
val jar = JarMap.open(apk)
|
||||
val je = jar.getJarEntry(Const.ANDROID_MANIFEST)
|
||||
val xml = jar.getRawData(je)
|
||||
|
||||
if (!findAndPatch(xml, APP_ID, pkg) ||
|
||||
!findAndPatch(xml, APP_NAME, label))
|
||||
return false
|
||||
|
||||
// Write apk changes
|
||||
jar.getOutputStream(je).write(xml)
|
||||
val keys = Keygen(get())
|
||||
SignAPK.sign(keys.cert, keys.key, jar, FileOutputStream(out).buffered())
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun patchAndHide(context: Context, label: String): Boolean {
|
||||
val dlStub = !isRunningAsStub && SDK_INT >= 28 &&
|
||||
Info.env.magiskVersionCode >= Const.Version.PROVIDER_CONNECT
|
||||
val src = if (dlStub) {
|
||||
val stub = File(context.cacheDir, "stub.apk")
|
||||
val svc = get<GithubRawServices>()
|
||||
try {
|
||||
svc.fetchFile(Info.remote.stub.link).blockingGet().byteStream().use {
|
||||
it.writeTo(stub)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
stub.path
|
||||
} else {
|
||||
context.packageCodePath
|
||||
}
|
||||
|
||||
// Generate a new random package name and signature
|
||||
val repack = File(context.cacheDir, "patched.apk")
|
||||
val pkg = genPackageName("com.", APP_ID.length)
|
||||
Config.keyStoreRaw = ""
|
||||
|
||||
if (!patch(src, repack.path, pkg, label))
|
||||
return false
|
||||
|
||||
// Install the application
|
||||
repack.setReadable(true, false)
|
||||
if (!Shell.su("force_pm_install $repack").exec().isSuccess)
|
||||
return false
|
||||
|
||||
Config.suManager = pkg.toString()
|
||||
Config.export()
|
||||
Shell.su("pm uninstall $APP_ID").submit()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun hideManager(context: Context, label: String) {
|
||||
val progress = Notifications.progress(context, context.getString(R.string.hide_manager_title))
|
||||
Notifications.mgr.notify(Const.ID.HIDE_MANAGER_NOTIFICATION_ID, progress.build())
|
||||
Single.fromCallable {
|
||||
patchAndHide(context, label)
|
||||
}.subscribeK {
|
||||
if (!it)
|
||||
Utils.toast(R.string.hide_manager_fail_toast, Toast.LENGTH_LONG)
|
||||
Notifications.mgr.cancel(Const.ID.HIDE_MANAGER_NOTIFICATION_ID)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import java.io.FilterInputStream
|
||||
import java.io.InputStream
|
||||
|
||||
class ProgressInputStream(
|
||||
base: InputStream,
|
||||
val progressEmitter: (Long) -> Unit = {}
|
||||
) : FilterInputStream(base) {
|
||||
|
||||
private var bytesRead = 0L
|
||||
private var lastUpdate = 0L
|
||||
|
||||
private fun emitProgress() {
|
||||
val cur = System.currentTimeMillis()
|
||||
if (cur - lastUpdate > 1000) {
|
||||
lastUpdate = cur
|
||||
UiThreadHandler.run { progressEmitter(bytesRead) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun read(): Int {
|
||||
val b = read()
|
||||
if (b >= 0) {
|
||||
bytesRead++
|
||||
emitProgress()
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
override fun read(b: ByteArray): Int {
|
||||
return read(b, 0, b.size)
|
||||
}
|
||||
|
||||
override fun read(b: ByteArray, off: Int, len: Int): Int {
|
||||
val sz = super.read(b, off, len)
|
||||
if (sz > 0) {
|
||||
bytesRead += sz
|
||||
emitProgress()
|
||||
}
|
||||
return sz
|
||||
}
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.content.Context
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import com.topjohnwu.magisk.extensions.rawResource
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
|
||||
class RootInit : Shell.Initializer() {
|
||||
|
||||
override fun onInit(context: Context, shell: Shell): Boolean {
|
||||
return init(context.wrap(), shell)
|
||||
}
|
||||
|
||||
fun init(context: Context, shell: Shell): Boolean {
|
||||
val job = shell.newJob()
|
||||
job.add(context.rawResource(R.raw.manager))
|
||||
if (shell.isRoot) {
|
||||
job.add(context.rawResource(R.raw.util_functions))
|
||||
.add("SHA1=`grep_prop SHA1 /sbin/.magisk/config`")
|
||||
Const.MAGISK_DISABLE_FILE = SuFile("/cache/.disable_magisk")
|
||||
}
|
||||
|
||||
job.add(
|
||||
"export BOOTMODE=true",
|
||||
"mount_partitions",
|
||||
"get_flags",
|
||||
"run_migrations"
|
||||
).exec()
|
||||
|
||||
fun getvar(name: String) = ShellUtils.fastCmd(shell, "echo \$$name").toBoolean()
|
||||
|
||||
Info.keepVerity = getvar("KEEPVERITY")
|
||||
Info.keepEnc = getvar("KEEPFORCEENCRYPT")
|
||||
Info.isSAR = getvar("SYSTEM_ROOT")
|
||||
Info.ramdisk = shell.newJob().add("check_boot_ramdisk").exec().isSuccess
|
||||
Info.recovery = getvar("RECOVERYMODE")
|
||||
Info.isAB = getvar("ISAB")
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
interface SafetyNetHelper {
|
||||
|
||||
val version: Int
|
||||
|
||||
fun attest()
|
||||
|
||||
interface Callback {
|
||||
fun onResponse(responseCode: Int)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val RESPONSE_ERR = 0x01
|
||||
const val CONNECTION_FAIL = 0x02
|
||||
|
||||
const val BASIC_PASS = 0x10
|
||||
const val CTS_PASS = 0x20
|
||||
}
|
||||
}
|
78
app/src/main/java/com/topjohnwu/magisk/core/utils/Utils.kt
Normal file
78
app/src/main/java/com/topjohnwu/magisk/core/utils/Utils.kt
Normal file
@@ -0,0 +1,78 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.widget.Toast
|
||||
import androidx.work.*
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.UpdateCheckService
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object Utils {
|
||||
|
||||
fun toast(msg: CharSequence, duration: Int) {
|
||||
UiThreadHandler.run { Toast.makeText(get(), msg, duration).show() }
|
||||
}
|
||||
|
||||
fun toast(resId: Int, duration: Int) {
|
||||
UiThreadHandler.run { Toast.makeText(get(), resId, duration).show() }
|
||||
}
|
||||
|
||||
fun dpInPx(dp: Int): Int {
|
||||
val scale = get<Resources>().displayMetrics.density
|
||||
return (dp * scale + 0.5).toInt()
|
||||
}
|
||||
|
||||
fun showSuperUser(): Boolean {
|
||||
return Info.env.isActive && (Const.USER_ID == 0
|
||||
|| Config.suMultiuserMode != Config.Value.MULTIUSER_MODE_OWNER_MANAGED)
|
||||
}
|
||||
|
||||
fun scheduleUpdateCheck(context: Context) {
|
||||
if (Config.checkUpdate) {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.setRequiresDeviceIdle(true)
|
||||
.build()
|
||||
val request = PeriodicWorkRequest
|
||||
.Builder(UpdateCheckService::class.java, 12, TimeUnit.HOURS)
|
||||
.setConstraints(constraints)
|
||||
.build()
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
Const.ID.CHECK_MAGISK_UPDATE_WORKER_ID,
|
||||
ExistingPeriodicWorkPolicy.REPLACE, request
|
||||
)
|
||||
} else {
|
||||
WorkManager.getInstance(context)
|
||||
.cancelUniqueWork(Const.ID.CHECK_MAGISK_UPDATE_WORKER_ID)
|
||||
}
|
||||
}
|
||||
|
||||
fun openLink(context: Context, link: Uri) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, link)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
if (intent.resolveActivity(context.packageManager) != null) {
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
toast(
|
||||
R.string.open_link_failed_toast,
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun ensureDownloadPath(path: String) =
|
||||
File(Environment.getExternalStorageDirectory(), path).run {
|
||||
if ((exists() && isDirectory) || mkdirs()) this else null
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun File.unzip(folder: File, path: String = "", junkPath: Boolean = false) {
|
||||
inputStream().buffered().use {
|
||||
it.unzip(folder, path, junkPath)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun InputStream.unzip(folder: File, path: String, junkPath: Boolean) {
|
||||
try {
|
||||
val zin = ZipInputStream(this)
|
||||
var entry: ZipEntry
|
||||
while (true) {
|
||||
entry = zin.nextEntry ?: break
|
||||
if (!entry.name.startsWith(path) || entry.isDirectory) {
|
||||
// Ignore directories, only create files
|
||||
continue
|
||||
}
|
||||
val name = if (junkPath)
|
||||
entry.name.substring(entry.name.lastIndexOf('/') + 1)
|
||||
else
|
||||
entry.name
|
||||
|
||||
var dest = File(folder, name)
|
||||
if (!dest.parentFile!!.exists() && !dest.parentFile!!.mkdirs()) {
|
||||
dest = SuFile(folder, name)
|
||||
dest.parentFile!!.mkdirs()
|
||||
}
|
||||
SuFileOutputStream(dest).use { out -> zin.copyTo(out) }
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
throw e
|
||||
}
|
||||
}
|
@@ -0,0 +1,126 @@
|
||||
package com.topjohnwu.magisk.core.view
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import androidx.core.app.TaskStackBuilder
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.graphics.drawable.toIcon
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.*
|
||||
import com.topjohnwu.magisk.core.Const.ID.PROGRESS_NOTIFICATION_CHANNEL
|
||||
import com.topjohnwu.magisk.core.Const.ID.UPDATE_NOTIFICATION_CHANNEL
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.getBitmap
|
||||
|
||||
object Notifications {
|
||||
|
||||
val mgr by lazy { get<Context>().getSystemService<NotificationManager>()!! }
|
||||
|
||||
fun setup(context: Context) {
|
||||
if (SDK_INT >= 26) {
|
||||
var channel = NotificationChannel(UPDATE_NOTIFICATION_CHANNEL,
|
||||
context.getString(R.string.update_channel), NotificationManager.IMPORTANCE_DEFAULT)
|
||||
mgr.createNotificationChannel(channel)
|
||||
channel = NotificationChannel(PROGRESS_NOTIFICATION_CHANNEL,
|
||||
context.getString(R.string.progress_channel), NotificationManager.IMPORTANCE_LOW)
|
||||
mgr.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateBuilder(context: Context): Notification.Builder {
|
||||
return Notification.Builder(context).apply {
|
||||
val bitmap = context.getBitmap(R.drawable.ic_magisk_outline)
|
||||
setLargeIcon(bitmap)
|
||||
if (SDK_INT >= 26) {
|
||||
setSmallIcon(bitmap.toIcon())
|
||||
setChannelId(UPDATE_NOTIFICATION_CHANNEL)
|
||||
} else {
|
||||
setSmallIcon(R.drawable.ic_magisk_outline)
|
||||
setVibrate(longArrayOf(0, 100, 100, 100))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun magiskUpdate(context: Context) {
|
||||
val intent = context.intent<SplashActivity>()
|
||||
.putExtra(Const.Key.OPEN_SECTION, "magisk")
|
||||
val stackBuilder = TaskStackBuilder.create(context)
|
||||
stackBuilder.addParentStack(SplashActivity::class.java.cmp(context.packageName))
|
||||
stackBuilder.addNextIntent(intent)
|
||||
val pendingIntent = stackBuilder.getPendingIntent(
|
||||
Const.ID.MAGISK_UPDATE_NOTIFICATION_ID,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
val builder = updateBuilder(
|
||||
context
|
||||
)
|
||||
.setContentTitle(context.getString(R.string.magisk_update_title))
|
||||
.setContentText(context.getString(R.string.manager_download_install))
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
|
||||
mgr.notify(Const.ID.MAGISK_UPDATE_NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
fun managerUpdate(context: Context) {
|
||||
val intent = context.intent<GeneralReceiver>()
|
||||
.setAction(Const.Key.BROADCAST_MANAGER_UPDATE)
|
||||
.putExtra(Const.Key.INTENT_SET_APP, Info.remote.app)
|
||||
|
||||
val pendingIntent = PendingIntent.getBroadcast(context,
|
||||
Const.ID.APK_UPDATE_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
val builder = updateBuilder(
|
||||
context
|
||||
)
|
||||
.setContentTitle(context.getString(R.string.manager_update_title))
|
||||
.setContentText(context.getString(R.string.manager_download_install))
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
|
||||
mgr.notify(Const.ID.APK_UPDATE_NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
fun dtboPatched(context: Context) {
|
||||
val intent = context.intent<GeneralReceiver>()
|
||||
.setAction(Const.Key.BROADCAST_REBOOT)
|
||||
val pendingIntent = PendingIntent.getBroadcast(context,
|
||||
Const.ID.DTBO_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
val builder = updateBuilder(
|
||||
context
|
||||
)
|
||||
.setContentTitle(context.getString(R.string.dtbo_patched_title))
|
||||
.setContentText(context.getString(R.string.dtbo_patched_reboot))
|
||||
|
||||
if (SDK_INT >= 23) {
|
||||
val action = Notification.Action.Builder(
|
||||
context.getBitmap(R.drawable.ic_refresh).toIcon(),
|
||||
context.getString(R.string.reboot), pendingIntent).build()
|
||||
builder.addAction(action)
|
||||
} else {
|
||||
builder.addAction(
|
||||
R.drawable.ic_refresh,
|
||||
context.getString(R.string.reboot), pendingIntent)
|
||||
}
|
||||
|
||||
mgr.notify(Const.ID.DTBO_NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
fun progress(context: Context, title: CharSequence): Notification.Builder {
|
||||
val builder = if (SDK_INT >= 26) {
|
||||
Notification.Builder(context, PROGRESS_NOTIFICATION_CHANNEL)
|
||||
} else {
|
||||
Notification.Builder(context).setPriority(Notification.PRIORITY_LOW)
|
||||
}
|
||||
builder.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setContentTitle(title)
|
||||
.setProgress(0, 0, true)
|
||||
.setOngoing(true)
|
||||
return builder
|
||||
}
|
||||
}
|
@@ -0,0 +1,87 @@
|
||||
package com.topjohnwu.magisk.core.view
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.graphics.drawable.toAdaptiveIcon
|
||||
import androidx.core.graphics.drawable.toIcon
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.*
|
||||
import com.topjohnwu.magisk.core.utils.Utils
|
||||
import com.topjohnwu.magisk.extensions.getBitmap
|
||||
|
||||
object Shortcuts {
|
||||
|
||||
fun setup(context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= 25) {
|
||||
val manager = context.getSystemService<ShortcutManager>()
|
||||
manager?.dynamicShortcuts =
|
||||
getShortCuts(context)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = 25)
|
||||
private fun getShortCuts(context: Context): List<ShortcutInfo> {
|
||||
val shortCuts = mutableListOf<ShortcutInfo>()
|
||||
val intent = context.intent<SplashActivity>()
|
||||
|
||||
fun getIcon(id: Int): Icon {
|
||||
return if (Build.VERSION.SDK_INT >= 26)
|
||||
context.getBitmap(id).toAdaptiveIcon()
|
||||
else
|
||||
context.getBitmap(id).toIcon()
|
||||
}
|
||||
|
||||
if (Utils.showSuperUser()) {
|
||||
shortCuts.add(
|
||||
ShortcutInfo.Builder(context, "superuser")
|
||||
.setShortLabel(context.getString(R.string.superuser))
|
||||
.setIntent(
|
||||
Intent(intent)
|
||||
.putExtra(Const.Key.OPEN_SECTION, "superuser")
|
||||
.setAction(Intent.ACTION_VIEW)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
)
|
||||
.setIcon(getIcon(R.drawable.sc_superuser))
|
||||
.setRank(0)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
if (Info.env.magiskHide) {
|
||||
shortCuts.add(
|
||||
ShortcutInfo.Builder(context, "magiskhide")
|
||||
.setShortLabel(context.getString(R.string.magiskhide))
|
||||
.setIntent(
|
||||
Intent(intent)
|
||||
.putExtra(Const.Key.OPEN_SECTION, "magiskhide")
|
||||
.setAction(Intent.ACTION_VIEW)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
)
|
||||
.setIcon(getIcon(R.drawable.sc_magiskhide))
|
||||
.setRank(1)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
if (!Config.coreOnly && Info.env.isActive) {
|
||||
shortCuts.add(
|
||||
ShortcutInfo.Builder(context, "modules")
|
||||
.setShortLabel(context.getString(R.string.modules))
|
||||
.setIntent(
|
||||
Intent(intent)
|
||||
.putExtra(Const.Key.OPEN_SECTION, "modules")
|
||||
.setAction(Intent.ACTION_VIEW)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
)
|
||||
.setIcon(getIcon(R.drawable.sc_extension))
|
||||
.setRank(2)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
return shortCuts
|
||||
}
|
||||
}
|
@@ -1,190 +0,0 @@
|
||||
package com.topjohnwu.magisk.data.database;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.Const;
|
||||
import com.topjohnwu.magisk.model.entity.Policy;
|
||||
import com.topjohnwu.magisk.model.entity.SuLogEntry;
|
||||
import com.topjohnwu.magisk.utils.LocaleManager;
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class MagiskDB {
|
||||
|
||||
private static final String POLICY_TABLE = "policies";
|
||||
private static final String LOG_TABLE = "logs";
|
||||
private static final String SETTINGS_TABLE = "settings";
|
||||
private static final String STRINGS_TABLE = "strings";
|
||||
|
||||
private PackageManager pm;
|
||||
|
||||
public MagiskDB(Context context) {
|
||||
pm = context.getPackageManager();
|
||||
}
|
||||
|
||||
public void deletePolicy(Policy policy) {
|
||||
deletePolicy(policy.uid);
|
||||
}
|
||||
|
||||
private List<String> rawSQL(String fmt, Object... args) {
|
||||
return Shell.su("magisk --sqlite '" + Utils.fmt(fmt, args) + "'").exec().getOut();
|
||||
}
|
||||
|
||||
private List<ContentValues> SQL(String fmt, Object... args) {
|
||||
List<ContentValues> list = new ArrayList<>();
|
||||
for (String raw : rawSQL(fmt, args)) {
|
||||
ContentValues values = new ContentValues();
|
||||
String[] cols = raw.split("\\|");
|
||||
for (String col : cols) {
|
||||
String[] pair = col.split("=", 2);
|
||||
if (pair.length != 2)
|
||||
continue;
|
||||
values.put(pair[0], pair[1]);
|
||||
}
|
||||
list.add(values);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private String toSQL(ContentValues values) {
|
||||
StringBuilder keys = new StringBuilder(), vals = new StringBuilder();
|
||||
keys.append('(');
|
||||
vals.append("VALUES(");
|
||||
boolean first = true;
|
||||
for (Map.Entry<String, Object> entry : values.valueSet()) {
|
||||
if (!first) {
|
||||
keys.append(',');
|
||||
vals.append(',');
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
keys.append(entry.getKey());
|
||||
vals.append('"');
|
||||
vals.append(entry.getValue());
|
||||
vals.append('"');
|
||||
}
|
||||
keys.append(')');
|
||||
vals.append(')');
|
||||
keys.append(vals);
|
||||
return keys.toString();
|
||||
}
|
||||
|
||||
public void clearOutdated() {
|
||||
rawSQL(
|
||||
"DELETE FROM %s WHERE until > 0 AND until < %d;" +
|
||||
"DELETE FROM %s WHERE time < %d",
|
||||
POLICY_TABLE, System.currentTimeMillis() / 1000,
|
||||
LOG_TABLE, System.currentTimeMillis() - Config.suLogTimeout * 86400000
|
||||
);
|
||||
}
|
||||
|
||||
public void deletePolicy(String pkg) {
|
||||
rawSQL("DELETE FROM %s WHERE package_name=\"%s\"", POLICY_TABLE, pkg);
|
||||
}
|
||||
|
||||
public void deletePolicy(int uid) {
|
||||
rawSQL("DELETE FROM %s WHERE uid=%d", POLICY_TABLE, uid);
|
||||
}
|
||||
|
||||
public Policy getPolicy(int uid) {
|
||||
List<ContentValues> res =
|
||||
SQL("SELECT * FROM %s WHERE uid=%d", POLICY_TABLE, uid);
|
||||
if (!res.isEmpty()) {
|
||||
try {
|
||||
return new Policy(res.get(0), pm);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
deletePolicy(uid);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void updatePolicy(Policy policy) {
|
||||
rawSQL("REPLACE INTO %s %s", POLICY_TABLE, toSQL(policy.getContentValues()));
|
||||
}
|
||||
|
||||
public List<Policy> getPolicyList() {
|
||||
List<Policy> list = new ArrayList<>();
|
||||
for (ContentValues values : SQL("SELECT * FROM %s WHERE uid/100000=%d", POLICY_TABLE, Const.USER_ID)) {
|
||||
try {
|
||||
list.add(new Policy(values, pm));
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
deletePolicy(values.getAsInteger("uid"));
|
||||
}
|
||||
}
|
||||
Collections.sort(list);
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<List<SuLogEntry>> getLogs() {
|
||||
List<List<SuLogEntry>> ret = new ArrayList<>();
|
||||
List<SuLogEntry> list = null;
|
||||
String dateString = null, newString;
|
||||
for (ContentValues values : SQL("SELECT * FROM %s ORDER BY time DESC", LOG_TABLE)) {
|
||||
Date date = new Date(values.getAsLong("time"));
|
||||
newString = DateFormat.getDateInstance(DateFormat.MEDIUM, LocaleManager.locale).format(date);
|
||||
if (!TextUtils.equals(dateString, newString)) {
|
||||
dateString = newString;
|
||||
list = new ArrayList<>();
|
||||
ret.add(list);
|
||||
}
|
||||
list.add(new SuLogEntry(values));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public void addLog(SuLogEntry log) {
|
||||
rawSQL("INSERT INTO %s %s", LOG_TABLE, toSQL(log.getContentValues()));
|
||||
}
|
||||
|
||||
public void clearLogs() {
|
||||
rawSQL("DELETE FROM %s", LOG_TABLE);
|
||||
}
|
||||
|
||||
public void rmSettings(String key) {
|
||||
rawSQL("DELETE FROM %s WHERE key=\"%s\"", SETTINGS_TABLE, key);
|
||||
}
|
||||
|
||||
public void setSettings(String key, int value) {
|
||||
ContentValues data = new ContentValues();
|
||||
data.put("key", key);
|
||||
data.put("value", value);
|
||||
rawSQL("REPLACE INTO %s %s", SETTINGS_TABLE, toSQL(data));
|
||||
}
|
||||
|
||||
public int getSettings(String key, int defaultValue) {
|
||||
List<ContentValues> res = SQL("SELECT value FROM %s WHERE key=\"%s\"", SETTINGS_TABLE, key);
|
||||
if (res.isEmpty())
|
||||
return defaultValue;
|
||||
return res.get(0).getAsInteger("value");
|
||||
}
|
||||
|
||||
public void setStrings(String key, String value) {
|
||||
if (value == null) {
|
||||
rawSQL("DELETE FROM %s WHERE key=\"%s\"", STRINGS_TABLE, key);
|
||||
return;
|
||||
}
|
||||
ContentValues data = new ContentValues();
|
||||
data.put("key", key);
|
||||
data.put("value", value);
|
||||
rawSQL("REPLACE INTO %s %s", STRINGS_TABLE, toSQL(data));
|
||||
}
|
||||
|
||||
public String getStrings(String key, String defaultValue) {
|
||||
List<ContentValues> res = SQL("SELECT value FROM %s WHERE key=\"%s\"", STRINGS_TABLE, key);
|
||||
if (res.isEmpty())
|
||||
return defaultValue;
|
||||
return res.get(0).getAsString("value");
|
||||
}
|
||||
}
|
67
app/src/main/java/com/topjohnwu/magisk/data/database/Repo.kt
Normal file
67
app/src/main/java/com/topjohnwu/magisk/data/database/Repo.kt
Normal file
@@ -0,0 +1,67 @@
|
||||
@file:JvmMultifileClass
|
||||
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import com.topjohnwu.magisk.core.model.module.Repo
|
||||
|
||||
interface RepoBase {
|
||||
|
||||
fun getRepos(offset: Int, limit: Int = LIMIT): List<Repo>
|
||||
fun searchRepos(query: String, offset: Int, limit: Int = LIMIT): List<Repo>
|
||||
|
||||
@Query("SELECT * FROM repos WHERE id = :id AND versionCode > :versionCode LIMIT 1")
|
||||
fun getUpdatableRepoById(id: String, versionCode: Int): Repo?
|
||||
|
||||
@Query("SELECT * FROM repos WHERE id = :id LIMIT 1")
|
||||
fun getRepoById(id: String): Repo?
|
||||
|
||||
companion object {
|
||||
const val LIMIT = 10
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface RepoByUpdatedDao : RepoBase {
|
||||
|
||||
@Query("SELECT * FROM repos ORDER BY last_update DESC LIMIT :limit OFFSET :offset")
|
||||
override fun getRepos(offset: Int, limit: Int): List<Repo>
|
||||
|
||||
@Query(
|
||||
"""SELECT *
|
||||
FROM repos
|
||||
WHERE
|
||||
(author LIKE '%' || :query || '%') ||
|
||||
(name LIKE '%' || :query || '%') ||
|
||||
(description LIKE '%' || :query || '%')
|
||||
ORDER BY last_update DESC
|
||||
LIMIT :limit
|
||||
OFFSET :offset"""
|
||||
)
|
||||
override fun searchRepos(query: String, offset: Int, limit: Int): List<Repo>
|
||||
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface RepoByNameDao : RepoBase {
|
||||
|
||||
@Query("SELECT * FROM repos ORDER BY name COLLATE NOCASE LIMIT :limit OFFSET :offset")
|
||||
override fun getRepos(offset: Int, limit: Int): List<Repo>
|
||||
|
||||
@Query(
|
||||
"""SELECT *
|
||||
FROM repos
|
||||
WHERE
|
||||
(author LIKE '%' || :query || '%') ||
|
||||
(name LIKE '%' || :query || '%') ||
|
||||
(description LIKE '%' || :query || '%')
|
||||
ORDER BY name COLLATE NOCASE
|
||||
LIMIT :limit
|
||||
OFFSET :offset"""
|
||||
)
|
||||
override fun searchRepos(query: String, offset: Int, limit: Int): List<Repo>
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import androidx.room.*
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.model.module.Repo
|
||||
|
||||
@Database(version = 6, entities = [Repo::class, RepoEtag::class], exportSchema = false)
|
||||
abstract class RepoDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun repoDao() : RepoDao
|
||||
abstract fun repoByUpdatedDao(): RepoByUpdatedDao
|
||||
abstract fun repoByNameDao(): RepoByNameDao
|
||||
}
|
||||
|
||||
@Dao
|
||||
abstract class RepoDao(private val db: RepoDatabase) {
|
||||
|
||||
val repoIDList get() = getRepoID().map { it.id }
|
||||
|
||||
val repos: List<Repo> get() = when (Config.repoOrder) {
|
||||
Config.Value.ORDER_NAME -> getReposNameOrder()
|
||||
else -> getReposDateOrder()
|
||||
}
|
||||
|
||||
var etagKey: String
|
||||
set(value) = addEtagRaw(RepoEtag(0, value))
|
||||
get() = etagRaw()?.key.orEmpty()
|
||||
|
||||
fun clear() = db.clearAllTables()
|
||||
|
||||
@Query("SELECT * FROM repos ORDER BY last_update DESC")
|
||||
protected abstract fun getReposDateOrder(): List<Repo>
|
||||
|
||||
@Query("SELECT * FROM repos ORDER BY name COLLATE NOCASE")
|
||||
protected abstract fun getReposNameOrder(): List<Repo>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract fun addRepo(repo: Repo)
|
||||
|
||||
@Query("SELECT * FROM repos WHERE id = :id")
|
||||
abstract fun getRepo(id: String): Repo?
|
||||
|
||||
@Query("SELECT id FROM repos")
|
||||
protected abstract fun getRepoID(): List<RepoID>
|
||||
|
||||
@Delete
|
||||
abstract fun removeRepo(repo: Repo)
|
||||
|
||||
@Query("DELETE FROM repos WHERE id = :id")
|
||||
abstract fun removeRepo(id: String)
|
||||
|
||||
@Query("DELETE FROM repos WHERE id IN (:idList)")
|
||||
abstract fun removeRepos(idList: Collection<String>)
|
||||
|
||||
@Query("SELECT * FROM etag")
|
||||
protected abstract fun etagRaw(): RepoEtag?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
protected abstract fun addEtagRaw(etag: RepoEtag)
|
||||
}
|
||||
|
||||
data class RepoID(
|
||||
@PrimaryKey val id: String
|
||||
)
|
||||
|
||||
@Entity(tableName = "etag")
|
||||
data class RepoEtag(
|
||||
@PrimaryKey val id: Int,
|
||||
val key: String
|
||||
)
|
||||
|
@@ -1,107 +0,0 @@
|
||||
package com.topjohnwu.magisk.data.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.model.entity.Repo;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class RepoDatabaseHelper extends SQLiteOpenHelper {
|
||||
|
||||
private static final int DATABASE_VER = 5;
|
||||
private static final String TABLE_NAME = "repos";
|
||||
|
||||
private SQLiteDatabase mDb;
|
||||
|
||||
public RepoDatabaseHelper(Context context) {
|
||||
super(context, "repo.db", null, DATABASE_VER);
|
||||
mDb = getWritableDatabase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
onUpgrade(db, 0, DATABASE_VER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
if (oldVersion != newVersion) {
|
||||
// Nuke old DB and create new table
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
||||
db.execSQL(
|
||||
"CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " " +
|
||||
"(id TEXT, name TEXT, version TEXT, versionCode INT, " +
|
||||
"author TEXT, description TEXT, last_update INT, PRIMARY KEY(id))");
|
||||
Config.remove(Config.Key.ETAG_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
onUpgrade(db, 0, DATABASE_VER);
|
||||
}
|
||||
|
||||
public void clearRepo() {
|
||||
mDb.delete(TABLE_NAME, null, null);
|
||||
}
|
||||
|
||||
|
||||
public void removeRepo(String id) {
|
||||
mDb.delete(TABLE_NAME, "id=?", new String[] { id });
|
||||
}
|
||||
|
||||
public void removeRepo(Repo repo) {
|
||||
removeRepo(repo.getId());
|
||||
}
|
||||
|
||||
public void removeRepo(Iterable<String> list) {
|
||||
for (String id : list) {
|
||||
if (id == null) continue;
|
||||
mDb.delete(TABLE_NAME, "id=?", new String[] { id });
|
||||
}
|
||||
}
|
||||
|
||||
public void addRepo(Repo repo) {
|
||||
mDb.replace(TABLE_NAME, null, repo.getContentValues());
|
||||
}
|
||||
|
||||
public Repo getRepo(String id) {
|
||||
try (Cursor c = mDb.query(TABLE_NAME, null, "id=?", new String[] { id }, null, null, null)) {
|
||||
if (c.moveToNext()) {
|
||||
return new Repo(c);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Cursor getRawCursor() {
|
||||
return mDb.query(TABLE_NAME, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
public Cursor getRepoCursor() {
|
||||
String orderBy = null;
|
||||
switch ((int) Config.get(Config.Key.REPO_ORDER)) {
|
||||
case Config.Value.ORDER_NAME:
|
||||
orderBy = "name COLLATE NOCASE";
|
||||
break;
|
||||
case Config.Value.ORDER_DATE:
|
||||
orderBy = "last_update DESC";
|
||||
}
|
||||
return mDb.query(TABLE_NAME, null, null, null, null, null, orderBy);
|
||||
}
|
||||
|
||||
public Set<String> getRepoIDSet() {
|
||||
HashSet<String> set = new HashSet<>(300);
|
||||
try (Cursor c = mDb.query(TABLE_NAME, null, null, null, null, null, null)) {
|
||||
while (c.moveToNext()) {
|
||||
set.add(c.getString(c.getColumnIndex("id")));
|
||||
}
|
||||
}
|
||||
return set;
|
||||
}
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import androidx.room.*
|
||||
import com.topjohnwu.magisk.model.entity.MagiskLog
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Single
|
||||
import java.util.*
|
||||
|
||||
@Database(version = 1, entities = [MagiskLog::class], exportSchema = false)
|
||||
abstract class SuLogDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun suLogDao(): SuLogDao
|
||||
}
|
||||
|
||||
@Dao
|
||||
abstract class SuLogDao(private val db: SuLogDatabase) {
|
||||
|
||||
private val twoWeeksAgo =
|
||||
Calendar.getInstance().apply { add(Calendar.WEEK_OF_YEAR, -2) }.timeInMillis
|
||||
|
||||
fun deleteAll() = Completable.fromAction { db.clearAllTables() }
|
||||
|
||||
fun fetchAll() = deleteOutdated().andThen(fetch())
|
||||
|
||||
@Query("SELECT * FROM logs ORDER BY time DESC")
|
||||
protected abstract fun fetch(): Single<MutableList<MagiskLog>>
|
||||
|
||||
@Insert
|
||||
abstract fun insert(log: MagiskLog): Completable
|
||||
|
||||
@Query("DELETE FROM logs WHERE time < :timeout")
|
||||
protected abstract fun deleteOutdated(
|
||||
timeout: Long = twoWeeksAgo
|
||||
): Completable
|
||||
|
||||
}
|
@@ -0,0 +1,81 @@
|
||||
package com.topjohnwu.magisk.data.network
|
||||
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.model.UpdateInfo
|
||||
import com.topjohnwu.magisk.core.tasks.GithubRepoInfo
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Single
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.adapter.rxjava2.Result
|
||||
import retrofit2.http.*
|
||||
|
||||
interface GithubRawServices {
|
||||
|
||||
//region topjohnwu/magisk_files
|
||||
|
||||
@GET("$MAGISK_FILES/master/stable.json")
|
||||
fun fetchStableUpdate(): Single<UpdateInfo>
|
||||
|
||||
@GET("$MAGISK_FILES/master/beta.json")
|
||||
fun fetchBetaUpdate(): Single<UpdateInfo>
|
||||
|
||||
@GET("$MAGISK_FILES/canary/release.json")
|
||||
fun fetchCanaryUpdate(): Single<UpdateInfo>
|
||||
|
||||
@GET("$MAGISK_FILES/canary/debug.json")
|
||||
fun fetchCanaryDebugUpdate(): Single<UpdateInfo>
|
||||
|
||||
@GET
|
||||
fun fetchCustomUpdate(@Url url: String): Single<UpdateInfo>
|
||||
|
||||
@GET("$MAGISK_FILES/{$REVISION}/snet.jar")
|
||||
@Streaming
|
||||
fun fetchSafetynet(@Path(REVISION) revision: String = Const.SNET_REVISION): Single<ResponseBody>
|
||||
|
||||
@GET("$MAGISK_FILES/{$REVISION}/bootctl")
|
||||
@Streaming
|
||||
fun fetchBootctl(@Path(REVISION) revision: String = Const.BOOTCTL_REVISION): Single<ResponseBody>
|
||||
|
||||
@GET("$MAGISK_MASTER/scripts/module_installer.sh")
|
||||
@Streaming
|
||||
fun fetchInstaller(): Single<ResponseBody>
|
||||
|
||||
@GET("$MAGISK_MODULES/{$MODULE}/master/{$FILE}")
|
||||
fun fetchModuleInfo(@Path(MODULE) id: String, @Path(FILE) file: String): Single<String>
|
||||
|
||||
//endregion
|
||||
|
||||
/**
|
||||
* This method shall be used exclusively for fetching files from urls from previous requests.
|
||||
* Him, who uses it in a wrong way, shall die in an eternal flame.
|
||||
* */
|
||||
@GET
|
||||
@Streaming
|
||||
fun fetchFile(@Url url: String): Single<ResponseBody>
|
||||
|
||||
@GET
|
||||
fun fetchString(@Url url: String): Single<String>
|
||||
|
||||
|
||||
companion object {
|
||||
private const val REVISION = "revision"
|
||||
private const val MODULE = "module"
|
||||
private const val FILE = "file"
|
||||
|
||||
|
||||
private const val MAGISK_FILES = "topjohnwu/magisk_files"
|
||||
private const val MAGISK_MASTER = "topjohnwu/Magisk/master"
|
||||
private const val MAGISK_MODULES = "Magisk-Modules-Repo"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface GithubApiServices {
|
||||
|
||||
@GET("repos")
|
||||
fun fetchRepos(@Query("page") page: Int,
|
||||
@Header(Const.Key.IF_NONE_MATCH) etag: String,
|
||||
@Query("sort") sort: String = "pushed",
|
||||
@Query("per_page") count: Int = 100): Flowable<Result<List<GithubRepoInfo>>>
|
||||
|
||||
}
|
@@ -0,0 +1,106 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
|
||||
import com.topjohnwu.magisk.core.magiskdb.StringDao
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
interface DBConfig {
|
||||
val settingsDao: SettingsDao
|
||||
val stringDao: StringDao
|
||||
|
||||
fun dbSettings(
|
||||
name: String,
|
||||
default: Int
|
||||
) = DBSettingsValue(name, default)
|
||||
|
||||
fun dbSettings(
|
||||
name: String,
|
||||
default: Boolean
|
||||
) = DBBoolSettings(name, default)
|
||||
|
||||
fun dbStrings(
|
||||
name: String,
|
||||
default: String,
|
||||
sync: Boolean = false
|
||||
) = DBStringsValue(name, default, sync)
|
||||
|
||||
}
|
||||
|
||||
class DBSettingsValue(
|
||||
private val name: String,
|
||||
private val default: Int
|
||||
) : ReadWriteProperty<DBConfig, Int> {
|
||||
|
||||
private var value: Int? = null
|
||||
|
||||
@Synchronized
|
||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Int {
|
||||
if (value == null)
|
||||
value = thisRef.settingsDao.fetch(name, default).blockingGet()
|
||||
return value!!
|
||||
}
|
||||
|
||||
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Int) {
|
||||
synchronized(this) {
|
||||
this.value = value
|
||||
}
|
||||
thisRef.settingsDao.put(name, value)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
class DBBoolSettings(
|
||||
name: String,
|
||||
default: Boolean
|
||||
) : ReadWriteProperty<DBConfig, Boolean> {
|
||||
|
||||
val base = DBSettingsValue(name, if (default) 1 else 0)
|
||||
|
||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Boolean =
|
||||
base.getValue(thisRef, property) != 0
|
||||
|
||||
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Boolean) =
|
||||
base.setValue(thisRef, property, if (value) 1 else 0)
|
||||
}
|
||||
|
||||
class DBStringsValue(
|
||||
private val name: String,
|
||||
private val default: String,
|
||||
private val sync: Boolean
|
||||
) : ReadWriteProperty<DBConfig, String> {
|
||||
|
||||
private var value: String? = null
|
||||
|
||||
@Synchronized
|
||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): String {
|
||||
if (value == null)
|
||||
value = thisRef.stringDao.fetch(name, default).blockingGet()
|
||||
return value!!
|
||||
}
|
||||
|
||||
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: String) {
|
||||
synchronized(this) {
|
||||
this.value = value
|
||||
}
|
||||
if (value.isEmpty()) {
|
||||
if (sync) {
|
||||
thisRef.stringDao.delete(name).blockingAwait()
|
||||
} else {
|
||||
thisRef.stringDao.delete(name)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
} else {
|
||||
if (sync) {
|
||||
thisRef.stringDao.put(name, value).blockingAwait()
|
||||
} else {
|
||||
thisRef.stringDao.put(name, value)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.data.database.SuLogDao
|
||||
import com.topjohnwu.magisk.model.entity.MagiskLog
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Single
|
||||
|
||||
|
||||
class LogRepository(
|
||||
private val logDao: SuLogDao
|
||||
) {
|
||||
|
||||
fun fetchLogs() = logDao.fetchAll()
|
||||
|
||||
fun fetchMagiskLogs() = Single.fromCallable {
|
||||
Shell.su("tail -n 5000 ${Const.MAGISK_LOG}").exec().out
|
||||
}.flattenAsFlowable { it }.filter { it.isNotEmpty() }
|
||||
|
||||
fun clearLogs() = logDao.deleteAll()
|
||||
|
||||
fun clearMagiskLogs() = Completable.fromAction {
|
||||
Shell.su("echo -n > ${Const.MAGISK_LOG}").exec()
|
||||
}
|
||||
|
||||
fun insert(log: MagiskLog) = logDao.insert(log)
|
||||
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.extensions.getLabel
|
||||
import com.topjohnwu.magisk.extensions.packageName
|
||||
import com.topjohnwu.magisk.extensions.toSingle
|
||||
import com.topjohnwu.magisk.model.entity.HideAppInfo
|
||||
import com.topjohnwu.magisk.model.entity.HideTarget
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Single
|
||||
|
||||
class MagiskRepository(
|
||||
private val apiRaw: GithubRawServices,
|
||||
private val packageManager: PackageManager
|
||||
) {
|
||||
|
||||
fun fetchSafetynet() = apiRaw.fetchSafetynet()
|
||||
|
||||
fun fetchUpdate() = when (Config.updateChannel) {
|
||||
Config.Value.DEFAULT_CHANNEL, Config.Value.STABLE_CHANNEL -> apiRaw.fetchStableUpdate()
|
||||
Config.Value.BETA_CHANNEL -> apiRaw.fetchBetaUpdate()
|
||||
Config.Value.CANARY_CHANNEL -> apiRaw.fetchCanaryUpdate()
|
||||
Config.Value.CANARY_DEBUG_CHANNEL -> apiRaw.fetchCanaryDebugUpdate()
|
||||
Config.Value.CUSTOM_CHANNEL -> apiRaw.fetchCustomUpdate(
|
||||
Config.customChannelUrl)
|
||||
else -> throw IllegalArgumentException()
|
||||
}.flatMap {
|
||||
// If remote version is lower than current installed, try switching to beta
|
||||
if (it.magisk.versionCode < Info.env.magiskVersionCode
|
||||
&& Config.updateChannel == Config.Value.DEFAULT_CHANNEL) {
|
||||
Config.updateChannel = Config.Value.BETA_CHANNEL
|
||||
apiRaw.fetchBetaUpdate()
|
||||
} else {
|
||||
Single.just(it)
|
||||
}
|
||||
}.doOnSuccess { Info.remote = it }
|
||||
|
||||
fun fetchApps() =
|
||||
Single.fromCallable { packageManager.getInstalledApplications(0) }
|
||||
.flattenAsFlowable { it }
|
||||
.filter { it.enabled && !blacklist.contains(it.packageName) }
|
||||
.map {
|
||||
val label = it.getLabel(packageManager)
|
||||
val icon = it.loadIcon(packageManager)
|
||||
HideAppInfo(it, label, icon)
|
||||
}
|
||||
.filter { it.processes.isNotEmpty() }
|
||||
.toList()
|
||||
|
||||
fun fetchHideTargets() = Shell.su("magiskhide --ls").toSingle()
|
||||
.map { it.exec().out }
|
||||
.flattenAsFlowable { it }
|
||||
.map { HideTarget(it) }
|
||||
.toList()
|
||||
|
||||
fun toggleHide(isEnabled: Boolean, packageName: String, process: String) =
|
||||
Shell.su("magiskhide --${isEnabled.state} $packageName $process").submit()
|
||||
|
||||
private val Boolean.state get() = if (this) "add" else "rm"
|
||||
|
||||
companion object {
|
||||
private val blacklist by lazy { listOf(
|
||||
packageName,
|
||||
"android",
|
||||
"com.android.chrome",
|
||||
"com.chrome.beta",
|
||||
"com.chrome.dev",
|
||||
"com.chrome.canary",
|
||||
"com.android.webview",
|
||||
"com.google.android.webview"
|
||||
) }
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.core.model.module.Repo
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
|
||||
class StringRepository(
|
||||
private val api: GithubRawServices
|
||||
) {
|
||||
|
||||
fun getString(url: String) = api.fetchString(url)
|
||||
|
||||
fun getMetadata(repo: Repo) = api.fetchModuleInfo(repo.id, "module.prop")
|
||||
|
||||
fun getReadme(repo: Repo) = api.fetchModuleInfo(repo.id, "README.md")
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.databinding.BindingAdapter
|
||||
|
||||
@BindingAdapter("gone")
|
||||
fun setGone(view: View, gone: Boolean) {
|
||||
view.isGone = gone
|
||||
}
|
||||
|
||||
@BindingAdapter("invisible")
|
||||
fun setInvisible(view: View, invisible: Boolean) {
|
||||
view.isInvisible = invisible
|
||||
}
|
||||
|
||||
@BindingAdapter("goneUnless")
|
||||
fun setGoneUnless(view: View, goneUnless: Boolean) {
|
||||
setGone(view, goneUnless.not())
|
||||
}
|
||||
|
||||
@BindingAdapter("invisibleUnless")
|
||||
fun setInvisibleUnless(view: View, invisibleUnless: Boolean) {
|
||||
setInvisible(view, invisibleUnless.not())
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.graphics.drawable.InsetDrawable
|
||||
import androidx.databinding.BindingAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.magisk.extensions.startEndToLeftRight
|
||||
import com.topjohnwu.magisk.extensions.toPx
|
||||
import com.topjohnwu.magisk.utils.KItemDecoration
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@BindingAdapter(
|
||||
"dividerColor",
|
||||
"dividerHorizontal",
|
||||
"dividerSize",
|
||||
"dividerAfterLast",
|
||||
"dividerMarginStart",
|
||||
"dividerMarginEnd",
|
||||
"dividerMarginTop",
|
||||
"dividerMarginBottom",
|
||||
requireAll = false
|
||||
)
|
||||
fun setDivider(
|
||||
view: RecyclerView,
|
||||
color: Int,
|
||||
horizontal: Boolean,
|
||||
_size: Float,
|
||||
_afterLast: Boolean?,
|
||||
marginStartF: Float,
|
||||
marginEndF: Float,
|
||||
marginTopF: Float,
|
||||
marginBottomF: Float
|
||||
) {
|
||||
val orientation = if (horizontal) RecyclerView.HORIZONTAL else RecyclerView.VERTICAL
|
||||
val size = if (_size > 0) _size.roundToInt() else 1.toPx()
|
||||
val (width, height) = if (horizontal) size to 1 else 1 to size
|
||||
val afterLast = _afterLast ?: true
|
||||
|
||||
val marginStart = marginStartF.roundToInt()
|
||||
val marginEnd = marginEndF.roundToInt()
|
||||
val marginTop = marginTopF.roundToInt()
|
||||
val marginBottom = marginBottomF.roundToInt()
|
||||
val (marginLeft, marginRight) = view.context.startEndToLeftRight(marginStart, marginEnd)
|
||||
|
||||
val drawable = GradientDrawable().apply {
|
||||
setSize(width, height)
|
||||
shape = GradientDrawable.RECTANGLE
|
||||
setColor(color)
|
||||
}.let {
|
||||
InsetDrawable(it, marginLeft, marginTop, marginRight, marginBottom)
|
||||
}
|
||||
|
||||
val decoration = KItemDecoration(view.context, orientation)
|
||||
.setDeco(drawable)
|
||||
.apply { showAfterLast = afterLast }
|
||||
view.addItemDecoration(decoration)
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
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)
|
||||
}
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
||||
|
||||
abstract class RvItem {
|
||||
|
||||
abstract val layoutRes: Int
|
||||
|
||||
@CallSuper
|
||||
open fun bind(binding: ItemBinding<*>) {
|
||||
binding.set(BR.item, layoutRes)
|
||||
}
|
||||
|
||||
/**
|
||||
* This callback is useful if you want to manipulate your views directly.
|
||||
* If you want to use this callback, you must set [me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter]
|
||||
* on your RecyclerView and call it from there. You can use [BindingBoundAdapter] for your convenience.
|
||||
*/
|
||||
open fun onBindingBound(binding: ViewDataBinding) {}
|
||||
}
|
||||
|
||||
abstract class ComparableRvItem<in T> : RvItem() {
|
||||
|
||||
abstract fun itemSameAs(other: T): Boolean
|
||||
abstract fun contentSameAs(other: T): Boolean
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
open fun genericItemSameAs(other: Any): Boolean = other::class == this::class && itemSameAs(other as T)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
open fun genericContentSameAs(other: Any): Boolean = other::class == this::class && contentSameAs(other as T)
|
||||
|
||||
companion object {
|
||||
val callback = object : DiffObservableList.Callback<ComparableRvItem<*>> {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: ComparableRvItem<*>,
|
||||
newItem: ComparableRvItem<*>
|
||||
) = oldItem.genericItemSameAs(newItem)
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: ComparableRvItem<*>,
|
||||
newItem: ComparableRvItem<*>
|
||||
) = oldItem.genericContentSameAs(newItem)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,64 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.topjohnwu.magisk.core.ResMgr
|
||||
import com.topjohnwu.magisk.utils.RxBus
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val SUTimeout = named("su_timeout")
|
||||
val Protected = named("protected")
|
||||
|
||||
val applicationModule = module {
|
||||
single { RxBus() }
|
||||
factory { ResMgr.resource }
|
||||
factory { get<Context>().packageManager }
|
||||
factory(Protected) { createDEContext(get()) }
|
||||
single(SUTimeout) { get<Context>(Protected).getSharedPreferences("su_timeout", 0) }
|
||||
single { PreferenceManager.getDefaultSharedPreferences(get<Context>(Protected)) }
|
||||
single { ActivityTracker() }
|
||||
factory { get<ActivityTracker>().foreground ?: NullActivity }
|
||||
single { LocalBroadcastManager.getInstance(get()) }
|
||||
}
|
||||
|
||||
private fun createDEContext(context: Context): Context {
|
||||
return if (Build.VERSION.SDK_INT >= 24)
|
||||
context.createDeviceProtectedStorageContext()
|
||||
else context
|
||||
}
|
||||
|
||||
class ActivityTracker : Application.ActivityLifecycleCallbacks {
|
||||
|
||||
@Volatile
|
||||
var foreground: Activity? = null
|
||||
|
||||
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
||||
|
||||
override fun onActivityStarted(activity: Activity) {}
|
||||
|
||||
@Synchronized
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
foreground = activity
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
foreground = null
|
||||
}
|
||||
|
||||
override fun onActivityStopped(activity: Activity) {}
|
||||
|
||||
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {}
|
||||
}
|
||||
|
||||
@SuppressLint("Registered")
|
||||
object NullActivity : Activity()
|
34
app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt
Normal file
34
app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt
Normal file
@@ -0,0 +1,34 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
|
||||
import com.topjohnwu.magisk.core.magiskdb.StringDao
|
||||
import com.topjohnwu.magisk.core.tasks.RepoUpdater
|
||||
import com.topjohnwu.magisk.data.database.RepoDatabase
|
||||
import com.topjohnwu.magisk.data.database.SuLogDatabase
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
val databaseModule = module {
|
||||
single { PolicyDao(get()) }
|
||||
single { SettingsDao() }
|
||||
single { StringDao() }
|
||||
single { createRepoDatabase(get()) }
|
||||
single { get<RepoDatabase>().repoDao() }
|
||||
single { get<RepoDatabase>().repoByNameDao() }
|
||||
single { get<RepoDatabase>().repoByUpdatedDao() }
|
||||
single { createSuLogDatabase(get(Protected)).suLogDao() }
|
||||
single { RepoUpdater(get(), get()) }
|
||||
}
|
||||
|
||||
fun createRepoDatabase(context: Context) =
|
||||
Room.databaseBuilder(context, RepoDatabase::class.java, "repo.db")
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
|
||||
fun createSuLogDatabase(context: Context) =
|
||||
Room.databaseBuilder(context, SuLogDatabase::class.java, "sulogs.db")
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
9
app/src/main/java/com/topjohnwu/magisk/di/Modules.kt
Normal file
9
app/src/main/java/com/topjohnwu/magisk/di/Modules.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
val koinModules = listOf(
|
||||
applicationModule,
|
||||
networkingModule,
|
||||
databaseModule,
|
||||
repositoryModule,
|
||||
viewModelModules
|
||||
)
|
@@ -0,0 +1,76 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.data.network.GithubApiServices
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.net.Networking
|
||||
import com.topjohnwu.magisk.net.NoSSLv3SocketFactory
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.html.HtmlPlugin
|
||||
import io.noties.markwon.image.ImagesPlugin
|
||||
import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.koin.dsl.module
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.converter.scalars.ScalarsConverterFactory
|
||||
|
||||
val networkingModule = module {
|
||||
single { createOkHttpClient(get()) }
|
||||
single { createRetrofit(get()) }
|
||||
single { createApiService<GithubRawServices>(get(), Const.Url.GITHUB_RAW_URL) }
|
||||
single { createApiService<GithubApiServices>(get(), Const.Url.GITHUB_API_URL) }
|
||||
single { createMarkwon(get(), get()) }
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun createOkHttpClient(context: Context): OkHttpClient {
|
||||
val builder = OkHttpClient.Builder()
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.HEADERS
|
||||
}
|
||||
builder.addInterceptor(httpLoggingInterceptor)
|
||||
}
|
||||
|
||||
if (!Networking.init(context)) {
|
||||
builder.sslSocketFactory(NoSSLv3SocketFactory())
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun createMoshiConverterFactory(): MoshiConverterFactory {
|
||||
val moshi = Moshi.Builder().build()
|
||||
return MoshiConverterFactory.create(moshi)
|
||||
}
|
||||
|
||||
fun createRetrofit(okHttpClient: OkHttpClient): Retrofit.Builder {
|
||||
return Retrofit.Builder()
|
||||
.addConverterFactory(ScalarsConverterFactory.create())
|
||||
.addConverterFactory(createMoshiConverterFactory())
|
||||
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
|
||||
.client(okHttpClient)
|
||||
}
|
||||
|
||||
inline fun <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseUrl: String): T {
|
||||
return retrofitBuilder
|
||||
.baseUrl(baseUrl)
|
||||
.build()
|
||||
.create(T::class.java)
|
||||
}
|
||||
|
||||
fun createMarkwon(context: Context, okHttpClient: OkHttpClient): Markwon {
|
||||
return Markwon.builder(context)
|
||||
.usePlugin(HtmlPlugin.create())
|
||||
.usePlugin(ImagesPlugin.create {
|
||||
it.addSchemeHandler(OkHttpNetworkSchemeHandler.create(okHttpClient))
|
||||
})
|
||||
.build()
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
||||
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
||||
import com.topjohnwu.magisk.data.repository.StringRepository
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
val repositoryModule = module {
|
||||
single { MagiskRepository(get(), get()) }
|
||||
single { LogRepository(get()) }
|
||||
single { StringRepository(get()) }
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import com.topjohnwu.magisk.legacy.flash.FlashViewModel
|
||||
import com.topjohnwu.magisk.legacy.surequest.SuRequestViewModel
|
||||
import com.topjohnwu.magisk.ui.MainViewModel
|
||||
import com.topjohnwu.magisk.ui.hide.HideViewModel
|
||||
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.module.ModuleViewModel
|
||||
import com.topjohnwu.magisk.ui.request.RequestViewModel
|
||||
import com.topjohnwu.magisk.ui.safetynet.SafetynetViewModel
|
||||
import com.topjohnwu.magisk.ui.settings.SettingsViewModel
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
|
||||
import com.topjohnwu.magisk.ui.theme.ThemeViewModel
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
val viewModelModules = module {
|
||||
viewModel { HideViewModel(get()) }
|
||||
viewModel { HomeViewModel(get()) }
|
||||
viewModel { LogViewModel(get()) }
|
||||
viewModel { ModuleViewModel(get(), get(), get()) }
|
||||
viewModel { RequestViewModel() }
|
||||
viewModel { SafetynetViewModel(get()) }
|
||||
viewModel { SettingsViewModel(get()) }
|
||||
viewModel { SuperuserViewModel(get(), get(), get()) }
|
||||
viewModel { ThemeViewModel() }
|
||||
viewModel { InstallViewModel() }
|
||||
viewModel { MainViewModel() }
|
||||
|
||||
// Legacy
|
||||
viewModel { FlashViewModel(get()) }
|
||||
viewModel { SuRequestViewModel(get(), get(), get(SUTimeout), get()) }
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import androidx.databinding.Observable
|
||||
import androidx.databinding.ObservableBoolean
|
||||
import androidx.databinding.ObservableField
|
||||
import androidx.databinding.ObservableInt
|
||||
|
||||
fun <T> ObservableField<T>.addOnPropertyChangedCallback(
|
||||
removeAfterChanged: Boolean = false,
|
||||
callback: (T?) -> Unit
|
||||
) {
|
||||
addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
||||
callback(get())
|
||||
if (removeAfterChanged) removeOnPropertyChangedCallback(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun ObservableInt.addOnPropertyChangedCallback(
|
||||
removeAfterChanged: Boolean = false,
|
||||
callback: (Int) -> Unit
|
||||
) {
|
||||
addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
||||
callback(get())
|
||||
if (removeAfterChanged) removeOnPropertyChangedCallback(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun ObservableBoolean.addOnPropertyChangedCallback(
|
||||
removeAfterChanged: Boolean = false,
|
||||
callback: (Boolean) -> Unit
|
||||
) {
|
||||
addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
||||
callback(get())
|
||||
if (removeAfterChanged) removeOnPropertyChangedCallback(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
inline fun <T> ObservableField<T>.update(block: (T?) -> Unit) {
|
||||
set(get().apply(block))
|
||||
}
|
||||
|
||||
inline fun <T> ObservableField<T>.updateNonNull(block: (T) -> Unit) {
|
||||
update {
|
||||
it ?: return@update
|
||||
block(it)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun ObservableInt.update(block: (Int) -> Unit) {
|
||||
set(get().apply(block))
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.content.res.Resources
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun Int.toDp(): Int = ceil(this / Resources.getSystem().displayMetrics.density).roundToInt()
|
||||
|
||||
fun Int.toPx(): Int = (this * Resources.getSystem().displayMetrics.density).roundToInt()
|
205
app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt
Normal file
205
app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt
Normal file
@@ -0,0 +1,205 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import androidx.databinding.ObservableField
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import io.reactivex.*
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposables
|
||||
import io.reactivex.functions.BiFunction
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import androidx.databinding.Observable as BindingObservable
|
||||
|
||||
fun <T> Observable<T>.applySchedulers(
|
||||
subscribeOn: Scheduler = Schedulers.io(),
|
||||
observeOn: Scheduler = AndroidSchedulers.mainThread()
|
||||
): Observable<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
|
||||
|
||||
fun <T> Flowable<T>.applySchedulers(
|
||||
subscribeOn: Scheduler = Schedulers.io(),
|
||||
observeOn: Scheduler = AndroidSchedulers.mainThread()
|
||||
): Flowable<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
|
||||
|
||||
fun <T> Single<T>.applySchedulers(
|
||||
subscribeOn: Scheduler = Schedulers.io(),
|
||||
observeOn: Scheduler = AndroidSchedulers.mainThread()
|
||||
): Single<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
|
||||
|
||||
fun <T> Maybe<T>.applySchedulers(
|
||||
subscribeOn: Scheduler = Schedulers.io(),
|
||||
observeOn: Scheduler = AndroidSchedulers.mainThread()
|
||||
): Maybe<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
|
||||
|
||||
fun Completable.applySchedulers(
|
||||
subscribeOn: Scheduler = Schedulers.io(),
|
||||
observeOn: Scheduler = AndroidSchedulers.mainThread()
|
||||
): Completable = this.subscribeOn(subscribeOn).observeOn(observeOn)
|
||||
|
||||
/*=== ALIASES FOR OBSERVABLES ===*/
|
||||
|
||||
typealias OnCompleteListener = () -> Unit
|
||||
typealias OnSuccessListener<T> = (T) -> Unit
|
||||
typealias OnErrorListener = (Throwable) -> Unit
|
||||
|
||||
/*=== ALIASES FOR OBSERVABLES ===*/
|
||||
|
||||
fun <T> Observable<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {},
|
||||
onNext: OnSuccessListener<T> = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onNext, onError, onComplete)
|
||||
|
||||
fun <T> Single<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onNext: OnSuccessListener<T> = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onNext, onError)
|
||||
|
||||
fun <T> Maybe<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {},
|
||||
onSuccess: OnSuccessListener<T> = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onSuccess, onError, onComplete)
|
||||
|
||||
fun <T> Flowable<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {},
|
||||
onNext: OnSuccessListener<T> = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onNext, onError, onComplete)
|
||||
|
||||
fun Completable.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onComplete, onError)
|
||||
|
||||
|
||||
fun <T> Observable<out T>.updateBy(
|
||||
field: KObservableField<T?>
|
||||
) = doOnNextUi { field.value = it }
|
||||
.doOnErrorUi { field.value = null }
|
||||
|
||||
fun <T> Single<out T>.updateBy(
|
||||
field: KObservableField<T?>
|
||||
) = doOnSuccessUi { field.value = it }
|
||||
.doOnErrorUi { field.value = null }
|
||||
|
||||
fun <T> Maybe<out T>.updateBy(
|
||||
field: KObservableField<T?>
|
||||
) = doOnSuccessUi { field.value = it }
|
||||
.doOnErrorUi { field.value = null }
|
||||
.doOnComplete { field.value = field.value }
|
||||
|
||||
fun <T> Flowable<out T>.updateBy(
|
||||
field: KObservableField<T?>
|
||||
) = doOnNextUi { field.value = it }
|
||||
.doOnErrorUi { field.value = null }
|
||||
|
||||
fun Completable.updateBy(
|
||||
field: KObservableField<Boolean>
|
||||
) = doOnCompleteUi { field.value = true }
|
||||
.doOnErrorUi { field.value = false }
|
||||
|
||||
|
||||
fun <T> Observable<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { UiThreadHandler.run { body() } }
|
||||
|
||||
fun <T> Single<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { UiThreadHandler.run { body() } }
|
||||
|
||||
fun <T> Maybe<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { UiThreadHandler.run { body() } }
|
||||
|
||||
fun <T> Flowable<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { UiThreadHandler.run { body() } }
|
||||
|
||||
fun Completable.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { UiThreadHandler.run { body() } }
|
||||
|
||||
|
||||
fun <T> Observable<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Single<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Maybe<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Flowable<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun Completable.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { UiThreadHandler.run { body(it) } }
|
||||
|
||||
|
||||
fun <T> Observable<T>.doOnNextUi(body: (T) -> Unit) =
|
||||
doOnNext { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Flowable<T>.doOnNextUi(body: (T) -> Unit) =
|
||||
doOnNext { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Single<T>.doOnSuccessUi(body: (T) -> Unit) =
|
||||
doOnSuccess { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Maybe<T>.doOnSuccessUi(body: (T) -> Unit) =
|
||||
doOnSuccess { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Maybe<T>.doOnCompleteUi(body: () -> Unit) =
|
||||
doOnComplete { UiThreadHandler.run { body() } }
|
||||
|
||||
fun Completable.doOnCompleteUi(body: () -> Unit) =
|
||||
doOnComplete { UiThreadHandler.run { body() } }
|
||||
|
||||
|
||||
fun <T, R> Observable<List<T>>.mapList(
|
||||
transformer: (T) -> R
|
||||
) = flatMapIterable { it }
|
||||
.map(transformer)
|
||||
.toList()
|
||||
|
||||
fun <T, R> Single<List<T>>.mapList(
|
||||
transformer: (T) -> R
|
||||
) = flattenAsFlowable { it }
|
||||
.map(transformer)
|
||||
.toList()
|
||||
|
||||
fun <T, R> Maybe<List<T>>.mapList(
|
||||
transformer: (T) -> R
|
||||
) = flattenAsFlowable { it }
|
||||
.map(transformer)
|
||||
.toList()
|
||||
|
||||
fun <T, R> Flowable<List<T>>.mapList(
|
||||
transformer: (T) -> R
|
||||
) = flatMapIterable { it }
|
||||
.map(transformer)
|
||||
.toList()
|
||||
|
||||
fun <T> ObservableField<T>.toObservable(): Observable<T> {
|
||||
val observableField = this
|
||||
return Observable.create { emitter ->
|
||||
observableField.get()?.let { emitter.onNext(it) }
|
||||
|
||||
val callback = object : BindingObservable.OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: BindingObservable?, propertyId: Int) {
|
||||
observableField.get()?.let { emitter.onNext(it) }
|
||||
}
|
||||
}
|
||||
observableField.addOnPropertyChangedCallback(callback)
|
||||
emitter.setDisposable(Disposables.fromAction {
|
||||
observableField.removeOnPropertyChangedCallback(callback)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : Any> T.toSingle() = Single.just(this)
|
||||
|
||||
inline fun <T1, T2, R> zip(
|
||||
t1: Single<T1>,
|
||||
t2: Single<T2>,
|
||||
crossinline zipper: (T1, T2) -> R
|
||||
) = Single.zip(t1, t2, BiFunction<T1, T2, R> { rt1, rt2 -> zipper(rt1, rt2) })
|
126
app/src/main/java/com/topjohnwu/magisk/extensions/Snackbar.kt
Normal file
126
app/src/main/java/com/topjohnwu/magisk/extensions/Snackbar.kt
Normal file
@@ -0,0 +1,126 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
||||
fun AppCompatActivity.snackbar(
|
||||
view: View,
|
||||
@StringRes messageRes: Int,
|
||||
length: Int = Snackbar.LENGTH_SHORT,
|
||||
f: Snackbar.() -> Unit = {}
|
||||
) {
|
||||
snackbar(view, getString(messageRes), length, f)
|
||||
}
|
||||
|
||||
fun AppCompatActivity.snackbar(
|
||||
view: View,
|
||||
message: String,
|
||||
length: Int = Snackbar.LENGTH_SHORT,
|
||||
f: Snackbar.() -> Unit = {}
|
||||
) = Snackbar.make(view, message, length)
|
||||
.apply(f)
|
||||
.show()
|
||||
|
||||
fun Fragment.snackbar(
|
||||
view: View,
|
||||
@StringRes messageRes: Int,
|
||||
length: Int = Snackbar.LENGTH_SHORT,
|
||||
f: Snackbar.() -> Unit = {}
|
||||
) {
|
||||
snackbar(view, getString(messageRes), length, f)
|
||||
}
|
||||
|
||||
fun Fragment.snackbar(
|
||||
view: View,
|
||||
message: String,
|
||||
length: Int = Snackbar.LENGTH_SHORT,
|
||||
f: Snackbar.() -> Unit = {}
|
||||
) = Snackbar.make(view, message, length)
|
||||
.apply(f)
|
||||
.show()
|
||||
|
||||
fun Snackbar.action(init: KSnackbar.() -> Unit) = apply {
|
||||
val config = KSnackbar().apply(init)
|
||||
|
||||
setAction(config.title(context), config.onClickListener)
|
||||
|
||||
when {
|
||||
config.hasValidColor -> setActionTextColor(config.color(context) ?: return@apply)
|
||||
config.hasValidColorStateList -> setActionTextColor(config.colorStateList(context) ?: return@apply)
|
||||
}
|
||||
}
|
||||
|
||||
class KSnackbar {
|
||||
var colorRes: Int = -1
|
||||
var colorStateListRes: Int = -1
|
||||
|
||||
var title: CharSequence = ""
|
||||
var titleRes: Int = -1
|
||||
|
||||
internal var onClickListener: (View) -> Unit = {}
|
||||
internal val hasValidColor get() = colorRes != -1
|
||||
internal val hasValidColorStateList get() = colorStateListRes != -1
|
||||
|
||||
fun onClicked(listener: (View) -> Unit) {
|
||||
onClickListener = listener
|
||||
}
|
||||
|
||||
internal fun title(context: Context) = if (title.isBlank()) context.getString(titleRes) else title
|
||||
internal fun colorStateList(context: Context) = context.colorStateListCompat(colorStateListRes)
|
||||
internal fun color(context: Context) = context.colorCompat(colorRes)
|
||||
}
|
||||
|
||||
@Deprecated("Kotlin DSL version is preferred", ReplaceWith("action {}"))
|
||||
fun Snackbar.action(
|
||||
@StringRes actionRes: Int,
|
||||
@ColorRes colorRes: Int? = null,
|
||||
listener: (View) -> Unit
|
||||
) {
|
||||
view.resources.getString(actionRes)
|
||||
colorRes?.let { ContextCompat.getColor(view.context, colorRes) }
|
||||
action {}
|
||||
}
|
||||
|
||||
@Deprecated("Kotlin DSL version is preferred", ReplaceWith("action {}"))
|
||||
fun Snackbar.action(action: String, @ColorInt color: Int? = null, listener: (View) -> Unit) {
|
||||
setAction(action, listener)
|
||||
color?.let { setActionTextColor(color) }
|
||||
}
|
||||
|
||||
fun Snackbar.textColorRes(@ColorRes colorRes: Int) {
|
||||
textColor(context.colorCompat(colorRes) ?: return)
|
||||
}
|
||||
|
||||
fun Snackbar.textColor(@ColorInt color: Int) {
|
||||
val tv = view.findViewById<TextView>(com.google.android.material.R.id.snackbar_text)
|
||||
tv.setTextColor(color)
|
||||
}
|
||||
|
||||
fun Snackbar.backgroundColorRes(@ColorRes colorRes: Int) {
|
||||
backgroundColor(context.colorCompat(colorRes) ?: return)
|
||||
}
|
||||
|
||||
fun Snackbar.backgroundColor(@ColorInt color: Int) {
|
||||
ViewCompat.setBackgroundTintList(
|
||||
view,
|
||||
ColorStateList.valueOf(color)
|
||||
)
|
||||
}
|
||||
|
||||
fun Snackbar.alert() {
|
||||
textColor(0xF44336)
|
||||
}
|
||||
|
||||
fun Snackbar.success() {
|
||||
textColor(0x4CAF50)
|
||||
}
|
330
app/src/main/java/com/topjohnwu/magisk/extensions/XAndroid.kt
Normal file
330
app/src/main/java/com/topjohnwu/magisk/extensions/XAndroid.kt
Normal file
@@ -0,0 +1,330 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.ComponentInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.*
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.database.Cursor
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.AdaptiveIconDrawable
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.provider.OpenableColumns
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.topjohnwu.magisk.FileProvider
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.utils.Utils
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import com.topjohnwu.magisk.utils.DynamicClassLoader
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.lang.reflect.Array as JArray
|
||||
|
||||
val packageName: String get() = get<Context>().packageName
|
||||
|
||||
val PackageInfo.processes
|
||||
get() = activities?.processNames.orEmpty() +
|
||||
services?.processNames.orEmpty() +
|
||||
receivers?.processNames.orEmpty() +
|
||||
providers?.processNames.orEmpty()
|
||||
|
||||
val Array<out ComponentInfo>.processNames get() = mapNotNull { it.processName }
|
||||
|
||||
val ApplicationInfo.packageInfo: PackageInfo?
|
||||
get() {
|
||||
val pm: PackageManager by inject()
|
||||
|
||||
return try {
|
||||
val request = GET_ACTIVITIES or
|
||||
GET_SERVICES or
|
||||
GET_RECEIVERS or
|
||||
GET_PROVIDERS
|
||||
pm.getPackageInfo(packageName, request)
|
||||
} catch (e1: Exception) {
|
||||
try {
|
||||
pm.activities(packageName).apply {
|
||||
services = pm.services(packageName)
|
||||
receivers = pm.receivers(packageName)
|
||||
providers = pm.providers(packageName)
|
||||
}
|
||||
} catch (e2: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val Uri.fileName: String
|
||||
get() {
|
||||
var name: String? = null
|
||||
get<Context>().contentResolver.query(this, null, null, null, null)?.use { c ->
|
||||
val nameIndex = c.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
if (nameIndex != -1) {
|
||||
c.moveToFirst()
|
||||
name = c.getString(nameIndex)
|
||||
}
|
||||
}
|
||||
if (name == null && path != null) {
|
||||
val idx = path!!.lastIndexOf('/')
|
||||
name = path!!.substring(idx + 1)
|
||||
}
|
||||
return name.orEmpty()
|
||||
}
|
||||
|
||||
fun PackageManager.activities(packageName: String) =
|
||||
getPackageInfo(packageName, GET_ACTIVITIES)
|
||||
|
||||
fun PackageManager.services(packageName: String) =
|
||||
getPackageInfo(packageName, GET_SERVICES).services
|
||||
|
||||
fun PackageManager.receivers(packageName: String) =
|
||||
getPackageInfo(packageName, GET_RECEIVERS).receivers
|
||||
|
||||
fun PackageManager.providers(packageName: String) =
|
||||
getPackageInfo(packageName, GET_PROVIDERS).providers
|
||||
|
||||
fun Context.rawResource(id: Int) = resources.openRawResource(id)
|
||||
|
||||
fun Context.readUri(uri: Uri) =
|
||||
contentResolver.openInputStream(uri) ?: throw FileNotFoundException()
|
||||
|
||||
fun Context.getBitmap(id: Int): Bitmap {
|
||||
var drawable = AppCompatResources.getDrawable(this, id)!!
|
||||
if (drawable is BitmapDrawable)
|
||||
return drawable.bitmap
|
||||
if (SDK_INT >= 26 && drawable is AdaptiveIconDrawable) {
|
||||
drawable = LayerDrawable(arrayOf(drawable.background, drawable.foreground))
|
||||
}
|
||||
val bitmap = Bitmap.createBitmap(
|
||||
drawable.intrinsicWidth, drawable.intrinsicHeight,
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
val canvas = Canvas(bitmap)
|
||||
drawable.setBounds(0, 0, canvas.width, canvas.height)
|
||||
drawable.draw(canvas)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
fun Intent.startActivity(context: Context) = context.startActivity(this)
|
||||
|
||||
fun Intent.startActivityWithRoot() {
|
||||
val args = mutableListOf("am", "start", "--user", Const.USER_ID.toString())
|
||||
val cmd = toCommand(args).joinToString(" ")
|
||||
Shell.su(cmd).submit()
|
||||
}
|
||||
|
||||
fun Intent.toCommand(args: MutableList<String> = mutableListOf()): MutableList<String> {
|
||||
action?.also {
|
||||
args.add("-a")
|
||||
args.add(it)
|
||||
}
|
||||
component?.also {
|
||||
args.add("-n")
|
||||
args.add(it.flattenToString())
|
||||
}
|
||||
data?.also {
|
||||
args.add("-d")
|
||||
args.add(it.toString())
|
||||
}
|
||||
categories?.also {
|
||||
for (cat in it) {
|
||||
args.add("-c")
|
||||
args.add(cat)
|
||||
}
|
||||
}
|
||||
type?.also {
|
||||
args.add("-t")
|
||||
args.add(it)
|
||||
}
|
||||
extras?.also {
|
||||
loop@ for (key in it.keySet()) {
|
||||
val v = it[key] ?: continue
|
||||
var value: Any = v
|
||||
val arg: String
|
||||
when {
|
||||
v is String -> arg = "--es"
|
||||
v is Boolean -> arg = "--ez"
|
||||
v is Int -> arg = "--ei"
|
||||
v is Long -> arg = "--el"
|
||||
v is Float -> arg = "--ef"
|
||||
v is Uri -> arg = "--eu"
|
||||
v is ComponentName -> {
|
||||
arg = "--ecn"
|
||||
value = v.flattenToString()
|
||||
}
|
||||
v is List<*> -> {
|
||||
if (v.isEmpty())
|
||||
continue@loop
|
||||
|
||||
arg = if (v[0] is Int)
|
||||
"--eial"
|
||||
else if (v[0] is Long)
|
||||
"--elal"
|
||||
else if (v[0] is Float)
|
||||
"--efal"
|
||||
else if (v[0] is String)
|
||||
"--esal"
|
||||
else
|
||||
continue@loop /* Unsupported */
|
||||
|
||||
val sb = StringBuilder()
|
||||
for (o in v) {
|
||||
sb.append(o.toString().replace(",", "\\,"))
|
||||
sb.append(',')
|
||||
}
|
||||
// Remove trailing comma
|
||||
sb.deleteCharAt(sb.length - 1)
|
||||
value = sb
|
||||
}
|
||||
v.javaClass.isArray -> {
|
||||
arg = if (v is IntArray)
|
||||
"--eia"
|
||||
else if (v is LongArray)
|
||||
"--ela"
|
||||
else if (v is FloatArray)
|
||||
"--efa"
|
||||
else if (v is Array<*> && v.isArrayOf<String>())
|
||||
"--esa"
|
||||
else
|
||||
continue@loop /* Unsupported */
|
||||
|
||||
val sb = StringBuilder()
|
||||
val len = JArray.getLength(v)
|
||||
for (i in 0 until len) {
|
||||
sb.append(JArray.get(v, i)!!.toString().replace(",", "\\,"))
|
||||
sb.append(',')
|
||||
}
|
||||
// Remove trailing comma
|
||||
sb.deleteCharAt(sb.length - 1)
|
||||
value = sb
|
||||
}
|
||||
else -> continue@loop
|
||||
} /* Unsupported */
|
||||
|
||||
args.add(arg)
|
||||
args.add(key)
|
||||
args.add(value.toString())
|
||||
}
|
||||
}
|
||||
args.add("-f")
|
||||
args.add(flags.toString())
|
||||
return args
|
||||
}
|
||||
|
||||
fun File.provide(context: Context = get()): Uri {
|
||||
return FileProvider.getUriForFile(context, context.packageName + ".provider", this)
|
||||
}
|
||||
|
||||
fun File.mv(destination: File) {
|
||||
inputStream().writeTo(destination)
|
||||
deleteRecursively()
|
||||
}
|
||||
|
||||
fun String.toFile() = File(this)
|
||||
|
||||
fun Intent.chooser(title: String = "Pick an app") = Intent.createChooser(this, title)
|
||||
|
||||
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 {
|
||||
runCatching {
|
||||
if (labelRes > 0) {
|
||||
val res = pm.getResourcesForApplication(this)
|
||||
val config = Configuration()
|
||||
config.setLocale(currentLocale)
|
||||
res.updateConfiguration(config, res.displayMetrics)
|
||||
return res.getString(labelRes)
|
||||
}
|
||||
}
|
||||
|
||||
return loadLabel(pm).toString()
|
||||
}
|
||||
|
||||
fun 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) = ContextCompat.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 (SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 &&
|
||||
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())
|
||||
|
||||
@Suppress("FunctionName")
|
||||
inline fun <reified T> T.DynamicClassLoader(apk: File) =
|
||||
DynamicClassLoader(apk, T::class.java.classLoader)
|
||||
|
||||
fun Context.unwrap(): Context {
|
||||
var context = this
|
||||
while (true) {
|
||||
if (context is ContextWrapper)
|
||||
context = context.baseContext
|
||||
else
|
||||
break
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
fun Uri.writeTo(file: File) = toFile().copyTo(file)
|
||||
|
||||
fun Context.hasPermissions(vararg permissions: String) = permissions.all {
|
||||
ContextCompat.checkSelfPermission(this, it) == PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
fun Activity.hideKeyboard() {
|
||||
val view = currentFocus ?: return
|
||||
getSystemService<InputMethodManager>()
|
||||
?.hideSoftInputFromWindow(view.windowToken, 0)
|
||||
view.clearFocus()
|
||||
}
|
||||
|
||||
fun Fragment.hideKeyboard() {
|
||||
activity?.hideKeyboard()
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import androidx.databinding.ObservableList
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
|
||||
|
||||
fun KObservableField<Boolean>.toggle() {
|
||||
value = !value
|
||||
}
|
||||
|
||||
fun <T> ObservableList<T>.addOnListChangedCallback(
|
||||
onChanged: ((sender: ObservableList<T>) -> Unit)? = null,
|
||||
onItemRangeRemoved: ((sender: ObservableList<T>, positionStart: Int, itemCount: Int) -> Unit)? = null,
|
||||
onItemRangeMoved: ((sender: ObservableList<T>, fromPosition: Int, toPosition: Int, itemCount: Int) -> Unit)? = null,
|
||||
onItemRangeInserted: ((sender: ObservableList<T>, positionStart: Int, itemCount: Int) -> Unit)? = null,
|
||||
onItemRangeChanged: ((sender: ObservableList<T>, positionStart: Int, itemCount: Int) -> Unit)? = null
|
||||
) = addOnListChangedCallback(object : ObservableList.OnListChangedCallback<ObservableList<T>>() {
|
||||
override fun onChanged(sender: ObservableList<T>?) {
|
||||
onChanged?.invoke(sender ?: return)
|
||||
}
|
||||
|
||||
override fun onItemRangeRemoved(
|
||||
sender: ObservableList<T>?,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
onItemRangeRemoved?.invoke(
|
||||
sender ?: return,
|
||||
positionStart,
|
||||
itemCount
|
||||
)
|
||||
}
|
||||
|
||||
override fun onItemRangeMoved(
|
||||
sender: ObservableList<T>?,
|
||||
fromPosition: Int,
|
||||
toPosition: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
onItemRangeMoved?.invoke(
|
||||
sender ?: return,
|
||||
fromPosition,
|
||||
toPosition,
|
||||
itemCount
|
||||
)
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(
|
||||
sender: ObservableList<T>?,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
onItemRangeInserted?.invoke(
|
||||
sender ?: return,
|
||||
positionStart,
|
||||
itemCount
|
||||
)
|
||||
}
|
||||
|
||||
override fun onItemRangeChanged(
|
||||
sender: ObservableList<T>?,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
onItemRangeChanged?.invoke(
|
||||
sender ?: return,
|
||||
positionStart,
|
||||
itemCount
|
||||
)
|
||||
}
|
||||
})
|
135
app/src/main/java/com/topjohnwu/magisk/extensions/XJava.kt
Normal file
135
app/src/main/java/com/topjohnwu/magisk/extensions/XJava.kt
Normal file
@@ -0,0 +1,135 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.os.Build
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Method
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import kotlin.NoSuchElementException
|
||||
|
||||
fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
|
||||
var entry: ZipEntry? = nextEntry
|
||||
while (entry != null) {
|
||||
callback(entry)
|
||||
entry = nextEntry
|
||||
}
|
||||
}
|
||||
|
||||
fun InputStream.writeTo(file: File) =
|
||||
withStreams(this, file.outputStream()) { reader, writer -> reader.copyTo(writer) }
|
||||
|
||||
inline fun <In : InputStream, Out : OutputStream> withStreams(
|
||||
inStream: In,
|
||||
outStream: Out,
|
||||
withBoth: (In, Out) -> Unit
|
||||
) {
|
||||
inStream.use { reader ->
|
||||
outStream.use { writer ->
|
||||
withBoth(reader, writer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T, R> List<T>.firstMap(mapper: (T) -> R?): R {
|
||||
for (item: T in this) {
|
||||
return mapper(item) ?: continue
|
||||
}
|
||||
throw NoSuchElementException("Collection contains no element matching the predicate.")
|
||||
}
|
||||
|
||||
fun String.langTagToLocale(): Locale {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
return Locale.forLanguageTag(this)
|
||||
} else {
|
||||
val tok = split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
if (tok.isEmpty()) {
|
||||
return Locale("")
|
||||
}
|
||||
val language = when (tok[0]) {
|
||||
"und" -> "" // Undefined
|
||||
"fil" -> "tl" // Filipino
|
||||
else -> tok[0]
|
||||
}
|
||||
if (language.length != 2 && language.length != 3)
|
||||
return Locale("")
|
||||
if (tok.size == 1)
|
||||
return Locale(language)
|
||||
val country = tok[1]
|
||||
|
||||
return if (country.length != 2 && country.length != 3) Locale(language)
|
||||
else Locale(language, country)
|
||||
}
|
||||
}
|
||||
|
||||
fun Locale.toLangTag(): String {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
return toLanguageTag()
|
||||
} else {
|
||||
var language = language
|
||||
var country = country
|
||||
var variant = variant
|
||||
when {
|
||||
language.isEmpty() || !language.matches("\\p{Alpha}{2,8}".toRegex()) ->
|
||||
language = "und" // Follow the Locale#toLanguageTag() implementation
|
||||
language == "iw" -> language = "he" // correct deprecated "Hebrew"
|
||||
language == "in" -> language = "id" // correct deprecated "Indonesian"
|
||||
language == "ji" -> language = "yi" // correct deprecated "Yiddish"
|
||||
}
|
||||
// ensure valid country code, if not well formed, it's omitted
|
||||
|
||||
// variant subtags that begin with a letter must be at least 5 characters long
|
||||
// ensure valid country code, if not well formed, it's omitted
|
||||
if (!country.matches("\\p{Alpha}{2}|\\p{Digit}{3}".toRegex())) {
|
||||
country = ""
|
||||
}
|
||||
|
||||
// variant subtags that begin with a letter must be at least 5 characters long
|
||||
if (!variant.matches("\\p{Alnum}{5,8}|\\p{Digit}\\p{Alnum}{3}".toRegex())) {
|
||||
variant = ""
|
||||
}
|
||||
val tag = StringBuilder(language)
|
||||
if (country.isNotEmpty())
|
||||
tag.append('-').append(country)
|
||||
if (variant.isNotEmpty())
|
||||
tag.append('-').append(variant)
|
||||
return tag.toString()
|
||||
}
|
||||
}
|
||||
|
||||
fun SimpleDateFormat.parseOrNull(date: String) =
|
||||
runCatching { parse(date) }.onFailure { Timber.e(it) }.getOrNull()
|
||||
|
||||
// Reflection hacks
|
||||
|
||||
private val loadClass = ClassLoader::class.java.getMethod("loadClass", String::class.java)
|
||||
private val getDeclaredMethod = Class::class.java.getMethod("getDeclaredMethod",
|
||||
String::class.java, arrayOf<Class<*>>()::class.java)
|
||||
private val getDeclaredField = Class::class.java.getMethod("getDeclaredField", String::class.java)
|
||||
|
||||
fun ClassLoader.forceLoadClass(name: String) =
|
||||
runCatching { loadClass.invoke(this, name) }.getOrNull() as Class<*>?
|
||||
|
||||
fun Class<*>.forceGetDeclaredMethod(name: String, vararg types: Class<*>) =
|
||||
(runCatching { getDeclaredMethod.invoke(this, name, types) }.getOrNull() as Method?)?.also {
|
||||
it.isAccessible = true
|
||||
}
|
||||
|
||||
fun Class<*>.forceGetDeclaredField(name: String) =
|
||||
(runCatching { getDeclaredField.invoke(this, name) }.getOrNull() as Field?)?.also {
|
||||
it.isAccessible = true
|
||||
}
|
||||
|
||||
inline fun <reified T> T.forceGetClass(name: String) =
|
||||
T::class.java.classLoader?.forceLoadClass(name)
|
||||
|
||||
fun Class<*>.forceGetField(name: String): Field? =
|
||||
forceGetDeclaredField(name) ?: superclass?.forceGetField(name)
|
||||
|
||||
fun Class<*>.forceGetMethod(name: String, vararg types: Class<*>): Method? =
|
||||
forceGetDeclaredMethod(name, *types) ?: superclass?.forceGetMethod(name, *types)
|
17
app/src/main/java/com/topjohnwu/magisk/extensions/XKoin.kt
Normal file
17
app/src/main/java/com/topjohnwu/magisk/extensions/XKoin.kt
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import org.koin.core.context.GlobalContext
|
||||
import org.koin.core.parameter.ParametersDefinition
|
||||
import org.koin.core.qualifier.Qualifier
|
||||
|
||||
fun getKoin() = GlobalContext.get().koin
|
||||
|
||||
inline fun <reified T> inject(
|
||||
qualifier: Qualifier? = null,
|
||||
noinline parameters: ParametersDefinition? = null
|
||||
) = lazy { get<T>(qualifier, parameters) }
|
||||
|
||||
inline fun <reified T> get(
|
||||
qualifier: Qualifier? = null,
|
||||
noinline parameters: ParametersDefinition? = null
|
||||
): T = getKoin().get(qualifier, parameters)
|
83
app/src/main/java/com/topjohnwu/magisk/extensions/XList.kt
Normal file
83
app/src/main/java/com/topjohnwu/magisk/extensions/XList.kt
Normal file
@@ -0,0 +1,83 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import androidx.databinding.ObservableList
|
||||
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||
import io.reactivex.disposables.Disposable
|
||||
|
||||
fun <T> MutableList<T>.update(newList: List<T>) {
|
||||
clear()
|
||||
addAll(newList)
|
||||
}
|
||||
|
||||
fun List<String>.toShellCmd(): String {
|
||||
val sb = StringBuilder()
|
||||
for (s in this) {
|
||||
if (s.contains(" ")) {
|
||||
sb.append('"').append(s).append('"')
|
||||
} else {
|
||||
sb.append(s)
|
||||
}
|
||||
sb.append(' ')
|
||||
}
|
||||
sb.deleteCharAt(sb.length - 1)
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
fun <T1, T2> ObservableList<T1>.sendUpdatesTo(
|
||||
target: DiffObservableList<T2>,
|
||||
mapper: (List<T1>) -> List<T2>
|
||||
) = addOnListChangedCallback(object :
|
||||
ObservableList.OnListChangedCallback<ObservableList<T1>>() {
|
||||
override fun onChanged(sender: ObservableList<T1>?) {
|
||||
updateAsync(sender ?: return)
|
||||
}
|
||||
|
||||
override fun onItemRangeRemoved(sender: ObservableList<T1>?, p0: Int, p1: Int) {
|
||||
updateAsync(sender ?: return)
|
||||
}
|
||||
|
||||
override fun onItemRangeMoved(sender: ObservableList<T1>?, p0: Int, p1: Int, p2: Int) {
|
||||
updateAsync(sender ?: return)
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(sender: ObservableList<T1>?, p0: Int, p1: Int) {
|
||||
updateAsync(sender ?: return)
|
||||
}
|
||||
|
||||
override fun onItemRangeChanged(sender: ObservableList<T1>?, p0: Int, p1: Int) {
|
||||
updateAsync(sender ?: return)
|
||||
}
|
||||
|
||||
private var updater: Disposable? = null
|
||||
|
||||
private fun updateAsync(sender: List<T1>) {
|
||||
updater?.dispose()
|
||||
updater = sender.toSingle()
|
||||
.map { mapper(it) }
|
||||
.map { it to target.calculateDiff(it) }
|
||||
.subscribeK { target.update(it.first, it.second) }
|
||||
}
|
||||
})
|
||||
|
||||
fun <T1> ObservableList<T1>.copyNewInputInto(
|
||||
target: MutableList<T1>
|
||||
) = addOnListChangedCallback(object : ObservableList.OnListChangedCallback<ObservableList<T1>>() {
|
||||
override fun onChanged(p0: ObservableList<T1>?) = Unit
|
||||
override fun onItemRangeRemoved(p0: ObservableList<T1>?, p1: Int, p2: Int) = Unit
|
||||
override fun onItemRangeMoved(p0: ObservableList<T1>?, p1: Int, p2: Int, p3: Int) = Unit
|
||||
override fun onItemRangeChanged(p0: ObservableList<T1>?, p1: Int, p2: Int) = Unit
|
||||
override fun onItemRangeInserted(
|
||||
sender: ObservableList<T1>?,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
val positionEnd = positionStart + itemCount
|
||||
val addedValues = sender?.slice(positionStart until positionEnd).orEmpty()
|
||||
target.addAll(addedValues)
|
||||
}
|
||||
})
|
||||
|
||||
operator fun <E> SparseArrayCompat<E>.set(key: Int, value: E) {
|
||||
put(key, value)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user