mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-08-15 00:37:26 +00:00
Compare commits
903 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c0f2164bc5 | ||
![]() |
f6e4a27fdd | ||
![]() |
257ceb99f7 | ||
![]() |
706d53065b | ||
![]() |
0f95a7babe | ||
![]() |
7cb2806878 | ||
![]() |
9c0e18975c | ||
![]() |
3da318b48e | ||
![]() |
dfe1f2c108 | ||
![]() |
f42a87b51a | ||
![]() |
ab25857176 | ||
![]() |
7da36079c1 | ||
![]() |
2bef967af1 | ||
![]() |
7e4194418a | ||
![]() |
aa02057895 | ||
![]() |
fb8dc07599 | ||
![]() |
66e30a7723 | ||
![]() |
0298ab99c4 | ||
![]() |
d11358671e | ||
![]() |
8b5cb4c7b0 | ||
![]() |
aad52ae743 | ||
![]() |
8ddab84745 | ||
![]() |
6865652125 | ||
![]() |
ed4d0867e8 | ||
![]() |
1c71e02454 | ||
![]() |
f332e87cab | ||
![]() |
023dbc6cb5 | ||
![]() |
4dd3f55407 | ||
![]() |
7b9a71c9af | ||
![]() |
901d22cdfa | ||
![]() |
93e1266ee7 | ||
![]() |
0a4e7eea41 | ||
![]() |
e3801d6965 | ||
![]() |
336f1687c1 | ||
![]() |
d4e2f2df6e | ||
![]() |
f152b4c26e | ||
![]() |
bd935b0553 | ||
![]() |
a9b3b7a359 | ||
![]() |
7a007b342a | ||
![]() |
0783f3d5b6 | ||
![]() |
afe3c2bc1b | ||
![]() |
82f8948fd4 | ||
![]() |
b9cdc755d1 | ||
![]() |
a6f81c66e5 | ||
![]() |
1ff45ac5f5 | ||
![]() |
48bde7375f | ||
![]() |
0601fa3b3d | ||
![]() |
d0d3c8dbfd | ||
![]() |
8057de1973 | ||
![]() |
43c1105d62 | ||
![]() |
6adf516b30 | ||
![]() |
bf80b08b5f | ||
![]() |
3e0b1df46d | ||
![]() |
84811c80b6 | ||
![]() |
45e0df9c57 | ||
![]() |
bc51ce7c7b | ||
![]() |
b693d13b93 | ||
![]() |
39982d57ef | ||
![]() |
15e27e54fb | ||
![]() |
851404205b | ||
![]() |
117ae71025 | ||
![]() |
027ec70262 | ||
![]() |
55fdee4d65 | ||
![]() |
0d42f937dd | ||
![]() |
ac8372dd26 | ||
![]() |
5e56a6bbee | ||
![]() |
3c6c409df0 | ||
![]() |
d05408c89f | ||
![]() |
ee0ec3fbfa | ||
![]() |
122a73e086 | ||
![]() |
29a9b18c4c | ||
![]() |
1561272109 | ||
![]() |
3e61ab0d25 | ||
![]() |
a49dc6ccb7 | ||
![]() |
60f3d62f00 | ||
![]() |
e613855a4f | ||
![]() |
22662d7e03 | ||
![]() |
6e7e5be1a2 | ||
![]() |
8b2ab778c9 | ||
![]() |
35f3766ecf | ||
![]() |
995304dabb | ||
![]() |
803982a271 | ||
![]() |
9164bf22c2 | ||
![]() |
911a576893 | ||
![]() |
79ee85c0f9 | ||
![]() |
483dbcdc40 | ||
![]() |
a1096b5bf0 | ||
![]() |
5ac0e64edb | ||
![]() |
60b2624607 | ||
![]() |
d2e2847b03 | ||
![]() |
b9669f54f7 | ||
![]() |
8c7bd77d33 | ||
![]() |
ba1ce16b8b | ||
![]() |
68090943f4 | ||
![]() |
a4fb1297b0 | ||
![]() |
860a05abf2 | ||
![]() |
8bb2f356c0 | ||
![]() |
4950020635 | ||
![]() |
0a6140c6eb | ||
![]() |
bba2ac8817 | ||
![]() |
331b1f542f | ||
![]() |
ccb55205e6 | ||
![]() |
9cc91b30b3 | ||
![]() |
e836caf31e | ||
![]() |
beaa1e5be2 | ||
![]() |
ea545bae26 | ||
![]() |
1c9ec2df45 | ||
![]() |
b76c80e2ce | ||
![]() |
236990f4a3 | ||
![]() |
1ed32df20d | ||
![]() |
8476eb9f4b | ||
![]() |
735af7843b | ||
![]() |
ded73e958b | ||
![]() |
6dcb84d4f4 | ||
![]() |
501bc9f438 | ||
![]() |
f88e812b63 | ||
![]() |
be6386c410 | ||
![]() |
2af4fd17c4 | ||
![]() |
f870418bd0 | ||
![]() |
00659e4795 | ||
![]() |
cdda10207e | ||
![]() |
701700279f | ||
![]() |
a9d804724a | ||
![]() |
e033a9ab47 | ||
![]() |
059e5fb8aa | ||
![]() |
a78f255928 | ||
![]() |
1d10e69288 | ||
![]() |
63590d379c | ||
![]() |
5f63e88984 | ||
![]() |
75584e2b19 | ||
![]() |
1426ee2ebd | ||
![]() |
b6643b7bfc | ||
![]() |
721dfdf553 | ||
![]() |
2963747d14 | ||
![]() |
e7350d5041 | ||
![]() |
f37e8f4ca8 | ||
![]() |
594c2accc0 | ||
![]() |
7acfac6a91 | ||
![]() |
0646f48e14 | ||
![]() |
37565fd067 | ||
![]() |
26b2e7dc5d | ||
![]() |
c3313623e4 | ||
![]() |
2089223690 | ||
![]() |
52e1b84d41 | ||
![]() |
8794141b7f | ||
![]() |
f6126dd20e | ||
![]() |
18acfda99b | ||
![]() |
bec5edca84 | ||
![]() |
6fb20b3ee5 | ||
![]() |
eaf4d8064b | ||
![]() |
2a5f5b1bba | ||
![]() |
c538a77937 | ||
![]() |
aa9e7b1ed1 | ||
![]() |
a3066eddab | ||
![]() |
d1729fa787 | ||
![]() |
93961dde2c | ||
![]() |
1024e68eb6 | ||
![]() |
6ae2c9387d | ||
![]() |
fba83e2330 | ||
![]() |
f1295cb7d6 | ||
![]() |
dc61dfbde6 | ||
![]() |
21466426da | ||
![]() |
3f0136362b | ||
![]() |
e92d77bbec | ||
![]() |
07bd36c94b | ||
![]() |
b1dbbdef12 | ||
![]() |
3e479726ec | ||
![]() |
4cc41eccb3 | ||
![]() |
8f08ae59ac | ||
![]() |
e8d4e492d6 | ||
![]() |
b8090a8e18 | ||
![]() |
c609a01e55 | ||
![]() |
c97fb385cd | ||
![]() |
da6c57750e | ||
![]() |
6951d926f7 | ||
![]() |
6623195bd5 | ||
![]() |
ec31bb9a82 | ||
![]() |
8618cc383a | ||
![]() |
4b01e3a3c7 | ||
![]() |
7f748c23c1 | ||
![]() |
963d248cc7 | ||
![]() |
657056e636 | ||
![]() |
9d5efea66e | ||
![]() |
658d74e026 | ||
![]() |
5113f6d375 | ||
![]() |
96405c26d0 | ||
![]() |
4ea5f34bf3 | ||
![]() |
dbd13a2019 | ||
![]() |
06773235da | ||
![]() |
e57556a8af | ||
![]() |
b54b78c29d | ||
![]() |
317336f771 | ||
![]() |
b4e52f6135 | ||
![]() |
f2ca042915 | ||
![]() |
1060dd2906 | ||
![]() |
2e0f7a82fa | ||
![]() |
5798536559 | ||
![]() |
ab9a83c82f | ||
![]() |
c87fdbea0f | ||
![]() |
ec8fffe61c | ||
![]() |
61d52991f1 | ||
![]() |
9100186dce | ||
![]() |
d2bc2cfcf8 | ||
![]() |
5a71998b4e | ||
![]() |
42278f12ff | ||
![]() |
f5593e051c | ||
![]() |
a27e30cf54 | ||
![]() |
79140c7636 | ||
![]() |
1f4c595cd3 | ||
![]() |
b5b62e03af | ||
![]() |
67e2a4720e | ||
![]() |
f5c2d72429 | ||
![]() |
2f5331ab48 | ||
![]() |
7f8257152f | ||
![]() |
0cd80f2556 | ||
![]() |
1717387876 | ||
![]() |
109363ebf6 | ||
![]() |
716c4fa386 | ||
![]() |
9a09b4eb20 | ||
![]() |
95a5b57265 | ||
![]() |
13fbf397d1 | ||
![]() |
20be99ec8a | ||
![]() |
04c53c3578 | ||
![]() |
51bc27a869 | ||
![]() |
71b083794c | ||
![]() |
b100d0c503 | ||
![]() |
76061296c9 | ||
![]() |
bb303d2da1 | ||
![]() |
c91c070343 | ||
![]() |
aec06a6f61 | ||
![]() |
e8ba671fc2 | ||
![]() |
1860e5d133 | ||
![]() |
f2cb3c38fe | ||
![]() |
9a28dd4f6e | ||
![]() |
d2acd59ea8 | ||
![]() |
79dfdb29e7 | ||
![]() |
eb21c8b42e | ||
![]() |
541bb53553 | ||
![]() |
fe8997efae | ||
![]() |
23455c722c | ||
![]() |
5ce29c30d2 | ||
![]() |
70d67728fd | ||
![]() |
e546884b08 | ||
![]() |
b36e6d987d | ||
![]() |
53c3dd5e8b | ||
![]() |
da723b207a | ||
![]() |
e050f77198 | ||
![]() |
540b4b7ea9 | ||
![]() |
bbef22daf7 | ||
![]() |
9ed110c91b | ||
![]() |
a30d510eb1 | ||
![]() |
ef98eaed8f | ||
![]() |
2a257f327c | ||
![]() |
4060c2107c | ||
![]() |
cd23d27048 | ||
![]() |
18b86e4fd2 | ||
![]() |
5f2e22a259 | ||
![]() |
4e97b18977 | ||
![]() |
f9bde347bc | ||
![]() |
947a7d6a2f | ||
![]() |
872ab2e99b | ||
![]() |
90b8813bb7 | ||
![]() |
88d0f63294 | ||
![]() |
79fa0d3a90 | ||
![]() |
8e61080a4a | ||
![]() |
3f9a64417b | ||
![]() |
eb959379e8 | ||
![]() |
41a644afb9 | ||
![]() |
6b42db943d | ||
![]() |
1c325459eb | ||
![]() |
6d88d8ad95 | ||
![]() |
246997f273 | ||
![]() |
b6144ae582 | ||
![]() |
afe17c73b4 | ||
![]() |
b51b884fc7 | ||
![]() |
d3e4b29e62 | ||
![]() |
24059e7403 | ||
![]() |
107a2a6682 | ||
![]() |
56b4ab6672 | ||
![]() |
4662454938 | ||
![]() |
db4f78d463 | ||
![]() |
880de21596 | ||
![]() |
622dd84c9e | ||
![]() |
f983bfc883 | ||
![]() |
45cdb3fdb0 | ||
![]() |
9a707236b8 | ||
![]() |
e9e6ad3bb0 | ||
![]() |
ab78a81d15 | ||
![]() |
18340099b7 | ||
![]() |
a013696a41 | ||
![]() |
8a2a6d9232 | ||
![]() |
12aa6d86e4 | ||
![]() |
7d08969d28 | ||
![]() |
dda4aa8488 | ||
![]() |
cdaef3d801 | ||
![]() |
9159166128 | ||
![]() |
dc0882e043 | ||
![]() |
c811f015ef | ||
![]() |
d8f0b66fe1 | ||
![]() |
dc3d57deba | ||
![]() |
d089698475 | ||
![]() |
8ed2dd6687 | ||
![]() |
50305ca1fe | ||
![]() |
3e91567636 | ||
![]() |
0b4dd63d36 | ||
![]() |
38d0f85deb | ||
![]() |
c5b452f369 | ||
![]() |
6ce9225f52 | ||
![]() |
13a8820603 | ||
![]() |
503997a09a | ||
![]() |
17efdff134 | ||
![]() |
984f32f994 | ||
![]() |
eee7f097e3 | ||
![]() |
086059ec30 | ||
![]() |
7ff22c68c7 | ||
![]() |
1232113772 | ||
![]() |
039d4936cb | ||
![]() |
784dd80965 | ||
![]() |
1ffe9bd83b | ||
![]() |
0c28b23224 | ||
![]() |
ec1af9dc1e | ||
![]() |
ff4cea229a | ||
![]() |
3f81f9371f | ||
![]() |
60e89a7d22 | ||
![]() |
c50daa5c9e | ||
![]() |
58d00ab863 | ||
![]() |
ce916459c5 | ||
![]() |
4094d560ab | ||
![]() |
4dbf7eb04b | ||
![]() |
a39577c44d | ||
![]() |
125ee46685 | ||
![]() |
ce84f1762c | ||
![]() |
a687d1347b | ||
![]() |
6d9db20614 | ||
![]() |
c62dfc1bcc | ||
![]() |
aabe2696fe | ||
![]() |
ae0d605310 | ||
![]() |
2a694596b5 | ||
![]() |
ff0a76606e | ||
![]() |
dead74801d | ||
![]() |
ab207a1bb3 | ||
![]() |
f152e8c33d | ||
![]() |
797ba4fbf4 | ||
![]() |
a848f10bba | ||
![]() |
552ec1eb35 | ||
![]() |
1385d2a4f4 | ||
![]() |
3b5c9abf7a | ||
![]() |
e0fa032bd3 | ||
![]() |
7b69650fcd | ||
![]() |
08a8df489f | ||
![]() |
9f35a8a520 | ||
![]() |
0df891b336 | ||
![]() |
385853a290 | ||
![]() |
fa3ef8a1c1 | ||
![]() |
c93ada03c7 | ||
![]() |
0064b01ae0 | ||
![]() |
1469b82aa2 | ||
![]() |
2d5cf8a6fe | ||
![]() |
290959f74c | ||
![]() |
4d9f58ee72 | ||
![]() |
9241246de6 | ||
![]() |
58a5d52b78 | ||
![]() |
2906178ac3 | ||
![]() |
e0afbb647b | ||
![]() |
50be50cf6a | ||
![]() |
77a9d3a5bc | ||
![]() |
f9c7a4c933 | ||
![]() |
2b759b84b0 | ||
![]() |
1e45c63ea5 | ||
![]() |
b14a260827 | ||
![]() |
ade1597e03 | ||
![]() |
2739d3cb67 | ||
![]() |
dc5e78e142 | ||
![]() |
e9759a5868 | ||
![]() |
e7ab802498 | ||
![]() |
42672c2e27 | ||
![]() |
e65d61d313 | ||
![]() |
076da5c7c4 | ||
![]() |
9deaf2507c | ||
![]() |
5c114c67de | ||
![]() |
d904cb0441 | ||
![]() |
bd1dd9d863 | ||
![]() |
afebe734b8 | ||
![]() |
e21a78164e | ||
![]() |
1e0f96d0fd | ||
![]() |
bf650332d8 | ||
![]() |
f32e0af830 | ||
![]() |
4c94f90e5d | ||
![]() |
ffb4224640 | ||
![]() |
89fff4830b | ||
![]() |
16e4c67992 | ||
![]() |
cf47214ee4 | ||
![]() |
0feab753fb | ||
![]() |
d0b6318b90 | ||
![]() |
966e23b846 | ||
![]() |
5b8a1fc2a7 | ||
![]() |
02ea3ca525 | ||
![]() |
0632b146b8 | ||
![]() |
1b0b180761 | ||
![]() |
0d11f73a1d | ||
![]() |
533cb8eb58 | ||
![]() |
8ac1181e9a | ||
![]() |
5ca1892eb0 | ||
![]() |
e32db6a0e8 | ||
![]() |
82fff615d6 | ||
![]() |
24a8f0808d | ||
![]() |
4a7c3c06bc | ||
![]() |
da93bbc1fe | ||
![]() |
fa2dbe981e | ||
![]() |
ce6cceae8b | ||
![]() |
7b26e8b818 | ||
![]() |
2da5fcb00b | ||
![]() |
a079966f97 | ||
![]() |
468796c23d | ||
![]() |
5833aadef5 | ||
![]() |
eb261c8026 | ||
![]() |
a4c48847d1 | ||
![]() |
43288be091 | ||
![]() |
1ad7a6fe93 | ||
![]() |
4e0a3f5e72 | ||
![]() |
d7c33f647d | ||
![]() |
9087207dc0 | ||
![]() |
2760f37e6b | ||
![]() |
3fa3426032 | ||
![]() |
2e4dc91b96 | ||
![]() |
aaaaa3d044 | ||
![]() |
1edc4449d5 | ||
![]() |
f3cd4da026 | ||
![]() |
872c55207c | ||
![]() |
339ca6d666 | ||
![]() |
4aeac3b8f4 | ||
![]() |
d625beb7f3 | ||
![]() |
735b65c50c | ||
![]() |
efb1eab327 | ||
![]() |
49d4785da0 | ||
![]() |
28e65ce383 | ||
![]() |
c3b6a48373 | ||
![]() |
a42ebd429b | ||
![]() |
8f89010752 | ||
![]() |
105a18f719 | ||
![]() |
eb04ca4c4a | ||
![]() |
6092d7ca88 | ||
![]() |
66cad101c0 | ||
![]() |
0a14f43f9c | ||
![]() |
311c1f0dfd | ||
![]() |
0499588107 | ||
![]() |
d4d837a562 | ||
![]() |
fbcbb20178 | ||
![]() |
0914700fc6 | ||
![]() |
eeced2fb5b | ||
![]() |
6509e3d4f5 | ||
![]() |
317052604b | ||
![]() |
5538f7168c | ||
![]() |
dcb9e4cd93 | ||
![]() |
d9382f59bf | ||
![]() |
403a0c770a | ||
![]() |
f0f1cdc501 | ||
![]() |
4e272b70ef | ||
![]() |
8dc62a0232 | ||
![]() |
9225b47568 | ||
![]() |
d462873e74 | ||
![]() |
fc19b50290 | ||
![]() |
333fe6da0e | ||
![]() |
75fcda9f81 | ||
![]() |
44ba2a9903 | ||
![]() |
2fceb1ad96 | ||
![]() |
bacb5fa462 | ||
![]() |
67f8dc494e | ||
![]() |
3e4caabecb | ||
![]() |
dcd5183b24 | ||
![]() |
d80c6b42a6 | ||
![]() |
64effe9385 | ||
![]() |
96dd24e91d | ||
![]() |
fbb4f85ef0 | ||
![]() |
716f06846b | ||
![]() |
241f2656fa | ||
![]() |
e973d49517 | ||
![]() |
c3bf9a095b | ||
![]() |
abfc28db32 | ||
![]() |
8b5652ced5 | ||
![]() |
d6dbab53cd | ||
![]() |
46de1ed968 | ||
![]() |
9bebe07d5a | ||
![]() |
ee4db43136 | ||
![]() |
efac220998 | ||
![]() |
31026b43f4 | ||
![]() |
bc3fbe09f5 | ||
![]() |
7ac55068db | ||
![]() |
6abd9aa8a4 | ||
![]() |
c91ebfbcc1 | ||
![]() |
2f232fc670 | ||
![]() |
41f5c8d96c | ||
![]() |
4fd04e62af | ||
![]() |
63a9a7d643 | ||
![]() |
a63d6c03fd | ||
![]() |
fd552e68a9 | ||
![]() |
de4e26b488 | ||
![]() |
fa3865e962 | ||
![]() |
a6950b8aca | ||
![]() |
8df96ff664 | ||
![]() |
8b29267ad6 | ||
![]() |
0ef92a4866 | ||
![]() |
85bef8fa96 | ||
![]() |
ca9f9fee9a | ||
![]() |
b59e05c63e | ||
![]() |
3c0630bfc0 | ||
![]() |
bf84dd6518 | ||
![]() |
f575155a41 | ||
![]() |
bd240ba48c | ||
![]() |
106a2bb7df | ||
![]() |
82bbbe05b2 | ||
![]() |
9956dc0995 | ||
![]() |
fc76673802 | ||
![]() |
17b5291bbb | ||
![]() |
9908dfd79a | ||
![]() |
2dbaf9595c | ||
![]() |
9a16ab1bd7 | ||
![]() |
9e5cb6cb91 | ||
![]() |
8c19654d20 | ||
![]() |
d5a7a75d9d | ||
![]() |
851b676077 | ||
![]() |
765b51285a | ||
![]() |
8a338de696 | ||
![]() |
8a61ae621d | ||
![]() |
60e1e07e87 | ||
![]() |
e51a3dacb9 | ||
![]() |
9a8a27dbb9 | ||
![]() |
2eb001876a | ||
![]() |
b510dc51ac | ||
![]() |
d7f7508fa2 | ||
![]() |
e66b0bf3b2 | ||
![]() |
0555b73a19 | ||
![]() |
877a297de4 | ||
![]() |
49559ec0ec | ||
![]() |
30e45f863d | ||
![]() |
434efec860 | ||
![]() |
5022f00a55 | ||
![]() |
8aac373ca3 | ||
![]() |
c3586fe0a5 | ||
![]() |
11f254e5e5 | ||
![]() |
c61ec2465f | ||
![]() |
fd5ad91d26 | ||
![]() |
5c4c391f94 | ||
![]() |
4dacffd7a1 | ||
![]() |
61599059d5 | ||
![]() |
f32a29911b | ||
![]() |
b73d5753f2 | ||
![]() |
2eee335b5f | ||
![]() |
013a2e1336 | ||
![]() |
fbaf2bded6 | ||
![]() |
38a34a7eeb | ||
![]() |
70174e093b | ||
![]() |
0333e82e86 | ||
![]() |
36a8839cf8 | ||
![]() |
d0ed6e7fe3 | ||
![]() |
72dfbf5e44 | ||
![]() |
114a3c037f | ||
![]() |
782adc9a9f | ||
![]() |
e0642b018d | ||
![]() |
6bd4006652 | ||
![]() |
01efe7a4ea | ||
![]() |
7e133b0cf4 | ||
![]() |
fd808bd51e | ||
![]() |
b4e8860ee4 | ||
![]() |
fb3f8605fd | ||
![]() |
e394445f1b | ||
![]() |
ca1b0bf1ce | ||
![]() |
bf5798190d | ||
![]() |
ca5030a646 | ||
![]() |
e22324e434 | ||
![]() |
e46d4ecd3e | ||
![]() |
84f92bd661 | ||
![]() |
b44dcc2da0 | ||
![]() |
d6062944f1 | ||
![]() |
79f549795b | ||
![]() |
eaf7c3c486 | ||
![]() |
1ac379c17a | ||
![]() |
51a4dbf263 | ||
![]() |
2d91bfd9e6 | ||
![]() |
e437ffdbae | ||
![]() |
ccde8b73a2 | ||
![]() |
65f88e4ae2 | ||
![]() |
354440ee8a | ||
![]() |
59106e4f52 | ||
![]() |
d76c266fbc | ||
![]() |
31681c9c5f | ||
![]() |
0e5a32b476 | ||
![]() |
a22a1dd284 | ||
![]() |
27c59dbb65 | ||
![]() |
fb04e32480 | ||
![]() |
14a2f63b8b | ||
![]() |
9e81db8692 | ||
![]() |
1ed67eed35 | ||
![]() |
abc5457136 | ||
![]() |
4b238a9cd0 | ||
![]() |
f200d472ef | ||
![]() |
105b2fc114 | ||
![]() |
5ed4071f74 | ||
![]() |
551a478fdc | ||
![]() |
7c319f5fc3 | ||
![]() |
1fcf35ebeb | ||
![]() |
6d749a58c6 | ||
![]() |
34450cdddd | ||
![]() |
846bbb4da1 | ||
![]() |
d7a26dbf27 | ||
![]() |
a86d5b3e61 | ||
![]() |
b2bece9ef6 | ||
![]() |
f9cbf883ac | ||
![]() |
7f225b3973 | ||
![]() |
72e7605fce | ||
![]() |
a4c1ddd9f2 | ||
![]() |
ddd513110f | ||
![]() |
e33d623d40 | ||
![]() |
eec19ba9af | ||
![]() |
413b3f394b | ||
![]() |
88cee1212b | ||
![]() |
cf25fa8ed8 | ||
![]() |
3f053b8547 | ||
![]() |
79aa261ca2 | ||
![]() |
ac2a9da4c4 | ||
![]() |
d8b1d79879 | ||
![]() |
feb0f4b7b5 | ||
![]() |
6c8fe46590 | ||
![]() |
5e3c9e5022 | ||
![]() |
f7f821b93c | ||
![]() |
36a70e995f | ||
![]() |
537ae1a315 | ||
![]() |
87b6bf2c26 | ||
![]() |
9df6b0618a | ||
![]() |
c7e30ac63e | ||
![]() |
f5e547944a | ||
![]() |
d10680187d | ||
![]() |
f5aa6a3cf8 | ||
![]() |
c944277e78 | ||
![]() |
2e5402d741 | ||
![]() |
24f6024383 | ||
![]() |
15b1215972 | ||
![]() |
11222c89d4 | ||
![]() |
893a8ec8d9 | ||
![]() |
da2b00de59 | ||
![]() |
1276c28e03 | ||
![]() |
e458215f27 | ||
![]() |
fee4031d0f | ||
![]() |
0835ff88b2 | ||
![]() |
2e95d9f07e | ||
![]() |
fe2388394d | ||
![]() |
7fc9b908d4 | ||
![]() |
0ed524f173 | ||
![]() |
aed3ab994e | ||
![]() |
5347cedfa6 | ||
![]() |
5b28a713e0 | ||
![]() |
f1fb7404c2 | ||
![]() |
fc67c0195f | ||
![]() |
2f02f9a580 | ||
![]() |
07f712a1ce | ||
![]() |
c7044b0d20 | ||
![]() |
15866cfba9 | ||
![]() |
4c2570628d | ||
![]() |
113eec59f9 | ||
![]() |
f7abc03dac | ||
![]() |
ef3f188a2c | ||
![]() |
dd62fe89f7 | ||
![]() |
ec2d7d77eb | ||
![]() |
6c6368fd81 | ||
![]() |
ba31c6b625 | ||
![]() |
cad189d2dc | ||
![]() |
7cf3da1b3b | ||
![]() |
45fabf8e03 | ||
![]() |
2c12fe6eb2 | ||
![]() |
b41b2283f4 | ||
![]() |
e8e7cd5008 | ||
![]() |
7873433977 | ||
![]() |
52d19d3ea2 | ||
![]() |
6348d0a6fb | ||
![]() |
f7a650b9a4 | ||
![]() |
a97d278bcd | ||
![]() |
8647ba4729 | ||
![]() |
4631077c49 | ||
![]() |
18dab28c32 | ||
![]() |
8ffbffddb3 | ||
![]() |
f191db2fe0 | ||
![]() |
dc8f0f6feb | ||
![]() |
01a43b03bd | ||
![]() |
86db0cd2cd | ||
![]() |
ae6dd50ccd | ||
![]() |
77032eced1 | ||
![]() |
820427e93b | ||
![]() |
89e11c9cc8 | ||
![]() |
05cf53fe6f | ||
![]() |
97b72a5941 | ||
![]() |
7922f65243 | ||
![]() |
67f7935421 | ||
![]() |
9348c5bad9 | ||
![]() |
0f7caa66fb | ||
![]() |
bd14994eb9 | ||
![]() |
08818e8542 | ||
![]() |
706eba329d | ||
![]() |
f6a2b1c882 | ||
![]() |
c2e6622016 | ||
![]() |
53904b0627 | ||
![]() |
cef14d4576 | ||
![]() |
73203a55ca | ||
![]() |
397f7326a3 | ||
![]() |
4bbd7989dd | ||
![]() |
a0b47f3ca3 | ||
![]() |
89e9e7c176 | ||
![]() |
ddc2f317ab | ||
![]() |
867bab8513 | ||
![]() |
b1e0c5ff38 | ||
![]() |
6dbd9bfb12 | ||
![]() |
3c78344812 | ||
![]() |
594f268885 | ||
![]() |
93d5716414 | ||
![]() |
4b8e92f00a | ||
![]() |
fc6ef7dd57 | ||
![]() |
c881fd4964 | ||
![]() |
4bcc2b2f03 | ||
![]() |
6150055a05 | ||
![]() |
23a33b4351 | ||
![]() |
e02386a6ac | ||
![]() |
099e703834 | ||
![]() |
1ededc637e | ||
![]() |
0850bca9d3 | ||
![]() |
6d2fd480bf | ||
![]() |
ddf0c379be | ||
![]() |
45b5e89912 | ||
![]() |
a748d5291a | ||
![]() |
f5131fae56 | ||
![]() |
f79a40a67a | ||
![]() |
43146b8316 | ||
![]() |
b71b4bd4e5 | ||
![]() |
44895a86b8 | ||
![]() |
eecb66f4f1 | ||
![]() |
e7f1c03151 | ||
![]() |
56602cb9a3 | ||
![]() |
1e2f776b83 | ||
![]() |
ec3705f2ed | ||
![]() |
ae0dcabf43 | ||
![]() |
6030b00ee2 | ||
![]() |
a17908f6e1 | ||
![]() |
cb7148a24c | ||
![]() |
2f824f59dc | ||
![]() |
ad94f10205 | ||
![]() |
02b2290b16 | ||
![]() |
f8a814a588 | ||
![]() |
4c4338cc02 | ||
![]() |
5675a1ae7d | ||
![]() |
0952224c3d | ||
![]() |
4e26c10287 | ||
![]() |
f3e82b9ef1 | ||
![]() |
e50295d337 | ||
![]() |
fde78be2b4 | ||
![]() |
c071ac8973 | ||
![]() |
599ee57d39 | ||
![]() |
4499cebcd9 | ||
![]() |
cd6eca1dc2 | ||
![]() |
951273f8ef | ||
![]() |
51eeb89f67 | ||
![]() |
0efa73d96c | ||
![]() |
63512b39b2 | ||
![]() |
f392ade78d | ||
![]() |
0236ab887e | ||
![]() |
d4baae411b | ||
![]() |
e02e46d0fc | ||
![]() |
3c04dab472 | ||
![]() |
fc1844b4df | ||
![]() |
99ef20627a | ||
![]() |
4497e0aaca | ||
![]() |
c3e045e367 | ||
![]() |
501d3e6c32 | ||
![]() |
b27b9c1d18 | ||
![]() |
f7d3d1eeaf | ||
![]() |
0d72a4c8ba | ||
![]() |
dbdb0a2560 | ||
![]() |
18a09703de | ||
![]() |
bc6a14d30f | ||
![]() |
97db49a57b | ||
![]() |
eca2168685 | ||
![]() |
1bcef38739 | ||
![]() |
aac6ad73da | ||
![]() |
122b4d66b6 | ||
![]() |
0f8f4e361b | ||
![]() |
3733b589ac | ||
![]() |
6a2e781db2 | ||
![]() |
c6569ce022 | ||
![]() |
a62bdc58cb | ||
![]() |
912009494d | ||
![]() |
a5d7c41d20 | ||
![]() |
232ae2a189 | ||
![]() |
aa8b23105f | ||
![]() |
c113f854a2 | ||
![]() |
87de0e7a0e | ||
![]() |
85755e3022 | ||
![]() |
02dc1172be | ||
![]() |
dbf8c41209 | ||
![]() |
8c4fd759c6 | ||
![]() |
23dc19ad94 | ||
![]() |
0c99c4d93f | ||
![]() |
8ab045331b | ||
![]() |
a8d0936e04 | ||
![]() |
4e349acb50 | ||
![]() |
947e3b06b4 | ||
![]() |
5fd574a14f | ||
![]() |
03c1053871 | ||
![]() |
c7ed0ef5eb | ||
![]() |
2aede97754 | ||
![]() |
9b8a5e9bf3 | ||
![]() |
0f910f2d40 | ||
![]() |
15f155100c | ||
![]() |
2468f5a6c4 | ||
![]() |
945a52a99f | ||
![]() |
486b2c82a7 | ||
![]() |
800b7f4370 | ||
![]() |
8ca5a048d6 | ||
![]() |
44b7a3c3f1 | ||
![]() |
554ebe7206 | ||
![]() |
d7b87fcb8e | ||
![]() |
c94f9e1cc9 | ||
![]() |
68532fade3 | ||
![]() |
e219867cdf | ||
![]() |
765d5d9729 | ||
![]() |
43029f37b1 | ||
![]() |
7188462c55 | ||
![]() |
f9ff814955 | ||
![]() |
dfbd1305b3 | ||
![]() |
c9255ab31b | ||
![]() |
1e714af3cf | ||
![]() |
4c959cd983 | ||
![]() |
d959c35723 | ||
![]() |
69a9d7485b | ||
![]() |
dcf07ad8c7 | ||
![]() |
ed6cdb2eb4 | ||
![]() |
a73e7e9f99 | ||
![]() |
ab853e1fcf | ||
![]() |
37d38b62b1 | ||
![]() |
f9bb517142 | ||
![]() |
efe9b867d5 | ||
![]() |
d9cf33d1ba | ||
![]() |
ee3028e67d | ||
![]() |
d810e6c82d | ||
![]() |
e0a281583d | ||
![]() |
d739dcac2b | ||
![]() |
cdd4cb8ec2 | ||
![]() |
93ef90cd24 | ||
![]() |
e165a1e65c | ||
![]() |
4066e5bf14 | ||
![]() |
4729514a22 | ||
![]() |
93aedcfeb7 | ||
![]() |
47d18bb896 | ||
![]() |
61dafbe06e | ||
![]() |
474325da68 | ||
![]() |
9317401d57 | ||
![]() |
67d746a62c | ||
![]() |
2f1f68f12f | ||
![]() |
2742edd73f | ||
![]() |
834561a5de | ||
![]() |
11102b4dd6 | ||
![]() |
fef2da3c0b | ||
![]() |
9820296e92 | ||
![]() |
dbfde74c1e | ||
![]() |
b28668e18d | ||
![]() |
5f1174de27 | ||
![]() |
543ce937ec | ||
![]() |
5537b083a8 | ||
![]() |
6b0854749f | ||
![]() |
09ba4772b8 | ||
![]() |
06a1d08465 | ||
![]() |
d510ead877 | ||
![]() |
2968a1559e | ||
![]() |
cba26eedb5 | ||
![]() |
23e74b2781 | ||
![]() |
ef0277d10e | ||
![]() |
a623a5b7cc | ||
![]() |
be8479fdba | ||
![]() |
e97e6d467c | ||
![]() |
75ec890d46 | ||
![]() |
871a9c29c8 | ||
![]() |
a4f903d947 | ||
![]() |
1920a52829 | ||
![]() |
6e14a727b1 | ||
![]() |
ea855837df | ||
![]() |
d05ed0e59c | ||
![]() |
a9eb443072 | ||
![]() |
d382b00efd | ||
![]() |
ef9d077c7f | ||
![]() |
e4b20abf8e | ||
![]() |
e9f0a10175 | ||
![]() |
c3968a26cf | ||
![]() |
9371515ecc | ||
![]() |
a83e055b19 | ||
![]() |
6907651756 | ||
![]() |
fc2d0246e6 | ||
![]() |
bb9c362bab | ||
![]() |
51402e68d2 | ||
![]() |
1b8813228b | ||
![]() |
922e36cfb0 | ||
![]() |
edff094626 | ||
![]() |
aa72a080b0 | ||
![]() |
2a93d1c652 | ||
![]() |
6b2f23712c | ||
![]() |
375ab93ee3 | ||
![]() |
d5962e9d71 | ||
![]() |
ffaa264bd3 |
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
## READ BEFORE OPENING ISSUES
|
||||
|
||||
All bug reports require you to **USE CANARY BUILDS**. Please include the version name and version code in the bug report.
|
||||
|
||||
If you experience a bootloop, attach a `dmesg` (kernel logs) when the device refuse to boot. This may very likely require a custom kernel on some devices as `last_kmsg` or `pstore ramoops` are usually not enabled by default. In addition, please also upload the result of `cat /proc/mounts` when your device is working correctly **WITHOUT ROOT**.
|
||||
|
||||
If you experience issues during installation, in recovery, upload the recovery logs, or in Magisk, upload the install logs. Please also upload the `boot.img` or `recovery.img` that you are using for patching.
|
||||
|
||||
If you experience a crash of Magisk app, dump the full `logcat` **when the crash happens**.
|
||||
|
||||
If you experience other issues related to Magisk, upload `magisk.log`, and preferably also include a boot `logcat` (start dumping `logcat` when the device boots up)
|
||||
|
||||
**DO NOT** open issues regarding root detection.
|
||||
|
||||
**DO NOT** ask for instructions.
|
||||
|
||||
**DO NOT** report issues if you have any modules installed.
|
||||
|
||||
Without following the rules above, your issue will be closed without explanation.
|
||||
|
||||
-->
|
||||
|
||||
Device:
|
||||
Android version:
|
||||
Magisk version name:
|
||||
Magisk version code:
|
6
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
6
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: XDA Community Support
|
||||
url: https://forum.xda-developers.com/f/magisk.5903/
|
||||
about: Please ask and answer questions here.
|
||||
|
90
.github/workflows/build.yml
vendored
Normal file
90
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
name: Magisk Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'app/**'
|
||||
- 'native/**'
|
||||
- 'stub/**'
|
||||
- 'buildSrc/**'
|
||||
- 'build.py'
|
||||
- 'gradle.properties'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macOS-latest ]
|
||||
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: '11'
|
||||
|
||||
- name: Set up Python 3
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Set up GitHub env (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
$ndk_ver = Select-String -Path "gradle.properties" -Pattern "^magisk.fullNdkVersion=" | % { $_ -replace ".*=" }
|
||||
echo "ANDROID_SDK_ROOT=$env:ANDROID_SDK_ROOT" >> $env:GITHUB_ENV
|
||||
echo "MAGISK_NDK_VERSION=$ndk_ver" >> $env:GITHUB_ENV
|
||||
echo "GRADLE_OPTS=-Dorg.gradle.daemon=false" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Set up GitHub env (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
run: |
|
||||
ndk_ver=$(sed -n 's/^magisk.fullNdkVersion=//p' gradle.properties)
|
||||
echo ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT >> $GITHUB_ENV
|
||||
echo MAGISK_NDK_VERSION=$ndk_ver >> $GITHUB_ENV
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
|
||||
restore-keys: ${{ runner.os }}-gradle-
|
||||
|
||||
- name: Cache NDK
|
||||
id: ndk-cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ env.ANDROID_SDK_ROOT }}/ndk/magisk
|
||||
key: ${{ runner.os }}-ndk-${{ env.MAGISK_NDK_VERSION }}
|
||||
|
||||
- name: Set up NDK
|
||||
if: steps.ndk-cache.outputs.cache-hit != 'true'
|
||||
run: python build.py ndk
|
||||
|
||||
- name: Build release
|
||||
run: python build.py -vr all
|
||||
|
||||
- name: Build debug
|
||||
run: python build.py -v all
|
||||
|
||||
# Only upload artifacts built on Linux
|
||||
- name: Upload build artifact
|
||||
if: runner.os == 'Linux'
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: ${{ github.sha }}
|
||||
path: out
|
26
.github/workflows/issues.yml
vendored
Normal file
26
.github/workflows/issues.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Check Issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v2
|
||||
- name: Read latest version code
|
||||
run: |
|
||||
ver=$(sed -n 's/^magisk.versionCode=//p' gradle.properties)
|
||||
echo MAGISK_VERSION_CODE=$ver >> $GITHUB_ENV
|
||||
- if: contains(github.event.issue.body, format('Magisk version code{0} ', ':')) != true
|
||||
id: close
|
||||
name: Close Issue(template)
|
||||
uses: peter-evans/close-issue@v1
|
||||
with:
|
||||
comment: This issue is being automatically closed because it does not follow the issue template.
|
||||
- if: steps.close.conclusion == 'skipped' && contains(github.event.issue.body, format('Magisk version code{0} {1}', ':', env.MAGISK_VERSION_CODE)) != true
|
||||
name: Close Issue(latest canary)
|
||||
uses: peter-evans/close-issue@v1
|
||||
with:
|
||||
comment: This issue is being automatically closed because latest canary Magisk version code is ${{ env.MAGISK_VERSION_CODE }}.
|
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -25,6 +25,12 @@
|
||||
[submodule "pcre"]
|
||||
path = native/jni/external/pcre
|
||||
url = https://android.googlesource.com/platform/external/pcre
|
||||
[submodule "xhook"]
|
||||
path = native/jni/external/xhook
|
||||
url = https://github.com/iqiyi/xHook.git
|
||||
[submodule "libcxx"]
|
||||
path = native/jni/external/libcxx
|
||||
url = https://github.com/topjohnwu/libcxx.git
|
||||
[submodule "termux-elf-cleaner"]
|
||||
path = tools/termux-elf-cleaner
|
||||
url = https://github.com/termux/termux-elf-cleaner.git
|
||||
|
79
README.MD
79
README.MD
@@ -1,43 +1,68 @@
|
||||
# Magisk
|
||||

|
||||
|
||||
[Downloads](https://github.com/topjohnwu/Magisk/releases) \| [Documentation](https://topjohnwu.github.io/Magisk/) \| [XDA Thread](https://forum.xda-developers.com/apps/magisk/official-magisk-v7-universal-systemless-t3473445)
|
||||
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/count/count.json)
|
||||
|
||||
## Introduction
|
||||
|
||||
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.
|
||||
Magisk is a suite of open source tools for customizing Android, supporting devices higher than Android 4.2. It covers fundamental parts of Android customization: root, boot scripts, SELinux patches, AVB2.0 / dm-verity / forceencrypt removals etc.
|
||||
|
||||
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.
|
||||
Here are some feature highlights:
|
||||
|
||||
- **MagiskSU**: Provide root access to your device
|
||||
- **Magisk Modules**: Modify read-only partitions by installing modules
|
||||
- **MagiskHide**: Hide Magisk from root detections / system integrity checks
|
||||
|
||||
## Downloads
|
||||
|
||||
Please note that the only source of official Magisk information and downloads is [Github](https://github.com/topjohnwu/Magisk/). Beware of any other websites.
|
||||
|
||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v22.0)
|
||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v22.0)
|
||||
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-debug.apk)
|
||||
|
||||
## Useful Links
|
||||
|
||||
- [Installation Instruction](https://topjohnwu.github.io/Magisk/install.html)
|
||||
- [Frequently Asked Questions](https://topjohnwu.github.io/Magisk/faq.html)
|
||||
- [Magisk Documentation](https://topjohnwu.github.io/Magisk/)
|
||||
- [Magisk Troubleshoot Wiki](https://www.didgeridoohan.com/magisk/HomePage) (by [@Didgeridoohan](https://github.com/Didgeridoohan))
|
||||
|
||||
## Android Version Support
|
||||
|
||||
- Android 4.2+: MagiskSU and Magisk Modules Only
|
||||
- Android 4.4+: All core features available
|
||||
- Android 6.0+: Guaranteed MagiskHide support
|
||||
- Android 7.0+: Full MagiskHide protection
|
||||
|
||||
## Bug Reports
|
||||
|
||||
**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)
|
||||
**Only bug reports from Canary builds will be accepted.**
|
||||
|
||||
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.
|
||||
For installation issues, upload both boot image and install logs.<br>
|
||||
For Magisk issues, upload boot logcat or dmesg.<br>
|
||||
For Magisk app crashes, record and upload the logcat when the crash occurs.
|
||||
|
||||
## Building Environment Requirements
|
||||
## Building and Development
|
||||
|
||||
- Python 3: run `build.py` script
|
||||
- Java Development Kit (JDK) 8: Compile Magisk Manager and sign zips
|
||||
- Latest Android SDK: set `ANDROID_HOME` environment variable to the path to Android SDK
|
||||
- Android NDK: Install NDK along with SDK (`$ANDROID_HOME/ndk-bundle`), or optionally specify a custom path `ANDROID_NDK_HOME`
|
||||
- (Windows Only) Python package Colorama: Install with `pip install colorama`, used for ANSI color codes
|
||||
- Magisk builds on any OS Android Studio supports. Install Android Studio and do the initial setups.
|
||||
- Clone sources: `git clone --recurse-submodules https://github.com/topjohnwu/Magisk.git`
|
||||
- Install Python 3.6+ \
|
||||
(Windows only: select **'Add Python to PATH'** in installer, and run `pip install colorama` after install)
|
||||
- Configure to use the JDK bundled in Android Studio:
|
||||
- macOS: `export JAVA_HOME="/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home"`
|
||||
- Linux: `export PATH="/path/to/androidstudio/jre/bin:$PATH"`
|
||||
- Windows: Add `C:\Path\To\Android Studio\jre\bin` to environment variable `PATH`
|
||||
- Set environment variable `ANDROID_SDK_ROOT` to the Android SDK folder (can be found in Android Studio settings)
|
||||
- Run `./build.py ndk` to let the script download and install NDK for you
|
||||
- To start building, run `build.py` to see your options. \
|
||||
For each action, use `-h` to access help (e.g. `./build.py all -h`)
|
||||
- To start development, open the project with Android Studio. The IDE can be used for both app (Kotlin/Java) and native (C++/C) sources.
|
||||
- Optionally, set custom configs with `config.prop`. A sample `config.prop.sample` is provided.
|
||||
- To sign APKs and zips with your own private keys, set signing configs in `config.prop`. For more info, check [Google's Documentation](https://developer.android.com/studio/publish/app-signing.html#generate-key).
|
||||
|
||||
## Building Notes and Instructions
|
||||
## Translation Contributions
|
||||
|
||||
- Clone sources with submodules: `git clone --recurse-submodules https://github.com/topjohnwu/Magisk.git`
|
||||
- Building is supported on macOS, Linux, and Windows. Official releases are built and tested with [FrankeNDK](https://github.com/topjohnwu/FrankeNDK); point `ANDROID_NDK_HOME` to FrankeNDK if you want to use it for compiling.
|
||||
- Set configurations in `config.prop`. A sample file `config.prop.sample` is provided as an example.
|
||||
- Run `build.py` with argument `-h` to see the built-in help message. The `-h` option also works for each supported actions, e.g. `./build.py binary -h`
|
||||
- By default, `build.py` build binaries and Magisk Manager in debug mode. If you want to build Magisk Manager in release mode (via the `-r, --release` flag), you need a Java Keystore file `release-key.jks` (only `JKS` format is supported) to sign APKs and zips. For more information, check out [Google's Official Documentation](https://developer.android.com/studio/publish/app-signing.html#signing-manually).
|
||||
|
||||
## Translations
|
||||
|
||||
Default string resources for Magisk Manager and its stub APK are located here:
|
||||
Default string resources for the Magisk app and its stub APK are located here:
|
||||
|
||||
- `app/src/main/res/values/strings.xml`
|
||||
- `stub/src/main/res/values/strings.xml`
|
||||
|
5
app/.gitignore
vendored
5
app/.gitignore
vendored
@@ -6,6 +6,7 @@
|
||||
app/release
|
||||
*.hprof
|
||||
.externalNativeBuild/
|
||||
public.certificate.x509.pem
|
||||
private.key.pk8
|
||||
*.apk
|
||||
src/main/assets
|
||||
src/main/jniLibs
|
||||
src/main/resources
|
||||
|
135
app/build.gradle
135
app/build.gradle
@@ -1,135 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
kapt {
|
||||
correctErrorTypes = true
|
||||
useBuildCache = true
|
||||
mapDiagnosticLocations = true
|
||||
javacOptions {
|
||||
option("-Xmaxerrs", 1000)
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
applicationId 'com.topjohnwu.magisk'
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
multiDexEnabled true
|
||||
versionName props['appVersion']
|
||||
versionCode props['appVersionCode'] as Integer
|
||||
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments = ["room.incremental":"true"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
|
||||
'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
dataBinding {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude '/META-INF/**'
|
||||
exclude '/androidsupportmultidexversion.txt'
|
||||
exclude '/org/bouncycastle/**'
|
||||
exclude '/kotlin/**'
|
||||
exclude '/kotlinx/**'
|
||||
exclude '/okhttp3/**'
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
implementation project(':shared')
|
||||
implementation project(':signing')
|
||||
|
||||
implementation 'com.github.topjohnwu:jtar:1.0.0'
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
implementation 'com.ncapdevi:frag-nav:3.2.0'
|
||||
implementation 'com.github.pwittchen:reactivenetwork-rx2:3.0.6'
|
||||
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.2.18'
|
||||
implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:${vKotlin}"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${vKotlin}"
|
||||
|
||||
def vBAdapt = '3.1.1'
|
||||
def bindingAdapter = 'me.tatarka.bindingcollectionadapter2:bindingcollectionadapter'
|
||||
implementation "${bindingAdapter}:${vBAdapt}"
|
||||
implementation "${bindingAdapter}-recyclerview:${vBAdapt}"
|
||||
|
||||
def vMarkwon = '4.2.1'
|
||||
implementation "io.noties.markwon:core:${vMarkwon}"
|
||||
implementation "io.noties.markwon:html:${vMarkwon}"
|
||||
implementation "io.noties.markwon:image:${vMarkwon}"
|
||||
implementation 'com.caverock:androidsvg:1.4'
|
||||
|
||||
def vLibsu = '2.5.1'
|
||||
implementation "com.github.topjohnwu.libsu:core:${vLibsu}"
|
||||
implementation "com.github.topjohnwu.libsu:io:${vLibsu}"
|
||||
|
||||
def vKoin = '2.0.1'
|
||||
implementation "org.koin:koin-core:${vKoin}"
|
||||
implementation "org.koin:koin-android:${vKoin}"
|
||||
implementation "org.koin:koin-androidx-viewmodel:${vKoin}"
|
||||
|
||||
def vRetrofit = '2.7.1'
|
||||
implementation "com.squareup.retrofit2:retrofit:${vRetrofit}"
|
||||
implementation "com.squareup.retrofit2:converter-moshi:${vRetrofit}"
|
||||
implementation "com.squareup.retrofit2:converter-scalars:${vRetrofit}"
|
||||
implementation "com.squareup.retrofit2:adapter-rxjava2:${vRetrofit}"
|
||||
|
||||
def vOkHttp = '3.12.10'
|
||||
implementation("com.squareup.okhttp3:okhttp:${vOkHttp}") {
|
||||
force = true
|
||||
}
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:${vOkHttp}"
|
||||
|
||||
def vMoshi = '1.10.0-SNAPSHOT'
|
||||
implementation "com.squareup.moshi:moshi:${vMoshi}"
|
||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}"
|
||||
|
||||
def vRoom = '2.2.4'
|
||||
implementation "androidx.room:room-runtime:${vRoom}"
|
||||
implementation "androidx.room:room-rxjava2:${vRoom}"
|
||||
kapt "androidx.room:room-compiler:${vRoom}"
|
||||
|
||||
def vNav = '2.2.1'
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:${vNav}"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:${vNav}"
|
||||
|
||||
implementation 'androidx.biometric:biometric:1.0.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-beta01'
|
||||
implementation 'androidx.browser:browser:1.2.0'
|
||||
implementation 'androidx.preference:preference:1.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.2.2'
|
||||
implementation 'androidx.work:work-runtime:2.3.3'
|
||||
implementation 'androidx.transition:transition:1.3.1'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.core:core-ktx:1.2.0'
|
||||
implementation 'com.google.android.material:material:1.2.0-alpha03'
|
||||
}
|
258
app/build.gradle.kts
Normal file
258
app/build.gradle.kts
Normal file
@@ -0,0 +1,258 @@
|
||||
import org.apache.tools.ant.filters.FixCrLfFilter
|
||||
import java.io.PrintStream
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
kotlin("android")
|
||||
kotlin("plugin.parcelize")
|
||||
kotlin("kapt")
|
||||
id("androidx.navigation.safeargs.kotlin")
|
||||
}
|
||||
|
||||
kapt {
|
||||
correctErrorTypes = true
|
||||
useBuildCache = true
|
||||
mapDiagnosticLocations = true
|
||||
javacOptions {
|
||||
option("-Xmaxerrs", 1000)
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
applicationId = "com.topjohnwu.magisk"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
multiDexEnabled = true
|
||||
versionName = Config.version
|
||||
versionCode = Config.versionCode
|
||||
ndk.abiFilters("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
|
||||
javaCompileOptions.annotationProcessorOptions.arguments(
|
||||
mapOf("room.incremental" to "true")
|
||||
)
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
getByName("release") {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
dataBinding = true
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
includeInApk = false
|
||||
includeInBundle = false
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude("/META-INF/*")
|
||||
exclude("/org/bouncycastle/**")
|
||||
exclude("/kotlin/**")
|
||||
exclude("/kotlinx/**")
|
||||
exclude("/okhttp3/**")
|
||||
exclude("/*.txt")
|
||||
exclude("/*.bin")
|
||||
doNotStrip("**/*.so")
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
val syncLibs by tasks.registering(Sync::class) {
|
||||
into("src/main/jniLibs")
|
||||
into("armeabi-v7a") {
|
||||
from(rootProject.file("native/out/armeabi-v7a")) {
|
||||
include("busybox", "magiskboot", "magiskinit", "magisk")
|
||||
rename { if (it == "magisk") "libmagisk32.so" else "lib$it.so" }
|
||||
}
|
||||
from(rootProject.file("native/out/arm64-v8a")) {
|
||||
include("magisk")
|
||||
rename { if (it == "magisk") "libmagisk64.so" else "lib$it.so" }
|
||||
}
|
||||
}
|
||||
into("x86") {
|
||||
from(rootProject.file("native/out/x86")) {
|
||||
include("busybox", "magiskboot", "magiskinit", "magisk")
|
||||
rename { if (it == "magisk") "libmagisk32.so" else "lib$it.so" }
|
||||
}
|
||||
from(rootProject.file("native/out/x86_64")) {
|
||||
include("magisk")
|
||||
rename { if (it == "magisk") "libmagisk64.so" else "lib$it.so" }
|
||||
}
|
||||
}
|
||||
onlyIf {
|
||||
if (inputs.sourceFiles.files.size != 10)
|
||||
throw StopExecutionException("Please build binaries first! (./build.py binary)")
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
val createStubLibs by tasks.registering {
|
||||
dependsOn(syncLibs)
|
||||
doLast {
|
||||
val arm64 = project.file("src/main/jniLibs/arm64-v8a/libstub.so")
|
||||
arm64.parentFile.mkdirs()
|
||||
arm64.createNewFile()
|
||||
val x64 = project.file("src/main/jniLibs/x86_64/libstub.so")
|
||||
x64.parentFile.mkdirs()
|
||||
x64.createNewFile()
|
||||
}
|
||||
}
|
||||
|
||||
val syncAssets by tasks.registering(Sync::class) {
|
||||
dependsOn(createStubLibs)
|
||||
inputs.property("version", Config.version)
|
||||
inputs.property("versionCode", Config.versionCode)
|
||||
into("src/main/assets")
|
||||
from(rootProject.file("scripts")) {
|
||||
include("util_functions.sh", "boot_patch.sh", "uninstaller.sh", "addon.d.sh")
|
||||
}
|
||||
into("chromeos") {
|
||||
from(rootProject.file("tools/futility"))
|
||||
from(rootProject.file("tools/keys")) {
|
||||
include("kernel_data_key.vbprivk", "kernel.keyblock")
|
||||
}
|
||||
}
|
||||
filesMatching("**/util_functions.sh") {
|
||||
filter {
|
||||
it.replace("#MAGISK_VERSION_STUB",
|
||||
"MAGISK_VER='${Config.version}'\n" +
|
||||
"MAGISK_VER_CODE=${Config.versionCode}")
|
||||
}
|
||||
filter<FixCrLfFilter>("eol" to FixCrLfFilter.CrLf.newInstance("lf"))
|
||||
}
|
||||
}
|
||||
|
||||
val syncResources by tasks.registering(Sync::class) {
|
||||
dependsOn(syncAssets)
|
||||
into("src/main/resources/META-INF/com/google/android")
|
||||
from(rootProject.file("scripts/update_binary.sh")) {
|
||||
rename { "update-binary" }
|
||||
}
|
||||
from(rootProject.file("scripts/flash_script.sh")) {
|
||||
rename { "updater-script" }
|
||||
}
|
||||
}
|
||||
|
||||
tasks["preBuild"]?.dependsOn(syncResources)
|
||||
|
||||
android.applicationVariants.all {
|
||||
val keysDir = rootProject.file("tools/keys")
|
||||
val outSrcDir = File(buildDir, "generated/source/keydata/$name")
|
||||
val outSrc = File(outSrcDir, "com/topjohnwu/signing/KeyData.java")
|
||||
|
||||
fun PrintStream.newField(name: String, file: File) {
|
||||
println("public static byte[] $name() {")
|
||||
print("byte[] buf = {")
|
||||
val bytes = file.readBytes()
|
||||
print(bytes.joinToString(",") { "(byte)(${it.toInt() and 0xff})" })
|
||||
println("};")
|
||||
println("return buf;")
|
||||
println("}")
|
||||
}
|
||||
|
||||
val genSrcTask = tasks.register("generate${name.capitalize()}KeyData") {
|
||||
inputs.dir(keysDir)
|
||||
outputs.file(outSrc)
|
||||
doLast {
|
||||
outSrc.parentFile.mkdirs()
|
||||
PrintStream(outSrc).use {
|
||||
it.println("package com.topjohnwu.signing;")
|
||||
it.println("public final class KeyData {")
|
||||
|
||||
it.newField("testCert", File(keysDir, "testkey.x509.pem"))
|
||||
it.newField("testKey", File(keysDir, "testkey.pk8"))
|
||||
it.newField("verityCert", File(keysDir, "verity.x509.pem"))
|
||||
it.newField("verityKey", File(keysDir, "verity.pk8"))
|
||||
|
||||
it.println("}")
|
||||
}
|
||||
}
|
||||
}
|
||||
registerJavaGeneratingTask(genSrcTask.get(), outSrcDir)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
|
||||
implementation(kotlin("stdlib"))
|
||||
implementation(project(":app:shared"))
|
||||
|
||||
implementation("com.github.topjohnwu:jtar:1.0.0")
|
||||
implementation("com.github.topjohnwu:indeterminate-checkbox:1.0.7")
|
||||
implementation("com.github.topjohnwu:lz4-java:1.7.1")
|
||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
||||
|
||||
val vBC = "1.68"
|
||||
implementation("org.bouncycastle:bcprov-jdk15on:${vBC}")
|
||||
implementation("org.bouncycastle:bcpkix-jdk15on:${vBC}")
|
||||
|
||||
val vBAdapt = "4.0.0"
|
||||
val bindingAdapter = "me.tatarka.bindingcollectionadapter2:bindingcollectionadapter"
|
||||
implementation("${bindingAdapter}:${vBAdapt}")
|
||||
implementation("${bindingAdapter}-recyclerview:${vBAdapt}")
|
||||
|
||||
val vMarkwon = "4.6.2"
|
||||
implementation("io.noties.markwon:core:${vMarkwon}")
|
||||
implementation("io.noties.markwon:html:${vMarkwon}")
|
||||
implementation("io.noties.markwon:image:${vMarkwon}")
|
||||
implementation("com.caverock:androidsvg:1.4")
|
||||
|
||||
val vLibsu = "3.1.1"
|
||||
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
|
||||
implementation("com.github.topjohnwu.libsu:io:${vLibsu}")
|
||||
|
||||
val vKoin = "2.1.6"
|
||||
implementation("org.koin:koin-core:${vKoin}")
|
||||
implementation("org.koin:koin-android:${vKoin}")
|
||||
implementation("org.koin:koin-androidx-viewmodel:${vKoin}")
|
||||
|
||||
val vRetrofit = "2.9.0"
|
||||
implementation("com.squareup.retrofit2:retrofit:${vRetrofit}")
|
||||
implementation("com.squareup.retrofit2:converter-moshi:${vRetrofit}")
|
||||
implementation("com.squareup.retrofit2:converter-scalars:${vRetrofit}")
|
||||
|
||||
val vOkHttp = "3.12.12"
|
||||
implementation("com.squareup.okhttp3:okhttp") {
|
||||
version {
|
||||
strictly(vOkHttp)
|
||||
}
|
||||
}
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:${vOkHttp}")
|
||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:${vOkHttp}")
|
||||
|
||||
val vMoshi = "1.11.0"
|
||||
implementation("com.squareup.moshi:moshi:${vMoshi}")
|
||||
kapt("com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}")
|
||||
|
||||
val vRoom = "2.3.0-beta03"
|
||||
implementation("androidx.room:room-runtime:${vRoom}")
|
||||
implementation("androidx.room:room-ktx:${vRoom}")
|
||||
kapt("androidx.room:room-compiler:${vRoom}")
|
||||
|
||||
val vNav: String by rootProject.extra
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:${vNav}")
|
||||
implementation("androidx.navigation:navigation-ui-ktx:${vNav}")
|
||||
|
||||
implementation("androidx.biometric:biometric:1.1.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.0.4")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
implementation("androidx.browser:browser:1.3.0")
|
||||
implementation("androidx.preference:preference:1.1.1")
|
||||
implementation("androidx.recyclerview:recyclerview:1.1.0")
|
||||
implementation("androidx.fragment:fragment-ktx:1.3.1")
|
||||
implementation("androidx.work:work-runtime-ktx:2.5.0")
|
||||
implementation("androidx.transition:transition:1.4.0")
|
||||
implementation("androidx.multidex:multidex:2.0.1")
|
||||
implementation("androidx.core:core-ktx:1.3.2")
|
||||
implementation("com.google.android.material:material:1.3.0")
|
||||
}
|
26
app/proguard-rules.pro
vendored
26
app/proguard-rules.pro
vendored
@@ -18,34 +18,28 @@
|
||||
|
||||
# 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(...);
|
||||
public static void check*(...);
|
||||
public static void throw*(...);
|
||||
}
|
||||
|
||||
# Stubs
|
||||
-keep class a.* { *; }
|
||||
|
||||
# Snet
|
||||
-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);
|
||||
-keepclassmembers class com.topjohnwu.magisk.ui.safetynet.SafetyNetHelper { *; }
|
||||
-keep,allowobfuscation interface com.topjohnwu.magisk.ui.safetynet.SafetyNetHelper$Callback
|
||||
-keepclassmembers class * implements com.topjohnwu.magisk.ui.safetynet.SafetyNetHelper$Callback {
|
||||
void onResponse(org.json.JSONObject);
|
||||
}
|
||||
|
||||
# Fragments
|
||||
-keep,allowobfuscation class * extends androidx.fragment.app.Fragment
|
||||
# Stub
|
||||
-keep class com.topjohnwu.magisk.core.App { <init>(java.lang.Object); }
|
||||
|
||||
# Strip Timber verbose and debug logging
|
||||
-assumenosideeffects class timber.log.Timber.Tree {
|
||||
-assumenosideeffects class timber.log.Timber$Tree {
|
||||
public void v(**);
|
||||
public void d(**);
|
||||
}
|
||||
|
||||
# Excessive obfuscation
|
||||
-repackageclasses
|
||||
-repackageclasses 'a'
|
||||
-allowaccessmodification
|
||||
|
||||
# QOL
|
||||
|
@@ -1,5 +0,0 @@
|
||||
com.topjohnwu.magisk:color/xxxxxxxx = 0x7f010000
|
||||
com.topjohnwu.magisk:drawable/xxxxxxxx = 0x7f020000
|
||||
com.topjohnwu.magisk:string/xxxxxxxx = 0x7f030000
|
||||
com.topjohnwu.magisk:style/xxxxxxxx = 0x7f040000
|
||||
com.topjohnwu.magisk:xml/xxxxxxxx = 0x7f050000
|
14
app/shared/build.gradle.kts
Normal file
14
app/shared/build.gradle.kts
Normal file
@@ -0,0 +1,14 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
consumerProguardFiles("proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
|
||||
}
|
27
app/shared/src/main/AndroidManifest.xml
Normal file
27
app/shared/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.topjohnwu.shared"
|
||||
android:installLocation="internalOnly">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="29"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:label="Magisk"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="UnusedAttribute" />
|
||||
|
||||
</manifest>
|
@@ -4,31 +4,22 @@ import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ProviderInfo;
|
||||
import android.content.res.XmlResourceParser;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.text.TextUtils;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import com.topjohnwu.shared.R;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
|
||||
import static org.xmlpull.v1.XmlPullParser.START_TAG;
|
||||
|
||||
/**
|
||||
* Modified from androidx.core.content.FileProvider
|
||||
*/
|
||||
@@ -36,25 +27,12 @@ public class FileProvider extends ContentProvider {
|
||||
private static final String[] COLUMNS = {
|
||||
OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE };
|
||||
|
||||
private static final String TAG_ROOT_PATH = "root-path";
|
||||
private static final String TAG_FILES_PATH = "files-path";
|
||||
private static final String TAG_CACHE_PATH = "cache-path";
|
||||
private static final String TAG_EXTERNAL = "external-path";
|
||||
private static final String TAG_EXTERNAL_FILES = "external-files-path";
|
||||
private static final String TAG_EXTERNAL_CACHE = "external-cache-path";
|
||||
private static final String TAG_EXTERNAL_MEDIA = "external-media-path";
|
||||
|
||||
private static final String ATTR_NAME = "name";
|
||||
private static final String ATTR_PATH = "path";
|
||||
|
||||
private static final File DEVICE_ROOT = new File("/");
|
||||
|
||||
private static HashMap<String, PathStrategy> sCache = new HashMap<>();
|
||||
|
||||
private PathStrategy mStrategy;
|
||||
|
||||
public static ProviderCallHandler callHandler;
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return true;
|
||||
@@ -150,13 +128,6 @@ public class FileProvider extends ContentProvider {
|
||||
return file.delete() ? 1 : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle call(String method, String arg, Bundle extras) {
|
||||
if (callHandler != null)
|
||||
return callHandler.call(getContext(), method, arg, extras);
|
||||
return Bundle.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(Uri uri, String mode)
|
||||
throws FileNotFoundException {
|
||||
@@ -171,63 +142,36 @@ public class FileProvider extends ContentProvider {
|
||||
synchronized (sCache) {
|
||||
strat = sCache.get(authority);
|
||||
if (strat == null) {
|
||||
try {
|
||||
strat = parsePathStrategy(context, authority);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("Failed to parse xml", e);
|
||||
} catch (XmlPullParserException e) {
|
||||
throw new IllegalArgumentException("Failed to parse xml", e);
|
||||
}
|
||||
strat = createPathStrategy(context, authority);
|
||||
sCache.put(authority, strat);
|
||||
}
|
||||
}
|
||||
return strat;
|
||||
}
|
||||
|
||||
private static PathStrategy parsePathStrategy(Context context, String authority)
|
||||
throws IOException, XmlPullParserException {
|
||||
private static PathStrategy createPathStrategy(Context context, String authority) {
|
||||
final SimplePathStrategy strat = new SimplePathStrategy(authority);
|
||||
|
||||
final XmlResourceParser in = context.getResources().getXml(R.xml.file_paths);
|
||||
|
||||
int type;
|
||||
while ((type = in.next()) != END_DOCUMENT) {
|
||||
if (type == START_TAG) {
|
||||
final String tag = in.getName();
|
||||
|
||||
final String name = in.getAttributeValue(null, ATTR_NAME);
|
||||
String path = in.getAttributeValue(null, ATTR_PATH);
|
||||
|
||||
File target = null;
|
||||
if (TAG_ROOT_PATH.equals(tag)) {
|
||||
target = DEVICE_ROOT;
|
||||
} else if (TAG_FILES_PATH.equals(tag)) {
|
||||
target = context.getFilesDir();
|
||||
} else if (TAG_CACHE_PATH.equals(tag)) {
|
||||
target = context.getCacheDir();
|
||||
} else if (TAG_EXTERNAL.equals(tag)) {
|
||||
target = Environment.getExternalStorageDirectory();
|
||||
} else if (TAG_EXTERNAL_FILES.equals(tag)) {
|
||||
strat.addRoot("root_files", buildPath(DEVICE_ROOT, "."));
|
||||
strat.addRoot("internal_files", buildPath(context.getFilesDir(), "."));
|
||||
strat.addRoot("cache_files", buildPath(context.getCacheDir(), "."));
|
||||
strat.addRoot("external_files", buildPath(Environment.getExternalStorageDirectory(), "."));
|
||||
{
|
||||
File[] externalFilesDirs = getExternalFilesDirs(context, null);
|
||||
if (externalFilesDirs.length > 0) {
|
||||
target = externalFilesDirs[0];
|
||||
strat.addRoot("external_file_files", buildPath(externalFilesDirs[0], "."));
|
||||
}
|
||||
} else if (TAG_EXTERNAL_CACHE.equals(tag)) {
|
||||
}
|
||||
{
|
||||
File[] externalCacheDirs = getExternalCacheDirs(context);
|
||||
if (externalCacheDirs.length > 0) {
|
||||
target = externalCacheDirs[0];
|
||||
strat.addRoot("external_cache_files", buildPath(externalCacheDirs[0], "."));
|
||||
}
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
|
||||
&& TAG_EXTERNAL_MEDIA.equals(tag)) {
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
File[] externalMediaDirs = context.getExternalMediaDirs();
|
||||
if (externalMediaDirs.length > 0) {
|
||||
target = externalMediaDirs[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (target != null) {
|
||||
strat.addRoot(name, buildPath(target, path));
|
||||
}
|
||||
strat.addRoot("external_media_files", buildPath(externalMediaDirs[0], "."));
|
||||
}
|
||||
}
|
||||
|
@@ -47,10 +47,9 @@ public class Networking {
|
||||
} catch (Exception e) {
|
||||
if (Build.VERSION.SDK_INT < 21) {
|
||||
// Failed to update SSL provider, use NoSSLv3SocketFactory on SDK < 21
|
||||
// and return false to notify potential issues
|
||||
HttpsURLConnection.setDefaultSSLSocketFactory(new NoSSLv3SocketFactory());
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
package com.topjohnwu.magisk.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
|
||||
import com.topjohnwu.magisk.FileProvider;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class APKInstall {
|
||||
|
||||
public static Intent installIntent(Context c, File apk) {
|
||||
Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
intent.setData(FileProvider.getUriForFile(c, c.getPackageName() + ".provider", apk));
|
||||
} else {
|
||||
//noinspection ResultOfMethodCallIgnored SetWorldReadable
|
||||
apk.setReadable(true, false);
|
||||
intent.setData(Uri.fromFile(apk));
|
||||
}
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static void install(Context c, File apk) {
|
||||
c.startActivity(installIntent(c, apk));
|
||||
}
|
||||
|
||||
public static void registerInstallReceiver(Context c, BroadcastReceiver r) {
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
|
||||
filter.addAction(Intent.ACTION_PACKAGE_ADDED);
|
||||
filter.addDataScheme("package");
|
||||
c.getApplicationContext().registerReceiver(r, filter);
|
||||
}
|
||||
|
||||
public static void installHideResult(Activity c, File apk) {
|
||||
Intent intent = installIntent(c, apk);
|
||||
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
|
||||
c.startActivityForResult(intent, 0); // Ignore result, use install receiver
|
||||
}
|
||||
}
|
@@ -3,18 +3,19 @@
|
||||
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.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:name="a.e"
|
||||
android:allowBackup="true"
|
||||
android:name=".core.App"
|
||||
android:extractNativeLibs="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:multiArch="true"
|
||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
|
||||
|
||||
<!-- Splash -->
|
||||
<activity
|
||||
android:name="a.c"
|
||||
android:name=".core.SplashActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/SplashTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
@@ -27,43 +28,47 @@
|
||||
</activity>
|
||||
|
||||
<!-- Main -->
|
||||
<activity android:name="a.b" />
|
||||
|
||||
<!-- Flashing -->
|
||||
<activity android:name="a.f" />
|
||||
<activity android:name=".ui.MainActivity" />
|
||||
|
||||
<!-- Superuser -->
|
||||
<activity
|
||||
android:name="a.m"
|
||||
android:name=".ui.surequest.SuRequestActivity"
|
||||
android:directBootAware="true"
|
||||
android:excludeFromRecents="true"
|
||||
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"/>
|
||||
<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">
|
||||
android:name=".core.Receiver"
|
||||
android:directBootAware="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.REBOOT" />
|
||||
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
||||
<action android:name="android.intent.action.UID_REMOVED" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.PACKAGE_REPLACED" />
|
||||
<action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" />
|
||||
|
||||
<data android:scheme="package" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- DownloadService -->
|
||||
<service android:name="a.j" />
|
||||
<service android:name=".core.download.DownloadService" />
|
||||
|
||||
<!-- FileProvider -->
|
||||
<provider
|
||||
android:name=".core.Provider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:directBootAware="true"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true" />
|
||||
|
||||
<!-- Hardcode GMS version -->
|
||||
<meta-data
|
||||
@@ -74,16 +79,12 @@
|
||||
<provider
|
||||
android:name="androidx.work.impl.WorkManagerInitializer"
|
||||
android:authorities="${applicationId}.workmanager-init"
|
||||
tools:node="remove" />
|
||||
tools:node="remove"
|
||||
tools:ignore="ExportedContentProvider" />
|
||||
|
||||
<!-- 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>
|
||||
|
@@ -1,10 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.signing.BootSigner;
|
||||
|
||||
public class a {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
BootSigner.main(args);
|
||||
}
|
||||
}
|
@@ -1,26 +0,0 @@
|
||||
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()
|
121
app/src/main/java/com/topjohnwu/magisk/arch/BaseUIActivity.kt
Normal file
121
app/src/main/java/com/topjohnwu/magisk/arch/BaseUIActivity.kt
Normal file
@@ -0,0 +1,121 @@
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.res.use
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDirections
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||
import com.topjohnwu.magisk.ui.inflater.LayoutInflaterFactory
|
||||
import com.topjohnwu.magisk.ui.theme.Theme
|
||||
|
||||
abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
|
||||
BaseActivity(), BaseUIComponent<VM> {
|
||||
|
||||
protected lateinit var binding: Binding
|
||||
protected abstract val layoutRes: Int
|
||||
protected open val themeRes: Int = Theme.selected.themeRes
|
||||
|
||||
private val navHostFragment by lazy {
|
||||
supportFragmentManager.findFragmentById(navHostId) as? NavHostFragment
|
||||
}
|
||||
private val topFragment get() = navHostFragment?.childFragmentManager?.fragments?.getOrNull(0)
|
||||
protected val currentFragment get() = topFragment as? BaseUIFragment<*, *>
|
||||
|
||||
override val viewRoot: View get() = binding.root
|
||||
open val navigation: NavController? get() = navHostFragment?.navController
|
||||
|
||||
open val navHostId: Int = 0
|
||||
open val snackbarView get() = binding.root
|
||||
|
||||
init {
|
||||
val theme = Config.darkTheme
|
||||
AppCompatDelegate.setDefaultNightMode(theme)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
layoutInflater.factory2 = LayoutInflaterFactory(delegate)
|
||||
|
||||
setTheme(themeRes)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
startObserveEvents()
|
||||
|
||||
// We need to set the window background explicitly since for whatever reason it's not
|
||||
// propagated upstream
|
||||
obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
|
||||
.use { it.getDrawable(0) }
|
||||
.also { window.setBackgroundDrawable(it) }
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
window?.decorView?.let {
|
||||
it.systemUiVisibility = (it.systemUiVisibility
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
window?.decorView?.post {
|
||||
// If navigation bar is short enough (gesture navigation enabled), make it transparent
|
||||
if (window.decorView.rootWindowInsets?.systemWindowInsetBottom ?: 0 < Resources.getSystem().displayMetrics.density * 40) {
|
||||
window.navigationBarColor = Color.TRANSPARENT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
window.navigationBarDividerColor = Color.TRANSPARENT
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.isNavigationBarContrastEnforced = false
|
||||
window.isStatusBarContrastEnforced = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setContentView() {
|
||||
binding = DataBindingUtil.setContentView<Binding>(this, layoutRes).also {
|
||||
it.setVariable(BR.viewModel, viewModel)
|
||||
it.lifecycleOwner = this
|
||||
}
|
||||
}
|
||||
|
||||
fun setAccessibilityDelegate(delegate: View.AccessibilityDelegate?) {
|
||||
viewRoot.rootView.accessibilityDelegate = delegate
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.requestRefresh()
|
||||
}
|
||||
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
return currentFragment?.onKeyEvent(event) == true || super.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
override fun onEventDispatched(event: ViewEvent) = when (event) {
|
||||
is ContextExecutor -> event(this)
|
||||
is ActivityExecutor -> event(this)
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (navigation == null || currentFragment?.onBackPressed()?.not() == true) {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
fun NavDirections.navigate() {
|
||||
navigation?.navigate(this)
|
||||
}
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import android.view.View
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
|
||||
interface BaseUIComponent<VM : BaseViewModel> : LifecycleOwner {
|
||||
|
||||
val viewRoot: View
|
||||
val viewModel: VM
|
||||
|
||||
fun startObserveEvents() {
|
||||
viewModel.viewEvents.observe(this) {
|
||||
onEventDispatched(it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called for all [ViewEvent]s published by associated viewModel.
|
||||
*/
|
||||
fun onEventDispatched(event: ViewEvent) {}
|
||||
}
|
@@ -1,35 +1,31 @@
|
||||
package com.topjohnwu.magisk.ui.base
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.OnRebindCallback
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.NavDirections
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.extensions.startAnimations
|
||||
import com.topjohnwu.magisk.model.events.EventHandler
|
||||
import com.topjohnwu.magisk.model.events.ViewEvent
|
||||
import com.topjohnwu.magisk.ktx.startAnimations
|
||||
|
||||
abstract class BaseUIFragment<ViewModel : BaseViewModel, Binding : ViewDataBinding> :
|
||||
Fragment(), CompatView<ViewModel>, EventHandler {
|
||||
abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
|
||||
Fragment(), BaseUIComponent<VM> {
|
||||
|
||||
protected val activity get() = requireActivity() as BaseUIActivity<*, *>
|
||||
val activity get() = requireActivity() as BaseUIActivity<*, *>
|
||||
protected lateinit var binding: Binding
|
||||
protected abstract val layoutRes: Int
|
||||
|
||||
override val viewRoot: View get() = binding.root
|
||||
override val navigation by lazy { activity.navigation }
|
||||
private val delegate by lazy { CompatDelegate(this) }
|
||||
|
||||
override fun consumeSystemWindowInsets(insets: Insets) = insets
|
||||
private val navigation get() = activity.navigation
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
viewModel.viewEvents.observe(this, viewEventObserver)
|
||||
startObserveEvents()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
@@ -37,17 +33,27 @@ abstract class BaseUIFragment<ViewModel : BaseViewModel, Binding : ViewDataBindi
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
binding = DataBindingUtil.inflate<Binding>(inflater, layoutRes, container, false).apply {
|
||||
setVariable(BR.viewModel, viewModel)
|
||||
lifecycleOwner = this@BaseUIFragment
|
||||
binding = DataBindingUtil.inflate<Binding>(inflater, layoutRes, container, false).also {
|
||||
it.setVariable(BR.viewModel, viewModel)
|
||||
it.lifecycleOwner = this
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onEventDispatched(event: ViewEvent) {
|
||||
super.onEventDispatched(event)
|
||||
delegate.onEventExecute(event, this)
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
activity.supportActionBar?.subtitle = null
|
||||
}
|
||||
|
||||
override fun onEventDispatched(event: ViewEvent) = when(event) {
|
||||
is ContextExecutor -> event(requireContext())
|
||||
is ActivityExecutor -> event(activity)
|
||||
is FragmentExecutor -> event(this)
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
open fun onKeyEvent(event: KeyEvent): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
open fun onBackPressed(): Boolean = false
|
||||
@@ -55,27 +61,31 @@ abstract class BaseUIFragment<ViewModel : BaseViewModel, Binding : ViewDataBindi
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.addOnRebindCallback(object : OnRebindCallback<Binding>() {
|
||||
override fun onPreBind(binding: Binding): Boolean {
|
||||
this@BaseUIFragment.onPreBind(binding)
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
delegate.onCreate()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
delegate.onResume()
|
||||
viewModel.requestRefresh()
|
||||
}
|
||||
|
||||
protected open fun onPreBind(binding: Binding) {
|
||||
(binding.root as? ViewGroup)?.startAnimations()
|
||||
}
|
||||
|
||||
protected fun ViewEvent.dispatchOnSelf() = delegate.onEventExecute(this, this@BaseUIFragment)
|
||||
fun NavDirections.navigate() {
|
||||
navigation?.navigate(this)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface ReselectionTarget {
|
||||
|
||||
fun onReselected()
|
||||
|
||||
}
|
113
app/src/main/java/com/topjohnwu/magisk/arch/BaseViewModel.kt
Normal file
113
app/src/main/java/com/topjohnwu/magisk/arch/BaseViewModel.kt
Normal file
@@ -0,0 +1,113 @@
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import android.Manifest
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.databinding.Observable
|
||||
import androidx.databinding.PropertyChangeRegistry
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.NavDirections
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||
import com.topjohnwu.magisk.events.*
|
||||
import com.topjohnwu.magisk.utils.ObservableHost
|
||||
import com.topjohnwu.magisk.utils.set
|
||||
import kotlinx.coroutines.Job
|
||||
import org.koin.core.KoinComponent
|
||||
|
||||
abstract class BaseViewModel(
|
||||
initialState: State = State.LOADING
|
||||
) : ViewModel(), ObservableHost, KoinComponent {
|
||||
|
||||
override var callbacks: PropertyChangeRegistry? = null
|
||||
|
||||
enum class State {
|
||||
LOADED, LOADING, LOADING_FAILED
|
||||
}
|
||||
|
||||
@get:Bindable
|
||||
val loading get() = state == State.LOADING
|
||||
@get:Bindable
|
||||
val loaded get() = state == State.LOADED
|
||||
@get:Bindable
|
||||
val loadFailed get() = state == State.LOADING_FAILED
|
||||
|
||||
val isConnected get() = Info.isConnected
|
||||
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
|
||||
|
||||
@get:Bindable
|
||||
var insets = Insets.NONE
|
||||
set(value) = set(value, field, { field = it }, BR.insets)
|
||||
|
||||
var state= initialState
|
||||
set(value) = set(value, field, { field = it }, BR.loading, BR.loaded, BR.loadFailed)
|
||||
|
||||
private val _viewEvents = MutableLiveData<ViewEvent>()
|
||||
private var runningJob: Job? = null
|
||||
private val refreshCallback = object : Observable.OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
||||
requestRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
isConnected.addOnPropertyChangedCallback(refreshCallback)
|
||||
}
|
||||
|
||||
/** This should probably never be called manually, it's called manually via delegate. */
|
||||
@Synchronized
|
||||
fun requestRefresh() {
|
||||
if (runningJob?.isActive == true) {
|
||||
return
|
||||
}
|
||||
runningJob = refresh()
|
||||
}
|
||||
|
||||
protected open fun refresh(): Job? = null
|
||||
|
||||
@CallSuper
|
||||
override fun onCleared() {
|
||||
isConnected.removeOnPropertyChangedCallback(refreshCallback)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun withView(action: BaseActivity.() -> Unit) {
|
||||
ViewActionEvent(action).publish()
|
||||
}
|
||||
|
||||
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
||||
PermissionEvent(permission, callback).publish()
|
||||
}
|
||||
|
||||
fun withExternalRW(callback: () -> Unit) {
|
||||
withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) {
|
||||
if (!it) {
|
||||
SnackbarEvent(R.string.external_rw_permission_denied).publish()
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun back() = BackPressEvent().publish()
|
||||
|
||||
fun <Event : ViewEvent> Event.publish() {
|
||||
_viewEvents.postValue(this)
|
||||
}
|
||||
|
||||
fun <Event : ViewEventWithScope> Event.publish() {
|
||||
scope = viewModelScope
|
||||
_viewEvents.postValue(this)
|
||||
}
|
||||
|
||||
fun NavDirections.navigate() {
|
||||
_viewEvents.postValue(NavigationEvent(this))
|
||||
}
|
||||
|
||||
}
|
@@ -1,32 +1,33 @@
|
||||
package com.topjohnwu.magisk.ui.base
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
import com.topjohnwu.magisk.databinding.RvItem
|
||||
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||
import com.topjohnwu.magisk.utils.FilterableDiffObservableList
|
||||
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
|
||||
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
||||
import me.tatarka.bindingcollectionadapter2.OnItemBind
|
||||
|
||||
inline fun <T : ComparableRvItem<*>> diffListOf(
|
||||
fun <T : ComparableRvItem<*>> diffListOf(
|
||||
vararg newItems: T
|
||||
) = diffListOf(newItems.toList())
|
||||
|
||||
inline fun <T : ComparableRvItem<*>> diffListOf(
|
||||
fun <T : ComparableRvItem<*>> diffListOf(
|
||||
newItems: List<T>
|
||||
) = DiffObservableList(object : DiffObservableList.Callback<T> {
|
||||
override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.genericItemSameAs(newItem)
|
||||
override fun areContentsTheSame(oldItem: T, newItem: T) = oldItem.genericContentSameAs(newItem)
|
||||
}).also { it.update(newItems) }
|
||||
|
||||
inline fun <T : ComparableRvItem<*>> filterableListOf(
|
||||
fun <T : ComparableRvItem<*>> filterableListOf(
|
||||
vararg newItems: T
|
||||
) = FilterableDiffObservableList(object : DiffObservableList.Callback<T> {
|
||||
override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.genericItemSameAs(newItem)
|
||||
override fun areContentsTheSame(oldItem: T, newItem: T) = oldItem.genericContentSameAs(newItem)
|
||||
}).also { it.update(newItems.toList()) }
|
||||
|
||||
fun <T : ComparableRvItem<*>> adapterOf() = object : BindingRecyclerViewAdapter<T>() {
|
||||
fun <T : RvItem> adapterOf() = object : BindingRecyclerViewAdapter<T>() {
|
||||
override fun onBindBinding(
|
||||
binding: ViewDataBinding,
|
||||
variableId: Int,
|
||||
@@ -39,7 +40,7 @@ fun <T : ComparableRvItem<*>> adapterOf() = object : BindingRecyclerViewAdapter<
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T : ComparableRvItem<*>> itemBindingOf(
|
||||
inline fun <T : RvItem> itemBindingOf(
|
||||
crossinline body: (ItemBinding<*>) -> Unit = {}
|
||||
) = OnItemBind<T> { itemBinding, _, item ->
|
||||
item.bind(itemBinding)
|
17
app/src/main/java/com/topjohnwu/magisk/arch/Queryable.kt
Normal file
17
app/src/main/java/com/topjohnwu/magisk/arch/Queryable.kt
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import android.os.Handler
|
||||
import androidx.core.os.postDelayed
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
|
||||
interface Queryable {
|
||||
|
||||
val queryDelay: Long
|
||||
val queryHandler: Handler get() = UiThreadHandler.handler
|
||||
|
||||
fun submitQuery() {
|
||||
queryHandler.postDelayed(queryDelay) { query() }
|
||||
}
|
||||
|
||||
fun query()
|
||||
}
|
26
app/src/main/java/com/topjohnwu/magisk/arch/ViewEvent.kt
Normal file
26
app/src/main/java/com/topjohnwu/magisk/arch/ViewEvent.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
/**
|
||||
* Class for passing events from ViewModels to Activities/Fragments
|
||||
* (see https://medium.com/google-developers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150)
|
||||
*/
|
||||
abstract class ViewEvent
|
||||
|
||||
abstract class ViewEventWithScope: ViewEvent() {
|
||||
lateinit var scope: CoroutineScope
|
||||
}
|
||||
|
||||
interface ContextExecutor {
|
||||
operator fun invoke(context: Context)
|
||||
}
|
||||
|
||||
interface ActivityExecutor {
|
||||
operator fun invoke(activity: BaseUIActivity<*, *>)
|
||||
}
|
||||
|
||||
interface FragmentExecutor {
|
||||
operator fun invoke(fragment: BaseUIFragment<*, *>)
|
||||
}
|
@@ -1,25 +1,26 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.multidex.MultiDex
|
||||
import androidx.work.WorkManager
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
import com.topjohnwu.magisk.FileProvider
|
||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
||||
import com.topjohnwu.magisk.core.utils.RootInit
|
||||
import com.topjohnwu.magisk.core.utils.AppShellInit
|
||||
import com.topjohnwu.magisk.core.utils.BusyBoxInit
|
||||
import com.topjohnwu.magisk.core.utils.IODispatcherExecutor
|
||||
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.ktx.unwrap
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.context.startKoin
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
open class App() : Application() {
|
||||
@@ -30,11 +31,11 @@ open class App() : Application() {
|
||||
|
||||
init {
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER or Shell.FLAG_USE_MAGISK_BUSYBOX)
|
||||
Shell.Config.verboseLogging(BuildConfig.DEBUG)
|
||||
Shell.Config.addInitializers(RootInit::class.java)
|
||||
Shell.Config.setTimeout(2)
|
||||
FileProvider.callHandler = SuCallbackHandler
|
||||
Shell.setDefaultBuilder(Shell.Builder.create()
|
||||
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
||||
.setInitializers(BusyBoxInit::class.java, AppShellInit::class.java)
|
||||
.setTimeout(2))
|
||||
Shell.EXECUTOR = IODispatcherExecutor()
|
||||
|
||||
// Always log full stack trace with Timber
|
||||
Timber.plant(Timber.DebugTree())
|
||||
@@ -62,13 +63,19 @@ open class App() : Application() {
|
||||
val wrapped = impl.wrap()
|
||||
super.attachBaseContext(wrapped)
|
||||
|
||||
val info = base.applicationInfo
|
||||
val libDir = runCatching {
|
||||
info.javaClass.getDeclaredField("secondaryNativeLibraryDir").get(info) as String?
|
||||
}.getOrNull() ?: info.nativeLibraryDir
|
||||
Const.NATIVE_LIB_DIR = File(libDir)
|
||||
|
||||
// Normal startup
|
||||
startKoin {
|
||||
androidContext(wrapped)
|
||||
modules(koinModules)
|
||||
}
|
||||
ResMgr.init(impl)
|
||||
app.registerActivityLifecycleCallbacks(get<ActivityTracker>())
|
||||
AssetHack.init(impl)
|
||||
app.registerActivityLifecycleCallbacks(ForegroundTracker)
|
||||
WorkManager.initialize(impl.wrapJob(), androidx.work.Configuration.Builder().build())
|
||||
}
|
||||
|
||||
@@ -83,3 +90,25 @@ open class App() : Application() {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
}
|
||||
}
|
||||
|
||||
object ForegroundTracker : Application.ActivityLifecycleCallbacks {
|
||||
|
||||
@Volatile
|
||||
var foreground: Activity? = null
|
||||
|
||||
val hasForeground get() = foreground != null
|
||||
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
foreground = activity
|
||||
}
|
||||
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
foreground = null
|
||||
}
|
||||
|
||||
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
||||
override fun onActivityStarted(activity: Activity) {}
|
||||
override fun onActivityStopped(activity: Activity) {}
|
||||
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}
|
||||
override fun onActivityDestroyed(activity: Activity) {}
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
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
|
||||
@@ -10,18 +10,15 @@ 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.core.utils.refreshLocale
|
||||
import com.topjohnwu.magisk.data.preference.PreferenceModel
|
||||
import com.topjohnwu.magisk.data.repository.DBConfig
|
||||
import com.topjohnwu.magisk.di.Protected
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.magisk.model.preference.PreferenceModel
|
||||
import com.topjohnwu.magisk.ktx.inject
|
||||
import com.topjohnwu.magisk.ui.theme.Theme
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
object Config : PreferenceModel, DBConfig {
|
||||
|
||||
@@ -29,6 +26,15 @@ object Config : PreferenceModel, DBConfig {
|
||||
override val settingsDao: SettingsDao by inject()
|
||||
override val context: Context by inject(Protected)
|
||||
|
||||
@get:SuppressLint("ApplySharedPref")
|
||||
val prefsFile: File get() {
|
||||
// Flush prefs to disk
|
||||
prefs.edit().apply {
|
||||
remove(Key.ASKED_HOME)
|
||||
}.commit()
|
||||
return File("${context.filesDir.parent}/shared_prefs", "${fileName}.xml")
|
||||
}
|
||||
|
||||
object Key {
|
||||
// db configs
|
||||
const val ROOT_ACCESS = "root_access"
|
||||
@@ -43,24 +49,23 @@ object Config : PreferenceModel, DBConfig {
|
||||
const val SU_AUTO_RESPONSE = "su_auto_response"
|
||||
const val SU_NOTIFICATION = "su_notification"
|
||||
const val SU_REAUTH = "su_reauth"
|
||||
const val SU_TAPJACK = "su_tapjack"
|
||||
const val CHECK_UPDATES = "check_update"
|
||||
const val UPDATE_CHANNEL = "update_channel"
|
||||
const val CUSTOM_CHANNEL = "custom_channel"
|
||||
const val LOCALE = "locale"
|
||||
const val DARK_THEME = "dark_theme"
|
||||
const val DARK_THEME_EXTENDED = "dark_theme_extended"
|
||||
const val DARK_THEME = "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 DOWNLOAD_DIR = "download_dir"
|
||||
const val SAFETY = "safety_notice"
|
||||
const val THEME_ORDINAL = "theme_ordinal"
|
||||
const val BOOT_ID = "boot_id"
|
||||
const val LIST_SPAN_COUNT = "column_count"
|
||||
const val ASKED_HOME = "asked_home"
|
||||
const val DOH = "doh"
|
||||
|
||||
// system state
|
||||
const val MAGISKHIDE = "magiskhide"
|
||||
const val COREONLY = "disable"
|
||||
}
|
||||
|
||||
object Value {
|
||||
@@ -70,7 +75,6 @@ object Config : PreferenceModel, DBConfig {
|
||||
const val BETA_CHANNEL = 1
|
||||
const val CUSTOM_CHANNEL = 2
|
||||
const val CANARY_CHANNEL = 3
|
||||
const val CANARY_DEBUG_CHANNEL = 4
|
||||
|
||||
// root access mode
|
||||
const val ROOT_ACCESS_DISABLED = 0
|
||||
@@ -106,40 +110,44 @@ object Config : PreferenceModel, DBConfig {
|
||||
}
|
||||
|
||||
private val defaultChannel =
|
||||
if (isCanaryVersion) {
|
||||
if (BuildConfig.DEBUG)
|
||||
Value.CANARY_DEBUG_CHANNEL
|
||||
else
|
||||
Value.CANARY_CHANNEL
|
||||
} else Value.DEFAULT_CHANNEL
|
||||
else
|
||||
Value.DEFAULT_CHANNEL
|
||||
|
||||
@JvmStatic var keepVerity = false
|
||||
@JvmStatic var keepEnc = false
|
||||
@JvmStatic var recovery = false
|
||||
|
||||
var bootId by preference(Key.BOOT_ID, "")
|
||||
var askedHome by preference(Key.ASKED_HOME, false)
|
||||
|
||||
var downloadPath by preference(Key.DOWNLOAD_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||
var downloadDir by preference(Key.DOWNLOAD_DIR, "")
|
||||
var repoOrder by preference(Key.REPO_ORDER, Value.ORDER_DATE)
|
||||
|
||||
var suDefaultTimeout by preferenceStrInt(Key.SU_REQUEST_TIMEOUT, 10)
|
||||
var suAutoReponse by preferenceStrInt(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT)
|
||||
var suAutoResponse by preferenceStrInt(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT)
|
||||
var suNotification by preferenceStrInt(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST)
|
||||
var updateChannel by preferenceStrInt(Key.UPDATE_CHANNEL, defaultChannel)
|
||||
|
||||
var safetyNotice by preference(Key.SAFETY, true)
|
||||
var darkThemeExtended by preference(
|
||||
Key.DARK_THEME_EXTENDED,
|
||||
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
)
|
||||
var darkTheme by preference(Key.DARK_THEME, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
var themeOrdinal by preference(Key.THEME_ORDINAL, Theme.Piplup.ordinal)
|
||||
var suReAuth by preference(Key.SU_REAUTH, false)
|
||||
var suTapjack by preference(Key.SU_TAPJACK, true)
|
||||
var checkUpdate by preference(Key.CHECK_UPDATES, true)
|
||||
var doh by preference(Key.DOH, false)
|
||||
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, "")
|
||||
private var localePrefs by preference(Key.LOCALE, "")
|
||||
var locale
|
||||
get() = localePrefs
|
||||
set(value) {
|
||||
localePrefs = value
|
||||
refreshLocale()
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -148,41 +156,38 @@ object Config : PreferenceModel, DBConfig {
|
||||
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
||||
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
|
||||
|
||||
// Always return a path in external storage where we can write
|
||||
val downloadDirectory get() =
|
||||
Utils.ensureDownloadPath(downloadPath) ?: get<Context>().getExternalFilesDir(null)!!
|
||||
|
||||
private const val SU_FINGERPRINT = "su_fingerprint"
|
||||
|
||||
fun initialize() = prefs.also {
|
||||
if (it.getBoolean(SU_FINGERPRINT, false)) {
|
||||
suBiometric = true
|
||||
fun load(pkg: String?) {
|
||||
// Only try to load prefs when fresh install and a previous package name is set
|
||||
if (pkg != null && prefs.all.isEmpty()) runCatching {
|
||||
context.contentResolver.openInputStream(Provider.PREFS_URI(pkg))?.use {
|
||||
prefs.edit { parsePrefs(it) }
|
||||
}
|
||||
}
|
||||
}.edit {
|
||||
parsePrefs(this)
|
||||
|
||||
// Legacy stuff
|
||||
prefs.edit {
|
||||
// Settings migration
|
||||
if (prefs.getBoolean(SU_FINGERPRINT, false))
|
||||
suBiometric = true
|
||||
remove(SU_FINGERPRINT)
|
||||
|
||||
// Get actual state
|
||||
putBoolean(Key.COREONLY, Const.MAGISK_DISABLE_FILE.exists())
|
||||
prefs.getString(Key.UPDATE_CHANNEL, null).also {
|
||||
if (it == null)
|
||||
putString(Key.UPDATE_CHANNEL, defaultChannel.toString())
|
||||
else if (it.toInt() > Value.CANARY_CHANNEL)
|
||||
putString(Key.UPDATE_CHANNEL, Value.CANARY_CHANNEL.toString())
|
||||
}
|
||||
|
||||
// Write database configs
|
||||
putString(Key.ROOT_ACCESS, rootMode.toString())
|
||||
putString(Key.SU_MNT_NS, suMntNamespaceMode.toString())
|
||||
putString(Key.SU_MULTIUSER_MODE, suMultiuserMode.toString())
|
||||
putBoolean(Key.SU_BIOMETRIC, BiometricHelper.isEnabled)
|
||||
}.also {
|
||||
if (!prefs.contains(Key.UPDATE_CHANNEL))
|
||||
prefs.edit().putString(Key.UPDATE_CHANNEL, defaultChannel.toString()).apply()
|
||||
}
|
||||
}
|
||||
|
||||
private fun parsePrefs(editor: SharedPreferences.Editor) = editor.apply {
|
||||
val config = SuFile.open("/data/adb",
|
||||
Const.MANAGER_CONFIGS
|
||||
)
|
||||
if (config.exists()) runCatching {
|
||||
val input = SuFileInputStream(config)
|
||||
private fun SharedPreferences.Editor.parsePrefs(input: InputStream) {
|
||||
runCatching {
|
||||
val parser = Xml.newPullParser()
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
|
||||
parser.setInput(input, "UTF-8")
|
||||
@@ -226,19 +231,6 @@ object Config : PreferenceModel, DBConfig {
|
||||
else -> parser.next()
|
||||
}
|
||||
}
|
||||
config.delete()
|
||||
}
|
||||
}
|
||||
|
||||
fun export() {
|
||||
// Flush prefs to disk
|
||||
prefs.edit().commit()
|
||||
val context = get<Context>(Protected)
|
||||
val xml = File(
|
||||
"${context.filesDir.parent}/shared_prefs",
|
||||
"${context.packageName}_preferences.xml"
|
||||
)
|
||||
Shell.su("cat $xml > /data/adb/${Const.MANAGER_CONFIGS}").exec()
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,74 +1,77 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Process
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import java.io.File
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
object Const {
|
||||
|
||||
val CPU_ABI: String
|
||||
val CPU_ABI_32: String
|
||||
|
||||
init {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
CPU_ABI = Build.SUPPORTED_ABIS[0]
|
||||
CPU_ABI_32 = Build.SUPPORTED_32_BIT_ABIS.firstOrNull() ?: CPU_ABI
|
||||
} else {
|
||||
CPU_ABI = Build.CPU_ABI
|
||||
CPU_ABI_32 = CPU_ABI
|
||||
}
|
||||
}
|
||||
|
||||
// Paths
|
||||
const val MAGISK_PATH = "/sbin/.magisk/modules"
|
||||
var MAGISK_DISABLE_FILE = File("xxx")
|
||||
const val TMP_FOLDER_PATH = "/dev/tmp"
|
||||
lateinit var MAGISKTMP: String
|
||||
lateinit var NATIVE_LIB_DIR: File
|
||||
val MAGISK_PATH get() = "$MAGISKTMP/modules"
|
||||
const val TMPDIR = "/dev/tmp"
|
||||
const val MAGISK_LOG = "/cache/magisk.log"
|
||||
|
||||
// Versions
|
||||
const val SNET_EXT_VER = 13
|
||||
const val SNET_REVISION = "a6c47f86f10b310358afa9dbe837037dd5d561df"
|
||||
const val BOOTCTL_REVISION = "a6c47f86f10b310358afa9dbe837037dd5d561df"
|
||||
const val SNET_EXT_VER = 15
|
||||
const val SNET_REVISION = "22.0"
|
||||
const val BOOTCTL_REVISION = "22.0"
|
||||
|
||||
// Misc
|
||||
const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
||||
const val MAGISK_INSTALL_LOG_FILENAME = "magisk_install_log_%s.log"
|
||||
const val MANAGER_CONFIGS = ".tmp.magisk.config"
|
||||
val USER_ID = Process.myUid() / 100000
|
||||
|
||||
object Version {
|
||||
const val MIN_VERSION = "v19.0"
|
||||
const val MIN_VERCODE = 19000
|
||||
const val CONNECT_MODE = 20100
|
||||
const val PROVIDER_CONNECT = 20102
|
||||
const val MIN_VERSION = "v20.4"
|
||||
const val MIN_VERCODE = 20400
|
||||
|
||||
fun atLeast_21_0() = Info.env.magiskVersionCode >= 21000 || isCanary()
|
||||
fun atLeast_21_2() = Info.env.magiskVersionCode >= 21200 || isCanary()
|
||||
fun isCanary() = Info.env.magiskVersionCode % 100 != 0
|
||||
}
|
||||
|
||||
object ID {
|
||||
const val FETCH_ZIP = 2
|
||||
const val SELECT_BOOT = 3
|
||||
|
||||
// notifications
|
||||
const val MAGISK_UPDATE_NOTIFICATION_ID = 4
|
||||
const val APK_UPDATE_NOTIFICATION_ID = 5
|
||||
const val DTBO_NOTIFICATION_ID = 7
|
||||
const val HIDE_MANAGER_NOTIFICATION_ID = 8
|
||||
const val UPDATE_NOTIFICATION_CHANNEL = "update"
|
||||
const val PROGRESS_NOTIFICATION_CHANNEL = "progress"
|
||||
const val CHECK_MAGISK_UPDATE_WORKER_ID = "magisk_update"
|
||||
}
|
||||
|
||||
object Url {
|
||||
const val ZIP_URL = "https://github.com/Magisk-Modules-Repo/%s/archive/master.zip"
|
||||
const val PAYPAL_URL = "https://www.paypal.me/topjohnwu"
|
||||
const val PATREON_URL = "https://www.patreon.com/topjohnwu"
|
||||
const val TWITTER_URL = "https://twitter.com/topjohnwu"
|
||||
const val XDA_THREAD = "http://forum.xda-developers.com/showthread.php?t=3432382"
|
||||
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
|
||||
|
||||
val CHANGELOG_URL = if (BuildConfig.VERSION_CODE % 100 != 0) Info.remote.magisk.note
|
||||
else "https://topjohnwu.github.io/Magisk/releases/${BuildConfig.VERSION_CODE}.md"
|
||||
|
||||
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com/"
|
||||
const val GITHUB_API_URL = "https://api.github.com/users/Magisk-Modules-Repo/"
|
||||
const val GITHUB_API_URL = "https://api.github.com/"
|
||||
const val GITHUB_PAGE_URL = "https://topjohnwu.github.io/magisk-files/"
|
||||
const val GITHUB_OLD_PAGE_URL = "https://topjohnwu.github.io/magisk_files/"
|
||||
const val JS_DELIVR_URL = "https://cdn.jsdelivr.net/gh/"
|
||||
const val OFFICIAL_REPO = "https://magisk-modules-repo.github.io/submission/modules.json"
|
||||
}
|
||||
|
||||
object Key {
|
||||
// others
|
||||
const val LINK_KEY = "Link"
|
||||
const val IF_NONE_MATCH = "If-None-Match"
|
||||
const val ETAG_KEY = "ETag"
|
||||
// intents
|
||||
const val OPEN_SECTION = "section"
|
||||
const val OPEN_SETTINGS = "settings"
|
||||
const val INTENT_SET_APP = "app_json"
|
||||
const val FLASH_ACTION = "action"
|
||||
const val FLASH_DATA = "additional_data"
|
||||
const val DISMISS_ID = "dismiss_id"
|
||||
const val BROADCAST_MANAGER_UPDATE = "manager_update"
|
||||
const val BROADCAST_REBOOT = "reboot"
|
||||
const val PREV_PKG = "prev_pkg"
|
||||
}
|
||||
|
||||
object Value {
|
||||
@@ -79,4 +82,11 @@ object Const {
|
||||
const val UNINSTALL = "uninstall"
|
||||
}
|
||||
|
||||
object Nav {
|
||||
const val HOME = "home"
|
||||
const val SETTINGS = "settings"
|
||||
const val HIDE = "hide"
|
||||
const val MODULES = "modules"
|
||||
const val SUPERUSER = "superuser"
|
||||
}
|
||||
}
|
||||
|
@@ -1,54 +0,0 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import com.topjohnwu.magisk.core.base.BaseReceiver
|
||||
import com.topjohnwu.magisk.core.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()
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,6 +3,7 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.job.JobInfo
|
||||
import android.app.job.JobScheduler
|
||||
import android.app.job.JobWorkItem
|
||||
@@ -13,77 +14,66 @@ import android.content.Intent
|
||||
import android.content.res.AssetManager
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.util.DisplayMetrics
|
||||
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.wrap(inject: Boolean = false): Context =
|
||||
if (inject) ReInjectedContext(this) else InjectedContext(this)
|
||||
|
||||
fun Context.wrapJob(): Context = object : GlobalResContext(this) {
|
||||
fun Context.wrapJob(): Context = object : InjectedContext(this) {
|
||||
|
||||
override fun getApplicationContext(): Context {
|
||||
return this
|
||||
}
|
||||
override fun getApplicationContext() = 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)
|
||||
return super.getSystemService(name).let {
|
||||
when {
|
||||
!isRunningAsStub -> it
|
||||
name == JOB_SCHEDULER_SERVICE -> JobSchedulerWrapper(it as JobScheduler)
|
||||
else -> it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Class<*>.cmp(pkg: String): ComponentName {
|
||||
val name = ClassMap[this].name
|
||||
return ComponentName(pkg, Info.stub?.classToComponent?.get(name) ?: name)
|
||||
}
|
||||
fun Class<*>.cmp(pkg: String) =
|
||||
ComponentName(pkg, Info.stub?.classToComponent?.get(name) ?: name)
|
||||
|
||||
inline fun <reified T> Activity.redirect() = Intent(intent)
|
||||
.setComponent(T::class.java.cmp(packageName))
|
||||
.setFlags(0)
|
||||
|
||||
inline fun <reified T> Context.intent() = Intent().setComponent(T::class.java.cmp(packageName))
|
||||
|
||||
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!!
|
||||
}
|
||||
|
||||
private open class InjectedContext(base: Context) : ContextWrapper(base) {
|
||||
open val res: Resources get() = AssetHack.resource
|
||||
override fun getAssets(): AssetManager = res.assets
|
||||
override fun getResources() = res
|
||||
override fun getClassLoader() = javaClass.classLoader!!
|
||||
override fun createConfigurationContext(config: Configuration): Context {
|
||||
return ResContext(super.createConfigurationContext(config))
|
||||
return super.createConfigurationContext(config).wrap(true)
|
||||
}
|
||||
}
|
||||
|
||||
private class ResContext(base: Context) : GlobalResContext(base) {
|
||||
override val mRes by lazy { base.resources.patch() }
|
||||
|
||||
private class ReInjectedContext(base: Context) : InjectedContext(base) {
|
||||
override val res by lazy { base.resources.patch() }
|
||||
private fun Resources.patch(): Resources {
|
||||
updateConfig()
|
||||
if (isRunningAsStub)
|
||||
assets.addAssetPath(ResMgr.apk)
|
||||
assets.addAssetPath(AssetHack.apk)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
object ResMgr {
|
||||
object AssetHack {
|
||||
|
||||
lateinit var resource: Resources
|
||||
lateinit var apk: String
|
||||
@@ -98,73 +88,50 @@ object ResMgr {
|
||||
apk = context.packageResourcePath
|
||||
}
|
||||
}
|
||||
|
||||
fun newResource(): Resources {
|
||||
val asset = AssetManager::class.java.newInstance()
|
||||
asset.addAssetPath(apk)
|
||||
val config = Configuration(resource.configuration)
|
||||
val metrics = DisplayMetrics()
|
||||
metrics.setTo(resource.displayMetrics)
|
||||
return Resources(asset, metrics, config)
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
|
||||
override fun schedule(job: JobInfo) = base.schedule(job.patch())
|
||||
override fun enqueue(job: JobInfo, work: JobWorkItem) = base.enqueue(job.patch(), work)
|
||||
override fun cancel(jobId: Int) = base.cancel(jobId)
|
||||
override fun cancelAll() = base.cancelAll()
|
||||
override fun getAllPendingJobs(): List<JobInfo> = base.allPendingJobs
|
||||
override fun getPendingJob(jobId: Int) = 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
|
||||
)
|
||||
// Swap out the service of JobInfo
|
||||
val component = service.run {
|
||||
ComponentName(packageName,
|
||||
Info.stub?.classToComponent?.get(className) ?: className)
|
||||
}
|
||||
javaClass.getDeclaredField("service").apply {
|
||||
isAccessible = true
|
||||
}.set(this, component)
|
||||
|
||||
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" */
|
||||
// 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
|
||||
R.string.update_available,
|
||||
R.string.safetynet_api_error,
|
||||
R.raw.changelog,
|
||||
R.drawable.ic_device,
|
||||
R.drawable.ic_hide_select_md2,
|
||||
R.drawable.ic_more,
|
||||
R.drawable.ic_magisk_delete
|
||||
)
|
||||
|
@@ -1,61 +1,63 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import android.os.Build
|
||||
import androidx.databinding.ObservableBoolean
|
||||
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.magisk.core.utils.net.NetworkObserver
|
||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||
import com.topjohnwu.magisk.ktx.get
|
||||
import com.topjohnwu.magisk.ktx.getProperty
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils.fastCmd
|
||||
import java.io.FileInputStream
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
val isRunningAsStub get() = Info.stub != null
|
||||
val isCanaryVersion = !BuildConfig.VERSION_NAME.contains(".")
|
||||
|
||||
object Info {
|
||||
|
||||
val envRef = CachedValue { loadState() }
|
||||
var stub: DynAPK.Data? = null
|
||||
|
||||
@JvmStatic
|
||||
val env by envRef // Local
|
||||
var remote = UpdateInfo() // Remote
|
||||
@JvmStatic
|
||||
var stub: DynAPK.Data? = null // Stub
|
||||
val EMPTY_REMOTE = UpdateInfo()
|
||||
var remote = EMPTY_REMOTE
|
||||
suspend fun getRemote(svc: NetworkService): UpdateInfo? {
|
||||
return if (remote === EMPTY_REMOTE) {
|
||||
svc.fetchUpdate()?.apply { remote = this }
|
||||
} else remote
|
||||
}
|
||||
|
||||
// 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
|
||||
// Device state
|
||||
@JvmStatic val env by lazy { loadState() }
|
||||
@JvmField var isSAR = false
|
||||
@JvmField var isAB = false
|
||||
@JvmField val isVirtualAB = getProperty("ro.virtual_ab.enabled", "false") == "true"
|
||||
@JvmStatic val isFDE get() = crypto == "block"
|
||||
@JvmField var ramdisk = false
|
||||
@JvmField var hasGMS = true
|
||||
@JvmField val isPixel = Build.BRAND == "google"
|
||||
@JvmField val isEmulator = getProperty("ro.kernel.qemu", "0") == "1"
|
||||
var crypto = ""
|
||||
var noDataExec = false
|
||||
|
||||
val isConnected by lazy {
|
||||
KObservableField(false).also { field ->
|
||||
ReactiveNetwork.observeNetworkConnectivity(get())
|
||||
.subscribeK {
|
||||
field.value = it.available()
|
||||
ObservableBoolean(false).also { field ->
|
||||
NetworkObserver.observe(get()) {
|
||||
UiThreadHandler.run { field.set(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val isNewReboot by lazy {
|
||||
try {
|
||||
FileInputStream("/proc/sys/kernel/random/boot_id").bufferedReader().use {
|
||||
val id = it.readLine()
|
||||
val id = File("/proc/sys/kernel/random/boot_id").readText()
|
||||
if (id != Config.bootId) {
|
||||
Config.bootId = id
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
false
|
||||
}
|
||||
@@ -64,7 +66,7 @@ object Info {
|
||||
private fun loadState() = Env(
|
||||
fastCmd("magisk -v").split(":".toRegex())[0],
|
||||
runCatching { fastCmd("magisk -V").toInt() }.getOrDefault(-1),
|
||||
Shell.su("magiskhide --status").exec().isSuccess
|
||||
Shell.su("magiskhide status").exec().isSuccess
|
||||
)
|
||||
|
||||
class Env(
|
||||
@@ -73,8 +75,8 @@ object Info {
|
||||
hide: Boolean = false
|
||||
) {
|
||||
val magiskHide get() = Config.magiskHide
|
||||
val magiskVersionCode = when (code) {
|
||||
in Int.MIN_VALUE..Const.Version.MIN_VERCODE -> -1
|
||||
val magiskVersionCode = when {
|
||||
code < Const.Version.MIN_VERCODE -> -1
|
||||
else -> if (Shell.rootAccess()) code else -1
|
||||
}
|
||||
val isUnsupported = code > 0 && code < Const.Version.MIN_VERCODE
|
||||
|
39
app/src/main/java/com/topjohnwu/magisk/core/Provider.kt
Normal file
39
app/src/main/java/com/topjohnwu/magisk/core/Provider.kt
Normal file
@@ -0,0 +1,39 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ProviderInfo
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
||||
import com.topjohnwu.magisk.FileProvider
|
||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
||||
import java.io.File
|
||||
|
||||
open class Provider : FileProvider() {
|
||||
|
||||
override fun attachInfo(context: Context, info: ProviderInfo?) {
|
||||
super.attachInfo(context.wrap(), info)
|
||||
}
|
||||
|
||||
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
|
||||
SuCallbackHandler(context!!, method, extras)
|
||||
return Bundle.EMPTY
|
||||
}
|
||||
|
||||
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
|
||||
return when (uri.encodedPath ?: return null) {
|
||||
"/apk_file" -> ParcelFileDescriptor.open(File(context!!.packageCodePath), MODE_READ_ONLY)
|
||||
"/prefs_file" -> ParcelFileDescriptor.open(Config.prefsFile, MODE_READ_ONLY)
|
||||
else -> super.openFile(uri, mode)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun APK_URI(pkg: String) =
|
||||
Uri.Builder().scheme("content").authority("$pkg.provider").path("apk_file").build()
|
||||
|
||||
fun PREFS_URI(pkg: String) =
|
||||
Uri.Builder().scheme("content").authority("$pkg.provider").path("prefs_file").build()
|
||||
}
|
||||
}
|
49
app/src/main/java/com/topjohnwu/magisk/core/Receiver.kt
Normal file
49
app/src/main/java/com/topjohnwu/magisk/core/Receiver.kt
Normal file
@@ -0,0 +1,49 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import com.topjohnwu.magisk.core.base.BaseReceiver
|
||||
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.inject
|
||||
|
||||
open class Receiver : BaseReceiver() {
|
||||
|
||||
private val policyDB: PolicyDao by inject()
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun getPkg(intent: Intent): String? {
|
||||
val pkg = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME)
|
||||
return pkg ?: intent.data?.schemeSpecificPart
|
||||
}
|
||||
|
||||
private fun getUid(intent: Intent): Int? {
|
||||
val uid = intent.getIntExtra(Intent.EXTRA_UID, -1)
|
||||
return if (uid == -1) null else uid
|
||||
}
|
||||
|
||||
override fun onReceive(context: ContextWrapper, intent: Intent?) {
|
||||
intent ?: return
|
||||
|
||||
fun rmPolicy(uid: Int) = GlobalScope.launch {
|
||||
policyDB.delete(uid)
|
||||
}
|
||||
|
||||
when (intent.action ?: return) {
|
||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
||||
// This will only work pre-O
|
||||
if (Config.suReAuth)
|
||||
getUid(intent)?.let { rmPolicy(it) }
|
||||
}
|
||||
Intent.ACTION_UID_REMOVED -> {
|
||||
getUid(intent)?.let { rmPolicy(it) }
|
||||
getPkg(intent)?.let { Shell.su("magiskhide rm $it").submit() }
|
||||
}
|
||||
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setupDynamic(context)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,64 +1,92 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
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.magisk.BuildConfig.APPLICATION_ID
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||
import com.topjohnwu.magisk.ktx.get
|
||||
import com.topjohnwu.magisk.ui.MainActivity
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
open class SplashActivity : Activity() {
|
||||
open class SplashActivity : BaseActivity() {
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base.wrap())
|
||||
}
|
||||
private val latch = CountDownLatch(1)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTheme(R.style.SplashTheme)
|
||||
super.onCreate(savedInstanceState)
|
||||
Shell.getShell { Shell.EXECUTOR.execute(this::initAndStart) }
|
||||
// Pre-initialize root shell
|
||||
Shell.getShell(null) { 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) {
|
||||
private fun handleRepackage(pkg: String?) {
|
||||
if (packageName != APPLICATION_ID) {
|
||||
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()
|
||||
// Hidden, remove com.topjohnwu.magisk if exist as it could be malware
|
||||
packageManager.getApplicationInfo(APPLICATION_ID, 0)
|
||||
Shell.su("(pm uninstall $APPLICATION_ID)& >/dev/null 2>&1").exec()
|
||||
}
|
||||
} else {
|
||||
if (Config.suManager.isNotEmpty())
|
||||
Config.suManager = ""
|
||||
pkg ?: return
|
||||
if (!Shell.su("(pm uninstall $pkg)& >/dev/null 2>&1").exec().isSuccess)
|
||||
uninstallApp(pkg)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initAndStart() {
|
||||
Config.initialize()
|
||||
handleRepackage()
|
||||
if (isRunningAsStub && !Shell.rootAccess()) {
|
||||
runOnUiThread {
|
||||
MagiskDialog(this)
|
||||
.applyTitle(R.string.unsupport_nonroot_stub_title)
|
||||
.applyMessage(R.string.unsupport_nonroot_stub_msg)
|
||||
.applyButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||
titleRes = R.string.install
|
||||
onClick { HideAPK.restore(this@SplashActivity) }
|
||||
}
|
||||
.cancellable(false)
|
||||
.reveal()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val prevPkg = intent.getStringExtra(Const.Key.PREV_PKG)
|
||||
|
||||
Config.load(prevPkg)
|
||||
handleRepackage(prevPkg)
|
||||
Notifications.setup(this)
|
||||
Utils.scheduleUpdateCheck(this)
|
||||
Shortcuts.setup(this)
|
||||
UpdateCheckService.schedule(this)
|
||||
Shortcuts.setupDynamic(this)
|
||||
|
||||
// Patch DTB partitions if needed
|
||||
patchDTB(this)
|
||||
|
||||
// Pre-fetch network stuffs
|
||||
get<GithubRawServices>()
|
||||
// Pre-fetch network services
|
||||
get<NetworkService>()
|
||||
|
||||
DONE = true
|
||||
Navigation.start(intent, this)
|
||||
startActivity(redirect<MainActivity>())
|
||||
finish()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Suppress("DEPRECATION")
|
||||
private fun uninstallApp(pkg: String) {
|
||||
val uri = Uri.Builder().scheme("package").opaquePart(pkg).build()
|
||||
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, uri)
|
||||
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true)
|
||||
startActivityForResult(intent) { _, _ ->
|
||||
latch.countDown()
|
||||
}
|
||||
latch.await()
|
||||
}
|
||||
|
||||
companion object {
|
||||
var DONE = false
|
||||
}
|
||||
}
|
||||
|
@@ -1,31 +1,46 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.*
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.core.view.Notifications
|
||||
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.inject
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class UpdateCheckService(context: Context, workerParams: WorkerParameters)
|
||||
: Worker(context, workerParams) {
|
||||
: CoroutineWorker(context, workerParams), KoinComponent {
|
||||
|
||||
private val magiskRepo: MagiskRepository by inject()
|
||||
private val svc: NetworkService by inject()
|
||||
|
||||
override fun doWork(): Result {
|
||||
// Make sure shell initializer was ran
|
||||
Shell.getShell()
|
||||
return runCatching {
|
||||
magiskRepo.fetchUpdate().blockingGet()
|
||||
if (BuildConfig.VERSION_CODE < Info.remote.app.versionCode)
|
||||
override suspend fun doWork(): Result {
|
||||
return svc.fetchUpdate()?.run {
|
||||
if (Info.env.isActive && BuildConfig.VERSION_CODE < magisk.versionCode)
|
||||
Notifications.managerUpdate(applicationContext)
|
||||
else if (Info.env.magiskVersionCode < Info.remote.magisk.versionCode)
|
||||
Notifications.magiskUpdate(applicationContext)
|
||||
Result.success()
|
||||
}.getOrElse {
|
||||
Result.failure()
|
||||
} ?: Result.failure()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@SuppressLint("NewApi")
|
||||
fun schedule(context: Context) {
|
||||
if (Config.checkUpdate) {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.setRequiresDeviceIdle(true)
|
||||
.build()
|
||||
val request = PeriodicWorkRequestBuilder<UpdateCheckService>(12, TimeUnit.HOURS)
|
||||
.setConstraints(constraints)
|
||||
.build()
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
Const.ID.CHECK_MAGISK_UPDATE_WORKER_ID,
|
||||
ExistingPeriodicWorkPolicy.REPLACE, request)
|
||||
} else {
|
||||
WorkManager.getInstance(context)
|
||||
.cancelUniqueWork(Const.ID.CHECK_MAGISK_UPDATE_WORKER_ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,25 +1,37 @@
|
||||
package com.topjohnwu.magisk.core.base
|
||||
|
||||
import android.Manifest
|
||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import com.topjohnwu.magisk.extensions.set
|
||||
import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder
|
||||
import com.topjohnwu.magisk.ktx.set
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import kotlin.random.Random
|
||||
|
||||
typealias RequestCallback = BaseActivity.(Int, Intent?) -> Unit
|
||||
typealias ActivityResultCallback = BaseActivity.(Int, Intent?) -> Unit
|
||||
|
||||
abstract class BaseActivity : AppCompatActivity() {
|
||||
|
||||
private val resultCallbacks by lazy { SparseArrayCompat<RequestCallback>() }
|
||||
private val resultCallbacks by lazy { SparseArrayCompat<ActivityResultCallback>() }
|
||||
private val newRequestCode: Int get() {
|
||||
var requestCode: Int
|
||||
do {
|
||||
requestCode = Random.nextInt(0, 1 shl 15)
|
||||
} while (resultCallbacks.containsKey(requestCode))
|
||||
return requestCode
|
||||
}
|
||||
|
||||
override fun applyOverrideConfiguration(config: Configuration?) {
|
||||
// Force applying our preferred local
|
||||
@@ -28,31 +40,34 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base.wrap(false))
|
||||
super.attachBaseContext(base.wrap(true))
|
||||
}
|
||||
|
||||
fun withPermissions(vararg permissions: String, builder: PermissionRequestBuilder.() -> Unit) {
|
||||
fun withPermission(permission: String, builder: PermissionRequestBuilder.() -> Unit) {
|
||||
val request = PermissionRequestBuilder().apply(builder).build()
|
||||
val ungranted = permissions.filter {
|
||||
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
|
||||
|
||||
if (permission == WRITE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= 30) {
|
||||
// We do not need external rw on 30+
|
||||
request.onSuccess()
|
||||
return
|
||||
}
|
||||
|
||||
if (ungranted.isEmpty()) {
|
||||
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
|
||||
request.onSuccess()
|
||||
} else {
|
||||
val requestCode = Random.nextInt(256, 512)
|
||||
val requestCode = newRequestCode
|
||||
resultCallbacks[requestCode] = { result, _ ->
|
||||
if (result > 0)
|
||||
request.onSuccess()
|
||||
else
|
||||
request.onFailure()
|
||||
}
|
||||
ActivityCompat.requestPermissions(this, ungranted.toTypedArray(), requestCode)
|
||||
ActivityCompat.requestPermissions(this, arrayOf(permission), requestCode)
|
||||
}
|
||||
}
|
||||
|
||||
fun withExternalRW(builder: PermissionRequestBuilder.() -> Unit) {
|
||||
withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, builder = builder)
|
||||
withPermission(WRITE_EXTERNAL_STORAGE, builder = builder)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
@@ -66,26 +81,32 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
}
|
||||
resultCallbacks[requestCode]?.also {
|
||||
resultCallbacks.remove(requestCode)
|
||||
it(this@BaseActivity, if (success) 1 else -1, null)
|
||||
it(this, if (success) 1 else -1, null)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
resultCallbacks[requestCode]?.also {
|
||||
resultCallbacks[requestCode]?.also { callback ->
|
||||
resultCallbacks.remove(requestCode)
|
||||
it(this@BaseActivity, resultCode, data)
|
||||
callback(this, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
fun startActivityForResult(intent: Intent, requestCode: Int, listener: RequestCallback) {
|
||||
resultCallbacks[requestCode] = listener
|
||||
fun startActivityForResult(intent: Intent, callback: ActivityResultCallback) {
|
||||
val requestCode = newRequestCode
|
||||
resultCallbacks[requestCode] = callback
|
||||
try {
|
||||
startActivityForResult(intent, requestCode)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Utils.toast(R.string.app_not_found, Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
|
||||
override fun recreate() {
|
||||
startActivity(intent)
|
||||
startActivity(Intent().setComponent(intent.component))
|
||||
finish()
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.model.permissions
|
||||
package com.topjohnwu.magisk.core.base
|
||||
|
||||
typealias SimpleCallback = () -> Unit
|
||||
typealias PermissionRationaleCallback = (List<String>) -> Unit
|
@@ -0,0 +1,194 @@
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.ForegroundTracker
|
||||
import com.topjohnwu.magisk.core.base.BaseService
|
||||
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.ResponseBody
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.core.KoinComponent
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
import kotlin.random.Random.Default.nextInt
|
||||
|
||||
abstract class BaseDownloader : BaseService(), KoinComponent {
|
||||
|
||||
private val hasNotifications get() = notifications.isNotEmpty()
|
||||
private val notifications = Collections.synchronizedMap(HashMap<Int, Notification.Builder>())
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
val service: NetworkService by inject()
|
||||
|
||||
// -- Service overrides
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
intent.getParcelableExtra<Subject>(ACTION_KEY)?.let { subject ->
|
||||
update(subject.notifyID())
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
subject.startDownload()
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
notifyFail(subject)
|
||||
}
|
||||
}
|
||||
}
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
notifications.forEach { cancel(it.key) }
|
||||
notifications.clear()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
|
||||
// -- Download logic
|
||||
|
||||
private suspend fun Subject.startDownload() {
|
||||
val stream = service.fetchFile(url).toProgressStream(this)
|
||||
when (this) {
|
||||
is Subject.Module -> // Download and process on-the-fly
|
||||
stream.toModule(file, service.fetchInstaller().byteStream())
|
||||
is Subject.Manager -> handleAPK(this, stream)
|
||||
}
|
||||
val newId = notifyFinish(this)
|
||||
if (ForegroundTracker.hasForeground)
|
||||
onFinish(this, newId)
|
||||
if (!hasNotifications)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun ResponseBody.toProgressStream(subject: Subject): InputStream {
|
||||
val max = contentLength()
|
||||
val total = max.toFloat() / 1048576
|
||||
val id = subject.notifyID()
|
||||
|
||||
update(id) { it.setContentTitle(subject.title) }
|
||||
|
||||
return ProgressInputStream(byteStream()) {
|
||||
val progress = it.toFloat() / 1048576
|
||||
update(id) { notification ->
|
||||
if (max > 0) {
|
||||
broadcast(progress / total, subject)
|
||||
notification
|
||||
.setProgress(max.toInt(), it.toInt(), false)
|
||||
.setContentText("%.2f / %.2f MB".format(progress, total))
|
||||
} else {
|
||||
broadcast(-1f, subject)
|
||||
notification.setContentText("%.2f MB / ??".format(progress))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Notification managements
|
||||
|
||||
fun Subject.notifyID() = hashCode()
|
||||
|
||||
private fun notifyFail(subject: Subject) = lastNotify(subject.notifyID()) {
|
||||
broadcast(-2f, subject)
|
||||
it.setContentText(getString(R.string.download_file_error))
|
||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||
.setOngoing(false)
|
||||
}
|
||||
|
||||
private fun notifyFinish(subject: Subject) = lastNotify(subject.notifyID()) {
|
||||
broadcast(1f, subject)
|
||||
it.setIntent(subject)
|
||||
.setContentTitle(subject.title)
|
||||
.setContentText(getString(R.string.download_complete))
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setProgress(0, 0, false)
|
||||
.setOngoing(false)
|
||||
.setAutoCancel(true)
|
||||
}
|
||||
|
||||
private fun create() = Notifications.progress(this, "")
|
||||
|
||||
fun update(id: Int, editor: (Notification.Builder) -> Unit = {}) {
|
||||
val wasEmpty = !hasNotifications
|
||||
val notification = notifications.getOrPut(id, ::create).also(editor)
|
||||
if (wasEmpty)
|
||||
updateForeground()
|
||||
else
|
||||
notify(id, notification.build())
|
||||
}
|
||||
|
||||
private fun lastNotify(
|
||||
id: Int,
|
||||
editor: (Notification.Builder) -> Notification.Builder? = { null }
|
||||
) : Int {
|
||||
val notification = remove(id)?.run(editor) ?: return -1
|
||||
val newId: Int = nextInt()
|
||||
notify(newId, notification.build())
|
||||
return newId
|
||||
}
|
||||
|
||||
protected fun remove(id: Int) = notifications.remove(id)
|
||||
?.also { updateForeground(); cancel(id) }
|
||||
?: { cancel(id); null }()
|
||||
|
||||
private fun notify(id: Int, notification: Notification) {
|
||||
Notifications.mgr.notify(id, notification)
|
||||
}
|
||||
|
||||
private fun cancel(id: Int) {
|
||||
Notifications.mgr.cancel(id)
|
||||
}
|
||||
|
||||
private fun updateForeground() {
|
||||
if (hasNotifications) {
|
||||
val (id, notification) = notifications.entries.first()
|
||||
startForeground(id, notification.build())
|
||||
} else {
|
||||
stopForeground(false)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Implement custom logic
|
||||
|
||||
protected abstract suspend fun onFinish(subject: Subject, id: Int)
|
||||
|
||||
protected abstract fun Notification.Builder.setIntent(subject: Subject): Notification.Builder
|
||||
|
||||
// ---
|
||||
|
||||
companion object : KoinComponent {
|
||||
const val ACTION_KEY = "download_action"
|
||||
|
||||
private val progressBroadcast = MutableLiveData<Pair<Float, Subject>?>()
|
||||
|
||||
fun observeProgress(owner: LifecycleOwner, callback: (Float, Subject) -> Unit) {
|
||||
progressBroadcast.value = null
|
||||
progressBroadcast.observe(owner) {
|
||||
val (progress, subject) = it ?: return@observe
|
||||
callback(progress, subject)
|
||||
}
|
||||
}
|
||||
|
||||
private fun broadcast(progress: Float, subject: Subject) {
|
||||
progressBroadcast.postValue(progress to subject)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,164 +1,93 @@
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.webkit.MimeTypeMap
|
||||
import com.topjohnwu.magisk.R
|
||||
import androidx.core.net.toFile
|
||||
import com.topjohnwu.magisk.arch.BaseUIActivity
|
||||
import com.topjohnwu.magisk.core.ForegroundTracker
|
||||
import com.topjohnwu.magisk.core.download.Action.Flash
|
||||
import com.topjohnwu.magisk.core.download.Subject.Manager
|
||||
import com.topjohnwu.magisk.core.download.Subject.Module
|
||||
import com.topjohnwu.magisk.core.intent
|
||||
import com.topjohnwu.magisk.core.tasks.EnvFixTask
|
||||
import com.topjohnwu.magisk.extensions.chooser
|
||||
import com.topjohnwu.magisk.extensions.exists
|
||||
import com.topjohnwu.magisk.extensions.provide
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.legacy.flash.FlashActivity
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.*
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.Flash.Secondary
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*
|
||||
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import io.reactivex.Completable
|
||||
import org.koin.core.get
|
||||
import java.io.File
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import kotlin.random.Random.Default.nextInt
|
||||
|
||||
/* More of a facade for [RemoteFileService], but whatever... */
|
||||
@SuppressLint("Registered")
|
||||
open class DownloadService : RemoteFileService() {
|
||||
open class DownloadService : BaseDownloader() {
|
||||
|
||||
private val context get() = this
|
||||
private val File.type
|
||||
get() = MimeTypeMap.getSingleton()
|
||||
.getMimeTypeFromExtension(extension)
|
||||
?: "resource/folder"
|
||||
|
||||
override fun onFinished(subject: DownloadSubject, id: Int) = when (subject) {
|
||||
is Magisk -> onFinishedInternal(subject, id)
|
||||
is Module -> onFinishedInternal(subject, id)
|
||||
is Manager -> onFinishedInternal(subject, id)
|
||||
override suspend fun onFinish(subject: Subject, id: Int) = when (subject) {
|
||||
is Module -> subject.onFinish(id)
|
||||
is Manager -> subject.onFinish(id)
|
||||
}
|
||||
|
||||
private fun onFinishedInternal(
|
||||
subject: Magisk,
|
||||
id: Int
|
||||
) = when (val conf = subject.configuration) {
|
||||
Uninstall -> FlashActivity.uninstall(this, subject.file, id)
|
||||
EnvFix -> { remove(id); EnvFixTask(subject.file).exec() }
|
||||
is Patch -> FlashActivity.patch(this, subject.file, conf.fileUri, id)
|
||||
is Flash -> FlashActivity.flash(this, subject.file, conf is Secondary, id)
|
||||
private fun Module.onFinish(id: Int) = when (action) {
|
||||
Flash -> {
|
||||
UiThreadHandler.run {
|
||||
(ForegroundTracker.foreground as? BaseUIActivity<*, *>)
|
||||
?.navigation?.navigate(FlashFragment.install(file, id))
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
private fun onFinishedInternal(
|
||||
subject: Module,
|
||||
id: Int
|
||||
) = when (subject.configuration) {
|
||||
is Flash -> FlashActivity.install(this, subject.file, id)
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
private fun onFinishedInternal(
|
||||
subject: Manager,
|
||||
id: Int
|
||||
) {
|
||||
Completable.fromAction {
|
||||
handleAPK(subject)
|
||||
}.subscribeK {
|
||||
private fun Manager.onFinish(id: Int) {
|
||||
remove(id)
|
||||
when (subject.configuration) {
|
||||
is APK.Upgrade -> APKInstall.install(this, subject.file)
|
||||
is APK.Restore -> Unit
|
||||
}
|
||||
}
|
||||
APKInstall.install(context, file.toFile())
|
||||
}
|
||||
|
||||
// ---
|
||||
// --- Customize finish notification
|
||||
|
||||
override fun Notification.Builder.addActions(subject: DownloadSubject)
|
||||
override fun Notification.Builder.setIntent(subject: Subject)
|
||||
= when (subject) {
|
||||
is Magisk -> addActionsInternal(subject)
|
||||
is Module -> addActionsInternal(subject)
|
||||
is Manager -> addActionsInternal(subject)
|
||||
is Module -> setIntent(subject)
|
||||
is Manager -> setIntent(subject)
|
||||
}
|
||||
|
||||
private fun Notification.Builder.addActionsInternal(subject: Magisk)
|
||||
= when (val conf = subject.configuration) {
|
||||
Download -> this.apply {
|
||||
fileIntent(subject.file.parentFile!!)
|
||||
.takeIf { it.exists(get()) }
|
||||
?.let { addAction(0, R.string.download_open_parent, it.chooser()) }
|
||||
fileIntent(subject.file)
|
||||
.takeIf { it.exists(get()) }
|
||||
?.let { addAction(0, R.string.download_open_self, it.chooser()) }
|
||||
}
|
||||
Uninstall -> setContentIntent(FlashActivity.uninstallIntent(context, subject.file))
|
||||
is Flash -> setContentIntent(FlashActivity.flashIntent(context, subject.file, conf is Secondary))
|
||||
is Patch -> setContentIntent(FlashActivity.patchIntent(context, subject.file, conf.fileUri))
|
||||
else -> this
|
||||
private fun Notification.Builder.setIntent(subject: Module)
|
||||
= when (subject.action) {
|
||||
Flash -> setContentIntent(FlashFragment.installIntent(context, subject.file))
|
||||
else -> setContentIntent(Intent())
|
||||
}
|
||||
|
||||
private fun Notification.Builder.addActionsInternal(subject: Module)
|
||||
= when (subject.configuration) {
|
||||
Download -> this.apply {
|
||||
fileIntent(subject.file.parentFile!!)
|
||||
.takeIf { it.exists(get()) }
|
||||
?.let { addAction(0, R.string.download_open_parent, it.chooser()) }
|
||||
fileIntent(subject.file)
|
||||
.takeIf { it.exists(get()) }
|
||||
?.let { addAction(0, R.string.download_open_self, it.chooser()) }
|
||||
}
|
||||
is Flash -> setContentIntent(FlashActivity.installIntent(context, subject.file))
|
||||
else -> this
|
||||
}
|
||||
private fun Notification.Builder.setIntent(subject: Manager)
|
||||
= setContentIntent(APKInstall.installIntent(context, subject.file.toFile()))
|
||||
|
||||
private fun Notification.Builder.addActionsInternal(subject: Manager)
|
||||
= when (subject.configuration) {
|
||||
APK.Upgrade -> setContentIntent(APKInstall.installIntent(context, subject.file))
|
||||
else -> this
|
||||
}
|
||||
|
||||
@Suppress("ReplaceSingleLineLet")
|
||||
private fun Notification.Builder.setContentIntent(intent: Intent) =
|
||||
setContentIntent(
|
||||
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
||||
)
|
||||
|
||||
@Suppress("ReplaceSingleLineLet")
|
||||
private fun Notification.Builder.addAction(icon: Int, title: Int, intent: Intent) =
|
||||
addAction(icon, getString(title),
|
||||
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
||||
)
|
||||
|
||||
// ---
|
||||
|
||||
private fun fileIntent(file: File): Intent {
|
||||
return Intent(Intent.ACTION_VIEW)
|
||||
.setDataAndType(file.provide(this), file.type)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
|
||||
class Builder {
|
||||
lateinit var subject: DownloadSubject
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
inline operator fun invoke(context: Context, argBuilder: Builder.() -> Unit) {
|
||||
val app = context.applicationContext
|
||||
val builder = Builder().apply(argBuilder)
|
||||
val intent = app.intent<DownloadService>().putExtra(ARG_URL, builder.subject)
|
||||
private fun intent(context: Context, subject: Subject) =
|
||||
context.intent<DownloadService>().putExtra(ACTION_KEY, subject)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
app.startForegroundService(intent)
|
||||
fun pendingIntent(context: Context, subject: Subject): PendingIntent {
|
||||
return if (Build.VERSION.SDK_INT >= 26) {
|
||||
PendingIntent.getForegroundService(context, nextInt(),
|
||||
intent(context, subject), PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
} else {
|
||||
app.startService(intent)
|
||||
PendingIntent.getService(context, nextInt(),
|
||||
intent(context, subject), PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
}
|
||||
|
||||
fun start(context: Context, subject: Subject) {
|
||||
val app = context.applicationContext
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
app.startForegroundService(intent(app, subject))
|
||||
} else {
|
||||
app.startService(intent(app, subject))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,75 @@
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.net.toFile
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.ktx.relaunchApp
|
||||
import com.topjohnwu.magisk.ktx.withStreams
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
private fun Context.patch(apk: File) {
|
||||
val patched = File(apk.parent, "patched.apk")
|
||||
HideAPK.patch(this, apk, patched, packageName, applicationInfo.nonLocalizedLabel)
|
||||
apk.delete()
|
||||
patched.renameTo(apk)
|
||||
}
|
||||
|
||||
private fun BaseDownloader.notifyHide(id: Int) {
|
||||
update(id) {
|
||||
it.setProgress(0, 0, true)
|
||||
.setContentTitle(getString(R.string.hide_app_title))
|
||||
.setContentText("")
|
||||
}
|
||||
}
|
||||
|
||||
private class DupOutputStream(
|
||||
private val o1: OutputStream,
|
||||
private val o2: OutputStream
|
||||
) : OutputStream() {
|
||||
override fun write(b: Int) {
|
||||
o1.write(b)
|
||||
o2.write(b)
|
||||
}
|
||||
override fun write(b: ByteArray?, off: Int, len: Int) {
|
||||
o1.write(b, off, len)
|
||||
o2.write(b, off, len)
|
||||
}
|
||||
override fun close() {
|
||||
o1.close()
|
||||
o2.close()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun BaseDownloader.handleAPK(subject: Subject.Manager, stream: InputStream) {
|
||||
fun write(output: OutputStream) {
|
||||
val ext = subject.externalFile.outputStream()
|
||||
val o = DupOutputStream(ext, output)
|
||||
withStreams(stream, o) { src, out -> src.copyTo(out) }
|
||||
}
|
||||
|
||||
if (isRunningAsStub) {
|
||||
val apk = subject.file.toFile()
|
||||
val id = subject.notifyID()
|
||||
write(DynAPK.update(this).outputStream())
|
||||
if (Info.stub!!.version < subject.stub.versionCode) {
|
||||
// Also upgrade stub
|
||||
notifyHide(id)
|
||||
service.fetchFile(subject.stub.link).byteStream().writeTo(apk)
|
||||
patch(apk)
|
||||
} else {
|
||||
// Simply relaunch the app
|
||||
stopSelf()
|
||||
relaunchApp(this)
|
||||
}
|
||||
} else {
|
||||
write(subject.file.outputStream())
|
||||
}
|
||||
}
|
@@ -1,72 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
import com.topjohnwu.magisk.ProcessPhoenix
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.intent
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.utils.PatchAPK
|
||||
import com.topjohnwu.magisk.extensions.writeTo
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.APK.Restore
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.APK.Upgrade
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import java.io.File
|
||||
|
||||
private fun RemoteFileService.patch(apk: File, id: Int) {
|
||||
if (packageName == BuildConfig.APPLICATION_ID)
|
||||
return
|
||||
|
||||
update(id) {
|
||||
it.setProgress(0, 0, true)
|
||||
.setProgress(0, 0, true)
|
||||
.setContentTitle(getString(R.string.hide_manager_title))
|
||||
.setContentText("")
|
||||
}
|
||||
val patched = File(apk.parent, "patched.apk")
|
||||
PatchAPK.patch(apk.path, patched.path, packageName, applicationInfo.nonLocalizedLabel)
|
||||
apk.delete()
|
||||
patched.renameTo(apk)
|
||||
}
|
||||
|
||||
private fun RemoteFileService.upgrade(apk: File, id: Int) {
|
||||
if (isRunningAsStub) {
|
||||
// Move to upgrade location
|
||||
apk.copyTo(DynAPK.update(this), overwrite = true)
|
||||
apk.delete()
|
||||
if (Info.stub!!.version < Info.remote.stub.versionCode) {
|
||||
// We also want to upgrade stub
|
||||
service.fetchFile(Info.remote.stub.link).blockingGet().byteStream().use {
|
||||
it.writeTo(apk)
|
||||
}
|
||||
patch(apk, id)
|
||||
} else {
|
||||
// Simply relaunch the app
|
||||
ProcessPhoenix.triggerRebirth(this, intent<ProcessPhoenix>())
|
||||
}
|
||||
} else {
|
||||
patch(apk, id)
|
||||
}
|
||||
}
|
||||
|
||||
private fun RemoteFileService.restore(apk: File, id: Int) {
|
||||
update(id) {
|
||||
it.setProgress(0, 0, true)
|
||||
.setProgress(0, 0, true)
|
||||
.setContentTitle(getString(R.string.restore_img_msg))
|
||||
.setContentText("")
|
||||
}
|
||||
Config.export()
|
||||
// Make it world readable
|
||||
apk.setReadable(true, false)
|
||||
Shell.su("pm install $apk && pm uninstall $packageName").exec()
|
||||
}
|
||||
|
||||
fun RemoteFileService.handleAPK(subject: DownloadSubject.Manager) =
|
||||
when (subject.configuration) {
|
||||
is Upgrade -> upgrade(subject.file, subject.hashCode())
|
||||
is Restore -> restore(subject.file, subject.hashCode())
|
||||
}
|
@@ -1,13 +1,15 @@
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import com.topjohnwu.magisk.extensions.withStreams
|
||||
import java.io.File
|
||||
import android.net.Uri
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.ktx.forEach
|
||||
import com.topjohnwu.magisk.ktx.withStreams
|
||||
import java.io.InputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
fun InputStream.toModule(file: File, installer: InputStream) {
|
||||
fun InputStream.toModule(file: Uri, installer: InputStream) {
|
||||
|
||||
val input = ZipInputStream(buffered())
|
||||
val output = ZipOutputStream(file.outputStream().buffered())
|
||||
@@ -24,8 +26,7 @@ fun InputStream.toModule(file: File, installer: InputStream) {
|
||||
zout.write("#MAGISK\n".toByteArray(charset("UTF-8")))
|
||||
|
||||
var off = -1
|
||||
var entry: ZipEntry? = zin.nextEntry
|
||||
while (entry != null) {
|
||||
zin.forEach { entry ->
|
||||
if (off < 0) {
|
||||
off = entry.name.indexOf('/') + 1
|
||||
}
|
||||
@@ -37,8 +38,6 @@ fun InputStream.toModule(file: File, installer: InputStream) {
|
||||
zin.copyTo(zout)
|
||||
}
|
||||
}
|
||||
|
||||
entry = zin.nextEntry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,86 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import com.topjohnwu.magisk.core.base.BaseService
|
||||
import com.topjohnwu.magisk.core.view.Notifications
|
||||
import org.koin.core.KoinComponent
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
import kotlin.random.Random.Default.nextInt
|
||||
|
||||
abstract class NotificationService : BaseService(), KoinComponent {
|
||||
|
||||
private val hasNotifications get() = notifications.isNotEmpty()
|
||||
|
||||
private val notifications = Collections.synchronizedMap(HashMap<Int, Notification.Builder>())
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
notifications.forEach { cancel(it.key) }
|
||||
notifications.clear()
|
||||
}
|
||||
|
||||
abstract fun createNotification(): Notification.Builder
|
||||
|
||||
// --
|
||||
|
||||
fun update(
|
||||
id: Int,
|
||||
body: (Notification.Builder) -> Unit = {}
|
||||
) {
|
||||
val wasEmpty = notifications.isEmpty()
|
||||
val notification = notifications.getOrPut(id, ::createNotification).also(body)
|
||||
if (wasEmpty)
|
||||
updateForeground()
|
||||
else
|
||||
notify(id, notification.build())
|
||||
}
|
||||
|
||||
protected fun lastNotify(
|
||||
id: Int,
|
||||
editBody: (Notification.Builder) -> Notification.Builder? = { null }
|
||||
) : Int {
|
||||
val currentNotification = remove(id)?.run(editBody)
|
||||
|
||||
var newId = -1
|
||||
currentNotification?.let {
|
||||
newId = nextInt(Int.MAX_VALUE)
|
||||
notify(newId, it.build())
|
||||
}
|
||||
|
||||
if (!hasNotifications) {
|
||||
stopSelf()
|
||||
}
|
||||
return newId
|
||||
}
|
||||
|
||||
protected fun remove(id: Int) = notifications.remove(id).also {
|
||||
cancel(id)
|
||||
updateForeground()
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
private fun notify(id: Int, notification: Notification) {
|
||||
Notifications.mgr.notify(id, notification)
|
||||
}
|
||||
|
||||
private fun cancel(id: Int) {
|
||||
Notifications.mgr.cancel(id)
|
||||
}
|
||||
|
||||
private fun updateForeground() {
|
||||
if (hasNotifications) {
|
||||
val first = notifications.entries.first()
|
||||
startForeground(first.key, first.value.build())
|
||||
} else {
|
||||
stopForeground(true)
|
||||
}
|
||||
}
|
||||
|
||||
// --
|
||||
|
||||
override fun onBind(p0: Intent?): IBinder? = null
|
||||
}
|
@@ -1,135 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Notification
|
||||
import android.content.Intent
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
||||
import com.topjohnwu.magisk.core.view.Notifications
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.di.NullActivity
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.extensions.writeTo
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Magisk
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Module
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import io.reactivex.Completable
|
||||
import okhttp3.ResponseBody
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.core.KoinComponent
|
||||
import timber.log.Timber
|
||||
import java.io.InputStream
|
||||
|
||||
abstract class RemoteFileService : NotificationService() {
|
||||
|
||||
val service: GithubRawServices by inject()
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
intent?.getParcelableExtra<DownloadSubject>(ARG_URL)?.let { start(it) }
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
override fun createNotification() = Notifications.progress(this, "")
|
||||
|
||||
// ---
|
||||
|
||||
private fun start(subject: DownloadSubject) = checkExisting(subject)
|
||||
.onErrorResumeNext { download(subject) }
|
||||
.subscribeK(onError = {
|
||||
Timber.e(it)
|
||||
failNotify(subject)
|
||||
}) {
|
||||
val newId = finishNotify(subject)
|
||||
if (get<Activity>() !is NullActivity) {
|
||||
onFinished(subject, newId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkExisting(subject: DownloadSubject) = Completable.fromAction {
|
||||
check(subject is Magisk) { "Download cache is disabled" }
|
||||
check(subject.file.exists() &&
|
||||
ShellUtils.checkSum("MD5", subject.file, subject.magisk.md5)) {
|
||||
"The given file does not match checksum"
|
||||
}
|
||||
}
|
||||
|
||||
private fun download(subject: DownloadSubject) = service.fetchFile(subject.url)
|
||||
.map { it.toProgressStream(subject) }
|
||||
.flatMapCompletable { stream ->
|
||||
when (subject) {
|
||||
is Module -> service.fetchInstaller()
|
||||
.doOnSuccess { stream.toModule(subject.file, it.byteStream()) }
|
||||
.ignoreElement()
|
||||
else -> Completable.fromAction { stream.writeTo(subject.file) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun ResponseBody.toProgressStream(subject: DownloadSubject): InputStream {
|
||||
val maxRaw = contentLength()
|
||||
val max = maxRaw / 1_000_000f
|
||||
val id = subject.hashCode()
|
||||
|
||||
update(id) { it.setContentTitle(subject.title) }
|
||||
|
||||
return ProgressInputStream(byteStream()) {
|
||||
val progress = it / 1_000_000f
|
||||
update(id) { notification ->
|
||||
if (maxRaw > 0) {
|
||||
send(progress / max, subject)
|
||||
notification
|
||||
.setProgress(maxRaw.toInt(), it.toInt(), false)
|
||||
.setContentText("%.2f / %.2f MB".format(progress, max))
|
||||
} else {
|
||||
send(-1f, subject)
|
||||
notification.setContentText("%.2f MB / ??".format(progress))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun failNotify(subject: DownloadSubject) = lastNotify(subject.hashCode()) {
|
||||
send(0f, subject)
|
||||
it.setContentText(getString(R.string.download_file_error))
|
||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||
.setOngoing(false)
|
||||
}
|
||||
|
||||
private fun finishNotify(subject: DownloadSubject) = lastNotify(subject.hashCode()) {
|
||||
send(1f, subject)
|
||||
it.addActions(subject)
|
||||
.setContentText(getString(R.string.download_complete))
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setProgress(0, 0, false)
|
||||
.setOngoing(false)
|
||||
.setAutoCancel(true)
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
|
||||
@Throws(Throwable::class)
|
||||
protected abstract fun onFinished(subject: DownloadSubject, id: Int)
|
||||
|
||||
protected abstract fun Notification.Builder.addActions(subject: DownloadSubject)
|
||||
: Notification.Builder
|
||||
|
||||
companion object : KoinComponent {
|
||||
const val ARG_URL = "arg_url"
|
||||
|
||||
private val internalProgressBroadcast = MutableLiveData<Pair<Float, DownloadSubject>>()
|
||||
val progressBroadcast: LiveData<Pair<Float, DownloadSubject>> get() = internalProgressBroadcast
|
||||
|
||||
fun send(progress: Float, subject: DownloadSubject) {
|
||||
internalProgressBroadcast.postValue(progress to subject)
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
internalProgressBroadcast.value = null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,64 @@
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import androidx.core.net.toUri
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.model.MagiskJson
|
||||
import com.topjohnwu.magisk.core.model.StubJson
|
||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.ktx.cachedFile
|
||||
import com.topjohnwu.magisk.ktx.get
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
private fun cachedFile(name: String) = get<Context>().cachedFile(name).apply { delete() }.toUri()
|
||||
|
||||
sealed class Subject : Parcelable {
|
||||
|
||||
abstract val url: String
|
||||
abstract val file: Uri
|
||||
abstract val action: Action
|
||||
abstract val title: String
|
||||
|
||||
@Parcelize
|
||||
class Module(
|
||||
val module: OnlineModule,
|
||||
override val action: Action
|
||||
) : Subject() {
|
||||
override val url: String get() = module.zip_url
|
||||
override val title: String get() = module.downloadFilename
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
MediaStoreUtils.getFile(title).uri
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
class Manager(
|
||||
private val json: MagiskJson = Info.remote.magisk,
|
||||
val stub: StubJson = Info.remote.stub
|
||||
) : Subject() {
|
||||
override val action get() = Action.Download
|
||||
override val title: String get() = "Magisk-${json.version}(${json.versionCode})"
|
||||
override val url: String get() = json.link
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
cachedFile("manager.apk")
|
||||
}
|
||||
|
||||
val externalFile get() = MediaStoreUtils.getFile("$title.apk").uri
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Action : Parcelable {
|
||||
@Parcelize
|
||||
object Flash : Action()
|
||||
|
||||
@Parcelize
|
||||
object Download : Action()
|
||||
}
|
@@ -1,8 +1,6 @@
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
import androidx.annotation.StringDef
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Single
|
||||
|
||||
abstract class BaseDao {
|
||||
|
||||
@@ -20,25 +18,11 @@ abstract class BaseDao {
|
||||
@TableStrict
|
||||
abstract val table: String
|
||||
|
||||
inline fun <reified Builder : Query.Builder> query(builder: Builder.() -> Unit = {}) =
|
||||
inline fun <reified Builder : Query.Builder> buildQuery(builder: Builder.() -> Unit = {}) =
|
||||
Builder::class.java.newInstance()
|
||||
.apply { table = this@BaseDao.table }
|
||||
.apply(builder)
|
||||
.toString()
|
||||
.let { Query(it) }
|
||||
.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()
|
||||
|
@@ -3,10 +3,12 @@ package com.topjohnwu.magisk.core.magiskdb
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.model.MagiskPolicy
|
||||
import com.topjohnwu.magisk.core.model.toMap
|
||||
import com.topjohnwu.magisk.core.model.toPolicy
|
||||
import com.topjohnwu.magisk.extensions.now
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||
import com.topjohnwu.magisk.core.model.su.toMap
|
||||
import com.topjohnwu.magisk.core.model.su.toPolicy
|
||||
import com.topjohnwu.magisk.ktx.now
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@@ -17,55 +19,50 @@ class PolicyDao(
|
||||
|
||||
override val table: String = Table.POLICY
|
||||
|
||||
fun deleteOutdated(
|
||||
nowSeconds: Long = TimeUnit.MILLISECONDS.toSeconds(now)
|
||||
) = query<Delete> {
|
||||
suspend fun deleteOutdated() = buildQuery<Delete> {
|
||||
condition {
|
||||
greaterThan("until", "0")
|
||||
and {
|
||||
lessThan("until", nowSeconds.toString())
|
||||
lessThan("until", TimeUnit.MILLISECONDS.toSeconds(now).toString())
|
||||
}
|
||||
or {
|
||||
lessThan("until", "0")
|
||||
}
|
||||
}
|
||||
}.ignoreElement()
|
||||
}.commit()
|
||||
|
||||
fun delete(packageName: String) = query<Delete> {
|
||||
condition {
|
||||
equals("package_name", packageName)
|
||||
}
|
||||
}.ignoreElement()
|
||||
|
||||
fun delete(uid: Int) = query<Delete> {
|
||||
suspend fun delete(uid: Int) = buildQuery<Delete> {
|
||||
condition {
|
||||
equals("uid", uid)
|
||||
}
|
||||
}.ignoreElement()
|
||||
}.commit()
|
||||
|
||||
fun fetch(uid: Int) = query<Select> {
|
||||
suspend fun fetch(uid: Int) = buildQuery<Select> {
|
||||
condition {
|
||||
equals("uid", uid)
|
||||
}
|
||||
}.map { it.first().toPolicySafe() }
|
||||
}.query().first().toPolicyOrNull()
|
||||
|
||||
fun update(policy: MagiskPolicy) = query<Replace> {
|
||||
suspend fun update(policy: SuPolicy) = buildQuery<Replace> {
|
||||
values(policy.toMap())
|
||||
}.ignoreElement()
|
||||
}.commit()
|
||||
|
||||
fun fetchAll() = query<Select> {
|
||||
suspend fun <R: Any> fetchAll(mapper: (SuPolicy) -> R) = buildQuery<Select> {
|
||||
condition {
|
||||
equals("uid/100000", Const.USER_ID)
|
||||
}
|
||||
}.map { it.mapNotNull { it.toPolicySafe() } }
|
||||
}.query {
|
||||
it.toPolicyOrNull()?.let(mapper)
|
||||
}
|
||||
|
||||
|
||||
private fun Map<String, String>.toPolicySafe(): MagiskPolicy? {
|
||||
private fun Map<String, String>.toPolicyOrNull(): SuPolicy? {
|
||||
return runCatching { toPolicy(context.packageManager) }.getOrElse {
|
||||
Timber.e(it)
|
||||
if (it is PackageManager.NameNotFoundException) {
|
||||
val uid = getOrElse("uid") { null } ?: return null
|
||||
delete(uid).subscribe()
|
||||
GlobalScope.launch {
|
||||
delete(uid.toInt())
|
||||
}
|
||||
}
|
||||
null
|
||||
}
|
||||
|
@@ -1,6 +1,12 @@
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
import androidx.annotation.StringDef
|
||||
import com.topjohnwu.magisk.ktx.await
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class Query(private val _query: String) {
|
||||
val query get() = "magisk --sqlite '$_query'"
|
||||
@@ -9,6 +15,24 @@ class Query(private val _query: String) {
|
||||
val requestType: String
|
||||
var table: String
|
||||
}
|
||||
|
||||
suspend inline fun <R : Any> query(crossinline mapper: (Map<String, String>) -> R?): List<R> =
|
||||
withContext(Dispatchers.Default) {
|
||||
Shell.su(query).await().out.map { line ->
|
||||
async {
|
||||
line.split("\\|".toRegex())
|
||||
.map { it.split("=", limit = 2) }
|
||||
.filter { it.size == 2 }
|
||||
.map { it[0] to it[1] }
|
||||
.toMap()
|
||||
.let(mapper)
|
||||
}
|
||||
}.awaitAll().filterNotNull()
|
||||
}
|
||||
|
||||
suspend inline fun query() = query { it }
|
||||
|
||||
suspend inline fun commit() = Shell.su(query).to(null).await()
|
||||
}
|
||||
|
||||
class Delete : Query.Builder {
|
||||
|
@@ -4,17 +4,19 @@ class SettingsDao : BaseDao() {
|
||||
|
||||
override val table = Table.SETTINGS
|
||||
|
||||
fun delete(key: String) = query<Delete> {
|
||||
suspend fun delete(key: String) = buildQuery<Delete> {
|
||||
condition { equals("key", key) }
|
||||
}.ignoreElement()
|
||||
}.commit()
|
||||
|
||||
fun put(key: String, value: Int) = query<Replace> {
|
||||
suspend fun put(key: String, value: Int) = buildQuery<Replace> {
|
||||
values("key" to key, "value" to value)
|
||||
}.ignoreElement()
|
||||
}.commit()
|
||||
|
||||
fun fetch(key: String, default: Int = -1) = query<Select> {
|
||||
suspend fun fetch(key: String, default: Int = -1) = buildQuery<Select> {
|
||||
fields("value")
|
||||
condition { equals("key", key) }
|
||||
}.map { it.firstOrNull()?.values?.firstOrNull()?.toIntOrNull() ?: default }
|
||||
}.query {
|
||||
it["value"]?.toIntOrNull()
|
||||
}.firstOrNull() ?: default
|
||||
|
||||
}
|
||||
|
@@ -4,17 +4,19 @@ class StringDao : BaseDao() {
|
||||
|
||||
override val table = Table.STRINGS
|
||||
|
||||
fun delete(key: String) = query<Delete> {
|
||||
suspend fun delete(key: String) = buildQuery<Delete> {
|
||||
condition { equals("key", key) }
|
||||
}.ignoreElement()
|
||||
}.commit()
|
||||
|
||||
fun put(key: String, value: String) = query<Replace> {
|
||||
suspend fun put(key: String, value: String) = buildQuery<Replace> {
|
||||
values("key" to key, "value" to value)
|
||||
}.ignoreElement()
|
||||
}.commit()
|
||||
|
||||
fun fetch(key: String, default: String = "") = query<Select> {
|
||||
suspend fun fetch(key: String, default: String = "") = buildQuery<Select> {
|
||||
fields("value")
|
||||
condition { equals("key", key) }
|
||||
}.map { it.firstOrNull()?.values?.firstOrNull() ?: default }
|
||||
}.query {
|
||||
it["value"]
|
||||
}.firstOrNull() ?: default
|
||||
|
||||
}
|
||||
|
@@ -2,41 +2,52 @@ package com.topjohnwu.magisk.core.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class UpdateInfo(
|
||||
val app: ManagerJson = ManagerJson(),
|
||||
val uninstaller: UninstallerJson = UninstallerJson(),
|
||||
val magisk: MagiskJson = MagiskJson(),
|
||||
val stub: StubJson = StubJson()
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class UninstallerJson(
|
||||
val link: String = ""
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MagiskJson(
|
||||
val version: String = "",
|
||||
val versionCode: Int = -1,
|
||||
val link: String = "",
|
||||
val note: String = "",
|
||||
val md5: String = ""
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ManagerJson(
|
||||
data class MagiskJson(
|
||||
val version: String = "",
|
||||
val versionCode: Int = -1,
|
||||
val link: String = "",
|
||||
val note: String = ""
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StubJson(
|
||||
val versionCode: Int = -1,
|
||||
val link: String = ""
|
||||
) : Parcelable
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ModuleJson(
|
||||
val id: String,
|
||||
val last_update: Long,
|
||||
val prop_url: String,
|
||||
val zip_url: String,
|
||||
val notes_url: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RepoJson(
|
||||
val name: String,
|
||||
val last_update: Long,
|
||||
val modules: List<ModuleJson>
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CommitInfo(
|
||||
val sha: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class BranchInfo(
|
||||
val commit: CommitInfo
|
||||
)
|
||||
|
@@ -1,41 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.model.module
|
||||
|
||||
abstract class BaseModule : Comparable<BaseModule> {
|
||||
abstract var id: String
|
||||
protected set
|
||||
abstract var name: String
|
||||
protected set
|
||||
abstract var author: String
|
||||
protected set
|
||||
abstract var version: String
|
||||
protected set
|
||||
abstract var versionCode: Int
|
||||
protected set
|
||||
abstract var description: String
|
||||
protected set
|
||||
|
||||
@Throws(NumberFormatException::class)
|
||||
protected fun parseProps(props: List<String>) {
|
||||
for (line in props) {
|
||||
val prop = line.split("=".toRegex(), 2).map { it.trim() }
|
||||
if (prop.size != 2)
|
||||
continue
|
||||
|
||||
val key = prop[0]
|
||||
val value = prop[1]
|
||||
if (key.isEmpty() || key[0] == '#')
|
||||
continue
|
||||
|
||||
when (key) {
|
||||
"id" -> id = value
|
||||
"name" -> name = value
|
||||
"version" -> version = value
|
||||
"versionCode" -> versionCode = value.toInt()
|
||||
"author" -> author = value
|
||||
"description" -> description = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override operator fun compareTo(other: BaseModule) = name.compareTo(other.name, true)
|
||||
}
|
@@ -0,0 +1,89 @@
|
||||
package com.topjohnwu.magisk.core.model.module
|
||||
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class LocalModule(path: String) : Module() {
|
||||
override var id: String = ""
|
||||
override var name: String = ""
|
||||
override var author: String = ""
|
||||
override var version: String = ""
|
||||
override var versionCode: Int = -1
|
||||
override var description: String = ""
|
||||
|
||||
private val removeFile = SuFile(path, "remove")
|
||||
private val disableFile = SuFile(path, "disable")
|
||||
private val updateFile = SuFile(path, "update")
|
||||
private val ruleFile = SuFile(path, "sepolicy.rule")
|
||||
|
||||
val updated: Boolean get() = updateFile.exists()
|
||||
|
||||
var enable: Boolean
|
||||
get() = !disableFile.exists()
|
||||
set(enable) {
|
||||
val dir = "$PERSIST/$id"
|
||||
if (enable) {
|
||||
disableFile.delete()
|
||||
if (Const.Version.atLeast_21_2())
|
||||
Shell.su("copy_sepolicy_rules").submit()
|
||||
else
|
||||
Shell.su("mkdir -p $dir", "cp -af $ruleFile $dir").submit()
|
||||
} else {
|
||||
!disableFile.createNewFile()
|
||||
if (Const.Version.atLeast_21_2())
|
||||
Shell.su("copy_sepolicy_rules").submit()
|
||||
else
|
||||
Shell.su("rm -rf $dir").submit()
|
||||
}
|
||||
}
|
||||
|
||||
var remove: Boolean
|
||||
get() = removeFile.exists()
|
||||
set(remove) {
|
||||
if (remove) {
|
||||
removeFile.createNewFile()
|
||||
if (Const.Version.atLeast_21_2())
|
||||
Shell.su("copy_sepolicy_rules").submit()
|
||||
else
|
||||
Shell.su("rm -rf $PERSIST/$id").submit()
|
||||
} else {
|
||||
!removeFile.delete()
|
||||
if (Const.Version.atLeast_21_2())
|
||||
Shell.su("copy_sepolicy_rules").submit()
|
||||
else
|
||||
Shell.su("cp -af $ruleFile $PERSIST/$id").submit()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
runCatching {
|
||||
parseProps(Shell.su("dos2unix < $path/module.prop").exec().out)
|
||||
}
|
||||
|
||||
if (id.isEmpty()) {
|
||||
val sep = path.lastIndexOf('/')
|
||||
id = path.substring(sep + 1)
|
||||
}
|
||||
|
||||
if (name.isEmpty()) {
|
||||
name = id
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val PERSIST get() = "${Const.MAGISKTMP}/mirror/persist/magisk"
|
||||
|
||||
suspend fun installed() = withContext(Dispatchers.IO) {
|
||||
SuFile(Const.MAGISK_PATH)
|
||||
.listFiles { _, name -> name != "lost+found" && name != ".core" }
|
||||
.orEmpty()
|
||||
.filter { !it.isFile }
|
||||
.map { LocalModule("${Const.MAGISK_PATH}/${it.name}") }
|
||||
.sortedBy { it.name.toLowerCase() }
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,79 +1,41 @@
|
||||
package com.topjohnwu.magisk.core.model.module
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
abstract class Module : Comparable<Module> {
|
||||
abstract var id: String
|
||||
protected set
|
||||
abstract var name: String
|
||||
protected set
|
||||
abstract var author: String
|
||||
protected set
|
||||
abstract var version: String
|
||||
protected set
|
||||
abstract var versionCode: Int
|
||||
protected set
|
||||
abstract var description: String
|
||||
protected set
|
||||
|
||||
class Module(path: String) : BaseModule() {
|
||||
override var id: String = ""
|
||||
override var name: String = ""
|
||||
override var author: String = ""
|
||||
override var version: String = ""
|
||||
override var versionCode: Int = -1
|
||||
override var description: String = ""
|
||||
@Throws(NumberFormatException::class)
|
||||
protected fun parseProps(props: List<String>) {
|
||||
for (line in props) {
|
||||
val prop = line.split("=".toRegex(), 2).map { it.trim() }
|
||||
if (prop.size != 2)
|
||||
continue
|
||||
|
||||
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 key = prop[0]
|
||||
val value = prop[1]
|
||||
if (key.isEmpty() || key[0] == '#')
|
||||
continue
|
||||
|
||||
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()
|
||||
when (key) {
|
||||
"id" -> id = value
|
||||
"name" -> name = value
|
||||
"version" -> version = value
|
||||
"versionCode" -> versionCode = value.toInt()
|
||||
"author" -> author = value
|
||||
"description" -> description = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var remove: Boolean = removeFile.exists()
|
||||
set(remove) {
|
||||
field = if (remove) {
|
||||
Shell.su("rm -rf $PERSIST/$id").submit()
|
||||
removeFile.createNewFile()
|
||||
} else {
|
||||
Shell.su("cp -af $ruleFile $PERSIST/$id").submit()
|
||||
!removeFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
runCatching {
|
||||
parseProps(Shell.su("dos2unix < $path/module.prop").exec().out)
|
||||
}
|
||||
|
||||
if (id.isEmpty()) {
|
||||
val sep = path.lastIndexOf('/')
|
||||
id = path.substring(sep + 1)
|
||||
}
|
||||
|
||||
if (name.isEmpty()) {
|
||||
name = id
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val PERSIST = "/sbin/.magisk/mirror/persist/magisk"
|
||||
|
||||
@WorkerThread
|
||||
fun loadModules(): List<Module> {
|
||||
val moduleList = mutableListOf<Module>()
|
||||
val path = SuFile(Const.MAGISK_PATH)
|
||||
val modules =
|
||||
path.listFiles { _, name -> name != "lost+found" && name != ".core" }.orEmpty()
|
||||
for (file in modules) {
|
||||
if (file.isFile) continue
|
||||
val module = Module(Const.MAGISK_PATH + "/" + file.name)
|
||||
moduleList.add(module)
|
||||
}
|
||||
return moduleList.sortedBy { it.name.toLowerCase() }
|
||||
}
|
||||
}
|
||||
override operator fun compareTo(other: Module) = name.compareTo(other.name, true)
|
||||
}
|
||||
|
@@ -0,0 +1,66 @@
|
||||
package com.topjohnwu.magisk.core.model.module
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.topjohnwu.magisk.core.model.ModuleJson
|
||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||
import com.topjohnwu.magisk.ktx.get
|
||||
import com.topjohnwu.magisk.ktx.legalFilename
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
||||
@Entity(tableName = "modules")
|
||||
@Parcelize
|
||||
data class OnlineModule(
|
||||
@PrimaryKey override var id: String,
|
||||
override var name: String = "",
|
||||
override var author: String = "",
|
||||
override var version: String = "",
|
||||
override var versionCode: Int = -1,
|
||||
override var description: String = "",
|
||||
val last_update: Long,
|
||||
val prop_url: String,
|
||||
val zip_url: String,
|
||||
val notes_url: String
|
||||
) : Module(), Parcelable {
|
||||
|
||||
private val svc: NetworkService get() = get()
|
||||
|
||||
constructor(info: ModuleJson) : this(
|
||||
id = info.id,
|
||||
last_update = info.last_update,
|
||||
prop_url = info.prop_url,
|
||||
zip_url = info.zip_url,
|
||||
notes_url = info.notes_url
|
||||
)
|
||||
|
||||
val lastUpdate get() = Date(last_update)
|
||||
val lastUpdateString get() = DATE_FORMAT.format(lastUpdate)
|
||||
val downloadFilename get() = "$name-$version($versionCode).zip".legalFilename()
|
||||
|
||||
suspend fun notes() = svc.fetchString(notes_url)
|
||||
|
||||
@Throws(IllegalRepoException::class)
|
||||
suspend fun load() {
|
||||
try {
|
||||
val rawProps = svc.fetchString(prop_url)
|
||||
val props = rawProps.split("\\n".toRegex()).dropLastWhile { it.isEmpty() }
|
||||
parseProps(props)
|
||||
} catch (e: Exception) {
|
||||
throw IllegalRepoException("Repo [$id] parse error:", e)
|
||||
}
|
||||
|
||||
if (versionCode < 0) {
|
||||
throw IllegalRepoException("Repo [$id] does not contain versionCode")
|
||||
}
|
||||
}
|
||||
|
||||
class IllegalRepoException(msg: String, cause: Throwable? = null) : Exception(msg, cause)
|
||||
|
||||
companion object {
|
||||
private val DATE_FORMAT = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
|
||||
}
|
||||
|
||||
}
|
@@ -1,71 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.model.module
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.data.repository.StringRepository
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.legalFilename
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
||||
@Entity(tableName = "repos")
|
||||
@Parcelize
|
||||
data class Repo(
|
||||
@PrimaryKey override var id: String,
|
||||
override var name: String,
|
||||
override var author: String,
|
||||
override var version: String,
|
||||
override var versionCode: Int,
|
||||
override var description: String,
|
||||
var last_update: Long
|
||||
) : BaseModule(), Parcelable {
|
||||
|
||||
private val stringRepo: StringRepository get() = get()
|
||||
|
||||
val lastUpdate get() = Date(last_update)
|
||||
|
||||
val lastUpdateString: String get() = dateFormat.format(lastUpdate)
|
||||
|
||||
val downloadFilename: String get() = "$name-$version($versionCode).zip".legalFilename()
|
||||
|
||||
val readme get() = stringRepo.getReadme(this)
|
||||
|
||||
val zipUrl: String get() = Const.Url.ZIP_URL.format(id)
|
||||
|
||||
constructor(id: String) : this(id, "", "", "", -1, "", 0)
|
||||
|
||||
@Throws(IllegalRepoException::class)
|
||||
fun update() {
|
||||
val props = runCatching {
|
||||
stringRepo.getMetadata(this).blockingGet()
|
||||
.orEmpty().split("\\n".toRegex()).dropLastWhile { it.isEmpty() }
|
||||
}.getOrElse {
|
||||
throw IllegalRepoException("Repo [$id] module.prop download error: " + it.message)
|
||||
}
|
||||
|
||||
props.runCatching {
|
||||
parseProps(this)
|
||||
}.onFailure {
|
||||
throw IllegalRepoException("Repo [$id] parse error: " + it.message)
|
||||
}
|
||||
|
||||
if (versionCode < 0) {
|
||||
throw IllegalRepoException("Repo [$id] does not contain versionCode")
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IllegalRepoException::class)
|
||||
fun update(lastUpdate: Date) {
|
||||
last_update = lastUpdate.time
|
||||
update()
|
||||
}
|
||||
|
||||
class IllegalRepoException(message: String) : Exception(message)
|
||||
|
||||
companion object {
|
||||
val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)!!
|
||||
}
|
||||
}
|
@@ -1,16 +1,15 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
package com.topjohnwu.magisk.core.model.su
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.PrimaryKey
|
||||
import com.topjohnwu.magisk.core.model.MagiskPolicy
|
||||
import com.topjohnwu.magisk.core.model.MagiskPolicy.Companion.ALLOW
|
||||
import com.topjohnwu.magisk.extensions.now
|
||||
import com.topjohnwu.magisk.extensions.timeFormatTime
|
||||
import com.topjohnwu.magisk.extensions.toTime
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.ALLOW
|
||||
import com.topjohnwu.magisk.ktx.now
|
||||
import com.topjohnwu.magisk.ktx.timeFormatTime
|
||||
import com.topjohnwu.magisk.ktx.toTime
|
||||
|
||||
@Entity(tableName = "logs")
|
||||
data class MagiskLog(
|
||||
data class SuLog(
|
||||
val fromUid: Int,
|
||||
val toUid: Int,
|
||||
val fromPid: Int,
|
||||
@@ -24,8 +23,8 @@ data class MagiskLog(
|
||||
@Ignore val timeString = time.toTime(timeFormatTime)
|
||||
}
|
||||
|
||||
fun MagiskPolicy.toLog(
|
||||
fun SuPolicy.toLog(
|
||||
toUid: Int,
|
||||
fromPid: Int,
|
||||
command: String
|
||||
) = MagiskLog(uid, toUid, fromPid, packageName, appName, command, policy == ALLOW, now)
|
||||
) = SuLog(uid, toUid, fromPid, packageName, appName, command, policy == ALLOW, now)
|
@@ -1,20 +1,22 @@
|
||||
package com.topjohnwu.magisk.core.model
|
||||
@file:SuppressLint("InlinedApi")
|
||||
|
||||
import android.content.pm.ApplicationInfo
|
||||
package com.topjohnwu.magisk.core.model.su
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.core.model.MagiskPolicy.Companion.INTERACTIVE
|
||||
import com.topjohnwu.magisk.extensions.getLabel
|
||||
import android.graphics.drawable.Drawable
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.INTERACTIVE
|
||||
import com.topjohnwu.magisk.ktx.getLabel
|
||||
|
||||
|
||||
data class MagiskPolicy(
|
||||
data class SuPolicy(
|
||||
var uid: Int,
|
||||
val packageName: String,
|
||||
val appName: String,
|
||||
val icon: Drawable,
|
||||
var policy: Int = INTERACTIVE,
|
||||
var until: Long = -1L,
|
||||
val logging: Boolean = true,
|
||||
val notification: Boolean = true,
|
||||
val applicationInfo: ApplicationInfo
|
||||
val notification: Boolean = true
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@@ -25,7 +27,7 @@ data class MagiskPolicy(
|
||||
|
||||
}
|
||||
|
||||
fun MagiskPolicy.toMap() = mapOf(
|
||||
fun SuPolicy.toMap() = mapOf(
|
||||
"uid" to uid,
|
||||
"package_name" to packageName,
|
||||
"policy" to policy,
|
||||
@@ -35,36 +37,36 @@ fun MagiskPolicy.toMap() = mapOf(
|
||||
)
|
||||
|
||||
@Throws(PackageManager.NameNotFoundException::class)
|
||||
fun Map<String, String>.toPolicy(pm: PackageManager): MagiskPolicy {
|
||||
fun Map<String, String>.toPolicy(pm: PackageManager): SuPolicy {
|
||||
val uid = get("uid")?.toIntOrNull() ?: -1
|
||||
val packageName = get("package_name").orEmpty()
|
||||
val info = pm.getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES)
|
||||
val info = pm.getApplicationInfo(packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES)
|
||||
|
||||
if (info.uid != uid)
|
||||
throw PackageManager.NameNotFoundException()
|
||||
|
||||
return MagiskPolicy(
|
||||
return SuPolicy(
|
||||
uid = uid,
|
||||
packageName = packageName,
|
||||
appName = info.getLabel(pm),
|
||||
icon = info.loadIcon(pm),
|
||||
policy = get("policy")?.toIntOrNull() ?: INTERACTIVE,
|
||||
until = get("until")?.toLongOrNull() ?: -1L,
|
||||
logging = get("logging")?.toIntOrNull() != 0,
|
||||
notification = get("notification")?.toIntOrNull() != 0,
|
||||
applicationInfo = info,
|
||||
appName = info.getLabel(pm)
|
||||
notification = get("notification")?.toIntOrNull() != 0
|
||||
)
|
||||
}
|
||||
|
||||
@Throws(PackageManager.NameNotFoundException::class)
|
||||
fun Int.toPolicy(pm: PackageManager, policy: Int = INTERACTIVE): MagiskPolicy {
|
||||
fun Int.toPolicy(pm: PackageManager, policy: Int = INTERACTIVE): SuPolicy {
|
||||
val pkg = pm.getPackagesForUid(this)?.firstOrNull()
|
||||
?: throw PackageManager.NameNotFoundException()
|
||||
val info = pm.getApplicationInfo(pkg, PackageManager.GET_UNINSTALLED_PACKAGES)
|
||||
return MagiskPolicy(
|
||||
val info = pm.getApplicationInfo(pkg, PackageManager.MATCH_UNINSTALLED_PACKAGES)
|
||||
return SuPolicy(
|
||||
uid = info.uid,
|
||||
packageName = pkg,
|
||||
policy = policy,
|
||||
applicationInfo = info,
|
||||
appName = info.getLabel(pm)
|
||||
appName = info.getLabel(pm),
|
||||
icon = info.loadIcon(pm),
|
||||
policy = policy
|
||||
)
|
||||
}
|
@@ -7,36 +7,30 @@ 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.core.model.su.SuPolicy
|
||||
import com.topjohnwu.magisk.core.model.su.toLog
|
||||
import com.topjohnwu.magisk.core.model.su.toPolicy
|
||||
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.magisk.ktx.get
|
||||
import com.topjohnwu.magisk.ktx.startActivity
|
||||
import com.topjohnwu.magisk.ktx.startActivityWithRoot
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
object SuCallbackHandler : ProviderCallHandler {
|
||||
object SuCallbackHandler {
|
||||
|
||||
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
|
||||
|
||||
@@ -51,20 +45,8 @@ object SuCallbackHandler : ProviderCallHandler {
|
||||
}
|
||||
|
||||
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)
|
||||
REQUEST -> handleRequest(context, data)
|
||||
LOG -> handleLogging(context, data)
|
||||
NOTIFY -> handleNotify(context, data)
|
||||
TEST -> {
|
||||
val mode = data.getInt("mode", 2)
|
||||
@@ -78,13 +60,26 @@ object SuCallbackHandler : ProviderCallHandler {
|
||||
|
||||
private fun Any?.toInt(): Int? {
|
||||
return when (this) {
|
||||
is Int -> this
|
||||
is Long -> this.toInt()
|
||||
is Number -> this.toInt()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLogs(context: Context, data: Bundle) {
|
||||
private fun handleRequest(context: Context, data: Bundle) {
|
||||
val intent = context.intent<SuRequestActivity>()
|
||||
.setAction(REQUEST)
|
||||
.putExtras(data)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
// Android Q does not allow starting activity from background
|
||||
intent.startActivityWithRoot()
|
||||
} else {
|
||||
intent.startActivity(context)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLogging(context: Context, data: Bundle) {
|
||||
val fromUid = data["from.uid"].toInt() ?: return
|
||||
if (fromUid == Process.myUid())
|
||||
return
|
||||
@@ -110,7 +105,9 @@ object SuCallbackHandler : ProviderCallHandler {
|
||||
)
|
||||
|
||||
val logRepo = get<LogRepository>()
|
||||
logRepo.insert(log).subscribeK(onError = { Timber.e(it) })
|
||||
GlobalScope.launch {
|
||||
logRepo.insert(log)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNotify(context: Context, data: Bundle) {
|
||||
@@ -128,9 +125,9 @@ object SuCallbackHandler : ProviderCallHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private fun notify(context: Context, policy: MagiskPolicy) {
|
||||
private fun notify(context: Context, policy: SuPolicy) {
|
||||
if (policy.notification && Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
|
||||
val resId = if (policy.policy == MagiskPolicy.ALLOW)
|
||||
val resId = if (policy.policy == SuPolicy.ALLOW)
|
||||
R.string.su_allow_toast
|
||||
else
|
||||
R.string.su_deny_toast
|
||||
|
@@ -1,57 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.su
|
||||
|
||||
import android.net.LocalSocket
|
||||
import android.net.LocalSocketAddress
|
||||
import androidx.collection.ArrayMap
|
||||
import timber.log.Timber
|
||||
import java.io.*
|
||||
|
||||
abstract class SuConnector @Throws(IOException::class)
|
||||
protected constructor(name: String) {
|
||||
|
||||
private val socket: LocalSocket = LocalSocket()
|
||||
protected var out: DataOutputStream
|
||||
protected var input: DataInputStream
|
||||
|
||||
init {
|
||||
socket.connect(LocalSocketAddress(name, LocalSocketAddress.Namespace.ABSTRACT))
|
||||
out = DataOutputStream(BufferedOutputStream(socket.outputStream))
|
||||
input = DataInputStream(BufferedInputStream(socket.inputStream))
|
||||
}
|
||||
|
||||
private fun readString(): String {
|
||||
val len = input.readInt()
|
||||
val buf = ByteArray(len)
|
||||
input.readFully(buf)
|
||||
return String(buf, Charsets.UTF_8)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun readRequest(): Map<String, String> {
|
||||
val ret = ArrayMap<String, String>()
|
||||
while (true) {
|
||||
val name = readString()
|
||||
if (name == "eof")
|
||||
break
|
||||
ret[name] = readString()
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
fun response() {
|
||||
runCatching {
|
||||
onResponse()
|
||||
out.flush()
|
||||
}.onFailure { Timber.e(it) }
|
||||
|
||||
runCatching {
|
||||
input.close()
|
||||
out.close()
|
||||
socket.close()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
protected abstract fun onResponse()
|
||||
|
||||
}
|
@@ -2,87 +2,96 @@ package com.topjohnwu.magisk.core.su
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.CountDownTimer
|
||||
import android.net.LocalSocket
|
||||
import android.net.LocalSocketAddress
|
||||
import androidx.collection.ArrayMap
|
||||
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 com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||
import com.topjohnwu.magisk.core.model.su.toPolicy
|
||||
import com.topjohnwu.magisk.ktx.now
|
||||
import kotlinx.coroutines.*
|
||||
import timber.log.Timber
|
||||
import java.io.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.TimeUnit.SECONDS
|
||||
|
||||
abstract class SuRequestHandler(
|
||||
private val packageManager: PackageManager,
|
||||
class SuRequestHandler(
|
||||
private val pm: 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()
|
||||
}
|
||||
) : Closeable {
|
||||
|
||||
protected lateinit var policy: MagiskPolicy
|
||||
private lateinit var output: DataOutputStream
|
||||
lateinit var policy: SuPolicy
|
||||
private set
|
||||
|
||||
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 true to indicate undetermined policy, require user interaction
|
||||
suspend fun start(intent: Intent): Boolean {
|
||||
if (!init(intent))
|
||||
return false
|
||||
}
|
||||
|
||||
// Never allow com.topjohnwu.magisk (could be malware)
|
||||
if (policy.packageName == BuildConfig.APPLICATION_ID)
|
||||
return false
|
||||
|
||||
when (Config.suAutoReponse) {
|
||||
when (Config.suAutoResponse) {
|
||||
Config.Value.SU_AUTO_DENY -> {
|
||||
respond(MagiskPolicy.DENY, 0)
|
||||
return true
|
||||
respond(SuPolicy.DENY, 0)
|
||||
return false
|
||||
}
|
||||
Config.Value.SU_AUTO_ALLOW -> {
|
||||
respond(MagiskPolicy.ALLOW, 0)
|
||||
return true
|
||||
respond(SuPolicy.ALLOW, 0)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
timer.start()
|
||||
cleanupTasks.add {
|
||||
timer.cancel()
|
||||
}
|
||||
|
||||
onStart()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun respond() {
|
||||
connector.response()
|
||||
cleanupTasks.forEach { it() }
|
||||
onRespond()
|
||||
private suspend fun <T> Deferred<T>.timedAwait() : T? {
|
||||
return withTimeoutOrNull(SECONDS.toMillis(1)) {
|
||||
await()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun close() {
|
||||
if (::output.isInitialized)
|
||||
output.close()
|
||||
}
|
||||
|
||||
private class SuRequestError : IOException()
|
||||
|
||||
private suspend fun init(intent: Intent) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val uid: Int
|
||||
if (Const.Version.atLeast_21_0()) {
|
||||
val name = intent.getStringExtra("fifo") ?: throw SuRequestError()
|
||||
uid = intent.getIntExtra("uid", -1).also { if (it < 0) throw SuRequestError() }
|
||||
output = DataOutputStream(FileOutputStream(name).buffered())
|
||||
} else {
|
||||
val name = intent.getStringExtra("socket") ?: throw SuRequestError()
|
||||
val socket = LocalSocket()
|
||||
socket.connect(LocalSocketAddress(name, LocalSocketAddress.Namespace.ABSTRACT))
|
||||
output = DataOutputStream(BufferedOutputStream(socket.outputStream))
|
||||
val input = DataInputStream(BufferedInputStream(socket.inputStream))
|
||||
val map = async { input.readRequest() }.timedAwait() ?: throw SuRequestError()
|
||||
uid = map["uid"]?.toIntOrNull() ?: throw SuRequestError()
|
||||
}
|
||||
policy = uid.toPolicy(pm)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is IOException, is PackageManager.NameNotFoundException -> {
|
||||
Timber.e(e)
|
||||
runCatching { close() }
|
||||
false
|
||||
}
|
||||
else -> throw e // Unexpected error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun respond(action: Int, time: Int) {
|
||||
@@ -95,9 +104,36 @@ abstract class SuRequestHandler(
|
||||
policy.until = until
|
||||
policy.uid = policy.uid % 100000 + Const.USER_ID * 100000
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
output.writeInt(policy.policy)
|
||||
output.flush()
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
} finally {
|
||||
runCatching { close() }
|
||||
if (until >= 0)
|
||||
policyDB.update(policy).blockingAwait()
|
||||
|
||||
respond()
|
||||
policyDB.update(policy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun DataInputStream.readRequest(): Map<String, String> {
|
||||
fun readString(): String {
|
||||
val len = readInt()
|
||||
val buf = ByteArray(len)
|
||||
readFully(buf)
|
||||
return String(buf, Charsets.UTF_8)
|
||||
}
|
||||
val ret = ArrayMap<String, String>()
|
||||
while (true) {
|
||||
val name = readString()
|
||||
if (name == "eof")
|
||||
break
|
||||
ret[name] = readString()
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,37 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import androidx.annotation.MainThread
|
||||
|
||||
interface FlashResultListener {
|
||||
|
||||
@MainThread
|
||||
fun onResult(success: Boolean)
|
||||
|
||||
}
|
@@ -2,96 +2,87 @@ package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toFile
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||
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.ktx.writeTo
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Single
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.inject
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
|
||||
abstract class FlashZip(
|
||||
open class FlashZip(
|
||||
private val mUri: Uri,
|
||||
private val console: MutableList<String>,
|
||||
private val logs: MutableList<String>
|
||||
) : FlashResultListener {
|
||||
): KoinComponent {
|
||||
|
||||
private val context: Context by inject()
|
||||
private val installFolder = File(context.cacheDir, "flash").apply {
|
||||
if (!exists()) mkdirs()
|
||||
}
|
||||
private val tmpFile: File = File(installFolder, "install.zip")
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun unzipAndCheck(): Boolean {
|
||||
val parentFile = tmpFile.parentFile ?: return false
|
||||
tmpFile.unzip(parentFile, "META-INF/com/google/android", true)
|
||||
|
||||
val updaterScript = File(parentFile, "updater-script")
|
||||
return Shell
|
||||
.su("grep -q '#MAGISK' $updaterScript")
|
||||
.exec()
|
||||
.isSuccess
|
||||
}
|
||||
private val installDir = File(context.cacheDir, "flash")
|
||||
private lateinit var zipFile: File
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun flash(): Boolean {
|
||||
installDir.deleteRecursively()
|
||||
installDir.mkdirs()
|
||||
|
||||
zipFile = if (mUri.scheme == "file") {
|
||||
mUri.toFile()
|
||||
} else {
|
||||
File(installDir, "install.zip").also {
|
||||
console.add("- Copying zip to temp directory")
|
||||
|
||||
runCatching {
|
||||
context.readUri(mUri).use { input ->
|
||||
tmpFile.outputStream().use { out -> input.copyTo(out) }
|
||||
}
|
||||
}.getOrElse {
|
||||
when (it) {
|
||||
try {
|
||||
mUri.inputStream().writeTo(it)
|
||||
} catch (e: IOException) {
|
||||
when (e) {
|
||||
is FileNotFoundException -> console.add("! Invalid Uri")
|
||||
is IOException -> console.add("! Cannot copy to cache")
|
||||
else -> console.add("! Cannot copy to cache")
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
throw it
|
||||
}
|
||||
|
||||
val isMagiskModule = runCatching {
|
||||
unzipAndCheck()
|
||||
val isValid = runCatching {
|
||||
zipFile.unzip(installDir, "META-INF/com/google/android", true)
|
||||
val script = File(installDir, "updater-script")
|
||||
script.readText().contains("#MAGISK")
|
||||
}.getOrElse {
|
||||
console.add("! Unzip error")
|
||||
throw it
|
||||
}
|
||||
|
||||
if (!isMagiskModule) {
|
||||
console.add("! This zip is not a Magisk Module!")
|
||||
if (!isValid) {
|
||||
console.add("! This zip is not a Magisk module!")
|
||||
return false
|
||||
}
|
||||
|
||||
console.add("- Installing ${mUri.fileName}")
|
||||
console.add("- Installing ${mUri.displayName}")
|
||||
|
||||
val parentFile = tmpFile.parent ?: return false
|
||||
|
||||
return Shell
|
||||
.su(
|
||||
"cd $parentFile",
|
||||
"BOOTMODE=true sh update-binary dummy 1 $tmpFile"
|
||||
)
|
||||
.to(console, logs)
|
||||
.exec().isSuccess
|
||||
return Shell.su("sh $installDir/update-binary dummy 1 \'$zipFile\'")
|
||||
.to(console, logs).exec().isSuccess
|
||||
}
|
||||
|
||||
fun exec() = Single
|
||||
.fromCallable {
|
||||
runCatching {
|
||||
flash()
|
||||
}.getOrElse {
|
||||
it.printStackTrace()
|
||||
open suspend fun exec() = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
if (!flash()) {
|
||||
console.add("! Installation failed")
|
||||
false
|
||||
}.apply {
|
||||
Shell.su("cd /", "rm -rf ${tmpFile.parent} ${Const.TMP_FOLDER_PATH}")
|
||||
.submit()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
false
|
||||
} finally {
|
||||
Shell.su("cd /", "rm -rf $installDir ${Const.TMPDIR}").submit()
|
||||
}
|
||||
}
|
||||
.subscribeK(onError = { onResult(false) }) { onResult(it) }
|
||||
.let { Unit } // ignores result disposable
|
||||
|
||||
}
|
||||
|
@@ -1,51 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.os.postDelayed
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
|
||||
sealed class Flashing(
|
||||
uri: Uri,
|
||||
private val console: MutableList<String>,
|
||||
log: MutableList<String>,
|
||||
private val resultListener: FlashResultListener
|
||||
) : FlashZip(uri, console, log) {
|
||||
|
||||
override fun onResult(success: Boolean) {
|
||||
if (!success) {
|
||||
console.add("! Installation failed")
|
||||
}
|
||||
|
||||
resultListener.onResult(success)
|
||||
}
|
||||
|
||||
class Install(
|
||||
uri: Uri,
|
||||
console: MutableList<String>,
|
||||
log: MutableList<String>,
|
||||
resultListener: FlashResultListener
|
||||
) : Flashing(uri, console, log, resultListener)
|
||||
|
||||
class Uninstall(
|
||||
uri: Uri,
|
||||
console: MutableList<String>,
|
||||
log: MutableList<String>,
|
||||
resultListener: FlashResultListener
|
||||
) : Flashing(uri, console, log, resultListener) {
|
||||
|
||||
private val context: Context by inject()
|
||||
|
||||
override fun onResult(success: Boolean) {
|
||||
if (success) {
|
||||
UiThreadHandler.handler.postDelayed(3000) {
|
||||
Shell.su("pm uninstall " + context.packageName).exec()
|
||||
}
|
||||
}
|
||||
super.onResult(success)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
168
app/src/main/java/com/topjohnwu/magisk/core/tasks/HideAPK.kt
Normal file
168
app/src/main/java/com/topjohnwu/magisk/core/tasks/HideAPK.kt
Normal file
@@ -0,0 +1,168 @@
|
||||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
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.Provider
|
||||
import com.topjohnwu.magisk.core.utils.AXML
|
||||
import com.topjohnwu.magisk.core.utils.Keygen
|
||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||
import com.topjohnwu.magisk.ktx.inject
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.signing.JarMap
|
||||
import com.topjohnwu.signing.SignApk
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.lang.ref.WeakReference
|
||||
import java.security.SecureRandom
|
||||
|
||||
object HideAPK {
|
||||
|
||||
private const val ALPHA = "abcdefghijklmnopqrstuvwxyz"
|
||||
private const val ALPHADOTS = "$ALPHA....."
|
||||
private const val APP_NAME = "Magisk"
|
||||
private const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
||||
|
||||
// Some arbitrary limit
|
||||
const val MAX_LABEL_LENGTH = 32
|
||||
|
||||
private val svc: NetworkService by inject()
|
||||
private val Context.APK_URI get() = Provider.APK_URI(packageName)
|
||||
private val Context.PREFS_URI get() = Provider.PREFS_URI(packageName)
|
||||
|
||||
private fun genPackageName(): String {
|
||||
val random = SecureRandom()
|
||||
val len = 5 + random.nextInt(15)
|
||||
val builder = StringBuilder(len)
|
||||
var next: Char
|
||||
var prev = 0.toChar()
|
||||
for (i in 0 until len) {
|
||||
next = if (prev == '.' || i == 0 || i == len - 1) {
|
||||
ALPHA[random.nextInt(ALPHA.length)]
|
||||
} else {
|
||||
ALPHADOTS[random.nextInt(ALPHADOTS.length)]
|
||||
}
|
||||
builder.append(next)
|
||||
prev = next
|
||||
}
|
||||
if (!builder.contains('.')) {
|
||||
// Pick a random index and set it as dot
|
||||
val idx = random.nextInt(len - 2)
|
||||
builder[idx + 1] = '.'
|
||||
}
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
fun patch(
|
||||
context: Context,
|
||||
apk: File, out: File,
|
||||
pkg: String, label: CharSequence
|
||||
): Boolean {
|
||||
try {
|
||||
val jar = JarMap.open(apk, true)
|
||||
val je = jar.getJarEntry(ANDROID_MANIFEST)
|
||||
val xml = AXML(jar.getRawData(je))
|
||||
|
||||
if (!xml.findAndPatch(APPLICATION_ID to pkg, APP_NAME to label.toString()))
|
||||
return false
|
||||
|
||||
// Write apk changes
|
||||
jar.getOutputStream(je).write(xml.bytes)
|
||||
val keys = Keygen(context)
|
||||
SignApk.sign(keys.cert, keys.key, jar, FileOutputStream(out))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private class WaitPackageReceiver(
|
||||
private val pkg: String,
|
||||
activity: Activity
|
||||
) : BroadcastReceiver() {
|
||||
|
||||
private val activity = WeakReference(activity)
|
||||
|
||||
private fun launchApp(): Unit = activity.get()?.run {
|
||||
val intent = packageManager.getLaunchIntentForPackage(pkg) ?: return
|
||||
Config.suManager = if (pkg == APPLICATION_ID) "" else pkg
|
||||
grantUriPermission(pkg, APK_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
grantUriPermission(pkg, PREFS_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
intent.putExtra(Const.Key.PREV_PKG, packageName)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
} ?: Unit
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.action ?: return) {
|
||||
Intent.ACTION_PACKAGE_REPLACED, Intent.ACTION_PACKAGE_ADDED -> {
|
||||
val newPkg = intent.data?.encodedSchemeSpecificPart.orEmpty()
|
||||
if (newPkg == pkg) {
|
||||
context.unregisterReceiver(this)
|
||||
launchApp()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private suspend fun patchAndHide(activity: Activity, label: String): Boolean {
|
||||
val stub = File(activity.cacheDir, "stub.apk")
|
||||
try {
|
||||
svc.fetchFile(Info.remote.stub.link).byteStream().writeTo(stub)
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
// Generate a new random package name and signature
|
||||
val repack = File(activity.cacheDir, "patched.apk")
|
||||
val pkg = genPackageName()
|
||||
Config.keyStoreRaw = ""
|
||||
|
||||
if (!patch(activity, stub, repack, pkg, label))
|
||||
return false
|
||||
|
||||
// Install and auto launch app
|
||||
APKInstall.registerInstallReceiver(activity, WaitPackageReceiver(pkg, activity))
|
||||
if (!Shell.su("adb_pm_install $repack").exec().isSuccess)
|
||||
APKInstall.installHideResult(activity, repack)
|
||||
return true
|
||||
}
|
||||
|
||||
suspend fun hide(activity: Activity, label: String) {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
patchAndHide(activity, label)
|
||||
}
|
||||
if (!result) {
|
||||
Utils.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
|
||||
fun restore(activity: Activity) {
|
||||
val apk = DynAPK.current(activity)
|
||||
APKInstall.registerInstallReceiver(activity, WaitPackageReceiver(APPLICATION_ID, activity))
|
||||
Shell.su("adb_pm_install $apk").submit {
|
||||
if (!it.isSuccess)
|
||||
APKInstall.installHideResult(activity, apk)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,370 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.text.TextUtils
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.net.toUri
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.di.Protected
|
||||
import com.topjohnwu.magisk.extensions.*
|
||||
import com.topjohnwu.signing.SignBoot
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import com.topjohnwu.superuser.internal.NOPList
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
||||
import io.reactivex.Single
|
||||
import org.kamranzafar.jtar.TarEntry
|
||||
import org.kamranzafar.jtar.TarHeader
|
||||
import org.kamranzafar.jtar.TarInputStream
|
||||
import org.kamranzafar.jtar.TarOutputStream
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
abstract class MagiskInstallImpl : FlashResultListener {
|
||||
|
||||
protected lateinit var installDir: File
|
||||
private lateinit var srcBoot: String
|
||||
private lateinit var destFile: File
|
||||
private lateinit var zipUri: Uri
|
||||
|
||||
private val console: MutableList<String>
|
||||
private val logs: MutableList<String>
|
||||
private var tarOut: TarOutputStream? = null
|
||||
|
||||
private val service: GithubRawServices by inject()
|
||||
protected val context: Context by inject()
|
||||
|
||||
protected constructor() {
|
||||
console = NOPList.getInstance()
|
||||
logs = NOPList.getInstance()
|
||||
}
|
||||
|
||||
constructor(zip: Uri, out: MutableList<String>, err: MutableList<String>) {
|
||||
console = out
|
||||
logs = err
|
||||
zipUri = zip
|
||||
installDir = File(get<Context>(Protected).filesDir.parent, "install")
|
||||
"rm -rf $installDir".sh()
|
||||
installDir.mkdirs()
|
||||
}
|
||||
|
||||
private fun findImage(): Boolean {
|
||||
srcBoot = "find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
|
||||
if (srcBoot.isEmpty()) {
|
||||
console.add("! Unable to detect target image")
|
||||
return false
|
||||
}
|
||||
console.add("- Target image: $srcBoot")
|
||||
return true
|
||||
}
|
||||
|
||||
private fun findSecondaryImage(): Boolean {
|
||||
val slot = "echo \$SLOT".fsh()
|
||||
val target = if (slot == "_a") "_b" else "_a"
|
||||
console.add("- Target slot: $target")
|
||||
srcBoot = arrayOf(
|
||||
"SLOT=$target",
|
||||
"find_boot_image",
|
||||
"SLOT=$slot",
|
||||
"echo \"\$BOOTIMAGE\"").fsh()
|
||||
if (srcBoot.isEmpty()) {
|
||||
console.add("! Unable to detect target image")
|
||||
return false
|
||||
}
|
||||
console.add("- Target image: $srcBoot")
|
||||
return true
|
||||
}
|
||||
|
||||
private fun extractZip(): Boolean {
|
||||
val arch: String
|
||||
arch = if (Build.VERSION.SDK_INT >= 21) {
|
||||
val abis = listOf(*Build.SUPPORTED_ABIS)
|
||||
if (abis.contains("x86")) "x86" else "arm"
|
||||
} else {
|
||||
if (TextUtils.equals(Build.CPU_ABI, "x86")) "x86" else "arm"
|
||||
}
|
||||
|
||||
console.add("- Device platform: " + Build.CPU_ABI)
|
||||
|
||||
try {
|
||||
ZipInputStream(context.readUri(zipUri).buffered()).use { zi ->
|
||||
lateinit var ze: ZipEntry
|
||||
while (zi.nextEntry?.let { ze = it } != null) {
|
||||
if (ze.isDirectory)
|
||||
continue
|
||||
var name: String? = null
|
||||
val names = arrayOf("$arch/", "common/", "META-INF/com/google/android/update-binary")
|
||||
for (n in names) {
|
||||
ze.name.run {
|
||||
if (startsWith(n)) {
|
||||
name = substring(lastIndexOf('/') + 1)
|
||||
}
|
||||
}
|
||||
name ?: continue
|
||||
break
|
||||
}
|
||||
if (name == null && ze.name.startsWith("chromeos/"))
|
||||
name = ze.name
|
||||
if (name == null)
|
||||
continue
|
||||
val dest = if (installDir is SuFile)
|
||||
SuFile(installDir, name)
|
||||
else
|
||||
File(installDir, name)
|
||||
dest.parentFile!!.mkdirs()
|
||||
SuFileOutputStream(dest).use { zi.copyTo(it) }
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
console.add("! Cannot unzip zip")
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
val init64 = SuFile.open(installDir, "magiskinit64")
|
||||
if (Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_64_BIT_ABIS.isNotEmpty()) {
|
||||
init64.renameTo(SuFile.open(installDir, "magiskinit"))
|
||||
} else {
|
||||
init64.delete()
|
||||
}
|
||||
"cd $installDir; chmod 755 *".sh()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun newEntry(name: String, size: Long): TarEntry {
|
||||
console.add("-- Writing: $name")
|
||||
return TarEntry(TarHeader.createHeader(name, size, 0, false, 420 /* 0644 */))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun handleTar(input: InputStream) {
|
||||
console.add("- Processing tar file")
|
||||
var vbmeta = false
|
||||
val tarOut = TarOutputStream(destFile)
|
||||
this.tarOut = tarOut
|
||||
TarInputStream(input).use { tarIn ->
|
||||
lateinit var entry: TarEntry
|
||||
while (tarIn.nextEntry?.let { entry = it } != null) {
|
||||
if (entry.name.contains("boot.img") || entry.name.contains("recovery.img")) {
|
||||
val name = entry.name
|
||||
console.add("-- Extracting: $name")
|
||||
val extract = File(installDir, name)
|
||||
FileOutputStream(extract).use { tarIn.copyTo(it) }
|
||||
if (name.contains(".lz4")) {
|
||||
console.add("-- Decompressing: $name")
|
||||
"./magiskboot decompress $extract".sh()
|
||||
}
|
||||
} else if (entry.name.contains("vbmeta.img")) {
|
||||
vbmeta = true
|
||||
val buf = ByteBuffer.allocate(256)
|
||||
buf.put("AVB0".toByteArray()) // magic
|
||||
buf.putInt(1) // required_libavb_version_major
|
||||
buf.putInt(120, 2) // flags
|
||||
buf.position(128) // release_string
|
||||
buf.put("avbtool 1.1.0".toByteArray())
|
||||
tarOut.putNextEntry(newEntry("vbmeta.img", 256))
|
||||
tarOut.write(buf.array())
|
||||
} else {
|
||||
console.add("-- Writing: " + entry.name)
|
||||
tarOut.putNextEntry(entry)
|
||||
tarIn.copyTo(tarOut)
|
||||
}
|
||||
}
|
||||
val boot = SuFile.open(installDir, "boot.img")
|
||||
val recovery = SuFile.open(installDir, "recovery.img")
|
||||
if (vbmeta && recovery.exists() && boot.exists()) {
|
||||
// Install Magisk to recovery
|
||||
srcBoot = recovery.path
|
||||
// Repack boot image to prevent restore
|
||||
arrayOf(
|
||||
"./magiskboot unpack boot.img",
|
||||
"./magiskboot repack boot.img",
|
||||
"./magiskboot cleanup",
|
||||
"mv new-boot.img boot.img").sh()
|
||||
SuFileInputStream(boot).use {
|
||||
tarOut.putNextEntry(newEntry("boot.img", boot.length()))
|
||||
it.copyTo(tarOut)
|
||||
}
|
||||
boot.delete()
|
||||
} else {
|
||||
if (!boot.exists()) {
|
||||
console.add("! No boot image found")
|
||||
throw IOException()
|
||||
}
|
||||
srcBoot = boot.path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFile(uri: Uri): Boolean {
|
||||
try {
|
||||
context.readUri(uri).buffered().use {
|
||||
it.mark(500)
|
||||
val magic = ByteArray(5)
|
||||
if (it.skip(257) != 257L || it.read(magic) != magic.size) {
|
||||
console.add("! Invalid file")
|
||||
return false
|
||||
}
|
||||
it.reset()
|
||||
if (magic.contentEquals("ustar".toByteArray())) {
|
||||
destFile = File(Config.downloadDirectory, "magisk_patched.tar")
|
||||
handleTar(it)
|
||||
} else {
|
||||
// Raw image
|
||||
srcBoot = File(installDir, "boot.img").path
|
||||
destFile = File(Config.downloadDirectory, "magisk_patched.img")
|
||||
console.add("- Copying image to cache")
|
||||
FileOutputStream(srcBoot).use { out -> it.copyTo(out) }
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
console.add("! Process error")
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun patchBoot(): Boolean {
|
||||
var isSigned = false
|
||||
try {
|
||||
SuFileInputStream(srcBoot).use {
|
||||
isSigned = SignBoot.verifySignature(it, null)
|
||||
if (isSigned) {
|
||||
console.add("- Boot image is signed with AVB 1.0")
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
console.add("! Unable to check signature")
|
||||
return false
|
||||
}
|
||||
|
||||
if (!("KEEPFORCEENCRYPT=${Info.keepEnc} KEEPVERITY=${Info.keepVerity} " +
|
||||
"RECOVERYMODE=${Info.recovery} sh update-binary " +
|
||||
"sh boot_patch.sh $srcBoot").sh().isSuccess) {
|
||||
return false
|
||||
}
|
||||
|
||||
val job = Shell.sh(
|
||||
"./magiskboot cleanup",
|
||||
"mv bin/busybox busybox",
|
||||
"rm -rf magisk.apk bin boot.img update-binary",
|
||||
"cd /")
|
||||
|
||||
val patched = File(installDir, "new-boot.img")
|
||||
if (isSigned) {
|
||||
console.add("- Signing boot image with verity keys")
|
||||
val signed = File(installDir, "signed.img")
|
||||
try {
|
||||
withStreams(SuFileInputStream(patched), signed.outputStream().buffered()) {
|
||||
input, out -> SignBoot.doSignature("/boot", input, out, null, null)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
console.add("! Unable to sign image")
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
job.add("mv -f $signed $patched")
|
||||
}
|
||||
job.exec()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun flashBoot(): Boolean {
|
||||
if (!"direct_install $installDir $srcBoot".sh().isSuccess)
|
||||
return false
|
||||
arrayOf(
|
||||
"(KEEPVERITY=${Info.keepVerity} patch_dtb_partitions)",
|
||||
"run_migrations"
|
||||
).sh()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun storeBoot(): Boolean {
|
||||
val patched = SuFile.open(installDir, "new-boot.img")
|
||||
try {
|
||||
val os = tarOut?.let {
|
||||
it.putNextEntry(newEntry(
|
||||
if (srcBoot.contains("recovery")) "recovery.img" else "boot.img",
|
||||
patched.length()))
|
||||
tarOut = null
|
||||
it
|
||||
} ?: destFile.outputStream()
|
||||
patched.suInputStream().use { it.copyTo(os); os.close() }
|
||||
} catch (e: IOException) {
|
||||
console.add("! Failed to output to $destFile")
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
patched.delete()
|
||||
console.add("")
|
||||
console.add("****************************")
|
||||
console.add(" Output file is placed in ")
|
||||
console.add(" $destFile ")
|
||||
console.add("****************************")
|
||||
return true
|
||||
}
|
||||
|
||||
private fun postOTA(): Boolean {
|
||||
val bootctl = SuFile("/data/adb/bootctl")
|
||||
try {
|
||||
withStreams(service.fetchBootctl().blockingGet().byteStream(), bootctl.suOutputStream()) {
|
||||
input, out -> input.copyTo(out)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
console.add("! Unable to download bootctl")
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
"post_ota ${bootctl.parent}".sh()
|
||||
|
||||
console.add("***************************************")
|
||||
console.add(" Next reboot will boot to second slot!")
|
||||
console.add("***************************************")
|
||||
return true
|
||||
}
|
||||
|
||||
private fun String.sh() = Shell.sh(this).to(console, logs).exec()
|
||||
private fun Array<String>.sh() = Shell.sh(*this).to(console, logs).exec()
|
||||
private fun String.fsh() = ShellUtils.fastCmd(this)
|
||||
private fun Array<String>.fsh() = ShellUtils.fastCmd(*this)
|
||||
|
||||
protected fun doPatchFile(patchFile: Uri) =
|
||||
extractZip() && handleFile(patchFile) && patchBoot() && storeBoot()
|
||||
|
||||
protected fun direct() = findImage() && extractZip() && patchBoot() && flashBoot()
|
||||
|
||||
protected fun secondSlot() =
|
||||
findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA()
|
||||
|
||||
protected fun fixEnv(zip: File): Boolean {
|
||||
installDir = SuFile("/data/adb/magisk")
|
||||
Shell.su("rm -rf /data/adb/magisk/*").exec()
|
||||
zipUri = zip.toUri()
|
||||
return extractZip() && Shell.su("fix_env").exec().isSuccess
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
protected abstract fun operations(): Boolean
|
||||
|
||||
fun exec() {
|
||||
Single.fromCallable { operations() }.subscribeK { onResult(it) }
|
||||
}
|
||||
|
||||
}
|
@@ -1,77 +1,513 @@
|
||||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.os.postDelayed
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
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.magisk.core.*
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||
import com.topjohnwu.magisk.di.Protected
|
||||
import com.topjohnwu.magisk.ktx.reboot
|
||||
import com.topjohnwu.magisk.ktx.symlink
|
||||
import com.topjohnwu.magisk.ktx.withStreams
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.signing.SignBoot
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import com.topjohnwu.superuser.internal.NOPList
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import java.io.File
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.jpountz.lz4.LZ4FrameInputStream
|
||||
import org.kamranzafar.jtar.TarEntry
|
||||
import org.kamranzafar.jtar.TarHeader
|
||||
import org.kamranzafar.jtar.TarInputStream
|
||||
import org.kamranzafar.jtar.TarOutputStream
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.inject
|
||||
import timber.log.Timber
|
||||
import java.io.*
|
||||
import java.nio.ByteBuffer
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
sealed class MagiskInstaller(
|
||||
file: Uri,
|
||||
private val console: MutableList<String>,
|
||||
logs: MutableList<String>,
|
||||
private val resultListener: FlashResultListener
|
||||
) : MagiskInstallImpl(file, console, logs) {
|
||||
abstract class MagiskInstallImpl protected constructor(
|
||||
protected val console: MutableList<String> = NOPList.getInstance(),
|
||||
private val logs: MutableList<String> = NOPList.getInstance()
|
||||
) : KoinComponent {
|
||||
|
||||
override fun onResult(success: Boolean) {
|
||||
protected var installDir = File("xxx")
|
||||
private lateinit var srcBoot: File
|
||||
|
||||
private val shell = Shell.getShell()
|
||||
private val service: NetworkService by inject()
|
||||
protected val context: Context by inject(Protected)
|
||||
private val useRootDir = shell.isRoot && Info.noDataExec
|
||||
|
||||
private fun findImage(): Boolean {
|
||||
val bootPath = "find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
|
||||
if (bootPath.isEmpty()) {
|
||||
console.add("! Unable to detect target image")
|
||||
return false
|
||||
}
|
||||
srcBoot = SuFile(bootPath)
|
||||
console.add("- Target image: $bootPath")
|
||||
return true
|
||||
}
|
||||
|
||||
private fun findSecondary(): Boolean {
|
||||
val slot = "echo \$SLOT".fsh()
|
||||
val target = if (slot == "_a") "_b" else "_a"
|
||||
console.add("- Target slot: $target")
|
||||
val bootPath = arrayOf(
|
||||
"SLOT=$target",
|
||||
"find_boot_image",
|
||||
"SLOT=$slot",
|
||||
"echo \"\$BOOTIMAGE\"").fsh()
|
||||
if (bootPath.isEmpty()) {
|
||||
console.add("! Unable to detect target image")
|
||||
return false
|
||||
}
|
||||
srcBoot = SuFile(bootPath)
|
||||
console.add("- Target image: $bootPath")
|
||||
return true
|
||||
}
|
||||
|
||||
private fun extractFiles(): Boolean {
|
||||
console.add("- Device platform: ${Const.CPU_ABI}")
|
||||
console.add("- Installing: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
||||
|
||||
installDir = File(context.filesDir.parent, "install")
|
||||
installDir.deleteRecursively()
|
||||
installDir.mkdirs()
|
||||
|
||||
try {
|
||||
// Extract binaries
|
||||
if (isRunningAsStub) {
|
||||
val zf = ZipFile(DynAPK.current(context))
|
||||
zf.entries().asSequence().filter {
|
||||
!it.isDirectory && it.name.startsWith("lib/${Const.CPU_ABI_32}/")
|
||||
}.forEach {
|
||||
val n = it.name.substring(it.name.lastIndexOf('/') + 1)
|
||||
val name = n.substring(3, n.length - 3)
|
||||
val dest = File(installDir, name)
|
||||
zf.getInputStream(it).writeTo(dest)
|
||||
}
|
||||
} else {
|
||||
val libs = Const.NATIVE_LIB_DIR.listFiles { _, name ->
|
||||
name.startsWith("lib") && name.endsWith(".so")
|
||||
} ?: emptyArray()
|
||||
for (lib in libs) {
|
||||
val name = lib.name.substring(3, lib.name.length - 3)
|
||||
symlink(lib.path, "$installDir/$name")
|
||||
}
|
||||
}
|
||||
|
||||
// Extract scripts
|
||||
for (script in listOf("util_functions.sh", "boot_patch.sh", "addon.d.sh")) {
|
||||
val dest = File(installDir, script)
|
||||
context.assets.open(script).writeTo(dest)
|
||||
}
|
||||
// Extract chromeos tools
|
||||
File(installDir, "chromeos").mkdir()
|
||||
for (file in listOf("futility", "kernel_data_key.vbprivk", "kernel.keyblock")) {
|
||||
val name = "chromeos/$file"
|
||||
val dest = File(installDir, name)
|
||||
context.assets.open(name).writeTo(dest)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
console.add("! Unable to extract files")
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
if (useRootDir) {
|
||||
// Move everything to tmpfs to workaround Samsung bullshit
|
||||
SuFile(Const.TMPDIR).also {
|
||||
arrayOf(
|
||||
"rm -rf $it",
|
||||
"mkdir -p $it",
|
||||
"cp_readlink $installDir $it",
|
||||
"rm -rf $installDir"
|
||||
).sh()
|
||||
installDir = it
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Optimization for SuFile I/O streams to skip an internal trial and error
|
||||
private fun installDirFile(name: String): File {
|
||||
return if (useRootDir)
|
||||
SuFile(installDir, name)
|
||||
else
|
||||
File(installDir, name)
|
||||
}
|
||||
|
||||
private fun InputStream.cleanPump(out: OutputStream) = withStreams(this, out) { src, _ ->
|
||||
src.copyTo(out)
|
||||
}
|
||||
|
||||
private fun newTarEntry(name: String, size: Long): TarEntry {
|
||||
console.add("-- Writing: $name")
|
||||
return TarEntry(TarHeader.createHeader(name, size, 0, false, 420 /* 0644 */))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun processTar(input: InputStream, output: OutputStream): OutputStream {
|
||||
console.add("- Processing tar file")
|
||||
val tarOut = TarOutputStream(output)
|
||||
TarInputStream(input).use { tarIn ->
|
||||
lateinit var entry: TarEntry
|
||||
|
||||
fun decompressedStream(): InputStream {
|
||||
val src = if (entry.name.endsWith(".lz4")) LZ4FrameInputStream(tarIn) else tarIn
|
||||
return object : FilterInputStream(src) {
|
||||
override fun available() = 0 /* Workaround bug in LZ4FrameInputStream */
|
||||
override fun close() { /* Never close src stream */ }
|
||||
}
|
||||
}
|
||||
|
||||
while (tarIn.nextEntry?.let { entry = it } != null) {
|
||||
if (entry.name.startsWith("boot.img") ||
|
||||
(Config.recovery && entry.name.contains("recovery.img"))) {
|
||||
val name = entry.name.replace(".lz4", "")
|
||||
console.add("-- Extracting: $name")
|
||||
|
||||
val extract = installDirFile(name)
|
||||
decompressedStream().cleanPump(SuFileOutputStream.open(extract))
|
||||
} else if (entry.name.contains("vbmeta.img")) {
|
||||
val rawData = decompressedStream().readBytes()
|
||||
// Valid vbmeta.img should be at least 256 bytes
|
||||
if (rawData.size < 256)
|
||||
continue
|
||||
|
||||
// Patch flags to AVB_VBMETA_IMAGE_FLAGS_HASHTREE_DISABLED |
|
||||
// AVB_VBMETA_IMAGE_FLAGS_VERIFICATION_DISABLED
|
||||
console.add("-- Patching: vbmeta.img")
|
||||
ByteBuffer.wrap(rawData).putInt(120, 3)
|
||||
tarOut.putNextEntry(newTarEntry("vbmeta.img", rawData.size.toLong()))
|
||||
tarOut.write(rawData)
|
||||
} else {
|
||||
console.add("-- Copying: ${entry.name}")
|
||||
tarOut.putNextEntry(entry)
|
||||
tarIn.copyTo(tarOut, bufferSize = 1024 * 1024)
|
||||
}
|
||||
}
|
||||
}
|
||||
val boot = installDirFile("boot.img")
|
||||
val recovery = installDirFile("recovery.img")
|
||||
if (Config.recovery && recovery.exists() && boot.exists()) {
|
||||
// Install to recovery
|
||||
srcBoot = recovery
|
||||
// Repack boot image to prevent auto restore
|
||||
arrayOf(
|
||||
"cd $installDir",
|
||||
"./magiskboot unpack boot.img",
|
||||
"./magiskboot repack boot.img",
|
||||
"cat new-boot.img > boot.img",
|
||||
"./magiskboot cleanup",
|
||||
"rm -f new-boot.img",
|
||||
"cd /").sh()
|
||||
SuFileInputStream.open(boot).use {
|
||||
tarOut.putNextEntry(newTarEntry("boot.img", boot.length()))
|
||||
it.copyTo(tarOut)
|
||||
}
|
||||
boot.delete()
|
||||
} else {
|
||||
if (!boot.exists()) {
|
||||
console.add("! No boot image found")
|
||||
throw IOException()
|
||||
}
|
||||
srcBoot = boot
|
||||
}
|
||||
return tarOut
|
||||
}
|
||||
|
||||
private fun handleFile(uri: Uri): Boolean {
|
||||
val outStream: OutputStream
|
||||
var outFile: MediaStoreUtils.UriFile? = null
|
||||
|
||||
// Process input file
|
||||
try {
|
||||
uri.inputStream().buffered().use { src ->
|
||||
src.mark(500)
|
||||
val magic = ByteArray(5)
|
||||
if (src.skip(257) != 257L || src.read(magic) != magic.size) {
|
||||
console.add("! Invalid input file")
|
||||
return false
|
||||
}
|
||||
src.reset()
|
||||
|
||||
val alpha = "abcdefghijklmnopqrstuvwxyz"
|
||||
val alphaNum = "$alpha${alpha.toUpperCase(Locale.ROOT)}0123456789"
|
||||
val random = SecureRandom()
|
||||
val filename = StringBuilder("magisk_patched-${BuildConfig.VERSION_CODE}_").run {
|
||||
for (i in 1..5) {
|
||||
append(alphaNum[random.nextInt(alphaNum.length)])
|
||||
}
|
||||
toString()
|
||||
}
|
||||
|
||||
outStream = if (magic.contentEquals("ustar".toByteArray())) {
|
||||
// tar file
|
||||
outFile = MediaStoreUtils.getFile("$filename.tar", true)
|
||||
processTar(src, outFile!!.uri.outputStream())
|
||||
} else {
|
||||
// raw image
|
||||
srcBoot = installDirFile("boot.img")
|
||||
console.add("- Copying image to cache")
|
||||
src.cleanPump(SuFileOutputStream.open(srcBoot))
|
||||
outFile = MediaStoreUtils.getFile("$filename.img", true)
|
||||
outFile!!.uri.outputStream()
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
console.add("! Process error")
|
||||
outFile?.delete()
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
// Patch file
|
||||
if (!patchBoot()) {
|
||||
outFile!!.delete()
|
||||
return false
|
||||
}
|
||||
|
||||
// Output file
|
||||
try {
|
||||
val newBoot = installDirFile("new-boot.img")
|
||||
if (outStream is TarOutputStream) {
|
||||
val name = if (srcBoot.path.contains("recovery")) "recovery.img" else "boot.img"
|
||||
outStream.putNextEntry(newTarEntry(name, newBoot.length()))
|
||||
}
|
||||
SuFileInputStream.open(newBoot).cleanPump(outStream)
|
||||
newBoot.delete()
|
||||
|
||||
console.add("")
|
||||
console.add("****************************")
|
||||
console.add(" Output file is written to ")
|
||||
console.add(" $outFile ")
|
||||
console.add("****************************")
|
||||
} catch (e: IOException) {
|
||||
console.add("! Failed to output to $outFile")
|
||||
outFile!!.delete()
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
// Fix up binaries
|
||||
srcBoot.delete()
|
||||
if (shell.isRoot) {
|
||||
"fix_env $installDir".sh()
|
||||
} else {
|
||||
"cp_readlink $installDir".sh()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun patchBoot(): Boolean {
|
||||
var isSigned = false
|
||||
if (srcBoot.let { it !is SuFile || !it.isCharacter }) {
|
||||
try {
|
||||
SuFileInputStream.open(srcBoot).use {
|
||||
if (SignBoot.verifySignature(it, null)) {
|
||||
isSigned = true
|
||||
console.add("- Boot image is signed with AVB 1.0")
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
console.add("! Unable to check signature")
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
val newBoot = installDirFile("new-boot.img")
|
||||
if (!useRootDir) {
|
||||
// Create output files before hand
|
||||
newBoot.createNewFile()
|
||||
File(installDir, "stock_boot.img").createNewFile()
|
||||
}
|
||||
|
||||
val cmds = arrayOf(
|
||||
"cd $installDir",
|
||||
"KEEPFORCEENCRYPT=${Config.keepEnc} " +
|
||||
"KEEPVERITY=${Config.keepVerity} " +
|
||||
"RECOVERYMODE=${Config.recovery} " +
|
||||
"sh boot_patch.sh $srcBoot")
|
||||
|
||||
if (!cmds.sh().isSuccess)
|
||||
return false
|
||||
|
||||
val job = shell.newJob().add("./magiskboot cleanup", "cd /")
|
||||
|
||||
if (isSigned) {
|
||||
console.add("- Signing boot image with verity keys")
|
||||
val signed = File.createTempFile("signed", ".img", context.cacheDir)
|
||||
try {
|
||||
val src = SuFileInputStream.open(newBoot).buffered()
|
||||
val out = signed.outputStream().buffered()
|
||||
withStreams(src, out) { _, _ ->
|
||||
SignBoot.doSignature(null, null, src, out, "/boot")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
console.add("! Unable to sign image")
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
job.add("cat $signed > $newBoot", "rm -f $signed")
|
||||
}
|
||||
job.exec()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun flashBoot() = "direct_install $installDir $srcBoot".sh().isSuccess
|
||||
|
||||
private suspend fun postOTA(): Boolean {
|
||||
try {
|
||||
val bootctl = File.createTempFile("bootctl", null, context.cacheDir)
|
||||
service.fetchBootctl().byteStream().writeTo(bootctl)
|
||||
"post_ota $bootctl".sh()
|
||||
} catch (e: IOException) {
|
||||
console.add("! Unable to download bootctl")
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
console.add("***************************************")
|
||||
console.add(" Next reboot will boot to second slot!")
|
||||
console.add("***************************************")
|
||||
return true
|
||||
}
|
||||
|
||||
private fun String.sh() = shell.newJob().add(this).to(console, logs).exec()
|
||||
private fun Array<String>.sh() = shell.newJob().add(*this).to(console, logs).exec()
|
||||
private fun String.fsh() = ShellUtils.fastCmd(shell, this)
|
||||
private fun Array<String>.fsh() = ShellUtils.fastCmd(shell, *this)
|
||||
|
||||
protected fun doPatchFile(patchFile: Uri) = extractFiles() && handleFile(patchFile)
|
||||
|
||||
protected fun direct() = findImage() && extractFiles() && patchBoot() && flashBoot()
|
||||
|
||||
protected suspend fun secondSlot() =
|
||||
findSecondary() && extractFiles() && patchBoot() && flashBoot() && postOTA()
|
||||
|
||||
protected fun fixEnv() = extractFiles() && "fix_env $installDir".sh().isSuccess
|
||||
|
||||
protected fun uninstall() = "run_uninstaller ${AssetHack.apk}".sh().isSuccess
|
||||
|
||||
@WorkerThread
|
||||
protected abstract suspend fun operations(): Boolean
|
||||
|
||||
open suspend fun exec(): Boolean {
|
||||
synchronized(Companion) {
|
||||
if (haveActiveSession)
|
||||
return false
|
||||
haveActiveSession = true
|
||||
}
|
||||
val result = withContext(Dispatchers.IO) { operations() }
|
||||
synchronized(Companion) {
|
||||
haveActiveSession = false
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var haveActiveSession = false
|
||||
}
|
||||
}
|
||||
|
||||
abstract class MagiskInstaller(
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>
|
||||
) : MagiskInstallImpl(console, logs) {
|
||||
|
||||
override suspend fun exec(): Boolean {
|
||||
val success = super.exec()
|
||||
if (success) {
|
||||
console.add("- All done!")
|
||||
} else {
|
||||
Shell.sh("rm -rf $installDir").submit()
|
||||
console.add("! Installation failed")
|
||||
}
|
||||
resultListener.onResult(success)
|
||||
return 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)
|
||||
logs: MutableList<String>
|
||||
) : MagiskInstaller(console, logs) {
|
||||
override suspend 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()
|
||||
logs: MutableList<String>
|
||||
) : MagiskInstaller(console, logs) {
|
||||
override suspend fun operations() = secondSlot()
|
||||
}
|
||||
|
||||
class Direct(
|
||||
file: Uri,
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>,
|
||||
resultListener: FlashResultListener
|
||||
) : MagiskInstaller(file, console, logs, resultListener) {
|
||||
override fun operations() = direct()
|
||||
logs: MutableList<String>
|
||||
) : MagiskInstaller(console, logs) {
|
||||
override suspend fun operations() = direct()
|
||||
}
|
||||
|
||||
}
|
||||
class Emulator(
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>
|
||||
) : MagiskInstaller(console, logs) {
|
||||
override suspend fun operations() = fixEnv()
|
||||
}
|
||||
|
||||
class EnvFixTask(
|
||||
private val zip: File
|
||||
) : MagiskInstallImpl() {
|
||||
override fun operations() = fixEnv(zip)
|
||||
class Uninstall(
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>
|
||||
) : MagiskInstallImpl(console, logs) {
|
||||
override suspend fun operations() = uninstall()
|
||||
|
||||
override fun onResult(success: Boolean) {
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(Intent(EnvFixDialog.DISMISS))
|
||||
override suspend fun exec(): Boolean {
|
||||
val success = super.exec()
|
||||
if (success) {
|
||||
UiThreadHandler.handler.postDelayed(3000) {
|
||||
Shell.su("pm uninstall ${context.packageName}").exec()
|
||||
}
|
||||
}
|
||||
return success
|
||||
}
|
||||
}
|
||||
|
||||
class FixEnv(private val callback: () -> Unit) : MagiskInstallImpl() {
|
||||
override suspend fun operations() = fixEnv()
|
||||
|
||||
override suspend fun exec(): Boolean {
|
||||
val success = super.exec()
|
||||
callback()
|
||||
Utils.toast(
|
||||
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
if (success)
|
||||
UiThreadHandler.handler.postDelayed(5000) { reboot() }
|
||||
return success
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,101 +1,42 @@
|
||||
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.core.model.module.OnlineModule
|
||||
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 com.topjohnwu.magisk.data.repository.NetworkService
|
||||
import com.topjohnwu.magisk.ktx.synchronized
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
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 svc: NetworkService,
|
||||
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
|
||||
suspend fun run(forced: Boolean) = withContext(Dispatchers.IO) {
|
||||
val cachedMap = HashMap<String, Date>().also { map ->
|
||||
repoDB.getModuleStubs().forEach { map[it.id] = Date(it.last_update) }
|
||||
}.synchronized()
|
||||
svc.fetchRepoInfo()?.let { info ->
|
||||
coroutineScope {
|
||||
info.modules.forEach {
|
||||
launch {
|
||||
val lastUpdated = cachedMap.remove(it.id)
|
||||
if (forced || lastUpdated?.before(Date(it.last_update)) != false) {
|
||||
try {
|
||||
val repo = OnlineModule(it).apply { load() }
|
||||
repoDB.addModule(repo)
|
||||
} catch (e: OnlineModule.IllegalRepoException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
repoDB.removeModules(cachedMap.keys)
|
||||
}
|
||||
}
|
||||
|
||||
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)!!
|
||||
}
|
||||
|
132
app/src/main/java/com/topjohnwu/magisk/core/utils/AXML.kt
Normal file
132
app/src/main/java/com/topjohnwu/magisk/core/utils/AXML.kt
Normal file
@@ -0,0 +1,132 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder.LITTLE_ENDIAN
|
||||
import java.nio.charset.Charset
|
||||
import java.util.*
|
||||
|
||||
class AXML(b: ByteArray) {
|
||||
|
||||
var bytes = b
|
||||
private set
|
||||
|
||||
companion object {
|
||||
private const val CHUNK_SIZE_OFF = 4
|
||||
private const val STRING_INDICES_OFF = 7 * 4
|
||||
private val UTF_16LE = Charset.forName("UTF-16LE")
|
||||
}
|
||||
|
||||
/**
|
||||
* String pool header:
|
||||
* 0: 0x1C0001
|
||||
* 1: chunk size
|
||||
* 2: number of strings
|
||||
* 3: number of styles (assert as 0)
|
||||
* 4: flags
|
||||
* 5: offset to string data
|
||||
* 6: offset to style data (assert as 0)
|
||||
*
|
||||
* Followed by an array of uint32_t with size = number of strings
|
||||
* Each entry points to an offset into the string data
|
||||
*/
|
||||
fun findAndPatch(vararg patterns: Pair<String, String>): Boolean {
|
||||
val buffer = ByteBuffer.wrap(bytes).order(LITTLE_ENDIAN)
|
||||
|
||||
fun findStringPool(): Int {
|
||||
var offset = 8
|
||||
while (offset < bytes.size) {
|
||||
if (buffer.getInt(offset) == 0x1C0001)
|
||||
return offset
|
||||
offset += buffer.getInt(offset + CHUNK_SIZE_OFF)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
var patch = false
|
||||
val start = findStringPool()
|
||||
if (start < 0)
|
||||
return false
|
||||
|
||||
// Read header
|
||||
buffer.position(start + 4)
|
||||
val intBuf = buffer.asIntBuffer()
|
||||
val size = intBuf.get()
|
||||
val count = intBuf.get()
|
||||
intBuf.get()
|
||||
intBuf.get()
|
||||
val dataOff = start + intBuf.get()
|
||||
intBuf.get()
|
||||
|
||||
val strings = ArrayList<String>(count)
|
||||
// Read and patch all strings
|
||||
loop@ for (i in 0 until count) {
|
||||
val off = dataOff + intBuf.get()
|
||||
val len = buffer.getShort(off)
|
||||
val str = String(bytes, off + 2, len * 2, UTF_16LE)
|
||||
for ((from, to) in patterns) {
|
||||
if (str.contains(from)) {
|
||||
strings.add(str.replace(from, to))
|
||||
patch = true
|
||||
continue@loop
|
||||
}
|
||||
}
|
||||
strings.add(str)
|
||||
}
|
||||
|
||||
if (!patch)
|
||||
return false
|
||||
|
||||
// Write everything before string data, will patch values later
|
||||
val baos = RawByteStream()
|
||||
baos.write(bytes, 0, dataOff)
|
||||
|
||||
// Write string data
|
||||
val strList = IntArray(count)
|
||||
for (i in 0 until count) {
|
||||
strList[i] = baos.size() - dataOff
|
||||
val str = strings[i]
|
||||
baos.write(str.length.toShortBytes())
|
||||
baos.write(str.toByteArray(UTF_16LE))
|
||||
// Null terminate
|
||||
baos.write(0)
|
||||
baos.write(0)
|
||||
}
|
||||
baos.align()
|
||||
|
||||
val sizeDiff = baos.size() - start - size
|
||||
val newBuffer = ByteBuffer.wrap(baos.buf).order(LITTLE_ENDIAN)
|
||||
|
||||
// Patch XML size
|
||||
newBuffer.putInt(CHUNK_SIZE_OFF, buffer.getInt(CHUNK_SIZE_OFF) + sizeDiff)
|
||||
// Patch string pool size
|
||||
newBuffer.putInt(start + CHUNK_SIZE_OFF, size + sizeDiff)
|
||||
// Patch index table
|
||||
newBuffer.position(start + STRING_INDICES_OFF)
|
||||
val newIntBuf = newBuffer.asIntBuffer()
|
||||
strList.forEach { newIntBuf.put(it) }
|
||||
|
||||
// Write the rest of the chunks
|
||||
val nextOff = start + size
|
||||
baos.write(bytes, nextOff, bytes.size - nextOff)
|
||||
|
||||
bytes = baos.toByteArray()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun Int.toShortBytes(): ByteArray {
|
||||
val b = ByteBuffer.allocate(2).order(LITTLE_ENDIAN)
|
||||
b.putShort(this.toShort())
|
||||
return b.array()
|
||||
}
|
||||
|
||||
private class RawByteStream : ByteArrayOutputStream() {
|
||||
val buf: ByteArray get() = buf
|
||||
|
||||
fun align(alignment: Int = 4) {
|
||||
val newCount = (count + alignment - 1) / alignment * alignment
|
||||
for (i in 0 until (newCount - count))
|
||||
write(0)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.concurrent.*
|
||||
|
||||
class IODispatcherExecutor : AbstractExecutorService() {
|
||||
|
||||
private val job = SupervisorJob().apply { invokeOnCompletion { future.run() } }
|
||||
private val scope = CoroutineScope(job + Dispatchers.IO)
|
||||
private val future = FutureTask(Callable { true })
|
||||
|
||||
override fun execute(command: Runnable) {
|
||||
scope.launch {
|
||||
command.run()
|
||||
}
|
||||
}
|
||||
|
||||
override fun shutdown() = job.cancel()
|
||||
|
||||
override fun shutdownNow(): List<Runnable> {
|
||||
job.cancel()
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun isShutdown() = job.isCancelled
|
||||
|
||||
override fun isTerminated() = job.isCancelled && job.isCompleted
|
||||
|
||||
override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean {
|
||||
return try {
|
||||
future.get(timeout, unit)
|
||||
} catch (e: TimeoutException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
@@ -5,13 +5,14 @@ import android.content.pm.PackageManager
|
||||
import android.util.Base64
|
||||
import android.util.Base64OutputStream
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.utils.PatchAPK.ALPHANUM
|
||||
import com.topjohnwu.signing.CryptoUtils.readCertificate
|
||||
import com.topjohnwu.signing.CryptoUtils.readPrivateKey
|
||||
import com.topjohnwu.signing.KeyData
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.math.BigInteger
|
||||
import java.security.KeyPairGenerator
|
||||
@@ -35,11 +36,12 @@ class Keygen(context: Context) : CertKeyProvider {
|
||||
private const val ALIAS = "magisk"
|
||||
private val PASSWORD get() = "magisk".toCharArray()
|
||||
private const val TESTKEY_CERT = "61ed377e85d386a8dfee6b864bd85b0bfaa5af81"
|
||||
private const val DNAME = "C=US,ST=California,L=Mountain View,O=Google Inc.,OU=Android,CN=Android"
|
||||
private const val BASE64_FLAG = Base64.NO_PADDING or Base64.NO_WRAP
|
||||
}
|
||||
|
||||
private val start = Calendar.getInstance()
|
||||
private val end = Calendar.getInstance().apply { add(Calendar.YEAR, 30) }
|
||||
private val start = Calendar.getInstance().apply { add(Calendar.MONTH, -3) }
|
||||
private val end = start.apply { add(Calendar.YEAR, 30) }
|
||||
|
||||
override val cert get() = provider.cert
|
||||
override val key get() = provider.key
|
||||
@@ -58,10 +60,10 @@ class Keygen(context: Context) : CertKeyProvider {
|
||||
|
||||
class TestProvider : CertKeyProvider {
|
||||
override val cert by lazy {
|
||||
readCertificate(javaClass.getResourceAsStream("/keys/testkey.x509.pem"))
|
||||
readCertificate(ByteArrayInputStream(KeyData.testCert()))
|
||||
}
|
||||
override val key by lazy {
|
||||
readPrivateKey(javaClass.getResourceAsStream("/keys/testkey.pk8"))
|
||||
readPrivateKey(ByteArrayInputStream(KeyData.testKey()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,17 +87,6 @@ class Keygen(context: Context) : CertKeyProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private fun randomString(): String {
|
||||
val rand = kotlin.random.Random.Default
|
||||
val len = rand.nextInt(5, 10)
|
||||
val sb = StringBuilder(len)
|
||||
for (i in 0..len) {
|
||||
val idx = rand.nextInt(ALPHANUM.length)
|
||||
sb.append(ALPHANUM[idx])
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun init(): KeyStore {
|
||||
val raw = Config.keyStoreRaw
|
||||
val ks = KeyStore.getInstance("PKCS12")
|
||||
@@ -117,7 +108,7 @@ class Keygen(context: Context) : CertKeyProvider {
|
||||
|
||||
// Generate new private key and certificate
|
||||
val kp = KeyPairGenerator.getInstance("RSA").apply { initialize(4096) }.genKeyPair()
|
||||
val dname = X500Name("CN=${randomString()}")
|
||||
val dname = X500Name(DNAME)
|
||||
val builder = JcaX509v3CertificateBuilder(dname, BigInteger(160, Random()),
|
||||
start.time, end.time, dname, kp.public)
|
||||
val signer = JcaContentSignerBuilder("SHA1WithRSA").build(kp.private)
|
||||
|
@@ -3,19 +3,16 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.AssetManager
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.util.DisplayMetrics
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.AssetHack
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.ResMgr
|
||||
import com.topjohnwu.magisk.core.addAssetPath
|
||||
import com.topjohnwu.magisk.extensions.langTagToLocale
|
||||
import com.topjohnwu.magisk.extensions.toLangTag
|
||||
import io.reactivex.Single
|
||||
import com.topjohnwu.magisk.ktx.langTagToLocale
|
||||
import com.topjohnwu.magisk.ktx.toLangTag
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
import kotlin.Comparator
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
var currentLocale: Locale = Locale.getDefault()
|
||||
@@ -23,14 +20,14 @@ var currentLocale: Locale = Locale.getDefault()
|
||||
@SuppressLint("ConstantLocale")
|
||||
val defaultLocale: Locale = Locale.getDefault()
|
||||
|
||||
val availableLocales = Single.fromCallable {
|
||||
private var cachedLocales: Pair<Array<String>, Array<String>>? = null
|
||||
|
||||
suspend fun availableLocales() = cachedLocales ?:
|
||||
withContext(Dispatchers.Default) {
|
||||
val compareId = R.string.app_changelog
|
||||
|
||||
// Create a completely new resource to prevent cross talk over app's configs
|
||||
val asset = AssetManager::class.java.newInstance().apply { addAssetPath(ResMgr.apk) }
|
||||
val config = Configuration(ResMgr.resource.configuration)
|
||||
val metrics = DisplayMetrics().apply { setTo(ResMgr.resource.displayMetrics) }
|
||||
val res = Resources(asset, metrics, config)
|
||||
// Create a completely new resource to prevent cross talk over active configs
|
||||
val res = AssetHack.newResource()
|
||||
|
||||
val locales = ArrayList<String>().apply {
|
||||
// Add default locale
|
||||
@@ -41,23 +38,19 @@ val availableLocales = Single.fromCallable {
|
||||
add("pt-BR")
|
||||
|
||||
// Then add all supported locales
|
||||
addAll(res.assets.locales)
|
||||
addAll(Resources.getSystem().assets.locales)
|
||||
}.map {
|
||||
it.langTagToLocale()
|
||||
}.distinctBy {
|
||||
config.setLocale(it)
|
||||
res.updateConfiguration(config, metrics)
|
||||
res.updateLocale(it)
|
||||
res.getString(compareId)
|
||||
}.sortedWith(Comparator { a, b ->
|
||||
}.sortedWith { a, b ->
|
||||
a.getDisplayName(a).compareTo(b.getDisplayName(b), true)
|
||||
})
|
||||
}
|
||||
|
||||
config.setLocale(defaultLocale)
|
||||
res.updateConfiguration(config, metrics)
|
||||
res.updateLocale(defaultLocale)
|
||||
val defName = res.getString(R.string.system_default)
|
||||
|
||||
Pair(locales, defName)
|
||||
}.map { (locales, defName) ->
|
||||
val names = ArrayList<String>(locales.size + 1)
|
||||
val values = ArrayList<String>(locales.size + 1)
|
||||
|
||||
@@ -69,14 +62,19 @@ val availableLocales = Single.fromCallable {
|
||||
values.add(locale.toLangTag())
|
||||
}
|
||||
|
||||
Pair(names.toTypedArray(), values.toTypedArray())
|
||||
}.cache()!!
|
||||
(names.toTypedArray() to values.toTypedArray()).also { cachedLocales = it }
|
||||
}
|
||||
|
||||
fun Resources.updateConfig(config: Configuration = configuration) {
|
||||
config.setLocale(currentLocale)
|
||||
updateConfiguration(config, displayMetrics)
|
||||
}
|
||||
|
||||
fun Resources.updateLocale(locale: Locale) {
|
||||
configuration.setLocale(locale)
|
||||
updateConfiguration(configuration, displayMetrics)
|
||||
}
|
||||
|
||||
fun refreshLocale() {
|
||||
val localeConfig = Config.locale
|
||||
currentLocale = when {
|
||||
@@ -84,5 +82,5 @@ fun refreshLocale() {
|
||||
else -> localeConfig.langTagToLocale()
|
||||
}
|
||||
Locale.setDefault(currentLocale)
|
||||
ResMgr.resource.updateConfig()
|
||||
AssetHack.resource.updateConfig()
|
||||
}
|
||||
|
@@ -0,0 +1,162 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.ktx.get
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.security.MessageDigest
|
||||
import kotlin.experimental.and
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
object MediaStoreUtils {
|
||||
|
||||
private val cr: ContentResolver by lazy { get<Context>().contentResolver }
|
||||
|
||||
@get:RequiresApi(api = 29)
|
||||
private val tableUri
|
||||
get() = MediaStore.Downloads.EXTERNAL_CONTENT_URI
|
||||
|
||||
private fun relativePath(name: String) =
|
||||
if (name.isEmpty()) Environment.DIRECTORY_DOWNLOADS
|
||||
else Environment.DIRECTORY_DOWNLOADS + File.separator + name
|
||||
|
||||
fun fullPath(name: String): String =
|
||||
File(Environment.getExternalStorageDirectory(), relativePath(name)).canonicalPath
|
||||
|
||||
private val relativePath get() = relativePath(Config.downloadDir)
|
||||
|
||||
@RequiresApi(api = 30)
|
||||
@Throws(IOException::class)
|
||||
private fun insertFile(displayName: String): MediaStoreFile {
|
||||
val values = ContentValues()
|
||||
values.put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
|
||||
values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
|
||||
|
||||
// When a file with the same name exists and was not created by us:
|
||||
// - Before Android 11, insert will return null
|
||||
// - On Android 11+, the system will automatically create a new name
|
||||
// Thus the reason to restrict this method call to API 30+
|
||||
val fileUri = cr.insert(tableUri, values) ?: throw IOException("Can't insert $displayName.")
|
||||
|
||||
val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA)
|
||||
cr.query(fileUri, projection, null, null, null)?.use { cursor ->
|
||||
val idIndex = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
|
||||
val dataColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
|
||||
if (cursor.moveToFirst()) {
|
||||
val id = cursor.getLong(idIndex)
|
||||
val data = cursor.getString(dataColumn)
|
||||
return MediaStoreFile(id, data)
|
||||
}
|
||||
}
|
||||
|
||||
throw IOException("Can't insert $displayName.")
|
||||
}
|
||||
|
||||
@RequiresApi(api = 29)
|
||||
private fun queryFile(displayName: String): UriFile? {
|
||||
val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA)
|
||||
// Before Android 10, we wrote the DISPLAY_NAME field when insert, so it can be used.
|
||||
val selection = "${MediaStore.MediaColumns.DISPLAY_NAME} == ?"
|
||||
val selectionArgs = arrayOf(displayName)
|
||||
val sortOrder = "${MediaStore.MediaColumns.DATE_ADDED} DESC"
|
||||
cr.query(tableUri, projection, selection, selectionArgs, sortOrder)?.use { cursor ->
|
||||
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
|
||||
val dataColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(idColumn)
|
||||
val data = cursor.getString(dataColumn)
|
||||
if (data.endsWith(relativePath + File.separator + displayName)) {
|
||||
return MediaStoreFile(id, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getFile(displayName: String, skipQuery: Boolean = false): UriFile {
|
||||
if (Build.VERSION.SDK_INT < 30) {
|
||||
// Fallback to file based I/O pre Android 11
|
||||
val parent = File(Environment.getExternalStorageDirectory(), relativePath)
|
||||
parent.mkdirs()
|
||||
return LegacyUriFile(File(parent, displayName))
|
||||
}
|
||||
|
||||
return if (skipQuery) insertFile(displayName)
|
||||
else queryFile(displayName) ?: insertFile(displayName)
|
||||
}
|
||||
|
||||
fun Uri.inputStream() = cr.openInputStream(this) ?: throw FileNotFoundException()
|
||||
|
||||
fun Uri.outputStream() = cr.openOutputStream(this, "rwt") ?: throw FileNotFoundException()
|
||||
|
||||
val Uri.displayName: String get() {
|
||||
if (scheme == "file") {
|
||||
// Simple uri wrapper over file, directly get file name
|
||||
return toFile().name
|
||||
}
|
||||
require(scheme == "content") { "Uri lacks 'content' scheme: $this" }
|
||||
val projection = arrayOf(OpenableColumns.DISPLAY_NAME)
|
||||
cr.query(this, projection, null, null, null)?.use { cursor ->
|
||||
val displayNameColumn = cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)
|
||||
if (cursor.moveToFirst()) {
|
||||
return cursor.getString(displayNameColumn)
|
||||
}
|
||||
}
|
||||
return this.toString()
|
||||
}
|
||||
|
||||
fun Uri.checkSum(alg: String, reference: String) = runCatching {
|
||||
this.inputStream().use {
|
||||
val digest = MessageDigest.getInstance(alg)
|
||||
it.copyTo(object : OutputStream() {
|
||||
override fun write(b: Int) {
|
||||
digest.update(b.toByte())
|
||||
}
|
||||
|
||||
override fun write(b: ByteArray, off: Int, len: Int) {
|
||||
digest.update(b, off, len)
|
||||
}
|
||||
})
|
||||
val sb = StringBuilder()
|
||||
digest.digest().forEach { b -> sb.append("%02x".format(b and 0xff.toByte())) }
|
||||
sb.toString() == reference
|
||||
}
|
||||
}.getOrElse { false }
|
||||
|
||||
interface UriFile {
|
||||
val uri: Uri
|
||||
fun delete(): Boolean
|
||||
}
|
||||
|
||||
private class LegacyUriFile(private val file: File) : UriFile {
|
||||
override val uri = file.toUri()
|
||||
override fun delete() = file.delete()
|
||||
override fun toString() = file.toString()
|
||||
}
|
||||
|
||||
@RequiresApi(api = 29)
|
||||
private class MediaStoreFile(private val id: Long, private val data: String) : UriFile {
|
||||
override val uri = ContentUris.withAppendedId(tableUri, id)
|
||||
override fun toString() = data
|
||||
override fun delete(): Boolean {
|
||||
val selection = "${MediaStore.MediaColumns._ID} == ?"
|
||||
val selectionArgs = arrayOf(id.toString())
|
||||
return cr.delete(uri, selection, selectionArgs) == 1
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,155 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.widget.Toast
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.view.Notifications
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.extensions.writeTo
|
||||
import com.topjohnwu.signing.JarMap
|
||||
import com.topjohnwu.signing.SignAPK
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Single
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.security.SecureRandom
|
||||
|
||||
object PatchAPK {
|
||||
|
||||
private const val ALPHA = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
private const val DIGITS = "0123456789"
|
||||
const val ALPHANUM = ALPHA + DIGITS
|
||||
private const val ALPHANUMDOTS = "$ALPHANUM............"
|
||||
|
||||
private const val APP_ID = "com.topjohnwu.magisk"
|
||||
private const val APP_NAME = "Magisk Manager"
|
||||
|
||||
private fun genPackageName(prefix: String, length: Int): CharSequence {
|
||||
val builder = StringBuilder(length)
|
||||
builder.append(prefix)
|
||||
val len = length - prefix.length
|
||||
val random = SecureRandom()
|
||||
var next: Char
|
||||
var prev = prefix[prefix.length - 1]
|
||||
for (i in 0 until len) {
|
||||
next = if (prev == '.' || i == len - 1) {
|
||||
ALPHA[random.nextInt(ALPHA.length)]
|
||||
} else {
|
||||
ALPHANUMDOTS[random.nextInt(ALPHANUMDOTS.length)]
|
||||
}
|
||||
builder.append(next)
|
||||
prev = next
|
||||
}
|
||||
return builder
|
||||
}
|
||||
|
||||
private fun findAndPatch(xml: ByteArray, from: CharSequence, to: CharSequence): Boolean {
|
||||
if (to.length > from.length)
|
||||
return false
|
||||
val buf = ByteBuffer.wrap(xml).order(ByteOrder.LITTLE_ENDIAN).asCharBuffer()
|
||||
val offList = mutableListOf<Int>()
|
||||
var i = 0
|
||||
loop@ while (i < buf.length - from.length) {
|
||||
for (j in from.indices) {
|
||||
if (buf.get(i + j) != from[j]) {
|
||||
++i
|
||||
continue@loop
|
||||
}
|
||||
}
|
||||
offList.add(i)
|
||||
i += from.length
|
||||
}
|
||||
if (offList.isEmpty())
|
||||
return false
|
||||
|
||||
val toBuf = to.toString().toCharArray().copyOf(from.length)
|
||||
for (off in offList) {
|
||||
buf.position(off)
|
||||
buf.put(toBuf)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun patch(apk: String, out: String, pkg: CharSequence, label: CharSequence): Boolean {
|
||||
try {
|
||||
val jar = JarMap.open(apk)
|
||||
val je = jar.getJarEntry(Const.ANDROID_MANIFEST)
|
||||
val xml = jar.getRawData(je)
|
||||
|
||||
if (!findAndPatch(xml, APP_ID, pkg) ||
|
||||
!findAndPatch(xml, APP_NAME, label))
|
||||
return false
|
||||
|
||||
// Write apk changes
|
||||
jar.getOutputStream(je).write(xml)
|
||||
val keys = Keygen(get())
|
||||
SignAPK.sign(keys.cert, keys.key, jar, FileOutputStream(out).buffered())
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun patchAndHide(context: Context, label: String): Boolean {
|
||||
val dlStub = !isRunningAsStub && SDK_INT >= 28 &&
|
||||
Info.env.magiskVersionCode >= Const.Version.PROVIDER_CONNECT
|
||||
val src = if (dlStub) {
|
||||
val stub = File(context.cacheDir, "stub.apk")
|
||||
val svc = get<GithubRawServices>()
|
||||
try {
|
||||
svc.fetchFile(Info.remote.stub.link).blockingGet().byteStream().use {
|
||||
it.writeTo(stub)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
stub.path
|
||||
} else {
|
||||
context.packageCodePath
|
||||
}
|
||||
|
||||
// Generate a new random package name and signature
|
||||
val repack = File(context.cacheDir, "patched.apk")
|
||||
val pkg = genPackageName("com.", APP_ID.length)
|
||||
Config.keyStoreRaw = ""
|
||||
|
||||
if (!patch(src, repack.path, pkg, label))
|
||||
return false
|
||||
|
||||
// Install the application
|
||||
repack.setReadable(true, false)
|
||||
if (!Shell.su("force_pm_install $repack").exec().isSuccess)
|
||||
return false
|
||||
|
||||
Config.suManager = pkg.toString()
|
||||
Config.export()
|
||||
Shell.su("pm uninstall $APP_ID").submit()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun hideManager(context: Context, label: String) {
|
||||
val progress = Notifications.progress(context, context.getString(R.string.hide_manager_title))
|
||||
Notifications.mgr.notify(Const.ID.HIDE_MANAGER_NOTIFICATION_ID, progress.build())
|
||||
Single.fromCallable {
|
||||
patchAndHide(context, label)
|
||||
}.subscribeK {
|
||||
if (!it)
|
||||
Utils.toast(R.string.hide_manager_fail_toast, Toast.LENGTH_LONG)
|
||||
Notifications.mgr.cancel(Const.ID.HIDE_MANAGER_NOTIFICATION_ID)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,6 +1,5 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import java.io.FilterInputStream
|
||||
import java.io.InputStream
|
||||
|
||||
@@ -16,7 +15,7 @@ class ProgressInputStream(
|
||||
val cur = System.currentTimeMillis()
|
||||
if (cur - lastUpdate > 1000) {
|
||||
lastUpdate = cur
|
||||
UiThreadHandler.run { progressEmitter(bytesRead) }
|
||||
progressEmitter(bytesRead)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,46 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
101
app/src/main/java/com/topjohnwu/magisk/core/utils/ShellInit.kt
Normal file
101
app/src/main/java/com/topjohnwu/magisk/core/utils/ShellInit.kt
Normal file
@@ -0,0 +1,101 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.*
|
||||
import com.topjohnwu.magisk.ktx.cachedFile
|
||||
import com.topjohnwu.magisk.ktx.deviceProtectedContext
|
||||
import com.topjohnwu.magisk.ktx.rawResource
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import java.io.File
|
||||
import java.util.jar.JarFile
|
||||
|
||||
abstract class BaseShellInit : Shell.Initializer() {
|
||||
final override fun onInit(context: Context, shell: Shell): Boolean {
|
||||
return init(context.wrap(), shell)
|
||||
}
|
||||
|
||||
abstract fun init(context: Context, shell: Shell): Boolean
|
||||
}
|
||||
|
||||
|
||||
class BusyBoxInit : BaseShellInit() {
|
||||
|
||||
override fun init(context: Context, shell: Shell): Boolean {
|
||||
shell.newJob().apply {
|
||||
add("export ASH_STANDALONE=1")
|
||||
|
||||
val localBB: File
|
||||
if (isRunningAsStub) {
|
||||
if (!shell.isRoot)
|
||||
return true
|
||||
val jar = JarFile(DynAPK.current(context))
|
||||
val bb = jar.getJarEntry("lib/${Const.CPU_ABI_32}/libbusybox.so")
|
||||
localBB = context.deviceProtectedContext.cachedFile("busybox")
|
||||
localBB.delete()
|
||||
jar.getInputStream(bb).writeTo(localBB)
|
||||
localBB.setExecutable(true)
|
||||
} else {
|
||||
localBB = File(Const.NATIVE_LIB_DIR, "libbusybox.so")
|
||||
}
|
||||
|
||||
if (shell.isRoot) {
|
||||
add("export MAGISKTMP=\$(magisk --path)/.magisk")
|
||||
// Test if we can properly execute stuff in /data
|
||||
Info.noDataExec = !shell.newJob().add("$localBB true").exec().isSuccess
|
||||
}
|
||||
|
||||
if (Info.noDataExec) {
|
||||
// Copy it out of /data to workaround Samsung bullshit
|
||||
add(
|
||||
"if [ -x \$MAGISKTMP/busybox/busybox ]; then",
|
||||
" cp -af $localBB \$MAGISKTMP/busybox/busybox",
|
||||
" exec \$MAGISKTMP/busybox/busybox sh",
|
||||
"else",
|
||||
" cp -af $localBB /dev/.busybox",
|
||||
" exec /dev/.busybox sh",
|
||||
"fi"
|
||||
)
|
||||
} else {
|
||||
// Directly execute the file
|
||||
add("exec $localBB sh")
|
||||
}
|
||||
}.exec()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class AppShellInit : BaseShellInit() {
|
||||
|
||||
override fun init(context: Context, shell: Shell): Boolean {
|
||||
|
||||
fun fastCmd(cmd: String) = ShellUtils.fastCmd(shell, cmd)
|
||||
fun getVar(name: String) = fastCmd("echo \$$name")
|
||||
fun getBool(name: String) = getVar(name).toBoolean()
|
||||
|
||||
shell.newJob().apply {
|
||||
add(context.rawResource(R.raw.manager))
|
||||
if (shell.isRoot) {
|
||||
add(context.assets.open("util_functions.sh"))
|
||||
}
|
||||
add("app_init")
|
||||
}.exec()
|
||||
|
||||
Const.MAGISKTMP = getVar("MAGISKTMP")
|
||||
Info.isSAR = getBool("SYSTEM_ROOT")
|
||||
Info.ramdisk = getBool("RAMDISKEXIST")
|
||||
Info.isAB = getBool("ISAB")
|
||||
Info.crypto = getVar("CRYPTOTYPE")
|
||||
|
||||
// Default presets
|
||||
Config.recovery = getBool("RECOVERYMODE")
|
||||
Config.keepVerity = getBool("KEEPVERITY")
|
||||
Config.keepEnc = getBool("KEEPFORCEENCRYPT")
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
@@ -1,78 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.widget.Toast
|
||||
import androidx.work.*
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.UpdateCheckService
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object Utils {
|
||||
|
||||
fun toast(msg: CharSequence, duration: Int) {
|
||||
UiThreadHandler.run { Toast.makeText(get(), msg, duration).show() }
|
||||
}
|
||||
|
||||
fun toast(resId: Int, duration: Int) {
|
||||
UiThreadHandler.run { Toast.makeText(get(), resId, duration).show() }
|
||||
}
|
||||
|
||||
fun dpInPx(dp: Int): Int {
|
||||
val scale = get<Resources>().displayMetrics.density
|
||||
return (dp * scale + 0.5).toInt()
|
||||
}
|
||||
|
||||
fun showSuperUser(): Boolean {
|
||||
return Info.env.isActive && (Const.USER_ID == 0
|
||||
|| Config.suMultiuserMode != Config.Value.MULTIUSER_MODE_OWNER_MANAGED)
|
||||
}
|
||||
|
||||
fun scheduleUpdateCheck(context: Context) {
|
||||
if (Config.checkUpdate) {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.setRequiresDeviceIdle(true)
|
||||
.build()
|
||||
val request = PeriodicWorkRequest
|
||||
.Builder(UpdateCheckService::class.java, 12, TimeUnit.HOURS)
|
||||
.setConstraints(constraints)
|
||||
.build()
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
Const.ID.CHECK_MAGISK_UPDATE_WORKER_ID,
|
||||
ExistingPeriodicWorkPolicy.REPLACE, request
|
||||
)
|
||||
} else {
|
||||
WorkManager.getInstance(context)
|
||||
.cancelUniqueWork(Const.ID.CHECK_MAGISK_UPDATE_WORKER_ID)
|
||||
}
|
||||
}
|
||||
|
||||
fun openLink(context: Context, link: Uri) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, link)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
if (intent.resolveActivity(context.packageManager) != null) {
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
toast(
|
||||
R.string.open_link_failed_toast,
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun ensureDownloadPath(path: String) =
|
||||
File(Environment.getExternalStorageDirectory(), path).run {
|
||||
if ((exists() && isDirectory) || mkdirs()) this else null
|
||||
}
|
||||
|
||||
}
|
@@ -36,7 +36,7 @@ fun InputStream.unzip(folder: File, path: String, junkPath: Boolean) {
|
||||
dest = SuFile(folder, name)
|
||||
dest.parentFile!!.mkdirs()
|
||||
}
|
||||
SuFileOutputStream(dest).use { out -> zin.copyTo(out) }
|
||||
SuFileOutputStream.open(dest).use { out -> zin.copyTo(out) }
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
|
@@ -0,0 +1,49 @@
|
||||
package com.topjohnwu.magisk.core.utils.net
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import androidx.collection.ArraySet
|
||||
|
||||
@TargetApi(21)
|
||||
open class LollipopNetworkObserver(
|
||||
context: Context,
|
||||
callback: ConnectionCallback
|
||||
): NetworkObserver(context, callback) {
|
||||
|
||||
private val networkCallback = NetCallback()
|
||||
|
||||
init {
|
||||
val request = NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.build()
|
||||
manager.registerNetworkCallback(request, networkCallback)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun getCurrentState() {
|
||||
callback(manager.activeNetworkInfo?.isConnected ?: false)
|
||||
}
|
||||
|
||||
override fun stopObserving() {
|
||||
manager.unregisterNetworkCallback(networkCallback)
|
||||
}
|
||||
|
||||
private inner class NetCallback : ConnectivityManager.NetworkCallback() {
|
||||
|
||||
private val activeList = ArraySet<Network>()
|
||||
|
||||
override fun onAvailable(network: Network) {
|
||||
activeList.add(network)
|
||||
callback(true)
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
activeList.remove(network)
|
||||
callback(!activeList.isEmpty())
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,53 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.topjohnwu.magisk.core.utils.net
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.PowerManager
|
||||
import androidx.core.content.getSystemService
|
||||
|
||||
@TargetApi(23)
|
||||
class MarshmallowNetworkObserver(
|
||||
context: Context,
|
||||
callback: ConnectionCallback
|
||||
): LollipopNetworkObserver(context, callback) {
|
||||
|
||||
private val receiver = IdleBroadcastReceiver()
|
||||
|
||||
init {
|
||||
val filter = IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)
|
||||
app.registerReceiver(receiver, filter)
|
||||
}
|
||||
|
||||
override fun stopObserving() {
|
||||
super.stopObserving()
|
||||
app.unregisterReceiver(receiver)
|
||||
}
|
||||
|
||||
override fun getCurrentState() {
|
||||
callback(manager.getNetworkCapabilities(manager.activeNetwork)
|
||||
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false)
|
||||
}
|
||||
|
||||
private inner class IdleBroadcastReceiver: BroadcastReceiver() {
|
||||
|
||||
private fun Context.isIdleMode(): Boolean {
|
||||
val pwm = getSystemService<PowerManager>() ?: return true
|
||||
val isIgnoringOptimizations = pwm.isIgnoringBatteryOptimizations(packageName)
|
||||
return pwm.isDeviceIdleMode && !isIgnoringOptimizations
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (context.isIdleMode()) {
|
||||
callback(false)
|
||||
} else {
|
||||
getCurrentState()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package com.topjohnwu.magisk.core.utils.net
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.getSystemService
|
||||
|
||||
typealias ConnectionCallback = (Boolean) -> Unit
|
||||
|
||||
abstract class NetworkObserver(
|
||||
context: Context,
|
||||
protected val callback: ConnectionCallback
|
||||
) {
|
||||
|
||||
protected val app: Context = context.applicationContext
|
||||
protected val manager = context.getSystemService<ConnectivityManager>()!!
|
||||
|
||||
protected abstract fun stopObserving()
|
||||
protected abstract fun getCurrentState()
|
||||
|
||||
companion object {
|
||||
fun observe(context: Context, callback: ConnectionCallback): NetworkObserver {
|
||||
return when (Build.VERSION.SDK_INT) {
|
||||
in 23 until Int.MAX_VALUE -> MarshmallowNetworkObserver(context, callback)
|
||||
in 21 until 23 -> LollipopNetworkObserver(context, callback)
|
||||
else -> PreLollipopNetworkObserver(context, callback)
|
||||
}.apply { getCurrentState() }
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.topjohnwu.magisk.core.utils.net
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.ConnectivityManager
|
||||
import androidx.core.net.ConnectivityManagerCompat
|
||||
|
||||
class PreLollipopNetworkObserver(
|
||||
context: Context,
|
||||
callback: ConnectionCallback
|
||||
): NetworkObserver(context, callback) {
|
||||
|
||||
private val receiver = ConnectivityBroadcastReceiver()
|
||||
|
||||
init {
|
||||
val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
|
||||
app.registerReceiver(receiver, filter)
|
||||
}
|
||||
|
||||
override fun stopObserving() {
|
||||
app.unregisterReceiver(receiver)
|
||||
}
|
||||
|
||||
override fun getCurrentState() {
|
||||
callback(manager.activeNetworkInfo?.isConnected ?: false)
|
||||
}
|
||||
|
||||
private inner class ConnectivityBroadcastReceiver: BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent) {
|
||||
val info = ConnectivityManagerCompat.getNetworkInfoFromBroadcast(manager, intent)
|
||||
callback(info?.isConnected ?: false)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,126 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.view
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import androidx.core.app.TaskStackBuilder
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.graphics.drawable.toIcon
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.*
|
||||
import com.topjohnwu.magisk.core.Const.ID.PROGRESS_NOTIFICATION_CHANNEL
|
||||
import com.topjohnwu.magisk.core.Const.ID.UPDATE_NOTIFICATION_CHANNEL
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.getBitmap
|
||||
|
||||
object Notifications {
|
||||
|
||||
val mgr by lazy { get<Context>().getSystemService<NotificationManager>()!! }
|
||||
|
||||
fun setup(context: Context) {
|
||||
if (SDK_INT >= 26) {
|
||||
var channel = NotificationChannel(UPDATE_NOTIFICATION_CHANNEL,
|
||||
context.getString(R.string.update_channel), NotificationManager.IMPORTANCE_DEFAULT)
|
||||
mgr.createNotificationChannel(channel)
|
||||
channel = NotificationChannel(PROGRESS_NOTIFICATION_CHANNEL,
|
||||
context.getString(R.string.progress_channel), NotificationManager.IMPORTANCE_LOW)
|
||||
mgr.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateBuilder(context: Context): Notification.Builder {
|
||||
return Notification.Builder(context).apply {
|
||||
val bitmap = context.getBitmap(R.drawable.ic_magisk_outline)
|
||||
setLargeIcon(bitmap)
|
||||
if (SDK_INT >= 26) {
|
||||
setSmallIcon(bitmap.toIcon())
|
||||
setChannelId(UPDATE_NOTIFICATION_CHANNEL)
|
||||
} else {
|
||||
setSmallIcon(R.drawable.ic_magisk_outline)
|
||||
setVibrate(longArrayOf(0, 100, 100, 100))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun magiskUpdate(context: Context) {
|
||||
val intent = context.intent<SplashActivity>()
|
||||
.putExtra(Const.Key.OPEN_SECTION, "magisk")
|
||||
val stackBuilder = TaskStackBuilder.create(context)
|
||||
stackBuilder.addParentStack(SplashActivity::class.java.cmp(context.packageName))
|
||||
stackBuilder.addNextIntent(intent)
|
||||
val pendingIntent = stackBuilder.getPendingIntent(
|
||||
Const.ID.MAGISK_UPDATE_NOTIFICATION_ID,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
val builder = updateBuilder(
|
||||
context
|
||||
)
|
||||
.setContentTitle(context.getString(R.string.magisk_update_title))
|
||||
.setContentText(context.getString(R.string.manager_download_install))
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
|
||||
mgr.notify(Const.ID.MAGISK_UPDATE_NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
fun managerUpdate(context: Context) {
|
||||
val intent = context.intent<GeneralReceiver>()
|
||||
.setAction(Const.Key.BROADCAST_MANAGER_UPDATE)
|
||||
.putExtra(Const.Key.INTENT_SET_APP, Info.remote.app)
|
||||
|
||||
val pendingIntent = PendingIntent.getBroadcast(context,
|
||||
Const.ID.APK_UPDATE_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
val builder = updateBuilder(
|
||||
context
|
||||
)
|
||||
.setContentTitle(context.getString(R.string.manager_update_title))
|
||||
.setContentText(context.getString(R.string.manager_download_install))
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
|
||||
mgr.notify(Const.ID.APK_UPDATE_NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
fun dtboPatched(context: Context) {
|
||||
val intent = context.intent<GeneralReceiver>()
|
||||
.setAction(Const.Key.BROADCAST_REBOOT)
|
||||
val pendingIntent = PendingIntent.getBroadcast(context,
|
||||
Const.ID.DTBO_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
val builder = updateBuilder(
|
||||
context
|
||||
)
|
||||
.setContentTitle(context.getString(R.string.dtbo_patched_title))
|
||||
.setContentText(context.getString(R.string.dtbo_patched_reboot))
|
||||
|
||||
if (SDK_INT >= 23) {
|
||||
val action = Notification.Action.Builder(
|
||||
context.getBitmap(R.drawable.ic_refresh).toIcon(),
|
||||
context.getString(R.string.reboot), pendingIntent).build()
|
||||
builder.addAction(action)
|
||||
} else {
|
||||
builder.addAction(
|
||||
R.drawable.ic_refresh,
|
||||
context.getString(R.string.reboot), pendingIntent)
|
||||
}
|
||||
|
||||
mgr.notify(Const.ID.DTBO_NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
fun progress(context: Context, title: CharSequence): Notification.Builder {
|
||||
val builder = if (SDK_INT >= 26) {
|
||||
Notification.Builder(context, PROGRESS_NOTIFICATION_CHANNEL)
|
||||
} else {
|
||||
Notification.Builder(context).setPriority(Notification.PRIORITY_LOW)
|
||||
}
|
||||
builder.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setContentTitle(title)
|
||||
.setProgress(0, 0, true)
|
||||
.setOngoing(true)
|
||||
return builder
|
||||
}
|
||||
}
|
@@ -1,87 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.view
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.graphics.drawable.toAdaptiveIcon
|
||||
import androidx.core.graphics.drawable.toIcon
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.*
|
||||
import com.topjohnwu.magisk.core.utils.Utils
|
||||
import com.topjohnwu.magisk.extensions.getBitmap
|
||||
|
||||
object Shortcuts {
|
||||
|
||||
fun setup(context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= 25) {
|
||||
val manager = context.getSystemService<ShortcutManager>()
|
||||
manager?.dynamicShortcuts =
|
||||
getShortCuts(context)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = 25)
|
||||
private fun getShortCuts(context: Context): List<ShortcutInfo> {
|
||||
val shortCuts = mutableListOf<ShortcutInfo>()
|
||||
val intent = context.intent<SplashActivity>()
|
||||
|
||||
fun getIcon(id: Int): Icon {
|
||||
return if (Build.VERSION.SDK_INT >= 26)
|
||||
context.getBitmap(id).toAdaptiveIcon()
|
||||
else
|
||||
context.getBitmap(id).toIcon()
|
||||
}
|
||||
|
||||
if (Utils.showSuperUser()) {
|
||||
shortCuts.add(
|
||||
ShortcutInfo.Builder(context, "superuser")
|
||||
.setShortLabel(context.getString(R.string.superuser))
|
||||
.setIntent(
|
||||
Intent(intent)
|
||||
.putExtra(Const.Key.OPEN_SECTION, "superuser")
|
||||
.setAction(Intent.ACTION_VIEW)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
)
|
||||
.setIcon(getIcon(R.drawable.sc_superuser))
|
||||
.setRank(0)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
if (Info.env.magiskHide) {
|
||||
shortCuts.add(
|
||||
ShortcutInfo.Builder(context, "magiskhide")
|
||||
.setShortLabel(context.getString(R.string.magiskhide))
|
||||
.setIntent(
|
||||
Intent(intent)
|
||||
.putExtra(Const.Key.OPEN_SECTION, "magiskhide")
|
||||
.setAction(Intent.ACTION_VIEW)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
)
|
||||
.setIcon(getIcon(R.drawable.sc_magiskhide))
|
||||
.setRank(1)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
if (!Config.coreOnly && Info.env.isActive) {
|
||||
shortCuts.add(
|
||||
ShortcutInfo.Builder(context, "modules")
|
||||
.setShortLabel(context.getString(R.string.modules))
|
||||
.setIntent(
|
||||
Intent(intent)
|
||||
.putExtra(Const.Key.OPEN_SECTION, "modules")
|
||||
.setAction(Intent.ACTION_VIEW)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
)
|
||||
.setIcon(getIcon(R.drawable.sc_extension))
|
||||
.setRank(2)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
return shortCuts
|
||||
}
|
||||
}
|
@@ -1,67 +0,0 @@
|
||||
@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>
|
||||
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user