mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-08-14 05:17:25 +00:00
Compare commits
611 Commits
manager-v7
...
v20.4
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 | ||
![]() |
24e2c3a5e9 | ||
![]() |
064523ef25 | ||
![]() |
85f293a44e | ||
![]() |
8e412bee5f | ||
![]() |
7d5555f82e | ||
![]() |
6720725d27 | ||
![]() |
fe5c65d798 | ||
![]() |
253f3cf1ba | ||
![]() |
db2e48b49f | ||
![]() |
5e089451af | ||
![]() |
6aa22267f4 | ||
![]() |
f76c020dd7 | ||
![]() |
722fba7805 | ||
![]() |
86551909fc | ||
![]() |
588e94c11d | ||
![]() |
9e66310c28 | ||
![]() |
93c422dce6 | ||
![]() |
7d6eebdae3 | ||
![]() |
f11bb609c9 | ||
![]() |
b910a92731 | ||
![]() |
ee7d297ca8 | ||
![]() |
a70c0174e1 | ||
![]() |
da707afa3f | ||
![]() |
a41597431c | ||
![]() |
d0b817381e | ||
![]() |
60a2e9b5dc | ||
![]() |
df3a37b0a3 | ||
![]() |
5f4718cd13 | ||
![]() |
3cc5cb3123 | ||
![]() |
8a2872afa4 | ||
![]() |
85941c4729 | ||
![]() |
82eeefb544 | ||
![]() |
f6061ba00e | ||
![]() |
9e3afcfe7a | ||
![]() |
21f2f86cb8 | ||
![]() |
04576ca828 | ||
![]() |
067cb0cd9d | ||
![]() |
17fb8f2298 | ||
![]() |
fbfc4e72ca | ||
![]() |
d2e171eabc | ||
![]() |
e50094af80 | ||
![]() |
93edf72993 | ||
![]() |
a230d63cf9 | ||
![]() |
2bb39bee2f | ||
![]() |
ce2ca5446a | ||
![]() |
8a014ff786 | ||
![]() |
dc09ec7598 | ||
![]() |
27fb0474d5 | ||
![]() |
7f0a87742a | ||
![]() |
47e236788c | ||
![]() |
236ad57608 | ||
![]() |
6d03798314 | ||
![]() |
c954a4f7bc | ||
![]() |
ba588d1097 | ||
![]() |
44f7c9a545 | ||
![]() |
b910db322b | ||
![]() |
c44a942fb7 | ||
![]() |
d713ad3499 | ||
![]() |
ddf40df649 | ||
![]() |
7c6d85221d | ||
![]() |
b66b82a6e9 | ||
![]() |
c44b85ea87 | ||
![]() |
fcbf56e93a | ||
![]() |
a539ffb188 | ||
![]() |
512f533a80 | ||
![]() |
96ef9cdbee | ||
![]() |
28fcbbcf7b | ||
![]() |
0f4326151f | ||
![]() |
e0e27774ad | ||
![]() |
1223b48b2c | ||
![]() |
d8338f0b48 | ||
![]() |
38019f7f42 | ||
![]() |
23978ef4d2 | ||
![]() |
3b4cb23112 | ||
![]() |
974cb1167f | ||
![]() |
6ccbc272c6 | ||
![]() |
0eb28c3265 | ||
![]() |
2daa131fb2 | ||
![]() |
51247d36c5 | ||
![]() |
37fa227fb5 | ||
![]() |
9dd272b357 | ||
![]() |
277298feae | ||
![]() |
ff24bc0b68 | ||
![]() |
700c51f95c | ||
![]() |
659914afbe | ||
![]() |
ee06aed94b | ||
![]() |
af1f5d5ab2 | ||
![]() |
4292ddd0ae | ||
![]() |
4a68fd65b6 | ||
![]() |
0e33632e79 | ||
![]() |
a9b20dae33 | ||
![]() |
e595937740 | ||
![]() |
72eb584e65 | ||
![]() |
8999a57f06 | ||
![]() |
8024089bde | ||
![]() |
5e01f785ae | ||
![]() |
d35d1b8860 | ||
![]() |
88027f2151 | ||
![]() |
cd41e7108b | ||
![]() |
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 | ||
![]() |
122e2f7a8e | ||
![]() |
b4e1585e2b | ||
![]() |
a5830599c4 |
5
.gitattributes
vendored
5
.gitattributes
vendored
@@ -17,3 +17,8 @@ tools/** binary
|
||||
*.apk binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.ttf binary
|
||||
|
||||
# Help GitHub detect languages
|
||||
native/jni/external/** linguist-vendored
|
||||
native/jni/systemproperties/** linguist-language=C++
|
||||
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -22,6 +22,9 @@
|
||||
[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
|
||||
|
32
README.MD
32
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 are 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](https://github.com/topjohnwu/Magisk/issues) 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
|
||||
|
||||
@@ -37,27 +44,6 @@ Default string resources for Magisk Manager and its stub APK are located here:
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Magisk, including all git submodules are free software:
|
||||
|
@@ -19,6 +19,12 @@ android {
|
||||
multiDexEnabled true
|
||||
versionName props['appVersion']
|
||||
versionCode props['appVersionCode'] as Integer
|
||||
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments = ["room.incremental":"true"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -26,7 +32,7 @@ android {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
|
||||
'proguard-rules.pro', 'proguard-kotlin.pro'
|
||||
'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,13 +41,12 @@ android {
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude '/META-INF/*.version'
|
||||
exclude '/META-INF/*.kotlin_module'
|
||||
exclude '/META-INF/rxkotlin.properties'
|
||||
exclude '/META-INF/**'
|
||||
exclude '/androidsupportmultidexversion.txt'
|
||||
exclude '/org/bouncycastle/**'
|
||||
exclude '/kotlin/**'
|
||||
exclude '/kotlinx/**'
|
||||
exclude '/okhttp3/**'
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
@@ -63,7 +68,7 @@ dependencies {
|
||||
implementation 'com.ncapdevi:frag-nav:3.2.0'
|
||||
implementation 'com.github.pwittchen:reactivenetwork-rx2:3.0.6'
|
||||
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.2.13'
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.2.18'
|
||||
implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||
|
||||
@@ -75,7 +80,7 @@ dependencies {
|
||||
implementation "${bindingAdapter}:${vBAdapt}"
|
||||
implementation "${bindingAdapter}-recyclerview:${vBAdapt}"
|
||||
|
||||
def vMarkwon = '4.1.2'
|
||||
def vMarkwon = '4.2.1'
|
||||
implementation "io.noties.markwon:core:${vMarkwon}"
|
||||
implementation "io.noties.markwon:html:${vMarkwon}"
|
||||
implementation "io.noties.markwon:image:${vMarkwon}"
|
||||
@@ -85,50 +90,46 @@ dependencies {
|
||||
implementation "com.github.topjohnwu.libsu:core:${vLibsu}"
|
||||
implementation "com.github.topjohnwu.libsu:io:${vLibsu}"
|
||||
|
||||
def vKoin = "2.0.1"
|
||||
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.6.2'
|
||||
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.6'
|
||||
implementation "com.squareup.okhttp3:okhttp:${vOkHttp}"
|
||||
def vOkHttp = '3.12.10'
|
||||
implementation("com.squareup.okhttp3:okhttp:${vOkHttp}") {
|
||||
force = true
|
||||
}
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:${vOkHttp}"
|
||||
|
||||
def vMoshi = "1.8.0"
|
||||
def vMoshi = '1.10.0-SNAPSHOT'
|
||||
implementation "com.squareup.moshi:moshi:${vMoshi}"
|
||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}"
|
||||
|
||||
def vKotshi = "2.0.1"
|
||||
implementation "se.ansman.kotshi:api:${vKotshi}"
|
||||
kapt "se.ansman.kotshi:compiler:${vKotshi}"
|
||||
|
||||
modules {
|
||||
module('androidx.room:room-runtime') {
|
||||
replacedBy('com.github.topjohnwu:room-runtime')
|
||||
}
|
||||
}
|
||||
def vRoom = "2.2.1"
|
||||
implementation "com.github.topjohnwu:room-runtime:${vRoom}"
|
||||
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.1.0"
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$vNav"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$vNav"
|
||||
def vNav = '2.2.1'
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:${vNav}"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:${vNav}"
|
||||
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha03'
|
||||
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-rc01'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.2.0-rc01'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.work:work-runtime:2.2.0'
|
||||
implementation 'androidx.transition:transition:1.3.0-rc01'
|
||||
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.1.0'
|
||||
implementation 'com.google.android.material:material:1.2.0-alpha01'
|
||||
implementation 'androidx.core:core-ktx:1.2.0'
|
||||
implementation 'com.google.android.material:material:1.2.0-alpha03'
|
||||
}
|
||||
|
@@ -1,20 +0,0 @@
|
||||
## So every class is case insensitive to avoid some bizare problems
|
||||
-dontusemixedcaseclassnames
|
||||
|
||||
## If reflection issues come up uncomment this, that should temporarily fix it
|
||||
#-keep class kotlin.** { *; }
|
||||
#-keep class kotlin.Metadata { *; }
|
||||
#-keepclassmembers class kotlin.Metadata {
|
||||
# public <methods>;
|
||||
#}
|
||||
|
||||
## Never warn about Kotlin, it should work as-is
|
||||
-dontwarn kotlin.**
|
||||
|
||||
## Removes runtime null checks - doesn't really matter if it crashes on kotlin or java NPE
|
||||
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
||||
static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
|
||||
}
|
||||
|
||||
## Useless option for dex
|
||||
-dontpreverify
|
43
app/proguard-rules.pro
vendored
43
app/proguard-rules.pro
vendored
@@ -16,33 +16,36 @@
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# 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.SafetyNetHelper { *; }
|
||||
-keep,allowobfuscation interface com.topjohnwu.magisk.utils.SafetyNetHelper$Callback
|
||||
-keepclassmembers class * implements com.topjohnwu.magisk.utils.SafetyNetHelper$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);
|
||||
}
|
||||
|
||||
# Keep all fragment constructors
|
||||
-keepclassmembers class * extends androidx.fragment.app.Fragment {
|
||||
public <init>(...);
|
||||
# Fragments
|
||||
-keep,allowobfuscation class * extends androidx.fragment.app.Fragment
|
||||
|
||||
# Strip Timber verbose and debug logging
|
||||
-assumenosideeffects class timber.log.Timber.Tree {
|
||||
public void v(**);
|
||||
public void d(**);
|
||||
}
|
||||
|
||||
# DelegateWorker
|
||||
-keep,allowobfuscation class * extends com.topjohnwu.magisk.base.DelegateWorker
|
||||
|
||||
# BootSigner
|
||||
-keep class a.a { *; }
|
||||
|
||||
# Workaround R8 bug
|
||||
-keep,allowobfuscation class com.topjohnwu.magisk.model.receiver.GeneralReceiver
|
||||
-keepclassmembers class a.e { *; }
|
||||
|
||||
# Strip logging
|
||||
-assumenosideeffects class timber.log.Timber.Tree { *; }
|
||||
|
||||
# Excessive obfuscation
|
||||
-repackageclasses 'a'
|
||||
-repackageclasses
|
||||
-allowaccessmodification
|
||||
|
||||
# QOL
|
||||
|
@@ -1,83 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
** Special Requirements **
|
||||
|
||||
This AndroidManifest.xml will be copied into the stub
|
||||
APK to allow APK delegation. This is why these special
|
||||
requirements exist.
|
||||
|
||||
* Class names *
|
||||
Class names a.a, a.c, a.e should not be changed as they are used
|
||||
externally. All other class names can be changed.
|
||||
|
||||
* Resource IDs *
|
||||
All resource IDs referred in AndroidManifest.xml is required to be
|
||||
included into the "shared" module to make the ID match with stub.
|
||||
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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:name="a.e"
|
||||
android:appComponentFactory="a.a"
|
||||
android:allowBackup="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning"
|
||||
tools:replace="android:appComponentFactory">
|
||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
|
||||
|
||||
<!-- Splash -->
|
||||
|
||||
<activity
|
||||
android:name="a.c"
|
||||
android:configChanges="orientation|screenSize"
|
||||
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>
|
||||
|
||||
<!-- Main -->
|
||||
|
||||
<activity
|
||||
android:name="a.b"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:exported="true" />
|
||||
<activity android:name="a.b" />
|
||||
|
||||
<!-- Flashing -->
|
||||
|
||||
<activity
|
||||
android:name="a.f"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:screenOrientation="nosensor" />
|
||||
<activity android:name="a.f" />
|
||||
|
||||
<!-- Superuser -->
|
||||
|
||||
<activity
|
||||
android:name="a.m"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:directBootAware="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false" />
|
||||
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.REBOOT" />
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
@@ -89,10 +63,7 @@
|
||||
</receiver>
|
||||
|
||||
<!-- DownloadService -->
|
||||
|
||||
<service
|
||||
android:name="a.j"
|
||||
android:exported="false" />
|
||||
<service android:name="a.j" />
|
||||
|
||||
<!-- Hardcode GMS version -->
|
||||
<meta-data
|
||||
@@ -105,6 +76,16 @@
|
||||
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>
|
||||
|
@@ -1,20 +1,8 @@
|
||||
package a;
|
||||
|
||||
import androidx.core.app.AppComponentFactory;
|
||||
|
||||
import com.topjohnwu.magisk.utils.PatchAPK;
|
||||
import com.topjohnwu.signing.BootSigner;
|
||||
|
||||
public class a extends AppComponentFactory {
|
||||
|
||||
@Deprecated
|
||||
public static boolean patchAPK(String in, String out, String pkg) {
|
||||
return PatchAPK.patch(in, out, pkg);
|
||||
}
|
||||
|
||||
public static boolean patchAPK(String in, String out, String pkg, String label) {
|
||||
return PatchAPK.patch(in, out, pkg, label);
|
||||
}
|
||||
public class a {
|
||||
|
||||
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,13 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.App;
|
||||
|
||||
public class e extends App {
|
||||
public e() {
|
||||
super();
|
||||
}
|
||||
|
||||
public e(Object o) {
|
||||
super(o);
|
||||
}
|
||||
}
|
@@ -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.DownloadService;
|
||||
|
||||
public class j extends DownloadService {
|
||||
/* 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 androidx.annotation.NonNull;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
import com.topjohnwu.magisk.base.DelegateWorker;
|
||||
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
|
||||
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.attachWorker(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,199 +0,0 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
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 androidx.annotation.StringRes
|
||||
import com.topjohnwu.magisk.extensions.langTagToLocale
|
||||
import com.topjohnwu.magisk.model.download.DownloadService
|
||||
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 com.topjohnwu.magisk.utils.currentLocale
|
||||
import com.topjohnwu.magisk.utils.defaultLocale
|
||||
import com.topjohnwu.magisk.utils.refreshLocale
|
||||
import com.topjohnwu.magisk.utils.updateConfig
|
||||
import java.util.*
|
||||
|
||||
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 = BuildConfig.APPLICATION_ID): ComponentName {
|
||||
val name = ClassMap[this].name
|
||||
return ComponentName(pkg, Info.stub?.componentMap?.get(name) ?: name)
|
||||
}
|
||||
|
||||
fun Context.intent(c: Class<*>): Intent {
|
||||
val cls = ClassMap[c]
|
||||
return Info.stub?.let {
|
||||
val className = it.componentMap.getOrElse(cls.name) { cls.name }
|
||||
Intent().setComponent(ComponentName(this, className))
|
||||
} ?: Intent(this, cls)
|
||||
}
|
||||
|
||||
private open class GlobalResContext(base: Context) : ContextWrapper(base) {
|
||||
open val mRes: Resources get() = ResourceMgr.resource
|
||||
private val loader by lazy { javaClass.classLoader!! }
|
||||
|
||||
override fun getResources(): Resources {
|
||||
return mRes
|
||||
}
|
||||
|
||||
override fun getClassLoader(): ClassLoader {
|
||||
return loader
|
||||
}
|
||||
|
||||
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(ResourceMgr.resApk)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
object ResourceMgr {
|
||||
|
||||
lateinit var resource: Resources
|
||||
lateinit var resApk: String
|
||||
|
||||
fun init(context: Context) {
|
||||
resource = context.resources
|
||||
refreshLocale()
|
||||
if (isRunningAsStub) {
|
||||
resApk = DynAPK.current(context).path
|
||||
resource.assets.addAssetPath(resApk)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = 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 patch the component of JobInfo to access WorkManager SystemJobService
|
||||
|
||||
val name = service.className
|
||||
val component = ComponentName(
|
||||
service.packageName,
|
||||
Info.stub!!.componentMap[name] ?: name)
|
||||
|
||||
// Clone the JobInfo except component
|
||||
val builder = JobInfo.Builder(id, component)
|
||||
.setExtras(extras)
|
||||
.setTransientExtras(transientExtras)
|
||||
.setClipData(clipData, clipGrantFlags)
|
||||
.setRequiredNetwork(requiredNetwork)
|
||||
.setEstimatedNetworkBytes(estimatedNetworkDownloadBytes, estimatedNetworkUploadBytes)
|
||||
.setRequiresCharging(isRequireCharging)
|
||||
.setRequiresDeviceIdle(isRequireDeviceIdle)
|
||||
.setRequiresBatteryNotLow(isRequireBatteryNotLow)
|
||||
.setRequiresStorageNotLow(isRequireStorageNotLow)
|
||||
.also {
|
||||
triggerContentUris?.let { uris ->
|
||||
for (uri in uris)
|
||||
it.addTriggerContentUri(uri)
|
||||
}
|
||||
}
|
||||
.setTriggerContentUpdateDelay(triggerContentUpdateDelay)
|
||||
.setTriggerContentMaxDelay(triggerContentMaxDelay)
|
||||
.setImportantWhileForeground(isImportantWhileForeground)
|
||||
.setPrefetch(isPrefetch)
|
||||
.setPersisted(isPersisted)
|
||||
|
||||
if (isPeriodic) {
|
||||
builder.setPeriodic(intervalMillis, flexMillis)
|
||||
} else {
|
||||
if (minLatencyMillis > 0)
|
||||
builder.setMinimumLatency(minLatencyMillis)
|
||||
if (maxExecutionDelayMillis > 0)
|
||||
builder.setOverrideDeadline(maxExecutionDelayMillis)
|
||||
}
|
||||
if (!isRequireDeviceIdle)
|
||||
builder.setBackoffCriteria(initialBackoffMillis, backoffPolicy)
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
UpdateCheckService::class.java to a.g::class.java,
|
||||
GeneralReceiver::class.java to a.h::class.java,
|
||||
DownloadService::class.java to a.j::class.java,
|
||||
SuRequestActivity::class.java to a.m::class.java
|
||||
)
|
||||
|
||||
operator fun get(c: Class<*>) = map.getOrElse(c) { throw IllegalArgumentException() }
|
||||
}
|
@@ -1,62 +0,0 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.model.entity.UpdateInfo
|
||||
import com.topjohnwu.magisk.utils.CachedValue
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
|
||||
val isRunningAsStub get() = Info.stub != null
|
||||
|
||||
object Info {
|
||||
|
||||
val envRef = CachedValue { loadState() }
|
||||
|
||||
val env by envRef // Local
|
||||
var remote = UpdateInfo() // Remote
|
||||
var stub: DynAPK.Data? = null // Stub
|
||||
|
||||
var keepVerity = false
|
||||
var keepEnc = false
|
||||
var recovery = false
|
||||
|
||||
val isConnected by lazy {
|
||||
KObservableField(false).also { field ->
|
||||
ReactiveNetwork.observeNetworkConnectivity(get())
|
||||
.subscribeK {
|
||||
field.value = it.available()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadState() = runCatching {
|
||||
val str = ShellUtils.fastCmd("magisk -v").split(":".toRegex())[0]
|
||||
val code = ShellUtils.fastCmd("magisk -V").toInt()
|
||||
val hide = Shell.su("magiskhide --status").exec().isSuccess
|
||||
var mode = -1
|
||||
if (code >= Const.Version.CONNECT_MODE) {
|
||||
mode = Shell.su("magisk --connect-mode").exec().code
|
||||
if (mode == 0) {
|
||||
// Manually trigger broadcast test
|
||||
Shell.su("magisk --broadcast-test").exec()
|
||||
}
|
||||
}
|
||||
Env(code, str, hide, mode)
|
||||
}.getOrElse { Env() }
|
||||
|
||||
class Env(
|
||||
val magiskVersionCode: Int = -1,
|
||||
val magiskVersionString: String = "",
|
||||
hide: Boolean = false,
|
||||
var connectionMode: Int = -1
|
||||
) {
|
||||
val magiskHide get() = Config.magiskHide
|
||||
|
||||
init {
|
||||
Config.magiskHide = hide
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,50 +0,0 @@
|
||||
package com.topjohnwu.magisk.base
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
|
||||
import com.topjohnwu.magisk.model.events.EventHandler
|
||||
import com.topjohnwu.magisk.model.events.ViewEvent
|
||||
|
||||
abstract class BaseFragment<ViewModel : BaseViewModel, Binding : ViewDataBinding> :
|
||||
Fragment(), EventHandler {
|
||||
|
||||
protected val activity get() = requireActivity() as BaseActivity<*, *>
|
||||
protected lateinit var binding: Binding
|
||||
protected abstract val layoutRes: Int
|
||||
protected abstract val viewModel: ViewModel
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
viewModel.viewEvents.observe(this, viewEventObserver)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
binding = DataBindingUtil.inflate<Binding>(inflater, layoutRes, container, false).apply {
|
||||
setVariable(BR.viewModel, viewModel)
|
||||
lifecycleOwner = this@BaseFragment
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onEventDispatched(event: ViewEvent) {
|
||||
super.onEventDispatched(event)
|
||||
activity.onEventDispatched(event)
|
||||
}
|
||||
|
||||
open fun onBackPressed(): Boolean = false
|
||||
|
||||
}
|
@@ -1,56 +0,0 @@
|
||||
package com.topjohnwu.magisk.base
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.preference.*
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
abstract class BasePreferenceFragment : PreferenceFragmentCompat(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
protected val prefs: SharedPreferences by inject()
|
||||
protected val activity get() = requireActivity() as BaseActivity<*, *>
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val v = super.onCreateView(inflater, container, savedInstanceState)
|
||||
prefs.registerOnSharedPreferenceChangeListener(this)
|
||||
return v
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
prefs.unregisterOnSharedPreferenceChangeListener(this)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun setAllPreferencesToAvoidHavingExtraSpace(preference: Preference) {
|
||||
preference.isIconSpaceReserved = false
|
||||
if (preference is PreferenceGroup)
|
||||
for (i in 0 until preference.preferenceCount)
|
||||
setAllPreferencesToAvoidHavingExtraSpace(preference.getPreference(i))
|
||||
}
|
||||
|
||||
override fun setPreferenceScreen(preferenceScreen: PreferenceScreen?) {
|
||||
if (preferenceScreen != null)
|
||||
setAllPreferencesToAvoidHavingExtraSpace(preferenceScreen)
|
||||
super.setPreferenceScreen(preferenceScreen)
|
||||
}
|
||||
|
||||
override fun onCreateAdapter(preferenceScreen: PreferenceScreen?): RecyclerView.Adapter<*> =
|
||||
object : PreferenceGroupAdapter(preferenceScreen) {
|
||||
@SuppressLint("RestrictedApi")
|
||||
override fun onPreferenceHierarchyChange(preference: Preference?) {
|
||||
if (preference != null)
|
||||
setAllPreferencesToAvoidHavingExtraSpace(preference)
|
||||
super.onPreferenceHierarchyChange(preference)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,35 +0,0 @@
|
||||
package com.topjohnwu.magisk.base.viewmodel
|
||||
|
||||
import android.app.Activity
|
||||
import com.topjohnwu.magisk.extensions.doOnSubscribeUi
|
||||
import com.topjohnwu.magisk.model.events.BackPressEvent
|
||||
import com.topjohnwu.magisk.model.events.PermissionEvent
|
||||
import com.topjohnwu.magisk.model.events.ViewActionEvent
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
import com.topjohnwu.magisk.Info.isConnected as gIsConnected
|
||||
|
||||
|
||||
abstract class BaseViewModel(
|
||||
initialState: State = State.LOADING
|
||||
) : LoadingViewModel(initialState) {
|
||||
|
||||
val isConnected = object : KObservableField<Boolean>(gIsConnected.value, gIsConnected) {
|
||||
override fun get(): Boolean {
|
||||
return gIsConnected.value
|
||||
}
|
||||
}
|
||||
|
||||
fun withView(action: Activity.() -> Unit) {
|
||||
ViewActionEvent(action).publish()
|
||||
}
|
||||
|
||||
fun withPermissions(vararg permissions: String): Observable<Boolean> {
|
||||
val subject = PublishSubject.create<Boolean>()
|
||||
return subject.doOnSubscribeUi { PermissionEvent(permissions.toList(), subject).publish() }
|
||||
}
|
||||
|
||||
fun back() = BackPressEvent().publish()
|
||||
|
||||
}
|
@@ -1,78 +0,0 @@
|
||||
package com.topjohnwu.magisk.base.viewmodel
|
||||
|
||||
import androidx.databinding.Bindable
|
||||
import com.topjohnwu.magisk.BR
|
||||
import io.reactivex.*
|
||||
|
||||
abstract class LoadingViewModel(defaultState: State = State.LOADING) :
|
||||
StatefulViewModel<LoadingViewModel.State>(defaultState) {
|
||||
|
||||
val loading @Bindable get() = state == State.LOADING
|
||||
val loaded @Bindable get() = state == State.LOADED
|
||||
val loadingFailed @Bindable get() = state == State.LOADING_FAILED
|
||||
|
||||
@Deprecated(
|
||||
"Direct access is recommended since 0.2. This access method will be removed in 1.0",
|
||||
ReplaceWith("state = State.LOADING", "com.topjohnwu.magisk.base.viewmodel.LoadingViewModel.State"),
|
||||
DeprecationLevel.WARNING
|
||||
)
|
||||
fun setLoading() {
|
||||
state = State.LOADING
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Direct access is recommended since 0.2. This access method will be removed in 1.0",
|
||||
ReplaceWith("state = State.LOADED", "com.topjohnwu.magisk.base.viewmodel.LoadingViewModel.State"),
|
||||
DeprecationLevel.WARNING
|
||||
)
|
||||
fun setLoaded() {
|
||||
state = State.LOADED
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Direct access is recommended since 0.2. This access method will be removed in 1.0",
|
||||
ReplaceWith("state = State.LOADING_FAILED", "com.topjohnwu.magisk.base.viewmodel.LoadingViewModel.State"),
|
||||
DeprecationLevel.WARNING
|
||||
)
|
||||
fun setLoadingFailed() {
|
||||
state = State.LOADING_FAILED
|
||||
}
|
||||
|
||||
override fun notifyStateChanged() {
|
||||
notifyPropertyChanged(BR.loading)
|
||||
notifyPropertyChanged(BR.loaded)
|
||||
notifyPropertyChanged(BR.loadingFailed)
|
||||
}
|
||||
|
||||
enum class State {
|
||||
LOADED, LOADING, LOADING_FAILED
|
||||
}
|
||||
|
||||
//region Rx
|
||||
protected fun <T> Observable<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
|
||||
doOnSubscribe { viewModel.state = State.LOADING }
|
||||
.doOnError { viewModel.state = State.LOADING_FAILED }
|
||||
.doOnNext { if (allowFinishing) viewModel.state = State.LOADED }
|
||||
|
||||
protected fun <T> Single<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
|
||||
doOnSubscribe { viewModel.state = State.LOADING }
|
||||
.doOnError { viewModel.state = State.LOADING_FAILED }
|
||||
.doOnSuccess { if (allowFinishing) viewModel.state = State.LOADED }
|
||||
|
||||
protected fun <T> Maybe<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
|
||||
doOnSubscribe { viewModel.state = State.LOADING }
|
||||
.doOnError { viewModel.state = State.LOADING_FAILED }
|
||||
.doOnComplete { if (allowFinishing) viewModel.state = State.LOADED }
|
||||
.doOnSuccess { if (allowFinishing) viewModel.state = State.LOADED }
|
||||
|
||||
protected fun <T> Flowable<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
|
||||
doOnSubscribe { viewModel.state = State.LOADING }
|
||||
.doOnError { viewModel.state = State.LOADING_FAILED }
|
||||
.doOnNext { if (allowFinishing) viewModel.state = State.LOADED }
|
||||
|
||||
protected fun Completable.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
|
||||
doOnSubscribe { viewModel.state = State.LOADING }
|
||||
.doOnError { viewModel.state = State.LOADING_FAILED }
|
||||
.doOnComplete { if (allowFinishing) viewModel.state = State.LOADED }
|
||||
//endregion
|
||||
}
|
@@ -1,46 +0,0 @@
|
||||
package com.topjohnwu.magisk.base.viewmodel
|
||||
|
||||
import androidx.databinding.Observable
|
||||
import androidx.databinding.PropertyChangeRegistry
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
/**
|
||||
* Copy of [android.databinding.BaseObservable] which extends [ViewModel]
|
||||
*/
|
||||
abstract class ObservableViewModel : TeanityViewModel(), Observable {
|
||||
|
||||
@Transient
|
||||
private var callbacks: PropertyChangeRegistry? = null
|
||||
|
||||
@Synchronized
|
||||
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
|
||||
if (callbacks == null) {
|
||||
callbacks = PropertyChangeRegistry()
|
||||
}
|
||||
callbacks?.add(callback)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
|
||||
callbacks?.remove(callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies listeners that all properties of this instance have changed.
|
||||
*/
|
||||
@Synchronized
|
||||
fun notifyChange() {
|
||||
callbacks?.notifyCallbacks(this, 0, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies listeners that a specific property has changed. The getter for the property
|
||||
* that changes should be marked with [android.databinding.Bindable] to generate a field in
|
||||
* `BR` to be used as `fieldId`.
|
||||
*
|
||||
* @param fieldId The generated BR id for the Bindable field.
|
||||
*/
|
||||
fun notifyPropertyChanged(fieldId: Int) {
|
||||
callbacks?.notifyCallbacks(this, fieldId, null)
|
||||
}
|
||||
}
|
@@ -1,15 +0,0 @@
|
||||
package com.topjohnwu.magisk.base.viewmodel
|
||||
|
||||
abstract class StatefulViewModel<State : Enum<*>>(
|
||||
val defaultState: State
|
||||
) : ObservableViewModel() {
|
||||
|
||||
var state: State = defaultState
|
||||
set(value) {
|
||||
field = value
|
||||
notifyStateChanged()
|
||||
}
|
||||
|
||||
open fun notifyStateChanged() = Unit
|
||||
|
||||
}
|
@@ -1,33 +0,0 @@
|
||||
package com.topjohnwu.magisk.base.viewmodel
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.topjohnwu.magisk.model.events.SimpleViewEvent
|
||||
import com.topjohnwu.magisk.model.events.ViewEvent
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.disposables.Disposable
|
||||
|
||||
abstract class TeanityViewModel : ViewModel() {
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
private val _viewEvents = MutableLiveData<ViewEvent>()
|
||||
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
fun <Event : ViewEvent> Event.publish() {
|
||||
_viewEvents.value = this
|
||||
}
|
||||
|
||||
fun Int.publish() {
|
||||
_viewEvents.value = SimpleViewEvent(this)
|
||||
}
|
||||
|
||||
fun Disposable.add() {
|
||||
disposables.add(this)
|
||||
}
|
||||
}
|
@@ -1,26 +1,26 @@
|
||||
package com.topjohnwu.magisk
|
||||
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.room.Room
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.impl.WorkDatabase
|
||||
import androidx.work.impl.WorkDatabase_Impl
|
||||
import com.topjohnwu.magisk.data.database.RepoDatabase
|
||||
import com.topjohnwu.magisk.data.database.RepoDatabase_Impl
|
||||
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.magisk.utils.RootInit
|
||||
import com.topjohnwu.magisk.utils.updateConfig
|
||||
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() {
|
||||
|
||||
@@ -34,12 +34,13 @@ open class App() : Application() {
|
||||
Shell.Config.verboseLogging(BuildConfig.DEBUG)
|
||||
Shell.Config.addInitializers(RootInit::class.java)
|
||||
Shell.Config.setTimeout(2)
|
||||
Room.setFactory {
|
||||
when (it) {
|
||||
WorkDatabase::class.java -> WorkDatabase_Impl()
|
||||
RepoDatabase::class.java -> RepoDatabase_Impl()
|
||||
else -> null
|
||||
}
|
||||
FileProvider.callHandler = SuCallbackHandler
|
||||
|
||||
// Always log full stack trace with Timber
|
||||
Timber.plant(Timber.DebugTree())
|
||||
Thread.setDefaultUncaughtExceptionHandler { _, e ->
|
||||
Timber.e(e)
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +48,6 @@ open class App() : Application() {
|
||||
// Basic setup
|
||||
if (BuildConfig.DEBUG)
|
||||
MultiDex.install(base)
|
||||
Timber.plant(Timber.DebugTree())
|
||||
|
||||
// Some context magic
|
||||
val app: Application
|
||||
@@ -67,7 +67,7 @@ open class App() : Application() {
|
||||
androidContext(wrapped)
|
||||
modules(koinModules)
|
||||
}
|
||||
ResourceMgr.init(impl)
|
||||
ResMgr.init(impl)
|
||||
app.registerActivityLifecycleCallbacks(get<ActivityTracker>())
|
||||
WorkManager.initialize(impl.wrapJob(), androidx.work.Configuration.Builder().build())
|
||||
}
|
@@ -1,20 +1,22 @@
|
||||
package com.topjohnwu.magisk
|
||||
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.data.database.SettingsDao
|
||||
import com.topjohnwu.magisk.data.database.StringDao
|
||||
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.extensions.packageName
|
||||
import com.topjohnwu.magisk.model.preference.PreferenceModel
|
||||
import com.topjohnwu.magisk.utils.FingerprintHelper
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.ui.theme.Theme
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
||||
@@ -32,7 +34,7 @@ object Config : PreferenceModel, DBConfig {
|
||||
const val ROOT_ACCESS = "root_access"
|
||||
const val SU_MULTIUSER_MODE = "multiuser_mode"
|
||||
const val SU_MNT_NS = "mnt_ns"
|
||||
const val SU_FINGERPRINT = "su_fingerprint"
|
||||
const val SU_BIOMETRIC = "su_biometric"
|
||||
const val SU_MANAGER = "requester"
|
||||
const val KEYSTORE = "keystore"
|
||||
|
||||
@@ -46,9 +48,15 @@ object Config : PreferenceModel, DBConfig {
|
||||
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"
|
||||
@@ -98,13 +106,14 @@ object Config : PreferenceModel, DBConfig {
|
||||
}
|
||||
|
||||
private val defaultChannel =
|
||||
if (Utils.isCanary) {
|
||||
if (isCanaryVersion) {
|
||||
if (BuildConfig.DEBUG)
|
||||
Value.CANARY_DEBUG_CHANNEL
|
||||
else
|
||||
Value.CANARY_CHANNEL
|
||||
}
|
||||
else Value.DEFAULT_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)
|
||||
@@ -114,12 +123,20 @@ object Config : PreferenceModel, DBConfig {
|
||||
var suNotification by preferenceStrInt(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST)
|
||||
var updateChannel by preferenceStrInt(Key.UPDATE_CHANNEL, defaultChannel)
|
||||
|
||||
var darkTheme by preference(Key.DARK_THEME, true)
|
||||
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, "")
|
||||
@@ -127,7 +144,7 @@ object Config : PreferenceModel, DBConfig {
|
||||
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 suFingerprint by dbSettings(Key.SU_FINGERPRINT, false)
|
||||
var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
|
||||
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
||||
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
|
||||
|
||||
@@ -135,9 +152,18 @@ object Config : PreferenceModel, DBConfig {
|
||||
val downloadDirectory get() =
|
||||
Utils.ensureDownloadPath(downloadPath) ?: get<Context>().getExternalFilesDir(null)!!
|
||||
|
||||
fun initialize() = prefs.edit {
|
||||
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())
|
||||
|
||||
@@ -145,16 +171,18 @@ object Config : PreferenceModel, DBConfig {
|
||||
putString(Key.ROOT_ACCESS, rootMode.toString())
|
||||
putString(Key.SU_MNT_NS, suMntNamespaceMode.toString())
|
||||
putString(Key.SU_MULTIUSER_MODE, suMultiuserMode.toString())
|
||||
putBoolean(Key.SU_FINGERPRINT, FingerprintHelper.useFingerprint())
|
||||
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)
|
||||
val config = SuFile.open("/data/adb",
|
||||
Const.MANAGER_CONFIGS
|
||||
)
|
||||
if (config.exists()) runCatching {
|
||||
val input = SuFileInputStream(config).buffered()
|
||||
val input = SuFileInputStream(config)
|
||||
val parser = Xml.newPullParser()
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
|
||||
parser.setInput(input, "UTF-8")
|
||||
@@ -205,9 +233,10 @@ object Config : PreferenceModel, DBConfig {
|
||||
fun export() {
|
||||
// Flush prefs to disk
|
||||
prefs.edit().commit()
|
||||
val context = get<Context>(Protected)
|
||||
val xml = File(
|
||||
"${get<Context>(Protected).filesDir.parent}/shared_prefs",
|
||||
"${packageName}_preferences.xml"
|
||||
"${context.filesDir.parent}/shared_prefs",
|
||||
"${context.packageName}_preferences.xml"
|
||||
)
|
||||
Shell.su("cat $xml > /data/adb/${Const.MANAGER_CONFIGS}").exec()
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.os.Process
|
||||
import java.io.File
|
||||
@@ -6,7 +6,7 @@ import java.io.File
|
||||
object Const {
|
||||
|
||||
// Paths
|
||||
const val MAGISK_PATH = "/sbin/.magisk/img"
|
||||
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"
|
||||
@@ -23,8 +23,10 @@ object Const {
|
||||
val USER_ID = Process.myUid() / 100000
|
||||
|
||||
object Version {
|
||||
const val MIN_SUPPORT = 18000
|
||||
const val CONNECT_MODE = 20002
|
||||
const val MIN_VERSION = "v19.0"
|
||||
const val MIN_VERCODE = 19000
|
||||
const val CONNECT_MODE = 20100
|
||||
const val PROVIDER_CONNECT = 20102
|
||||
}
|
||||
|
||||
object ID {
|
||||
@@ -60,6 +62,7 @@ object Const {
|
||||
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"
|
@@ -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
|
||||
}
|
||||
}
|
@@ -1,19 +1,20 @@
|
||||
package com.topjohnwu.magisk.model.update
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import androidx.work.ListenableWorker
|
||||
import android.content.Context
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.magisk.base.DelegateWorker
|
||||
import com.topjohnwu.magisk.core.view.Notifications
|
||||
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.superuser.Shell
|
||||
|
||||
class UpdateCheckService : DelegateWorker() {
|
||||
class UpdateCheckService(context: Context, workerParams: WorkerParameters)
|
||||
: Worker(context, workerParams) {
|
||||
|
||||
private val magiskRepo: MagiskRepository by inject()
|
||||
|
||||
override fun doWork(): ListenableWorker.Result {
|
||||
override fun doWork(): Result {
|
||||
// Make sure shell initializer was ran
|
||||
Shell.getShell()
|
||||
return runCatching {
|
||||
@@ -22,9 +23,9 @@ class UpdateCheckService : DelegateWorker() {
|
||||
Notifications.managerUpdate(applicationContext)
|
||||
else if (Info.env.magiskVersionCode < Info.remote.magisk.versionCode)
|
||||
Notifications.magiskUpdate(applicationContext)
|
||||
ListenableWorker.Result.success()
|
||||
Result.success()
|
||||
}.getOrElse {
|
||||
ListenableWorker.Result.failure()
|
||||
Result.failure()
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,51 +1,26 @@
|
||||
package com.topjohnwu.magisk.base
|
||||
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 android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import com.topjohnwu.magisk.extensions.set
|
||||
import com.topjohnwu.magisk.model.events.EventHandler
|
||||
import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder
|
||||
import com.topjohnwu.magisk.utils.currentLocale
|
||||
import com.topjohnwu.magisk.wrap
|
||||
import kotlin.random.Random
|
||||
|
||||
typealias RequestCallback = BaseActivity<*, *>.(Int, Intent?) -> Unit
|
||||
typealias RequestCallback = BaseActivity.(Int, Intent?) -> Unit
|
||||
|
||||
abstract class BaseActivity<ViewModel : BaseViewModel, Binding : ViewDataBinding> :
|
||||
AppCompatActivity(), EventHandler {
|
||||
|
||||
protected lateinit var binding: Binding
|
||||
protected abstract val layoutRes: Int
|
||||
protected abstract val viewModel: ViewModel
|
||||
protected open val themeRes: Int = R.style.MagiskTheme
|
||||
protected open val snackbarView get() = binding.root
|
||||
abstract class BaseActivity : AppCompatActivity() {
|
||||
|
||||
private val resultCallbacks by lazy { SparseArrayCompat<RequestCallback>() }
|
||||
|
||||
init {
|
||||
val theme = if (Config.darkTheme) {
|
||||
AppCompatDelegate.MODE_NIGHT_YES
|
||||
} else {
|
||||
AppCompatDelegate.MODE_NIGHT_NO
|
||||
}
|
||||
AppCompatDelegate.setDefaultNightMode(theme)
|
||||
}
|
||||
|
||||
override fun applyOverrideConfiguration(config: Configuration?) {
|
||||
// Force applying our preferred local
|
||||
config?.setLocale(currentLocale)
|
||||
@@ -56,18 +31,6 @@ abstract class BaseActivity<ViewModel : BaseViewModel, Binding : ViewDataBinding
|
||||
super.attachBaseContext(base.wrap(false))
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTheme(themeRes)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
viewModel.viewEvents.observe(this, viewEventObserver)
|
||||
|
||||
binding = DataBindingUtil.setContentView<Binding>(this, layoutRes).apply {
|
||||
setVariable(BR.viewModel, viewModel)
|
||||
lifecycleOwner = this@BaseActivity
|
||||
}
|
||||
}
|
||||
|
||||
fun withPermissions(vararg permissions: String, builder: PermissionRequestBuilder.() -> Unit) {
|
||||
val request = PermissionRequestBuilder().apply(builder).build()
|
||||
val ungranted = permissions.filter {
|
||||
@@ -93,7 +56,7 @@ abstract class BaseActivity<ViewModel : BaseViewModel, Binding : ViewDataBinding
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
var success = true
|
||||
for (res in grantResults) {
|
||||
if (res != PackageManager.PERMISSION_GRANTED) {
|
||||
@@ -101,18 +64,18 @@ abstract class BaseActivity<ViewModel : BaseViewModel, Binding : ViewDataBinding
|
||||
break
|
||||
}
|
||||
}
|
||||
resultCallbacks[requestCode]?.apply {
|
||||
resultCallbacks[requestCode]?.also {
|
||||
resultCallbacks.remove(requestCode)
|
||||
invoke(this@BaseActivity, if (success) 1 else -1, null)
|
||||
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]?.apply {
|
||||
resultCallbacks[requestCode]?.also {
|
||||
resultCallbacks.remove(requestCode)
|
||||
invoke(this@BaseActivity, resultCode, data)
|
||||
it(this@BaseActivity, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,4 +84,9 @@ abstract class BaseActivity<ViewModel : BaseViewModel, Binding : ViewDataBinding
|
||||
startActivityForResult(intent, requestCode)
|
||||
}
|
||||
|
||||
override fun recreate() {
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
}
|
@@ -1,10 +1,10 @@
|
||||
package com.topjohnwu.magisk.base
|
||||
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.wrap
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import org.koin.core.KoinComponent
|
||||
|
||||
abstract class BaseReceiver : BroadcastReceiver(), KoinComponent {
|
@@ -1,8 +1,8 @@
|
||||
package com.topjohnwu.magisk.base
|
||||
package com.topjohnwu.magisk.core.base
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import com.topjohnwu.magisk.wrap
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import org.koin.core.KoinComponent
|
||||
|
||||
abstract class BaseService : Service(), KoinComponent {
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.base
|
||||
package com.topjohnwu.magisk.core.base
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Network
|
||||
@@ -10,7 +10,7 @@ import androidx.work.ListenableWorker
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import java.util.*
|
||||
|
||||
abstract class DelegateWorker {
|
||||
abstract class BaseWorkerWrapper {
|
||||
|
||||
private lateinit var worker: ListenableWorker
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
@@ -8,16 +8,19 @@ 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.intent
|
||||
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.ui.flash.FlashActivity
|
||||
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
|
||||
@@ -43,6 +46,7 @@ open class DownloadService : RemoteFileService() {
|
||||
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
|
||||
@@ -60,10 +64,14 @@ open class DownloadService : RemoteFileService() {
|
||||
subject: Manager,
|
||||
id: Int
|
||||
) {
|
||||
remove(id)
|
||||
when (subject.configuration) {
|
||||
is APK.Upgrade -> APKInstall.install(this, subject.file)
|
||||
is APK.Restore -> Unit
|
||||
Completable.fromAction {
|
||||
handleAPK(subject)
|
||||
}.subscribeK {
|
||||
remove(id)
|
||||
when (subject.configuration) {
|
||||
is APK.Upgrade -> APKInstall.install(this, subject.file)
|
||||
is APK.Restore -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,13 +122,15 @@ open class DownloadService : RemoteFileService() {
|
||||
|
||||
@Suppress("ReplaceSingleLineLet")
|
||||
private fun Notification.Builder.setContentIntent(intent: Intent) =
|
||||
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
||||
.let { setContentIntent(it) }
|
||||
setContentIntent(
|
||||
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
||||
)
|
||||
|
||||
@Suppress("ReplaceSingleLineLet")
|
||||
private fun Notification.Builder.addAction(icon: Int, title: Int, intent: Intent) =
|
||||
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
||||
.let { addAction(icon, getString(title), it) }
|
||||
addAction(icon, getString(title),
|
||||
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
||||
)
|
||||
|
||||
// ---
|
||||
|
||||
@@ -140,7 +150,7 @@ open class DownloadService : RemoteFileService() {
|
||||
inline operator fun invoke(context: Context, argBuilder: Builder.() -> Unit) {
|
||||
val app = context.applicationContext
|
||||
val builder = Builder().apply(argBuilder)
|
||||
val intent = app.intent(DownloadService::class.java).putExtra(ARG_URL, builder.subject)
|
||||
val intent = app.intent<DownloadService>().putExtra(ARG_URL, builder.subject)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
app.startForegroundService(intent)
|
@@ -1,11 +1,18 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import com.topjohnwu.magisk.*
|
||||
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.magisk.utils.PatchAPK
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import java.io.File
|
||||
|
||||
@@ -13,14 +20,14 @@ private fun RemoteFileService.patch(apk: File, id: Int) {
|
||||
if (packageName == BuildConfig.APPLICATION_ID)
|
||||
return
|
||||
|
||||
update(id) { notification ->
|
||||
notification.setProgress(0, 0, true)
|
||||
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, patched, packageName, applicationInfo.nonLocalizedLabel.toString())
|
||||
PatchAPK.patch(apk.path, patched.path, packageName, applicationInfo.nonLocalizedLabel)
|
||||
apk.delete()
|
||||
patched.renameTo(apk)
|
||||
}
|
||||
@@ -38,7 +45,7 @@ private fun RemoteFileService.upgrade(apk: File, id: Int) {
|
||||
patch(apk, id)
|
||||
} else {
|
||||
// Simply relaunch the app
|
||||
ProcessPhoenix.triggerRebirth(this)
|
||||
ProcessPhoenix.triggerRebirth(this, intent<ProcessPhoenix>())
|
||||
}
|
||||
} else {
|
||||
patch(apk, id)
|
||||
@@ -46,11 +53,11 @@ private fun RemoteFileService.upgrade(apk: File, id: Int) {
|
||||
}
|
||||
|
||||
private fun RemoteFileService.restore(apk: File, id: Int) {
|
||||
update(id) { notification ->
|
||||
notification.setProgress(0, 0, true)
|
||||
.setProgress(0, 0, true)
|
||||
.setContentTitle(getString(R.string.restore_img_msg))
|
||||
.setContentText("")
|
||||
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
|
||||
@@ -58,8 +65,8 @@ private fun RemoteFileService.restore(apk: File, id: Int) {
|
||||
Shell.su("pm install $apk && pm uninstall $packageName").exec()
|
||||
}
|
||||
|
||||
fun RemoteFileService.handleAPK(subject: DownloadSubject.Manager)
|
||||
= when (subject.configuration) {
|
||||
fun RemoteFileService.handleAPK(subject: DownloadSubject.Manager) =
|
||||
when (subject.configuration) {
|
||||
is Upgrade -> upgrade(subject.file, subject.hashCode())
|
||||
is Restore -> restore(subject.file, subject.hashCode())
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import com.topjohnwu.magisk.extensions.withStreams
|
||||
import java.io.File
|
||||
@@ -41,4 +41,4 @@ fun InputStream.toModule(file: File, installer: InputStream) {
|
||||
entry = zin.nextEntry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,22 +1,20 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import com.topjohnwu.magisk.base.BaseService
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
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 {
|
||||
|
||||
abstract val defaultNotification: Notification.Builder
|
||||
|
||||
private val hasNotifications get() = notifications.isNotEmpty()
|
||||
|
||||
private val notifications =
|
||||
Collections.synchronizedMap(mutableMapOf<Int, Notification.Builder>())
|
||||
private val notifications = Collections.synchronizedMap(HashMap<Int, Notification.Builder>())
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
@@ -24,22 +22,23 @@ abstract class NotificationService : BaseService(), KoinComponent {
|
||||
notifications.clear()
|
||||
}
|
||||
|
||||
abstract fun createNotification(): Notification.Builder
|
||||
|
||||
// --
|
||||
|
||||
fun update(
|
||||
id: Int,
|
||||
body: (Notification.Builder) -> Unit = {}
|
||||
) {
|
||||
val notification = notifications.getOrPut(id) { defaultNotification }
|
||||
|
||||
notify(id, notification.also(body).build())
|
||||
|
||||
if (notifications.size == 1) {
|
||||
val wasEmpty = notifications.isEmpty()
|
||||
val notification = notifications.getOrPut(id, ::createNotification).also(body)
|
||||
if (wasEmpty)
|
||||
updateForeground()
|
||||
}
|
||||
else
|
||||
notify(id, notification.build())
|
||||
}
|
||||
|
||||
protected fun finishNotify(
|
||||
protected fun lastNotify(
|
||||
id: Int,
|
||||
editBody: (Notification.Builder) -> Notification.Builder? = { null }
|
||||
) : Int {
|
||||
@@ -57,6 +56,11 @@ abstract class NotificationService : BaseService(), KoinComponent {
|
||||
return newId
|
||||
}
|
||||
|
||||
protected fun remove(id: Int) = notifications.remove(id).also {
|
||||
cancel(id)
|
||||
updateForeground()
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
private fun notify(id: Int, notification: Notification) {
|
||||
@@ -67,16 +71,13 @@ abstract class NotificationService : BaseService(), KoinComponent {
|
||||
Notifications.mgr.cancel(id)
|
||||
}
|
||||
|
||||
protected fun remove(id: Int) = notifications.remove(id).also {
|
||||
cancel(id)
|
||||
updateForeground()
|
||||
}
|
||||
|
||||
private fun updateForeground() {
|
||||
if (hasNotifications)
|
||||
startForeground(notifications.keys.first(), notifications.values.first().build())
|
||||
else
|
||||
if (hasNotifications) {
|
||||
val first = notifications.entries.first()
|
||||
startForeground(first.key, first.value.build())
|
||||
} else {
|
||||
stopForeground(true)
|
||||
}
|
||||
}
|
||||
|
||||
// --
|
@@ -1,22 +1,26 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
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.*
|
||||
import com.topjohnwu.magisk.utils.ProgressInputStream
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
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
|
||||
|
||||
@@ -24,26 +28,20 @@ abstract class RemoteFileService : NotificationService() {
|
||||
|
||||
val service: GithubRawServices by inject()
|
||||
|
||||
override val defaultNotification
|
||||
get() = Notifications.progress(this, "")
|
||||
|
||||
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) }
|
||||
.doOnSubscribe { update(subject.hashCode()) { it.setContentTitle(subject.title) } }
|
||||
.subscribeK(onError = {
|
||||
Timber.e(it)
|
||||
finishNotify(subject.hashCode()) { notification ->
|
||||
notification.setContentText(getString(R.string.download_file_error))
|
||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||
.setOngoing(false)
|
||||
}
|
||||
failNotify(subject)
|
||||
}) {
|
||||
val newId = finishNotify(subject)
|
||||
if (get<Activity>() !is NullActivity) {
|
||||
@@ -53,47 +51,55 @@ abstract class RemoteFileService : NotificationService() {
|
||||
|
||||
private fun checkExisting(subject: DownloadSubject) = Completable.fromAction {
|
||||
check(subject is Magisk) { "Download cache is disabled" }
|
||||
|
||||
subject.file.also {
|
||||
check(it.exists() && ShellUtils.checkSum("MD5", it, subject.magisk.md5)) {
|
||||
"The given file does not match checksum"
|
||||
}
|
||||
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.toStream(subject.hashCode()) }
|
||||
.map { it.toProgressStream(subject) }
|
||||
.flatMapCompletable { stream ->
|
||||
when (subject) {
|
||||
is Module -> service.fetchInstaller()
|
||||
.doOnSuccess { stream.toModule(subject.file, it.byteStream()) }
|
||||
.ignoreElement()
|
||||
.doOnSuccess { stream.toModule(subject.file, it.byteStream()) }
|
||||
.ignoreElement()
|
||||
else -> Completable.fromAction { stream.writeTo(subject.file) }
|
||||
}
|
||||
}.doOnComplete {
|
||||
if (subject is Manager)
|
||||
handleAPK(subject)
|
||||
}
|
||||
|
||||
private fun ResponseBody.toStream(id: Int): InputStream {
|
||||
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))
|
||||
.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 finishNotify(subject: DownloadSubject) = finishNotify(subject.hashCode()) {
|
||||
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)
|
||||
@@ -111,8 +117,19 @@ abstract class RemoteFileService : NotificationService() {
|
||||
protected abstract fun Notification.Builder.addActions(subject: DownloadSubject)
|
||||
: Notification.Builder
|
||||
|
||||
companion object {
|
||||
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()
|
@@ -1,13 +1,12 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.data.database.base.*
|
||||
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 com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||
import com.topjohnwu.magisk.model.entity.toMap
|
||||
import com.topjohnwu.magisk.model.entity.toPolicy
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@@ -16,7 +15,7 @@ class PolicyDao(
|
||||
private val context: Context
|
||||
) : BaseDao() {
|
||||
|
||||
override val table: String = DatabaseDefinition.Table.POLICY
|
||||
override val table: String = Table.POLICY
|
||||
|
||||
fun deleteOutdated(
|
||||
nowSeconds: Long = TimeUnit.MILLISECONDS.toSeconds(now)
|
||||
@@ -72,4 +71,4 @@ class PolicyDao(
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -1,27 +1,17 @@
|
||||
package com.topjohnwu.magisk.data.database.base
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
import androidx.annotation.StringDef
|
||||
import com.topjohnwu.magisk.data.database.base.Order.Companion.ASC
|
||||
import com.topjohnwu.magisk.data.database.base.Order.Companion.DESC
|
||||
|
||||
interface MagiskQueryBuilder {
|
||||
class Query(private val _query: String) {
|
||||
val query get() = "magisk --sqlite '$_query'"
|
||||
|
||||
val requestType: String
|
||||
var table: String
|
||||
|
||||
companion object {
|
||||
inline operator fun <reified Builder : MagiskQueryBuilder> invoke(builder: Builder.() -> Unit): MagiskQuery =
|
||||
Builder::class.java.newInstance()
|
||||
.apply(builder)
|
||||
.toString()
|
||||
.let {
|
||||
MagiskQuery(it)
|
||||
}
|
||||
interface Builder {
|
||||
val requestType: String
|
||||
var table: String
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Delete : MagiskQueryBuilder {
|
||||
class Delete : Query.Builder {
|
||||
override val requestType: String = "DELETE FROM"
|
||||
override var table = ""
|
||||
|
||||
@@ -36,7 +26,7 @@ class Delete : MagiskQueryBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
class Select : MagiskQueryBuilder {
|
||||
class Select : Query.Builder {
|
||||
override val requestType: String get() = "SELECT $fields FROM"
|
||||
override lateinit var table: String
|
||||
|
||||
@@ -69,7 +59,7 @@ class Replace : Insert() {
|
||||
override val requestType: String = "REPLACE INTO"
|
||||
}
|
||||
|
||||
open class Insert : MagiskQueryBuilder {
|
||||
open class Insert : Query.Builder {
|
||||
override val requestType: String = "INSERT INTO"
|
||||
override lateinit var table: String
|
||||
|
||||
@@ -137,19 +127,11 @@ class Condition {
|
||||
}
|
||||
}
|
||||
|
||||
class Order {
|
||||
|
||||
@set:OrderStrict
|
||||
var order = DESC
|
||||
var field = ""
|
||||
|
||||
companion object {
|
||||
const val ASC = "ASC"
|
||||
const val DESC = "DESC"
|
||||
}
|
||||
|
||||
object Order {
|
||||
const val ASC = "ASC"
|
||||
const val DESC = "DESC"
|
||||
}
|
||||
|
||||
@StringDef(ASC, DESC)
|
||||
@StringDef(Order.ASC, Order.DESC)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class OrderStrict
|
||||
annotation class OrderStrict
|
@@ -1,10 +1,8 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import com.topjohnwu.magisk.data.database.base.*
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
class SettingsDao : BaseDao() {
|
||||
|
||||
override val table = DatabaseDefinition.Table.SETTINGS
|
||||
override val table = Table.SETTINGS
|
||||
|
||||
fun delete(key: String) = query<Delete> {
|
||||
condition { equals("key", key) }
|
||||
@@ -19,4 +17,4 @@ class SettingsDao : BaseDao() {
|
||||
condition { equals("key", key) }
|
||||
}.map { it.firstOrNull()?.values?.firstOrNull()?.toIntOrNull() ?: default }
|
||||
|
||||
}
|
||||
}
|
@@ -1,10 +1,8 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import com.topjohnwu.magisk.data.database.base.*
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
class StringDao : BaseDao() {
|
||||
|
||||
override val table = DatabaseDefinition.Table.STRINGS
|
||||
override val table = Table.STRINGS
|
||||
|
||||
fun delete(key: String) = query<Delete> {
|
||||
condition { equals("key", key) }
|
||||
@@ -19,4 +17,4 @@ class StringDao : BaseDao() {
|
||||
condition { equals("key", key) }
|
||||
}.map { it.firstOrNull()?.values?.firstOrNull() ?: default }
|
||||
|
||||
}
|
||||
}
|
@@ -1,17 +1,17 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
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
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.INTERACTIVE
|
||||
|
||||
|
||||
data class MagiskPolicy(
|
||||
val uid: Int,
|
||||
var uid: Int,
|
||||
val packageName: String,
|
||||
val appName: String,
|
||||
val policy: Int = INTERACTIVE,
|
||||
val until: Long = -1L,
|
||||
var policy: Int = INTERACTIVE,
|
||||
var until: Long = -1L,
|
||||
val logging: Boolean = true,
|
||||
val notification: Boolean = true,
|
||||
val applicationInfo: ApplicationInfo
|
||||
@@ -38,7 +38,7 @@ fun MagiskPolicy.toMap() = mapOf(
|
||||
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, 0)
|
||||
val info = pm.getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES)
|
||||
|
||||
if (info.uid != uid)
|
||||
throw PackageManager.NameNotFoundException()
|
||||
@@ -56,14 +56,15 @@ fun Map<String, String>.toPolicy(pm: PackageManager): MagiskPolicy {
|
||||
}
|
||||
|
||||
@Throws(PackageManager.NameNotFoundException::class)
|
||||
fun Int.toPolicy(pm: PackageManager): MagiskPolicy {
|
||||
fun Int.toPolicy(pm: PackageManager, policy: Int = INTERACTIVE): MagiskPolicy {
|
||||
val pkg = pm.getPackagesForUid(this)?.firstOrNull()
|
||||
?: throw PackageManager.NameNotFoundException()
|
||||
val info = pm.getApplicationInfo(pkg, 0)
|
||||
val info = pm.getApplicationInfo(pkg, PackageManager.GET_UNINSTALLED_PACKAGES)
|
||||
return MagiskPolicy(
|
||||
uid = this,
|
||||
uid = info.uid,
|
||||
packageName = pkg,
|
||||
policy = policy,
|
||||
applicationInfo = info,
|
||||
appName = info.loadLabel(pm).toString()
|
||||
appName = info.getLabel(pm)
|
||||
)
|
||||
}
|
||||
}
|
@@ -1,10 +1,10 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
package com.topjohnwu.magisk.core.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import se.ansman.kotshi.JsonSerializable
|
||||
|
||||
@JsonSerializable
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class UpdateInfo(
|
||||
val app: ManagerJson = ManagerJson(),
|
||||
val uninstaller: UninstallerJson = UninstallerJson(),
|
||||
@@ -12,12 +12,12 @@ data class UpdateInfo(
|
||||
val stub: StubJson = StubJson()
|
||||
)
|
||||
|
||||
@JsonSerializable
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class UninstallerJson(
|
||||
val link: String = ""
|
||||
)
|
||||
|
||||
@JsonSerializable
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MagiskJson(
|
||||
val version: String = "",
|
||||
val versionCode: Int = -1,
|
||||
@@ -27,7 +27,7 @@ data class MagiskJson(
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
@JsonSerializable
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ManagerJson(
|
||||
val version: String = "",
|
||||
val versionCode: Int = -1,
|
||||
@@ -35,7 +35,7 @@ data class ManagerJson(
|
||||
val note: String = ""
|
||||
) : Parcelable
|
||||
|
||||
@JsonSerializable
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StubJson(
|
||||
val versionCode: Int = -1,
|
||||
val link: String = ""
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.model.entity.module
|
||||
package com.topjohnwu.magisk.core.model.module
|
||||
|
||||
abstract class BaseModule : Comparable<BaseModule> {
|
||||
abstract var id: String
|
@@ -1,7 +1,7 @@
|
||||
package com.topjohnwu.magisk.model.entity.module
|
||||
package com.topjohnwu.magisk.core.model.module
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
|
||||
@@ -13,17 +13,21 @@ class Module(path: String) : BaseModule() {
|
||||
override var versionCode: Int = -1
|
||||
override var description: String = ""
|
||||
|
||||
private val removeFile: SuFile = SuFile(path, "remove")
|
||||
private val disableFile: SuFile = SuFile(path, "disable")
|
||||
private val updateFile: SuFile = SuFile(path, "update")
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -31,8 +35,10 @@ class Module(path: String) : BaseModule() {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -54,6 +60,8 @@ class Module(path: String) : BaseModule() {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val PERSIST = "/sbin/.magisk/mirror/persist/magisk"
|
||||
|
||||
@WorkerThread
|
||||
fun loadModules(): List<Module> {
|
||||
val moduleList = mutableListOf<Module>()
|
||||
@@ -65,7 +73,7 @@ class Module(path: String) : BaseModule() {
|
||||
val module = Module(Const.MAGISK_PATH + "/" + file.name)
|
||||
moduleList.add(module)
|
||||
}
|
||||
return moduleList.sortedBy { it.name }
|
||||
return moduleList.sortedBy { it.name.toLowerCase() }
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,9 +1,9 @@
|
||||
package com.topjohnwu.magisk.model.entity.module
|
||||
package com.topjohnwu.magisk.core.model.module
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.topjohnwu.magisk.Const
|
||||
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
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,12 +1,10 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.core.su
|
||||
|
||||
import android.net.LocalSocket
|
||||
import android.net.LocalSocketAddress
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import androidx.collection.ArrayMap
|
||||
import timber.log.Timber
|
||||
import java.io.*
|
||||
import java.nio.charset.Charset
|
||||
|
||||
abstract class SuConnector @Throws(IOException::class)
|
||||
protected constructor(name: String) {
|
||||
@@ -21,24 +19,23 @@ protected constructor(name: String) {
|
||||
input = DataInputStream(BufferedInputStream(socket.inputStream))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun readString(): String {
|
||||
val len = input.readInt()
|
||||
val buf = ByteArray(len)
|
||||
input.readFully(buf)
|
||||
return String(buf, Charset.forName("UTF-8"))
|
||||
return String(buf, Charsets.UTF_8)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun readSocketInput(): Bundle {
|
||||
val bundle = Bundle()
|
||||
fun readRequest(): Map<String, String> {
|
||||
val ret = ArrayMap<String, String>()
|
||||
while (true) {
|
||||
val name = readString()
|
||||
if (TextUtils.equals(name, "eof"))
|
||||
if (name == "eof")
|
||||
break
|
||||
bundle.putString(name, readString())
|
||||
ret[name] = readString()
|
||||
}
|
||||
return bundle
|
||||
return ret
|
||||
}
|
||||
|
||||
fun response() {
|
@@ -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)
|
||||
|
||||
}
|
@@ -1,13 +1,13 @@
|
||||
package com.topjohnwu.magisk.tasks
|
||||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.topjohnwu.magisk.Const
|
||||
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.magisk.utils.unzip
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Single
|
||||
import java.io.File
|
||||
@@ -18,7 +18,7 @@ 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 {
|
||||
@@ -94,6 +94,4 @@ abstract class FlashZip(
|
||||
.subscribeK(onError = { onResult(false) }) { onResult(it) }
|
||||
.let { Unit } // ignores result disposable
|
||||
|
||||
|
||||
protected abstract fun onResult(success: Boolean)
|
||||
}
|
@@ -1,10 +1,9 @@
|
||||
package com.topjohnwu.magisk.model.flash
|
||||
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.magisk.tasks.FlashZip
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
|
||||
@@ -28,16 +27,7 @@ sealed class Flashing(
|
||||
console: MutableList<String>,
|
||||
log: MutableList<String>,
|
||||
resultListener: FlashResultListener
|
||||
) : Flashing(uri, console, log, resultListener) {
|
||||
|
||||
override fun onResult(success: Boolean) {
|
||||
if (success) {
|
||||
//Utils.loadModules()
|
||||
}
|
||||
super.onResult(success)
|
||||
}
|
||||
|
||||
}
|
||||
) : Flashing(uri, console, log, resultListener)
|
||||
|
||||
class Uninstall(
|
||||
uri: Uri,
|
||||
@@ -58,4 +48,4 @@ sealed class Flashing(
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -1,13 +1,13 @@
|
||||
package com.topjohnwu.magisk.tasks
|
||||
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.MainThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.Info
|
||||
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.*
|
||||
@@ -32,19 +32,19 @@ import java.nio.ByteBuffer
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
abstract class MagiskInstaller {
|
||||
abstract class MagiskInstallImpl : FlashResultListener {
|
||||
|
||||
protected lateinit var srcBoot: String
|
||||
protected lateinit var destFile: File
|
||||
protected lateinit var installDir: File
|
||||
protected lateinit var zipUri: Uri
|
||||
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()
|
||||
private val context: Context by inject()
|
||||
protected val context: Context by inject()
|
||||
|
||||
protected constructor() {
|
||||
console = NOPList.getInstance()
|
||||
@@ -60,7 +60,7 @@ abstract class MagiskInstaller {
|
||||
installDir.mkdirs()
|
||||
}
|
||||
|
||||
protected fun findImage(): Boolean {
|
||||
private fun findImage(): Boolean {
|
||||
srcBoot = "find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
|
||||
if (srcBoot.isEmpty()) {
|
||||
console.add("! Unable to detect target image")
|
||||
@@ -70,7 +70,7 @@ abstract class MagiskInstaller {
|
||||
return true
|
||||
}
|
||||
|
||||
protected fun findSecondaryImage(): Boolean {
|
||||
private fun findSecondaryImage(): Boolean {
|
||||
val slot = "echo \$SLOT".fsh()
|
||||
val target = if (slot == "_a") "_b" else "_a"
|
||||
console.add("- Target slot: $target")
|
||||
@@ -87,7 +87,7 @@ abstract class MagiskInstaller {
|
||||
return true
|
||||
}
|
||||
|
||||
protected fun extractZip(): Boolean {
|
||||
private fun extractZip(): Boolean {
|
||||
val arch: String
|
||||
arch = if (Build.VERSION.SDK_INT >= 21) {
|
||||
val abis = listOf(*Build.SUPPORTED_ABIS)
|
||||
@@ -164,7 +164,7 @@ abstract class MagiskInstaller {
|
||||
FileOutputStream(extract).use { tarIn.copyTo(it) }
|
||||
if (name.contains(".lz4")) {
|
||||
console.add("-- Decompressing: $name")
|
||||
"./magiskboot --decompress $extract".sh()
|
||||
"./magiskboot decompress $extract".sh()
|
||||
}
|
||||
} else if (entry.name.contains("vbmeta.img")) {
|
||||
vbmeta = true
|
||||
@@ -189,9 +189,9 @@ abstract class MagiskInstaller {
|
||||
srcBoot = recovery.path
|
||||
// Repack boot image to prevent restore
|
||||
arrayOf(
|
||||
"./magiskboot --unpack boot.img",
|
||||
"./magiskboot --repack boot.img",
|
||||
"./magiskboot --cleanup",
|
||||
"./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()))
|
||||
@@ -208,7 +208,7 @@ abstract class MagiskInstaller {
|
||||
}
|
||||
}
|
||||
|
||||
protected fun handleFile(uri: Uri): Boolean {
|
||||
private fun handleFile(uri: Uri): Boolean {
|
||||
try {
|
||||
context.readUri(uri).buffered().use {
|
||||
it.mark(500)
|
||||
@@ -238,7 +238,7 @@ abstract class MagiskInstaller {
|
||||
return true
|
||||
}
|
||||
|
||||
protected fun patchBoot(): Boolean {
|
||||
private fun patchBoot(): Boolean {
|
||||
var isSigned = false
|
||||
try {
|
||||
SuFileInputStream(srcBoot).use {
|
||||
@@ -259,7 +259,7 @@ abstract class MagiskInstaller {
|
||||
}
|
||||
|
||||
val job = Shell.sh(
|
||||
"./magiskboot --cleanup",
|
||||
"./magiskboot cleanup",
|
||||
"mv bin/busybox busybox",
|
||||
"rm -rf magisk.apk bin boot.img update-binary",
|
||||
"cd /")
|
||||
@@ -284,21 +284,23 @@ abstract class MagiskInstaller {
|
||||
return true
|
||||
}
|
||||
|
||||
protected fun flashBoot(): Boolean {
|
||||
private fun flashBoot(): Boolean {
|
||||
if (!"direct_install $installDir $srcBoot".sh().isSuccess)
|
||||
return false
|
||||
if (!Info.keepVerity)
|
||||
"patch_dtbo_image".sh()
|
||||
arrayOf(
|
||||
"(KEEPVERITY=${Info.keepVerity} patch_dtb_partitions)",
|
||||
"run_migrations"
|
||||
).sh()
|
||||
return true
|
||||
}
|
||||
|
||||
protected fun storeBoot(): Boolean {
|
||||
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()))
|
||||
if (srcBoot.contains("recovery")) "recovery.img" else "boot.img",
|
||||
patched.length()))
|
||||
tarOut = null
|
||||
it
|
||||
} ?: destFile.outputStream()
|
||||
@@ -318,7 +320,7 @@ abstract class MagiskInstaller {
|
||||
return true
|
||||
}
|
||||
|
||||
protected fun postOTA(): Boolean {
|
||||
private fun postOTA(): Boolean {
|
||||
val bootctl = SuFile("/data/adb/bootctl")
|
||||
try {
|
||||
withStreams(service.fetchBootctl().blockingGet().byteStream(), bootctl.suOutputStream()) {
|
||||
@@ -343,12 +345,24 @@ abstract class MagiskInstaller {
|
||||
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
|
||||
|
||||
@MainThread
|
||||
protected abstract fun onResult(success: 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
|
||||
}
|
||||
|
||||
}
|
@@ -1,20 +1,17 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
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.Config
|
||||
import com.topjohnwu.magisk.di.koinModules
|
||||
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 com.topjohnwu.superuser.internal.InternalUtils
|
||||
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 org.koin.core.context.GlobalContext
|
||||
import org.koin.core.context.startKoin
|
||||
import timber.log.Timber
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.math.BigInteger
|
||||
import java.security.KeyPairGenerator
|
||||
@@ -32,13 +29,12 @@ private interface CertKeyProvider {
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
class Keygen: CertKeyProvider {
|
||||
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 ALPHANUM = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
private const val BASE64_FLAG = Base64.NO_PADDING or Base64.NO_WRAP
|
||||
}
|
||||
|
||||
@@ -50,10 +46,14 @@ class Keygen: CertKeyProvider {
|
||||
|
||||
private val provider: CertKeyProvider
|
||||
|
||||
inner class KeyStoreProvider : 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 }
|
||||
override val key by lazy { ks.getKey(
|
||||
ALIAS,
|
||||
PASSWORD
|
||||
) as PrivateKey }
|
||||
}
|
||||
|
||||
class TestProvider : CertKeyProvider {
|
||||
@@ -66,9 +66,6 @@ class Keygen: CertKeyProvider {
|
||||
}
|
||||
|
||||
init {
|
||||
// This object could possibly be accessed from an external app
|
||||
// Get context from reflection into Android's framework
|
||||
val context = InternalUtils.getContext()
|
||||
val pm = context.packageManager
|
||||
val info = pm.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES)
|
||||
val sig = info.signatures[0]
|
||||
@@ -100,21 +97,17 @@ class Keygen: CertKeyProvider {
|
||||
}
|
||||
|
||||
private fun init(): KeyStore {
|
||||
GlobalContext.getOrNull() ?: {
|
||||
// Invoked externally, do some basic initialization
|
||||
startKoin {
|
||||
modules(koinModules)
|
||||
}
|
||||
Timber.plant(Timber.DebugTree())
|
||||
}()
|
||||
|
||||
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)
|
||||
GZIPInputStream(Base64.decode(raw,
|
||||
BASE64_FLAG
|
||||
).inputStream()).use {
|
||||
ks.load(it,
|
||||
PASSWORD
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,14 +120,20 @@ class Keygen: CertKeyProvider {
|
||||
val dname = X500Name("CN=${randomString()}")
|
||||
val builder = JcaX509v3CertificateBuilder(dname, BigInteger(160, Random()),
|
||||
start.time, end.time, dname, kp.public)
|
||||
val signer = JcaContentSignerBuilder("SHA256WithRSA").build(kp.private)
|
||||
val signer = JcaContentSignerBuilder("SHA1WithRSA").build(kp.private)
|
||||
val cert = JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
||||
|
||||
// Store them into keystore
|
||||
ks.setKeyEntry(ALIAS, kp.private, PASSWORD, arrayOf(cert))
|
||||
ks.setKeyEntry(
|
||||
ALIAS, kp.private,
|
||||
PASSWORD, arrayOf(cert))
|
||||
val bytes = ByteArrayOutputStream()
|
||||
GZIPOutputStream(Base64OutputStream(bytes, BASE64_FLAG)).use {
|
||||
ks.store(it, PASSWORD)
|
||||
GZIPOutputStream(Base64OutputStream(bytes,
|
||||
BASE64_FLAG
|
||||
)).use {
|
||||
ks.store(it,
|
||||
PASSWORD
|
||||
)
|
||||
}
|
||||
Config.keyStoreRaw = bytes.toString("UTF-8")
|
||||
|
@@ -1,13 +1,16 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.topjohnwu.magisk.utils
|
||||
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 com.topjohnwu.magisk.Config
|
||||
import android.util.DisplayMetrics
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.ResourceMgr
|
||||
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
|
||||
@@ -22,41 +25,37 @@ val defaultLocale: Locale = Locale.getDefault()
|
||||
|
||||
val availableLocales = Single.fromCallable {
|
||||
val compareId = R.string.app_changelog
|
||||
val config = ResourceMgr.resource.configuration
|
||||
val metrics = ResourceMgr.resource.displayMetrics
|
||||
val res = Resources(ResourceMgr.resource.assets, metrics, config)
|
||||
|
||||
val locales = mutableListOf<Locale>().apply {
|
||||
// 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(Locale.ENGLISH)
|
||||
add("en")
|
||||
|
||||
// Add some special locales
|
||||
add(Locale.TAIWAN)
|
||||
add(Locale("pt", "BR"))
|
||||
add("zh-TW")
|
||||
add("pt-BR")
|
||||
|
||||
// Other locales
|
||||
val otherLocales = ResourceMgr.resource.assets.locales
|
||||
.map { it.langTagToLocale() }
|
||||
.distinctBy {
|
||||
config.setLocale(it)
|
||||
res.updateConfiguration(config, metrics)
|
||||
res.getString(compareId)
|
||||
}
|
||||
|
||||
addAll(otherLocales)
|
||||
// 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).toLowerCase(a)
|
||||
.compareTo(b.getDisplayName(b).toLowerCase(b))
|
||||
a.getDisplayName(a).compareTo(b.getDisplayName(b), true)
|
||||
})
|
||||
|
||||
config.setLocale(defaultLocale)
|
||||
res.updateConfiguration(config, metrics)
|
||||
val defName = res.getString(R.string.system_default)
|
||||
|
||||
// Restore back to current locale
|
||||
config.setLocale(currentLocale)
|
||||
res.updateConfiguration(config, metrics)
|
||||
|
||||
Pair(locales, defName)
|
||||
}.map { (locales, defName) ->
|
||||
val names = ArrayList<String>(locales.size + 1)
|
||||
@@ -85,5 +84,5 @@ fun refreshLocale() {
|
||||
else -> localeConfig.langTagToLocale()
|
||||
}
|
||||
Locale.setDefault(currentLocale)
|
||||
ResourceMgr.resource.updateConfig()
|
||||
ResMgr.resource.updateConfig()
|
||||
}
|
@@ -1,19 +1,22 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
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.*
|
||||
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.DynamicClassLoader
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.extensions.writeTo
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.signing.JarMap
|
||||
import com.topjohnwu.signing.SignAPK
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Single
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
@@ -23,14 +26,15 @@ import java.security.SecureRandom
|
||||
|
||||
object PatchAPK {
|
||||
|
||||
private const val LOWERALPHA = "abcdefghijklmnopqrstuvwxyz"
|
||||
private val UPPERALPHA = LOWERALPHA.toUpperCase()
|
||||
private val ALPHA = LOWERALPHA + UPPERALPHA
|
||||
private const val ALPHA = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
private const val DIGITS = "0123456789"
|
||||
private val ALPHANUM = ALPHA + DIGITS
|
||||
private val ALPHANUMDOTS = "$ALPHANUM............"
|
||||
const val ALPHANUM = ALPHA + DIGITS
|
||||
private const val ALPHANUMDOTS = "$ALPHANUM............"
|
||||
|
||||
private fun genPackageName(prefix: String, length: Int): String {
|
||||
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
|
||||
@@ -46,10 +50,10 @@ object PatchAPK {
|
||||
builder.append(next)
|
||||
prev = next
|
||||
}
|
||||
return builder.toString()
|
||||
return builder
|
||||
}
|
||||
|
||||
private fun findAndPatch(xml: ByteArray, from: String, to: String): Boolean {
|
||||
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()
|
||||
@@ -68,7 +72,7 @@ object PatchAPK {
|
||||
if (offList.isEmpty())
|
||||
return false
|
||||
|
||||
val toBuf = to.toCharArray().copyOf(from.length)
|
||||
val toBuf = to.toString().toCharArray().copyOf(from.length)
|
||||
for (off in offList) {
|
||||
buf.position(off)
|
||||
buf.put(toBuf)
|
||||
@@ -76,17 +80,40 @@ object PatchAPK {
|
||||
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 {
|
||||
// If not running as stub, and we are compatible with stub, use stub
|
||||
val src = if (!isRunningAsStub && SDK_INT >= 28 && Info.env.connectionMode == 3) {
|
||||
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>()
|
||||
runCatching {
|
||||
try {
|
||||
svc.fetchFile(Info.remote.stub.link).blockingGet().byteStream().use {
|
||||
it.writeTo(stub)
|
||||
}
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
stub.path
|
||||
@@ -96,7 +123,7 @@ object PatchAPK {
|
||||
|
||||
// Generate a new random package name and signature
|
||||
val repack = File(context.cacheDir, "patched.apk")
|
||||
val pkg = genPackageName("com.", BuildConfig.APPLICATION_ID.length)
|
||||
val pkg = genPackageName("com.", APP_ID.length)
|
||||
Config.keyStoreRaw = ""
|
||||
|
||||
if (!patch(src, repack.path, pkg, label))
|
||||
@@ -107,69 +134,22 @@ object PatchAPK {
|
||||
if (!Shell.su("force_pm_install $repack").exec().isSuccess)
|
||||
return false
|
||||
|
||||
Config.suManager = pkg
|
||||
Config.suManager = pkg.toString()
|
||||
Config.export()
|
||||
Shell.su("pm uninstall ${BuildConfig.APPLICATION_ID}").submit()
|
||||
Shell.su("pm uninstall $APP_ID").submit()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun patch(apk: String, out: String, pkg: String, label: String = "Manager"): Boolean {
|
||||
try {
|
||||
val jar = JarMap(apk)
|
||||
val je = jar.getJarEntry(Const.ANDROID_MANIFEST)
|
||||
val xml = jar.getRawData(je)
|
||||
|
||||
if (!findAndPatch(xml, BuildConfig.APPLICATION_ID, pkg) ||
|
||||
!findAndPatch(xml, "Magisk Manager", label))
|
||||
return false
|
||||
|
||||
// Write apk changes
|
||||
jar.getOutputStream(je).write(xml)
|
||||
val keys = Keygen()
|
||||
SignAPK.sign(keys.cert, keys.key, jar, FileOutputStream(out).buffered())
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun patch(apk: File, out: File, pkg: String, label: String): Boolean {
|
||||
try {
|
||||
if (apk.length() < 1 shl 18) {
|
||||
// APK is smaller than 256K, must be stub
|
||||
return patch(apk.path, out.path, pkg, label)
|
||||
}
|
||||
|
||||
// Try using the new APK to patch itself
|
||||
val loader = DynamicClassLoader(apk)
|
||||
val cls = loader.loadClass("a.a")
|
||||
|
||||
for (m in cls.declaredMethods) {
|
||||
val pars = m.parameterTypes
|
||||
if (pars.size == 4 && pars[0] == String::class.java) {
|
||||
return m.invoke(null, apk.path, out.path, pkg, label) as Boolean
|
||||
}
|
||||
}
|
||||
throw Exception("No matching method found")
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
// Fallback to use the current implementation
|
||||
return patch(apk.path, out.path, pkg, label)
|
||||
}
|
||||
}
|
||||
|
||||
fun hideManager(context: Context, label: String) {
|
||||
Completable.fromAction {
|
||||
val progress = Notifications.progress(context, context.getString(R.string.hide_manager_title))
|
||||
Notifications.mgr.notify(Const.ID.HIDE_MANAGER_NOTIFICATION_ID, progress.build())
|
||||
if (!patchAndHide(context, label))
|
||||
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)
|
||||
}.subscribeK()
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import java.io.FilterInputStream
|
||||
@@ -41,4 +41,4 @@ class ProgressInputStream(
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -7,19 +7,18 @@ import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.widget.Toast
|
||||
import androidx.work.*
|
||||
import com.topjohnwu.magisk.*
|
||||
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.magisk.model.update.UpdateCheckService
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object Utils {
|
||||
|
||||
val isCanary: Boolean = BuildConfig.VERSION_NAME.contains("-")
|
||||
|
||||
fun toast(msg: CharSequence, duration: Int) {
|
||||
UiThreadHandler.run { Toast.makeText(get(), msg, duration).show() }
|
||||
}
|
||||
@@ -34,26 +33,27 @@ object Utils {
|
||||
}
|
||||
|
||||
fun showSuperUser(): Boolean {
|
||||
return Shell.rootAccess() && (Const.USER_ID == 0
|
||||
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()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.setRequiresDeviceIdle(true)
|
||||
.build()
|
||||
val request = PeriodicWorkRequest
|
||||
.Builder(ClassMap[UpdateCheckService::class.java] as Class<Worker>, 12, TimeUnit.HOURS)
|
||||
.setConstraints(constraints)
|
||||
.build()
|
||||
.Builder(UpdateCheckService::class.java, 12, TimeUnit.HOURS)
|
||||
.setConstraints(constraints)
|
||||
.build()
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
Const.ID.CHECK_MAGISK_UPDATE_WORKER_ID,
|
||||
ExistingPeriodicWorkPolicy.REPLACE, request)
|
||||
Const.ID.CHECK_MAGISK_UPDATE_WORKER_ID,
|
||||
ExistingPeriodicWorkPolicy.REPLACE, request
|
||||
)
|
||||
} else {
|
||||
WorkManager.getInstance(context)
|
||||
.cancelUniqueWork(Const.ID.CHECK_MAGISK_UPDATE_WORKER_ID)
|
||||
.cancelUniqueWork(Const.ID.CHECK_MAGISK_UPDATE_WORKER_ID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,11 +63,14 @@ object Utils {
|
||||
if (intent.resolveActivity(context.packageManager) != null) {
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
toast(R.string.open_link_failed_toast, Toast.LENGTH_SHORT)
|
||||
toast(
|
||||
R.string.open_link_failed_toast,
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun ensureDownloadPath(path : String) =
|
||||
fun ensureDownloadPath(path: String) =
|
||||
File(Environment.getExternalStorageDirectory(), path).run {
|
||||
if ((exists() && isDirectory) || mkdirs()) this else null
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.view
|
||||
package com.topjohnwu.magisk.core.view
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
@@ -9,13 +9,12 @@ 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.*
|
||||
import com.topjohnwu.magisk.Const.ID.PROGRESS_NOTIFICATION_CHANNEL
|
||||
import com.topjohnwu.magisk.Const.ID.UPDATE_NOTIFICATION_CHANNEL
|
||||
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
|
||||
import com.topjohnwu.magisk.model.receiver.GeneralReceiver
|
||||
import com.topjohnwu.magisk.ui.SplashActivity
|
||||
|
||||
object Notifications {
|
||||
|
||||
@@ -47,15 +46,18 @@ object Notifications {
|
||||
}
|
||||
|
||||
fun magiskUpdate(context: Context) {
|
||||
val intent = context.intent(SplashActivity::class.java)
|
||||
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,
|
||||
val pendingIntent = stackBuilder.getPendingIntent(
|
||||
Const.ID.MAGISK_UPDATE_NOTIFICATION_ID,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
val builder = updateBuilder(context)
|
||||
val builder = updateBuilder(
|
||||
context
|
||||
)
|
||||
.setContentTitle(context.getString(R.string.magisk_update_title))
|
||||
.setContentText(context.getString(R.string.manager_download_install))
|
||||
.setAutoCancel(true)
|
||||
@@ -65,14 +67,16 @@ object Notifications {
|
||||
}
|
||||
|
||||
fun managerUpdate(context: Context) {
|
||||
val intent = context.intent(GeneralReceiver::class.java)
|
||||
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)
|
||||
val builder = updateBuilder(
|
||||
context
|
||||
)
|
||||
.setContentTitle(context.getString(R.string.manager_update_title))
|
||||
.setContentText(context.getString(R.string.manager_download_install))
|
||||
.setAutoCancel(true)
|
||||
@@ -82,12 +86,14 @@ object Notifications {
|
||||
}
|
||||
|
||||
fun dtboPatched(context: Context) {
|
||||
val intent = context.intent(GeneralReceiver::class.java)
|
||||
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)
|
||||
val builder = updateBuilder(
|
||||
context
|
||||
)
|
||||
.setContentTitle(context.getString(R.string.dtbo_patched_title))
|
||||
.setContentText(context.getString(R.string.dtbo_patched_reboot))
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.view
|
||||
package com.topjohnwu.magisk.core.view
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -7,28 +7,28 @@ 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.*
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.*
|
||||
import com.topjohnwu.magisk.core.utils.Utils
|
||||
import com.topjohnwu.magisk.extensions.getBitmap
|
||||
import com.topjohnwu.magisk.ui.SplashActivity
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
|
||||
object Shortcuts {
|
||||
|
||||
fun setup(context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= 25) {
|
||||
val manager = context.getSystemService(ShortcutManager::class.java)
|
||||
manager?.dynamicShortcuts = getShortCuts(context)
|
||||
val manager = context.getSystemService<ShortcutManager>()
|
||||
manager?.dynamicShortcuts =
|
||||
getShortCuts(context)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = 25)
|
||||
private fun getShortCuts(context: Context): List<ShortcutInfo> {
|
||||
val shortCuts = mutableListOf<ShortcutInfo>()
|
||||
val root = Shell.rootAccess()
|
||||
val intent = context.intent(SplashActivity::class.java)
|
||||
val intent = context.intent<SplashActivity>()
|
||||
|
||||
fun getIcon(id: Int): Icon {
|
||||
return if (Build.VERSION.SDK_INT >= 26)
|
||||
@@ -52,7 +52,7 @@ object Shortcuts {
|
||||
.build()
|
||||
)
|
||||
}
|
||||
if (root && Info.env.magiskHide) {
|
||||
if (Info.env.magiskHide) {
|
||||
shortCuts.add(
|
||||
ShortcutInfo.Builder(context, "magiskhide")
|
||||
.setShortLabel(context.getString(R.string.magiskhide))
|
||||
@@ -67,7 +67,7 @@ object Shortcuts {
|
||||
.build()
|
||||
)
|
||||
}
|
||||
if (!Config.coreOnly && root && Info.env.magiskVersionCode >= 0) {
|
||||
if (!Config.coreOnly && Info.env.isActive) {
|
||||
shortCuts.add(
|
||||
ShortcutInfo.Builder(context, "modules")
|
||||
.setShortLabel(context.getString(R.string.modules))
|
||||
@@ -78,19 +78,6 @@ object Shortcuts {
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
)
|
||||
.setIcon(getIcon(R.drawable.sc_extension))
|
||||
.setRank(3)
|
||||
.build()
|
||||
)
|
||||
shortCuts.add(
|
||||
ShortcutInfo.Builder(context, "downloads")
|
||||
.setShortLabel(context.getString(R.string.downloads))
|
||||
.setIntent(
|
||||
Intent(intent)
|
||||
.putExtra(Const.Key.OPEN_SECTION, "downloads")
|
||||
.setAction(Intent.ACTION_VIEW)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
)
|
||||
.setIcon(getIcon(R.drawable.sc_cloud_download))
|
||||
.setRank(2)
|
||||
.build()
|
||||
)
|
@@ -1,33 +0,0 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import com.topjohnwu.magisk.data.database.base.*
|
||||
import com.topjohnwu.magisk.model.entity.MagiskLog
|
||||
import com.topjohnwu.magisk.model.entity.toLog
|
||||
import com.topjohnwu.magisk.model.entity.toMap
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class LogDao : BaseDao() {
|
||||
|
||||
override val table = DatabaseDefinition.Table.LOG
|
||||
|
||||
fun deleteOutdated(
|
||||
suTimeout: Long = TimeUnit.DAYS.toMillis(14)
|
||||
) = query<Delete> {
|
||||
condition {
|
||||
lessThan("time", suTimeout.toString())
|
||||
}
|
||||
}.ignoreElement()
|
||||
|
||||
fun deleteAll() = query<Delete> {}.ignoreElement()
|
||||
|
||||
fun fetchAll() = query<Select> {
|
||||
orderBy("time", Order.DESC)
|
||||
}.flattenAsFlowable { it }
|
||||
.map { it.toLog() }
|
||||
.toList()
|
||||
|
||||
fun put(log: MagiskLog) = query<Insert> {
|
||||
values(log.toMap())
|
||||
}.ignoreElement()
|
||||
|
||||
}
|
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>
|
||||
|
||||
|
||||
}
|
@@ -1,11 +1,19 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import androidx.room.*
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||
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 {
|
||||
abstract class RepoDao(private val db: RepoDatabase) {
|
||||
|
||||
val repoIDList get() = getRepoID().map { it.id }
|
||||
|
||||
@@ -15,13 +23,10 @@ abstract class RepoDao {
|
||||
}
|
||||
|
||||
var etagKey: String
|
||||
set(etag) = addEtagRaw(RepoEtag(0, etag))
|
||||
set(value) = addEtagRaw(RepoEtag(0, value))
|
||||
get() = etagRaw()?.key.orEmpty()
|
||||
|
||||
fun clear() {
|
||||
clearRepos()
|
||||
clearEtag()
|
||||
}
|
||||
fun clear() = db.clearAllTables()
|
||||
|
||||
@Query("SELECT * FROM repos ORDER BY last_update DESC")
|
||||
protected abstract fun getReposDateOrder(): List<Repo>
|
||||
@@ -52,12 +57,6 @@ abstract class RepoDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
protected abstract fun addEtagRaw(etag: RepoEtag)
|
||||
|
||||
@Query("DELETE FROM repos")
|
||||
protected abstract fun clearRepos()
|
||||
|
||||
@Query("DELETE FROM etag")
|
||||
protected abstract fun clearEtag()
|
||||
}
|
||||
|
||||
data class RepoID(
|
||||
|
@@ -1,11 +0,0 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||
|
||||
@Database(version = 6, entities = [Repo::class, RepoEtag::class])
|
||||
abstract class RepoDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun repoDao() : RepoDao
|
||||
}
|
@@ -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
|
||||
|
||||
}
|
@@ -1,15 +0,0 @@
|
||||
package com.topjohnwu.magisk.data.database.base
|
||||
|
||||
abstract class BaseDao {
|
||||
|
||||
abstract val table: String
|
||||
|
||||
inline fun <reified Builder : MagiskQueryBuilder> query(builder: Builder.() -> Unit) =
|
||||
Builder::class.java.newInstance()
|
||||
.apply { table = this@BaseDao.table }
|
||||
.apply(builder)
|
||||
.toString()
|
||||
.let { MagiskQuery(it) }
|
||||
.query()
|
||||
|
||||
}
|
@@ -1,30 +0,0 @@
|
||||
package com.topjohnwu.magisk.data.database.base
|
||||
|
||||
import androidx.annotation.AnyThread
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Single
|
||||
|
||||
object DatabaseDefinition {
|
||||
|
||||
object Table {
|
||||
const val POLICY = "policies"
|
||||
const val LOG = "logs"
|
||||
const val SETTINGS = "settings"
|
||||
const val STRINGS = "strings"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
fun MagiskQuery.query() = query.su()
|
||||
|
||||
fun String.suRaw() = Single.fromCallable { Shell.su(this).exec().out }
|
||||
fun String.su() = suRaw().map { it.toMap() }
|
||||
|
||||
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()
|
@@ -1,5 +0,0 @@
|
||||
package com.topjohnwu.magisk.data.database.base
|
||||
|
||||
inline class MagiskQuery(private val _query: String) {
|
||||
val query get() = "magisk --sqlite '$_query'"
|
||||
}
|
@@ -1,8 +1,8 @@
|
||||
package com.topjohnwu.magisk.data.network
|
||||
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.model.entity.UpdateInfo
|
||||
import com.topjohnwu.magisk.tasks.GithubRepoInfo
|
||||
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
|
||||
@@ -78,4 +78,4 @@ interface GithubApiServices {
|
||||
@Query("sort") sort: String = "pushed",
|
||||
@Query("per_page") count: Int = 100): Flowable<Result<List<GithubRepoInfo>>>
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.data.database.SettingsDao
|
||||
import com.topjohnwu.magisk.data.database.StringDao
|
||||
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
|
||||
@@ -29,8 +29,8 @@ interface DBConfig {
|
||||
}
|
||||
|
||||
class DBSettingsValue(
|
||||
private val name: String,
|
||||
private val default: Int
|
||||
private val name: String,
|
||||
private val default: Int
|
||||
) : ReadWriteProperty<DBConfig, Int> {
|
||||
|
||||
private var value: Int? = null
|
||||
@@ -47,29 +47,29 @@ class DBSettingsValue(
|
||||
this.value = value
|
||||
}
|
||||
thisRef.settingsDao.put(name, value)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
class DBBoolSettings(
|
||||
name: String,
|
||||
default: Boolean
|
||||
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 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)
|
||||
base.setValue(thisRef, property, if (value) 1 else 0)
|
||||
}
|
||||
|
||||
class DBStringsValue(
|
||||
private val name: String,
|
||||
private val default: String,
|
||||
private val sync: Boolean
|
||||
private val name: String,
|
||||
private val default: String,
|
||||
private val sync: Boolean
|
||||
) : ReadWriteProperty<DBConfig, String> {
|
||||
|
||||
private var value: String? = null
|
||||
@@ -90,16 +90,16 @@ class DBStringsValue(
|
||||
thisRef.stringDao.delete(name).blockingAwait()
|
||||
} else {
|
||||
thisRef.stringDao.delete(name)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
} else {
|
||||
if (sync) {
|
||||
thisRef.stringDao.put(name, value).blockingAwait()
|
||||
} else {
|
||||
thisRef.stringDao.put(name, value)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,40 +1,29 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.data.database.LogDao
|
||||
import com.topjohnwu.magisk.data.database.base.suRaw
|
||||
import com.topjohnwu.magisk.extensions.toSingle
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.data.database.SuLogDao
|
||||
import com.topjohnwu.magisk.model.entity.MagiskLog
|
||||
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import java.util.concurrent.TimeUnit
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Single
|
||||
|
||||
|
||||
class LogRepository(
|
||||
private val logDao: LogDao
|
||||
private val logDao: SuLogDao
|
||||
) {
|
||||
|
||||
fun fetchLogs() = logDao.fetchAll()
|
||||
.map { it.sortByDescending { it.date.time }; it }
|
||||
.map { it.wrap() }
|
||||
|
||||
fun fetchMagiskLogs() = "tail -n 5000 ${Const.MAGISK_LOG}".suRaw()
|
||||
.filter { it.isNotEmpty() }
|
||||
fun fetchMagiskLogs() = Single.fromCallable {
|
||||
Shell.su("tail -n 5000 ${Const.MAGISK_LOG}").exec().out
|
||||
}.flattenAsFlowable { it }.filter { it.isNotEmpty() }
|
||||
|
||||
fun clearLogs() = logDao.deleteAll()
|
||||
fun clearOutdated() = logDao.deleteOutdated()
|
||||
|
||||
fun clearMagiskLogs() = Shell.su("echo -n > " + Const.MAGISK_LOG)
|
||||
.toSingle()
|
||||
.map { it.exec() }
|
||||
|
||||
fun put(log: MagiskLog) = logDao.put(log)
|
||||
|
||||
private fun List<MagiskLog>.wrap(): List<WrappedMagiskLog> {
|
||||
val day = TimeUnit.DAYS.toMillis(1)
|
||||
return groupBy { it.date.time / day }
|
||||
.map { WrappedMagiskLog(it.key * day, it.value) }
|
||||
fun clearMagiskLogs() = Completable.fromAction {
|
||||
Shell.su("echo -n > ${Const.MAGISK_LOG}").exec()
|
||||
}
|
||||
|
||||
fun insert(log: MagiskLog) = logDao.insert(log)
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,8 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.magisk.data.database.base.su
|
||||
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
|
||||
@@ -25,7 +24,8 @@ class MagiskRepository(
|
||||
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)
|
||||
Config.Value.CUSTOM_CHANNEL -> apiRaw.fetchCustomUpdate(
|
||||
Config.customChannelUrl)
|
||||
else -> throw IllegalArgumentException()
|
||||
}.flatMap {
|
||||
// If remote version is lower than current installed, try switching to beta
|
||||
@@ -57,7 +57,7 @@ class MagiskRepository(
|
||||
.toList()
|
||||
|
||||
fun toggleHide(isEnabled: Boolean, packageName: String, process: String) =
|
||||
"magiskhide --%s %s %s".format(isEnabled.state, packageName, process).su().ignoreElement()
|
||||
Shell.su("magiskhide --${isEnabled.state} $packageName $process").submit()
|
||||
|
||||
private val Boolean.state get() = if (this) "add" else "rm"
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.core.model.module.Repo
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||
|
||||
class StringRepository(
|
||||
private val api: GithubRawServices
|
||||
@@ -12,4 +12,4 @@ class StringRepository(
|
||||
fun getMetadata(repo: Repo) = api.fetchModuleInfo(repo.id, "module.prop")
|
||||
|
||||
fun getReadme(repo: Repo) = api.fetchModuleInfo(repo.id, "README.md")
|
||||
}
|
||||
}
|
||||
|
@@ -6,7 +6,9 @@ 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
|
||||
@@ -16,13 +18,14 @@ val Protected = named("protected")
|
||||
|
||||
val applicationModule = module {
|
||||
single { RxBus() }
|
||||
factory { get<Context>().resources }
|
||||
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 {
|
||||
|
@@ -2,22 +2,33 @@ package com.topjohnwu.magisk.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.topjohnwu.magisk.data.database.*
|
||||
import com.topjohnwu.magisk.tasks.RepoUpdater
|
||||
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 { LogDao() }
|
||||
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()
|
||||
Room.databaseBuilder(context, RepoDatabase::class.java, "repo.db")
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
|
||||
fun createSuLogDatabase(context: Context) =
|
||||
Room.databaseBuilder(context, SuLogDatabase::class.java, "sulogs.db")
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
|
@@ -1,10 +1,9 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.Const
|
||||
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
|
||||
@@ -20,7 +19,6 @@ import retrofit2.Retrofit
|
||||
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.converter.scalars.ScalarsConverterFactory
|
||||
import se.ansman.kotshi.KotshiJsonAdapterFactory
|
||||
|
||||
val networkingModule = module {
|
||||
single { createOkHttpClient(get()) }
|
||||
@@ -49,9 +47,7 @@ fun createOkHttpClient(context: Context): OkHttpClient {
|
||||
}
|
||||
|
||||
fun createMoshiConverterFactory(): MoshiConverterFactory {
|
||||
val moshi = Moshi.Builder()
|
||||
.add(KotshiJsonAdapterFactory)
|
||||
.build()
|
||||
val moshi = Moshi.Builder().build()
|
||||
return MoshiConverterFactory.create(moshi)
|
||||
}
|
||||
|
||||
@@ -63,9 +59,6 @@ fun createRetrofit(okHttpClient: OkHttpClient): Retrofit.Builder {
|
||||
.client(okHttpClient)
|
||||
}
|
||||
|
||||
@KotshiJsonAdapterFactory
|
||||
abstract class JsonAdapterFactory : JsonAdapter.Factory
|
||||
|
||||
inline fun <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseUrl: String): T {
|
||||
return retrofitBuilder
|
||||
.baseUrl(baseUrl)
|
||||
@@ -75,9 +68,9 @@ inline fun <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseU
|
||||
|
||||
fun createMarkwon(context: Context, okHttpClient: OkHttpClient): Markwon {
|
||||
return Markwon.builder(context)
|
||||
.usePlugin(HtmlPlugin.create())
|
||||
.usePlugin(ImagesPlugin.create {
|
||||
it.addSchemeHandler(OkHttpNetworkSchemeHandler.create(okHttpClient))
|
||||
})
|
||||
.build()
|
||||
.usePlugin(HtmlPlugin.create())
|
||||
.usePlugin(ImagesPlugin.create {
|
||||
it.addSchemeHandler(OkHttpNetworkSchemeHandler.create(okHttpClient))
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
@@ -1,27 +1,35 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import android.net.Uri
|
||||
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.flash.FlashViewModel
|
||||
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.surequest.SuRequestViewModel
|
||||
import com.topjohnwu.magisk.ui.theme.ThemeViewModel
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
val viewModelModules = module {
|
||||
viewModel { MainViewModel() }
|
||||
viewModel { HideViewModel(get()) }
|
||||
viewModel { HomeViewModel(get()) }
|
||||
viewModel { SuperuserViewModel(get(), get(), get(), get()) }
|
||||
viewModel { HideViewModel(get(), get()) }
|
||||
viewModel { LogViewModel(get()) }
|
||||
viewModel { ModuleViewModel(get(), get(), get()) }
|
||||
viewModel { LogViewModel(get(), get()) }
|
||||
viewModel { (action: String, file: Uri, additional: Uri) ->
|
||||
FlashViewModel(action, file, additional, 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()) }
|
||||
}
|
||||
|
@@ -1,6 +0,0 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
|
||||
fun ui(body: () -> Unit) = Handler(Looper.getMainLooper()).post(body)
|
@@ -2,6 +2,7 @@ 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
|
||||
@@ -43,35 +44,35 @@ typealias OnErrorListener = (Throwable) -> Unit
|
||||
/*=== ALIASES FOR OBSERVABLES ===*/
|
||||
|
||||
fun <T> Observable<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {},
|
||||
onNext: OnSuccessListener<T> = {}
|
||||
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> = {}
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onNext: OnSuccessListener<T> = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onNext, onError)
|
||||
|
||||
fun <T> Maybe<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {},
|
||||
onNext: OnSuccessListener<T> = {}
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {},
|
||||
onSuccess: OnSuccessListener<T> = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onNext, onError, onComplete)
|
||||
.subscribe(onSuccess, onError, onComplete)
|
||||
|
||||
fun <T> Flowable<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {},
|
||||
onNext: OnSuccessListener<T> = {}
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {},
|
||||
onNext: OnSuccessListener<T> = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onNext, onError, onComplete)
|
||||
|
||||
fun Completable.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {}
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onComplete, onError)
|
||||
|
||||
@@ -104,54 +105,54 @@ fun Completable.updateBy(
|
||||
|
||||
|
||||
fun <T> Observable<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { ui { body() } }
|
||||
doOnSubscribe { UiThreadHandler.run { body() } }
|
||||
|
||||
fun <T> Single<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { ui { body() } }
|
||||
doOnSubscribe { UiThreadHandler.run { body() } }
|
||||
|
||||
fun <T> Maybe<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { ui { body() } }
|
||||
doOnSubscribe { UiThreadHandler.run { body() } }
|
||||
|
||||
fun <T> Flowable<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { ui { body() } }
|
||||
doOnSubscribe { UiThreadHandler.run { body() } }
|
||||
|
||||
fun Completable.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { ui { body() } }
|
||||
doOnSubscribe { UiThreadHandler.run { body() } }
|
||||
|
||||
|
||||
fun <T> Observable<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { ui { body(it) } }
|
||||
doOnError { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Single<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { ui { body(it) } }
|
||||
doOnError { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Maybe<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { ui { body(it) } }
|
||||
doOnError { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Flowable<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { ui { body(it) } }
|
||||
doOnError { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun Completable.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { ui { body(it) } }
|
||||
doOnError { UiThreadHandler.run { body(it) } }
|
||||
|
||||
|
||||
fun <T> Observable<T>.doOnNextUi(body: (T) -> Unit) =
|
||||
doOnNext { ui { body(it) } }
|
||||
doOnNext { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Flowable<T>.doOnNextUi(body: (T) -> Unit) =
|
||||
doOnNext { ui { body(it) } }
|
||||
doOnNext { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Single<T>.doOnSuccessUi(body: (T) -> Unit) =
|
||||
doOnSuccess { ui { body(it) } }
|
||||
doOnSuccess { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Maybe<T>.doOnSuccessUi(body: (T) -> Unit) =
|
||||
doOnSuccess { ui { body(it) } }
|
||||
doOnSuccess { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Maybe<T>.doOnCompleteUi(body: () -> Unit) =
|
||||
doOnComplete { ui { body() } }
|
||||
doOnComplete { UiThreadHandler.run { body() } }
|
||||
|
||||
fun Completable.doOnCompleteUi(body: () -> Unit) =
|
||||
doOnComplete { ui { body() } }
|
||||
doOnComplete { UiThreadHandler.run { body() } }
|
||||
|
||||
|
||||
fun <T, R> Observable<List<T>>.mapList(
|
||||
@@ -197,5 +198,8 @@ fun <T> ObservableField<T>.toObservable(): Observable<T> {
|
||||
|
||||
fun <T : Any> T.toSingle() = Single.just(this)
|
||||
|
||||
fun <T1, T2, R> zip(t1: Single<T1>, t2: Single<T2>, zipper: (T1, T2) -> R) =
|
||||
Single.zip(t1, t2, BiFunction<T1, T2, R> { rt1, rt2 -> zipper(rt1, rt2) })
|
||||
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) })
|
@@ -1,5 +1,6 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
@@ -22,16 +23,20 @@ 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 com.topjohnwu.magisk.Const
|
||||
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.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.utils.currentLocale
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
@@ -282,7 +287,7 @@ fun Context.drawableCompat(@DrawableRes id: Int) = ContextCompat.getDrawable(thi
|
||||
* with respect to RTL layout direction
|
||||
*/
|
||||
fun Context.startEndToLeftRight(start: Int, end: Int): Pair<Int, Int> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 &&
|
||||
if (SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 &&
|
||||
resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
|
||||
) {
|
||||
return end to start
|
||||
@@ -293,10 +298,10 @@ fun Context.startEndToLeftRight(start: Int, end: Int): Pair<Int, Int> {
|
||||
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)
|
||||
inline fun <reified T> T.DynamicClassLoader(apk: File) =
|
||||
DynamicClassLoader(apk, T::class.java.classLoader)
|
||||
|
||||
fun Context.unwrap() : Context {
|
||||
fun Context.unwrap(): Context {
|
||||
var context = this
|
||||
while (true) {
|
||||
if (context is ContextWrapper)
|
||||
@@ -306,3 +311,20 @@ fun Context.unwrap() : Context {
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
@@ -1,8 +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
|
||||
)
|
||||
}
|
||||
})
|
@@ -1,11 +1,13 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.net.toFile
|
||||
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
|
||||
@@ -19,8 +21,6 @@ fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
fun Uri.writeTo(file: File) = toFile().copyTo(file)
|
||||
|
||||
fun InputStream.writeTo(file: File) =
|
||||
withStreams(this, file.outputStream()) { reader, writer -> reader.copyTo(writer) }
|
||||
|
||||
@@ -100,4 +100,36 @@ fun Locale.toLangTag(): String {
|
||||
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)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
||||
@@ -11,4 +11,6 @@ fun reboot(reason: String = if (Info.recovery) "recovery" else "") {
|
||||
}
|
||||
|
||||
fun File.suOutputStream() = SuFileOutputStream(this)
|
||||
fun File.suInputStream() = SuFileInputStream(this)
|
||||
fun File.suInputStream() = SuFileInputStream(this)
|
||||
|
||||
val hasRoot get() = Shell.rootAccess()
|
||||
|
@@ -1,15 +1,38 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
|
||||
val specialChars = arrayOf('!', '@', '#', '$', '%', '&', '?')
|
||||
val fullSpecialChars = arrayOf('!', '@', '#', '$', '%', '&', '?')
|
||||
|
||||
fun String.isCJK(): Boolean {
|
||||
for (i in 0 until length)
|
||||
if (isCJK(codePointAt(i)))
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
fun isCJK(codepoint: Int): Boolean {
|
||||
return if (Build.VERSION.SDK_INT < 19) false /* Pre 5.0 don't need to be pretty.. */
|
||||
else Character.isIdeographic(codepoint)
|
||||
}
|
||||
|
||||
fun String.replaceRandomWithSpecial(passes: Int): String {
|
||||
var string = this
|
||||
repeat(passes) {
|
||||
string = string.replaceRandomWithSpecial()
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
fun String.replaceRandomWithSpecial(): String {
|
||||
val sp = if (isCJK()) fullSpecialChars else specialChars
|
||||
var random: Char
|
||||
do {
|
||||
random = random()
|
||||
} while (random == '.')
|
||||
return replace(random, specialChars.random())
|
||||
return replace(random, sp.random())
|
||||
}
|
||||
|
||||
fun StringBuilder.appendIf(condition: Boolean, builder: StringBuilder.() -> Unit) =
|
||||
@@ -26,4 +49,4 @@ fun String.legalFilename() = replace(" ", "_").replace("'", "").replace("\"", ""
|
||||
.replace("$", "").replace("`", "").replace("*", "").replace("/", "_")
|
||||
.replace("#", "").replace("@", "").replace("\\", "_")
|
||||
|
||||
fun String.isEmptyInternal() = isNullOrBlank()
|
||||
fun String.isEmptyInternal() = isNullOrBlank()
|
||||
|
@@ -1,6 +1,6 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import com.topjohnwu.magisk.utils.currentLocale
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import java.text.DateFormat
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
@@ -14,7 +14,22 @@ fun String.toTime(format: DateFormat) = try {
|
||||
-1L
|
||||
}
|
||||
|
||||
val timeFormatFull by lazy { SimpleDateFormat("yyyy/MM/dd_HH:mm:ss", currentLocale) }
|
||||
val timeFormatStandard by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", currentLocale) }
|
||||
val timeFormatMedium by lazy { DateFormat.getDateInstance(DateFormat.MEDIUM, currentLocale) }
|
||||
val timeFormatTime by lazy { SimpleDateFormat("h:mm a", currentLocale) }
|
||||
val timeFormatFull by lazy { SimpleDateFormat("yyyy/MM/dd_HH:mm:ss",
|
||||
currentLocale
|
||||
) }
|
||||
val timeFormatStandard by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'",
|
||||
currentLocale
|
||||
) }
|
||||
val timeFormatMedium by lazy { DateFormat.getDateInstance(DateFormat.MEDIUM,
|
||||
currentLocale
|
||||
) }
|
||||
val timeFormatTime by lazy { SimpleDateFormat("h:mm a",
|
||||
currentLocale
|
||||
) }
|
||||
val timeDateFormat by lazy {
|
||||
DateFormat.getDateTimeInstance(
|
||||
DateFormat.DEFAULT,
|
||||
DateFormat.DEFAULT,
|
||||
currentLocale
|
||||
)
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user