mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-08-14 10:27:29 +00:00
Compare commits
1762 Commits
manager-v7
...
v21.2
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 | ||
![]() |
ba7cb47383 | ||
![]() |
48d417f9af | ||
![]() |
df4db6bf6b | ||
![]() |
b8ef491bc7 | ||
![]() |
ea1ebb8d00 | ||
![]() |
91b6d2852a | ||
![]() |
d7cd1b37f8 | ||
![]() |
160ff7bb07 | ||
![]() |
31142180cb | ||
![]() |
38b0fa04a8 | ||
![]() |
29817245ba | ||
![]() |
925fe6f152 | ||
![]() |
93fd574b75 | ||
![]() |
0de88bcbb9 | ||
![]() |
0b70bd2b60 | ||
![]() |
84ecba4629 | ||
![]() |
f7142e69b6 | ||
![]() |
ed7e560849 | ||
![]() |
47e50e8511 | ||
![]() |
72f6770d61 | ||
![]() |
7da35e5468 | ||
![]() |
7768274b2f | ||
![]() |
33f006655d | ||
![]() |
612b51d48f | ||
![]() |
8101f3f67d | ||
![]() |
e3c8d723e3 | ||
![]() |
4579825758 | ||
![]() |
ef91c33f55 | ||
![]() |
511d5993df | ||
![]() |
9f4958e869 | ||
![]() |
c07775f5e3 | ||
![]() |
e261579e72 | ||
![]() |
cf54cad3ce | ||
![]() |
a0998009c1 | ||
![]() |
d6fdbfe9b7 | ||
![]() |
07228279a3 | ||
![]() |
6877ef790f | ||
![]() |
a3809648dd | ||
![]() |
df15606b00 | ||
![]() |
4dc0d13688 | ||
![]() |
541fa5cb1f | ||
![]() |
ab9442d4ae | ||
![]() |
f5c099e9a7 | ||
![]() |
9582379e1b | ||
![]() |
db9a4b31f9 | ||
![]() |
409cb06ea0 | ||
![]() |
88d917b662 | ||
![]() |
faf077b494 | ||
![]() |
ee1f45aa91 | ||
![]() |
915fd3020b | ||
![]() |
642788abec | ||
![]() |
3cd11dd9a0 | ||
![]() |
bf2c5ce368 | ||
![]() |
65c510a211 | ||
![]() |
6fbc38d764 | ||
![]() |
200bf993d8 | ||
![]() |
38af82e152 | ||
![]() |
fc05f377fb | ||
![]() |
5c0e86383c | ||
![]() |
64f5ff5475 | ||
![]() |
758777111a | ||
![]() |
b90e0430f8 | ||
![]() |
0ce7da1bf6 | ||
![]() |
e6464c5c7f | ||
![]() |
c6b3f06b95 | ||
![]() |
581419b6a3 | ||
![]() |
696ab677be | ||
![]() |
0d229dac3b | ||
![]() |
3b8ea599f0 | ||
![]() |
3e70a61e33 | ||
![]() |
76f35d02b7 | ||
![]() |
356b417a04 | ||
![]() |
56147a80b5 | ||
![]() |
91728991d7 | ||
![]() |
0f7e59d288 | ||
![]() |
f33028c645 | ||
![]() |
f9149ad433 | ||
![]() |
0d7474cc88 | ||
![]() |
1e7e06d1cc | ||
![]() |
8453282fa6 | ||
![]() |
40f971d18a | ||
![]() |
ce7cb1eeae | ||
![]() |
d2701616da | ||
![]() |
10eb159e1b | ||
![]() |
36897ceb19 | ||
![]() |
9a8274130b | ||
![]() |
c8d050c3e3 | ||
![]() |
a46cd63c9d | ||
![]() |
e9e6eaf079 | ||
![]() |
cb5897af93 | ||
![]() |
d701d6eb82 | ||
![]() |
470ebb54e2 | ||
![]() |
632cab398e | ||
![]() |
189c4cc9d8 | ||
![]() |
70d5e2dee8 | ||
![]() |
c586106e51 | ||
![]() |
ffa85a616a | ||
![]() |
e5ea3e4a43 | ||
![]() |
0492e63862 | ||
![]() |
9952387356 | ||
![]() |
d7653e6e42 | ||
![]() |
e9fc40d285 | ||
![]() |
740559e3bc | ||
![]() |
9471577b3b | ||
![]() |
e85d5e54e2 | ||
![]() |
5fb071d80b | ||
![]() |
022151fefd | ||
![]() |
3b8d2fe8b7 | ||
![]() |
d51d549a28 | ||
![]() |
b5ac24f239 | ||
![]() |
3ca99005f8 | ||
![]() |
0b9f2921d2 | ||
![]() |
389501ad0c | ||
![]() |
082e4eb05c | ||
![]() |
47f885a566 | ||
![]() |
bc964b8588 | ||
![]() |
b57b3313e4 | ||
![]() |
f185cefa11 | ||
![]() |
9d256e02d7 | ||
![]() |
086c64c0be | ||
![]() |
798fe57025 | ||
![]() |
a03f744648 | ||
![]() |
64f35744c4 | ||
![]() |
b512528148 | ||
![]() |
fdfa037dca | ||
![]() |
db4ef1443d | ||
![]() |
810468c279 | ||
![]() |
8146d0830d | ||
![]() |
7e946b040c | ||
![]() |
97d24a7d4d | ||
![]() |
f8bea66313 | ||
![]() |
dd9129017f | ||
![]() |
cbe3602cb7 | ||
![]() |
1d831d65f3 | ||
![]() |
c35d020731 | ||
![]() |
c18db555a4 | ||
![]() |
373092af16 | ||
![]() |
1a2e157cda | ||
![]() |
b3bc1a3907 | ||
![]() |
4dd8d75cc0 | ||
![]() |
e5f50bb7e0 | ||
![]() |
45d5b4bea6 | ||
![]() |
ed58cf953a | ||
![]() |
ec26bc5ab7 | ||
![]() |
84e4bd3d41 | ||
![]() |
0ecfb63cd6 | ||
![]() |
ebdd6ec40c | ||
![]() |
0586760347 | ||
![]() |
d535f244ad | ||
![]() |
613d46824d | ||
![]() |
041355f182 | ||
![]() |
6977dc082f | ||
![]() |
d3dffe8165 | ||
![]() |
6812f9d202 | ||
![]() |
555e7cc907 | ||
![]() |
6180558068 | ||
![]() |
cf589f8c64 | ||
![]() |
e864919c0b | ||
![]() |
c72d83b637 | ||
![]() |
f2d2f28e23 | ||
![]() |
a7435dad6d | ||
![]() |
793f0b605c | ||
![]() |
5b56ca7ffc | ||
![]() |
5c988510b3 | ||
![]() |
290624844b | ||
![]() |
497efc9f5e | ||
![]() |
19d76b635c | ||
![]() |
4875def31c | ||
![]() |
155c0e3609 | ||
![]() |
00ea15dc19 | ||
![]() |
f04c4cb78a | ||
![]() |
6e4777692e | ||
![]() |
4638fdf2d7 | ||
![]() |
0783d385d5 | ||
![]() |
cf918e7df8 | ||
![]() |
1ba9faf35b | ||
![]() |
6e48294f2a | ||
![]() |
dea607b148 | ||
![]() |
e938e717b0 | ||
![]() |
2eed09ef1b | ||
![]() |
8a6b3644be | ||
![]() |
1d89fe503b | ||
![]() |
788db036fd | ||
![]() |
c38c473e11 | ||
![]() |
aef1f8f701 | ||
![]() |
83f9767254 | ||
![]() |
3e0352eee6 | ||
![]() |
28faff6425 | ||
![]() |
d0112f989c | ||
![]() |
9c4c310f46 | ||
![]() |
7bf7bfb9c6 | ||
![]() |
fbe776db0b | ||
![]() |
1e2de1bb14 | ||
![]() |
e395c9442f | ||
![]() |
30286f0ea5 | ||
![]() |
60ee742855 | ||
![]() |
a913ede48f | ||
![]() |
9592583783 | ||
![]() |
ad49d3ad26 | ||
![]() |
21ee73c2a3 | ||
![]() |
f5d0cc9f32 | ||
![]() |
b90c65370e | ||
![]() |
88920e0546 | ||
![]() |
d27773de03 | ||
![]() |
8abdaeb044 | ||
![]() |
9682d2f84a | ||
![]() |
a86b9e81e9 | ||
![]() |
a8bb7c68a3 | ||
![]() |
bdad29adab | ||
![]() |
fadcfe5f7a | ||
![]() |
fbd83b5ff3 | ||
![]() |
c351174fa4 | ||
![]() |
cc4f99fe28 | ||
![]() |
b2a9b88fe5 | ||
![]() |
da06e0ec76 | ||
![]() |
851ee81486 | ||
![]() |
0dc9f5c324 | ||
![]() |
36513c2301 | ||
![]() |
3a10597aed | ||
![]() |
2291be5d26 | ||
![]() |
345c3ef15e | ||
![]() |
c1dad11cb3 | ||
![]() |
12b219e7b2 | ||
![]() |
f8b48cf18d | ||
![]() |
12a9792c7d | ||
![]() |
ba55e2bc32 | ||
![]() |
c5e5b70e08 | ||
![]() |
327b186240 | ||
![]() |
5c1417e276 | ||
![]() |
0a2c99f1dc | ||
![]() |
836bfbdd02 | ||
![]() |
b13a35057a | ||
![]() |
c3e77b1ec1 | ||
![]() |
fb60bea659 | ||
![]() |
b2ddba4cbf | ||
![]() |
053251d566 | ||
![]() |
cf161a5dd9 | ||
![]() |
e4bcdbd0c4 | ||
![]() |
cae43b26f4 | ||
![]() |
b95cf9b9a3 | ||
![]() |
e6f443cb24 | ||
![]() |
087ccd69c9 | ||
![]() |
7532477a2f | ||
![]() |
433ae89e53 | ||
![]() |
de853a2651 | ||
![]() |
47c3045980 | ||
![]() |
dd50c19ba3 | ||
![]() |
707d7b3342 | ||
![]() |
ba1a2fbce4 | ||
![]() |
84f1e78660 | ||
![]() |
3490ba0a56 | ||
![]() |
1449486958 | ||
![]() |
9094cf7ce3 | ||
![]() |
df0a5b59f8 | ||
![]() |
0827044caf | ||
![]() |
342ae7c8cd | ||
![]() |
fc690b9f02 | ||
![]() |
22c9d836e0 | ||
![]() |
984997e73b | ||
![]() |
b39f407596 | ||
![]() |
615ad0cc5a | ||
![]() |
0b41cd8564 | ||
![]() |
7db523071d | ||
![]() |
974ee58b9c | ||
![]() |
1e88f2c382 | ||
![]() |
0bdcfcaaf5 | ||
![]() |
5f9c78d04f | ||
![]() |
afa178fdec | ||
![]() |
3a0e3c98f7 | ||
![]() |
fafa92d44b | ||
![]() |
fcedd06e72 | ||
![]() |
6a2acbe929 | ||
![]() |
4cfff40475 | ||
![]() |
904948dc7d | ||
![]() |
7342509b2e | ||
![]() |
ed837ba26f | ||
![]() |
13262fdb18 | ||
![]() |
baf18a8762 | ||
![]() |
c0b56b927f | ||
![]() |
242e64d72f | ||
![]() |
2262af728e | ||
![]() |
ea9947081f | ||
![]() |
e04f943980 | ||
![]() |
b38e940088 | ||
![]() |
bc0bb92f7a | ||
![]() |
8737be2623 | ||
![]() |
eb929160b3 | ||
![]() |
b8b0f257db | ||
![]() |
67b5f39df2 | ||
![]() |
7e9b3f1a60 | ||
![]() |
bce777d7c6 | ||
![]() |
465aaeff82 | ||
![]() |
40c64d50d5 | ||
![]() |
15bd2da824 | ||
![]() |
bd438ca288 | ||
![]() |
e0d02a61a9 | ||
![]() |
b3328a0ec2 | ||
![]() |
3c2041933f | ||
![]() |
e88b1cc443 | ||
![]() |
71b05b18a0 | ||
![]() |
b07b528e2a | ||
![]() |
1aeb6315ff | ||
![]() |
1b4a3d2d9f | ||
![]() |
3049a81c3b | ||
![]() |
2db1e5cb74 | ||
![]() |
78c64d39ec | ||
![]() |
46ba726232 | ||
![]() |
eb26e62889 | ||
![]() |
7f667fed18 | ||
![]() |
b2cb2b8b75 | ||
![]() |
d19f65ce4a | ||
![]() |
025b060506 | ||
![]() |
7fa2625a03 | ||
![]() |
33d62d7f21 | ||
![]() |
b336655a79 | ||
![]() |
3beffd84d6 | ||
![]() |
02761f5f35 | ||
![]() |
3b9f7885e0 | ||
![]() |
7668e45890 | ||
![]() |
695c8bc5d0 | ||
![]() |
06c42d05c3 | ||
![]() |
404104208f | ||
![]() |
b4d0ad9713 | ||
![]() |
89b1fa341b | ||
![]() |
3bda7cb26b | ||
![]() |
4f4f54a059 | ||
![]() |
12fda29280 | ||
![]() |
af060b3132 | ||
![]() |
8c500709e4 | ||
![]() |
490e6a6f23 | ||
![]() |
08177c3dd8 | ||
![]() |
d22b9c26b6 | ||
![]() |
85a350b6c8 | ||
![]() |
eae4eff92f | ||
![]() |
848be8f806 | ||
![]() |
4bb8ad19cf | ||
![]() |
c79b79b37e | ||
![]() |
8a03c366b8 | ||
![]() |
37677f389c | ||
![]() |
3e275b7dba | ||
![]() |
11b7076a43 | ||
![]() |
291c718ba2 | ||
![]() |
fcd6071c57 | ||
![]() |
476b61c4c9 | ||
![]() |
8cc5f096a2 | ||
![]() |
474d65207e | ||
![]() |
03428329ef | ||
![]() |
2692234b8c | ||
![]() |
bfb5d7e5ac | ||
![]() |
8c818e707f | ||
![]() |
3efea47ca8 | ||
![]() |
8d21988656 | ||
![]() |
89da45f9ac | ||
![]() |
34a0a00e3c | ||
![]() |
dec1094a59 | ||
![]() |
02e323133d | ||
![]() |
cb96b536a2 | ||
![]() |
627b40799c | ||
![]() |
73c4b21285 | ||
![]() |
78d7c45be3 | ||
![]() |
72edbfc455 | ||
![]() |
276535dad6 | ||
![]() |
e373e59661 | ||
![]() |
ac5ecf222e | ||
![]() |
a20594ed48 | ||
![]() |
cb59cc92a3 | ||
![]() |
34bb18448c | ||
![]() |
01253f050a | ||
![]() |
cc7e47bbb6 | ||
![]() |
5bee1c56a9 | ||
![]() |
474cc7d56d | ||
![]() |
bffdedddb4 | ||
![]() |
fd72f658c0 | ||
![]() |
42606162b2 | ||
![]() |
e82bc1b7bc | ||
![]() |
4f0e1c6c61 | ||
![]() |
550f6aff7e | ||
![]() |
67c50d7504 | ||
![]() |
94f0c61619 | ||
![]() |
8a86b30fd1 | ||
![]() |
d3b5cf82d8 | ||
![]() |
d26d804cc2 | ||
![]() |
4f9a25ee89 | ||
![]() |
6379108a75 | ||
![]() |
bb9ce0e897 | ||
![]() |
fbeaad077f | ||
![]() |
8918113a31 | ||
![]() |
c5385b5b4c | ||
![]() |
35475e1d25 | ||
![]() |
fb2c292f35 | ||
![]() |
afc3fb10c7 | ||
![]() |
0a239c2fef | ||
![]() |
f5342a09d3 | ||
![]() |
f72de687c5 | ||
![]() |
d6fb9868bf | ||
![]() |
9aff1a57d3 | ||
![]() |
7681fde4d0 | ||
![]() |
d3b7b41927 | ||
![]() |
833269fd0a | ||
![]() |
332c1a6c59 | ||
![]() |
0f1f43057e | ||
![]() |
784a7a7f24 | ||
![]() |
8e34baa59f | ||
![]() |
2926772bba | ||
![]() |
da159e4655 | ||
![]() |
a7f4496db7 | ||
![]() |
f972f02fff | ||
![]() |
1c77e26c05 | ||
![]() |
59c5363933 | ||
![]() |
b744bb0a5a | ||
![]() |
0f140b408c | ||
![]() |
7f6a6016d6 | ||
![]() |
44ed0a3279 | ||
![]() |
9964e1bb8e | ||
![]() |
8b8f725499 | ||
![]() |
bab856bce2 | ||
![]() |
711799b194 | ||
![]() |
3d285b91c6 | ||
![]() |
1dc531930d | ||
![]() |
3d3345acac | ||
![]() |
2105cacce3 | ||
![]() |
9d1d1710eb | ||
![]() |
c69dcf3e20 | ||
![]() |
eec5b37da1 | ||
![]() |
b29f0ca4d1 | ||
![]() |
576efbdc1b | ||
![]() |
a7f0510a3e | ||
![]() |
2ef088cb60 | ||
![]() |
7c320b6fc4 | ||
![]() |
e1bda4ee8b | ||
![]() |
5a4c82b860 | ||
![]() |
9b297b752e | ||
![]() |
1d6ba58ccd | ||
![]() |
1542447822 | ||
![]() |
a6f0aff659 | ||
![]() |
54930024f5 | ||
![]() |
c5f2f63458 | ||
![]() |
b2b81a5d0f | ||
![]() |
265dca3723 | ||
![]() |
171ddab32b | ||
![]() |
2aee0b0be0 | ||
![]() |
817cdf7113 | ||
![]() |
495e734428 | ||
![]() |
82120cf47f | ||
![]() |
027a5695f2 | ||
![]() |
d6d82edff5 | ||
![]() |
a12eb3fc6f | ||
![]() |
6c84574366 | ||
![]() |
1a38f25bd9 | ||
![]() |
ad40e53349 | ||
![]() |
a2ddf362d8 | ||
![]() |
65eca31635 | ||
![]() |
8b0b4a2c39 | ||
![]() |
bc5cbe9fba | ||
![]() |
f83f92d3fa | ||
![]() |
c0216c0653 | ||
![]() |
61de63a518 | ||
![]() |
d952cc2327 | ||
![]() |
19fd4dd89c | ||
![]() |
f941f5c0b0 | ||
![]() |
c7cad7e4aa | ||
![]() |
1c8988d3f7 | ||
![]() |
70a3dbe2b0 | ||
![]() |
efbb3ab25f | ||
![]() |
46447f7cfd | ||
![]() |
a6e62e07a2 | ||
![]() |
b1d25e0503 | ||
![]() |
25c557248c | ||
![]() |
b0e7c65504 | ||
![]() |
b18b044b63 | ||
![]() |
8f5f8db717 | ||
![]() |
016e28383b | ||
![]() |
f1427e9279 | ||
![]() |
169e9ab5ad | ||
![]() |
472cde29b8 | ||
![]() |
73525d19e9 | ||
![]() |
26618f8d73 | ||
![]() |
6f7c13b814 | ||
![]() |
e7d668502c | ||
![]() |
6fd357962f | ||
![]() |
0c9feedb37 | ||
![]() |
dad52724db | ||
![]() |
14ba002cbc | ||
![]() |
d48e9d5d72 | ||
![]() |
7da97489cc | ||
![]() |
a9f11b28c8 | ||
![]() |
b31d986c8d | ||
![]() |
2dad751889 | ||
![]() |
c85b1c56af | ||
![]() |
6dd34aec47 | ||
![]() |
4cd154675f | ||
![]() |
24e2c3a5e9 | ||
![]() |
064523ef25 | ||
![]() |
85f293a44e | ||
![]() |
8e412bee5f | ||
![]() |
7d5555f82e | ||
![]() |
6720725d27 | ||
![]() |
fe5c65d798 | ||
![]() |
253f3cf1ba | ||
![]() |
d8d72f92b3 | ||
![]() |
a30f5b175f | ||
![]() |
8277896ca1 | ||
![]() |
493068c073 | ||
![]() |
f4299fbea8 | ||
![]() |
10ce11d671 | ||
![]() |
db2e48b49f | ||
![]() |
5e089451af | ||
![]() |
6aa22267f4 | ||
![]() |
0f34457a10 | ||
![]() |
34c65e13bc | ||
![]() |
17a77e2577 | ||
![]() |
0f219e5ae6 | ||
![]() |
353c3c7d81 | ||
![]() |
0a89edf3b0 | ||
![]() |
e7155837d7 | ||
![]() |
f76c020dd7 | ||
![]() |
722fba7805 | ||
![]() |
86551909fc | ||
![]() |
588e94c11d | ||
![]() |
31e003bda5 | ||
![]() |
490e4d3180 | ||
![]() |
dc9f69bab0 | ||
![]() |
fdf04f77f2 | ||
![]() |
9e66310c28 | ||
![]() |
93c422dce6 | ||
![]() |
7d6eebdae3 | ||
![]() |
f11bb609c9 | ||
![]() |
5e87483f34 | ||
![]() |
f7aa451591 | ||
![]() |
321d11c2c6 | ||
![]() |
b910a92731 | ||
![]() |
ee447bc4ce | ||
![]() |
31153e4366 | ||
![]() |
7693024c29 | ||
![]() |
9628700a2f | ||
![]() |
38576173cb | ||
![]() |
19a769c12e | ||
![]() |
3c1db7d2f7 | ||
![]() |
626507093a | ||
![]() |
ee7d297ca8 | ||
![]() |
a70c0174e1 | ||
![]() |
da707afa3f | ||
![]() |
a41597431c | ||
![]() |
d0b817381e | ||
![]() |
60a2e9b5dc | ||
![]() |
df3a37b0a3 | ||
![]() |
5f4718cd13 | ||
![]() |
3cc5cb3123 | ||
![]() |
8a2872afa4 | ||
![]() |
85941c4729 | ||
![]() |
588b3d14a3 | ||
![]() |
815efa7791 | ||
![]() |
97a691ce2f | ||
![]() |
82eeefb544 | ||
![]() |
9d948f2c2b | ||
![]() |
f6061ba00e | ||
![]() |
9e3afcfe7a | ||
![]() |
0b87108174 | ||
![]() |
7fc7809cfc | ||
![]() |
c30be20e49 | ||
![]() |
25c64db0a1 | ||
![]() |
676e9c6593 | ||
![]() |
d459859361 | ||
![]() |
2be0cef446 | ||
![]() |
294db93fde | ||
![]() |
21f2f86cb8 | ||
![]() |
04576ca828 | ||
![]() |
067cb0cd9d | ||
![]() |
7f971f7173 | ||
![]() |
5c7b59524d | ||
![]() |
5133e5910e | ||
![]() |
1512c350df | ||
![]() |
a5fc7891a6 | ||
![]() |
3eb9633231 | ||
![]() |
ac67b48247 | ||
![]() |
81b65ea646 | ||
![]() |
45c1f6bc27 | ||
![]() |
0d31e5c8b1 | ||
![]() |
6378abf454 | ||
![]() |
f8fcaadb5b | ||
![]() |
0b5fd3ee76 | ||
![]() |
d010cb7e42 | ||
![]() |
71136d7347 | ||
![]() |
a18c552ddf | ||
![]() |
17fb8f2298 | ||
![]() |
fbfc4e72ca | ||
![]() |
d2e171eabc | ||
![]() |
e50094af80 | ||
![]() |
93edf72993 | ||
![]() |
a230d63cf9 | ||
![]() |
9656878ef3 | ||
![]() |
7ded7de39a | ||
![]() |
0f74e89b44 | ||
![]() |
953c40b083 | ||
![]() |
2bb39bee2f | ||
![]() |
ce2ca5446a | ||
![]() |
8a014ff786 | ||
![]() |
271b0287d8 | ||
![]() |
96a8a2a8b8 | ||
![]() |
dc09ec7598 | ||
![]() |
27fb0474d5 | ||
![]() |
7f0a87742a | ||
![]() |
47e236788c | ||
![]() |
75306f658f | ||
![]() |
325d9a0b86 | ||
![]() |
236ad57608 | ||
![]() |
6d03798314 | ||
![]() |
c954a4f7bc | ||
![]() |
ba588d1097 | ||
![]() |
44f7c9a545 | ||
![]() |
b910db322b | ||
![]() |
c44a942fb7 | ||
![]() |
d713ad3499 | ||
![]() |
ddf40df649 | ||
![]() |
7c6d85221d | ||
![]() |
b66b82a6e9 | ||
![]() |
c44b85ea87 | ||
![]() |
a02493fbaa | ||
![]() |
9c27d691dd | ||
![]() |
fcbf56e93a | ||
![]() |
a539ffb188 | ||
![]() |
512f533a80 | ||
![]() |
96ef9cdbee | ||
![]() |
935bd01f59 | ||
![]() |
eeb5d669f6 | ||
![]() |
28fcbbcf7b | ||
![]() |
0f4326151f | ||
![]() |
e0e27774ad | ||
![]() |
1223b48b2c | ||
![]() |
d8338f0b48 | ||
![]() |
38019f7f42 | ||
![]() |
78daa2eb62 | ||
![]() |
40eda05a30 | ||
![]() |
9f9de8c43b | ||
![]() |
23978ef4d2 | ||
![]() |
3b4cb23112 | ||
![]() |
974cb1167f | ||
![]() |
6ccbc272c6 | ||
![]() |
0eb28c3265 | ||
![]() |
2daa131fb2 | ||
![]() |
51247d36c5 | ||
![]() |
a910c8ccd8 | ||
![]() |
43bda2d4a4 | ||
![]() |
c7033dd757 | ||
![]() |
5673a9bace | ||
![]() |
34ff764515 | ||
![]() |
1b3a009da7 | ||
![]() |
a49002bb2c | ||
![]() |
7342fc2307 | ||
![]() |
9867a3bd60 | ||
![]() |
5ffb9eaa5b | ||
![]() |
37fa227fb5 | ||
![]() |
9dd272b357 | ||
![]() |
277298feae | ||
![]() |
ff24bc0b68 | ||
![]() |
b05b688267 | ||
![]() |
f3d7f85063 | ||
![]() |
de969a9dab | ||
![]() |
59fd38bbf8 | ||
![]() |
06dc6df270 | ||
![]() |
ff8460b361 | ||
![]() |
674d272eaa | ||
![]() |
c3e00c279d | ||
![]() |
175d920c94 | ||
![]() |
700c51f95c | ||
![]() |
04920883ea | ||
![]() |
659914afbe | ||
![]() |
ee06aed94b | ||
![]() |
af1f5d5ab2 | ||
![]() |
5e44b0b9d5 | ||
![]() |
23c1a1dab8 | ||
![]() |
f5d054b93c | ||
![]() |
d25ae5e0a9 | ||
![]() |
c42a51dcbb | ||
![]() |
da3fd92b31 | ||
![]() |
4a45ba3c14 | ||
![]() |
4292ddd0ae | ||
![]() |
4a68fd65b6 | ||
![]() |
0e33632e79 | ||
![]() |
a9b20dae33 | ||
![]() |
dbc8bed234 | ||
![]() |
f8b4190a11 | ||
![]() |
479972e3ae | ||
![]() |
3ea28b0afb | ||
![]() |
2b3cc28966 | ||
![]() |
751642b39a | ||
![]() |
d6c2c821a4 | ||
![]() |
dfc65b95f7 | ||
![]() |
b45d922463 | ||
![]() |
e595937740 | ||
![]() |
72eb584e65 | ||
![]() |
f87ee3fcf9 | ||
![]() |
e0927cd763 | ||
![]() |
8999a57f06 | ||
![]() |
8024089bde | ||
![]() |
5e01f785ae | ||
![]() |
d35d1b8860 | ||
![]() |
88027f2151 | ||
![]() |
21099eabfa | ||
![]() |
cd41e7108b | ||
![]() |
abbd2e6b72 | ||
![]() |
6da566faff | ||
![]() |
df7a866617 | ||
![]() |
1cc8f13d54 | ||
![]() |
086ce63c6c | ||
![]() |
f1dcecc6cf | ||
![]() |
fe1ce08a6c | ||
![]() |
1d64ddb7f5 | ||
![]() |
823b121cc7 | ||
![]() |
149d35c687 | ||
![]() |
3a18e68751 | ||
![]() |
6afcc83955 | ||
![]() |
277d8773f2 | ||
![]() |
f161cf8b0a | ||
![]() |
dc62ae95a6 | ||
![]() |
f4ecc315d0 | ||
![]() |
cb2a1e57fe | ||
![]() |
1396faf433 | ||
![]() |
dc8d2ae683 | ||
![]() |
191c7c50b6 | ||
![]() |
c6725b0518 | ||
![]() |
4820a6e01c | ||
![]() |
57a9b5bc0c | ||
![]() |
8c224da5d5 | ||
![]() |
14e49f3c80 | ||
![]() |
cc8f1adca3 | ||
![]() |
5b7ddbbb01 | ||
![]() |
6352fbb3b2 | ||
![]() |
122e2f7a8e | ||
![]() |
b4e1585e2b | ||
![]() |
d3f49334e2 | ||
![]() |
c4356171b3 | ||
![]() |
5c5625911d | ||
![]() |
6a10cc9c55 | ||
![]() |
6b317f918e | ||
![]() |
08b528dc4f | ||
![]() |
fc886a5a47 | ||
![]() |
0cb90e2e55 | ||
![]() |
64113a69b4 | ||
![]() |
544bb7459c | ||
![]() |
578a50b464 | ||
![]() |
3d4081d0af | ||
![]() |
b763b81f56 | ||
![]() |
947dae4900 | ||
![]() |
a5830599c4 | ||
![]() |
debd1d7d54 | ||
![]() |
cba0d04000 | ||
![]() |
695e7e6da0 | ||
![]() |
4cd4bfa1d7 | ||
![]() |
16b400964b | ||
![]() |
cf2d02c0dd | ||
![]() |
0fcd0de0d1 | ||
![]() |
748a35774f | ||
![]() |
a52a3e38ed | ||
![]() |
ee0cef06a6 | ||
![]() |
0e5a113a0c | ||
![]() |
a1ccd44013 | ||
![]() |
4d91e50d6d | ||
![]() |
120668c7bc | ||
![]() |
d81ccde569 | ||
![]() |
e8581b4adb | ||
![]() |
19906575a3 | ||
![]() |
9329094a4e | ||
![]() |
b44f5122fd | ||
![]() |
17981730a4 | ||
![]() |
53de6da26c | ||
![]() |
3e30ccdeee | ||
![]() |
baaaf7d5de | ||
![]() |
45d8d139a9 | ||
![]() |
fe644e10d0 | ||
![]() |
f383d11d10 | ||
![]() |
ef1b928532 | ||
![]() |
6e46d394b1 | ||
![]() |
f109038d12 | ||
![]() |
e31e687602 | ||
![]() |
86bfb22d4c | ||
![]() |
3f057367e3 | ||
![]() |
3d7ed5820e | ||
![]() |
0118f2efa7 | ||
![]() |
15312e4709 | ||
![]() |
bf1568a73a | ||
![]() |
13a2520ea5 | ||
![]() |
f53238f206 | ||
![]() |
9375748d9b | ||
![]() |
201df54e79 | ||
![]() |
0b54fe477b | ||
![]() |
4119e6669e | ||
![]() |
d33e5226b3 | ||
![]() |
d73f39c706 | ||
![]() |
087b451e17 | ||
![]() |
86481c74ff | ||
![]() |
5b937fb1fa | ||
![]() |
ff828116bc | ||
![]() |
ee39616a8b | ||
![]() |
cdb53ca049 | ||
![]() |
8cf475f708 | ||
![]() |
0cb449e1d6 | ||
![]() |
e6adb7abca | ||
![]() |
cfad7dd317 | ||
![]() |
dd35224f92 | ||
![]() |
1283590eeb | ||
![]() |
dca3fe396f | ||
![]() |
8d87eae11b | ||
![]() |
fd7eaacae0 | ||
![]() |
fba33cbbe9 | ||
![]() |
950ffcd790 | ||
![]() |
c178299013 | ||
![]() |
5d17c1f588 | ||
![]() |
a75c00d94e | ||
![]() |
cd19517414 | ||
![]() |
155f39aab5 | ||
![]() |
4514d0b467 | ||
![]() |
6f4a938a31 | ||
![]() |
1303ea95dd | ||
![]() |
727fe1bd15 | ||
![]() |
64ebc977e9 | ||
![]() |
e89c50d934 | ||
![]() |
c859ddfb8f | ||
![]() |
a6126c5eda | ||
![]() |
85d9bd9106 | ||
![]() |
39e9622205 | ||
![]() |
021994c9f3 | ||
![]() |
2e7ce2a769 | ||
![]() |
84f0ff2fad | ||
![]() |
e6561e5f84 | ||
![]() |
5fa452aa74 | ||
![]() |
2225ccb146 | ||
![]() |
5aafc78847 | ||
![]() |
0d03833cff | ||
![]() |
a797d5d396 | ||
![]() |
f2494374f8 | ||
![]() |
48395ba860 | ||
![]() |
5ba5f5f94e | ||
![]() |
42ce6fd334 | ||
![]() |
f5c3ee3ae1 | ||
![]() |
3c7ece1605 | ||
![]() |
870efc49ea | ||
![]() |
085ede6d93 | ||
![]() |
4ef19d17da | ||
![]() |
223913c30a | ||
![]() |
010e4de4e1 | ||
![]() |
41134466ed | ||
![]() |
8f07747452 | ||
![]() |
eb5ce5be1e | ||
![]() |
71d855e836 | ||
![]() |
33b7ab593c | ||
![]() |
8706d834b4 | ||
![]() |
7cfab33ebb | ||
![]() |
1ababc8c7f | ||
![]() |
1f75e63c37 | ||
![]() |
cb3f9b9740 | ||
![]() |
9784353223 | ||
![]() |
7d93ca5c73 | ||
![]() |
ac20063e86 | ||
![]() |
debaec32af | ||
![]() |
0e9b71e7a9 | ||
![]() |
85f5ff3c14 | ||
![]() |
3d81f167ea | ||
![]() |
fb70a2e52d | ||
![]() |
460e85a1b5 | ||
![]() |
539b64bd57 | ||
![]() |
90e38a06a2 | ||
![]() |
09ab910630 | ||
![]() |
c15f80b33f | ||
![]() |
b2e6ba3c4a | ||
![]() |
b16f696b0e | ||
![]() |
9adfb382e8 | ||
![]() |
44368383f4 | ||
![]() |
d1ff7e0ffe | ||
![]() |
42e7db8d13 | ||
![]() |
0c17ea5755 | ||
![]() |
cdaff5b39c | ||
![]() |
2b1b970e78 | ||
![]() |
0aebc0a8e3 | ||
![]() |
c3a89f589e | ||
![]() |
971cd73fb3 | ||
![]() |
1947860d61 | ||
![]() |
55aaa421e8 | ||
![]() |
a8932706d8 | ||
![]() |
a97972aac0 | ||
![]() |
094c3d559a | ||
![]() |
6fb032b3c2 | ||
![]() |
8ca188f4d4 | ||
![]() |
746a1d8d59 | ||
![]() |
63c5e00d86 | ||
![]() |
9d2e5d6665 | ||
![]() |
f6045bf8b5 | ||
![]() |
e83f40d5c5 | ||
![]() |
e5118418b2 | ||
![]() |
7cd814d917 | ||
![]() |
78282c1a49 | ||
![]() |
fd4214ccf3 | ||
![]() |
0785945635 | ||
![]() |
967bdeae7b | ||
![]() |
452db51669 | ||
![]() |
5875ced367 | ||
![]() |
fbac6bcfd0 | ||
![]() |
0dcd3ece9d | ||
![]() |
224fff89e3 | ||
![]() |
22e73644f9 | ||
![]() |
6a0f6ab319 | ||
![]() |
88a394836f | ||
![]() |
f822c1c2e4 | ||
![]() |
1d16d980b3 | ||
![]() |
501b18f986 | ||
![]() |
21ed759e53 | ||
![]() |
8d50dfd93c | ||
![]() |
51e40dd98c | ||
![]() |
b2048379af | ||
![]() |
011539f6f1 | ||
![]() |
5457c3803f | ||
![]() |
b3d777bb6c | ||
![]() |
12e00c3054 | ||
![]() |
40b683111c | ||
![]() |
9542ca773f | ||
![]() |
8af832a496 | ||
![]() |
6836130fda | ||
![]() |
724893879f | ||
![]() |
736729f5ef | ||
![]() |
aa47966347 | ||
![]() |
d64d12afe8 | ||
![]() |
1f8df419c4 | ||
![]() |
7ba8202af5 | ||
![]() |
d7b691cf59 | ||
![]() |
7058d5e4cd | ||
![]() |
52fd508fea | ||
![]() |
41045b62dc | ||
![]() |
188ea2644a | ||
![]() |
4c8f357978 | ||
![]() |
4bb2fd6ba6 | ||
![]() |
33c9f74508 | ||
![]() |
f53fe67372 | ||
![]() |
51ff724691 | ||
![]() |
291bf93f9d | ||
![]() |
5fcd629f16 | ||
![]() |
ab90901793 | ||
![]() |
4f206fd918 | ||
![]() |
7233285437 | ||
![]() |
8e348a11c2 | ||
![]() |
085ea6d0a1 | ||
![]() |
aaf88b1895 | ||
![]() |
4f4a9412a3 | ||
![]() |
a92e039363 | ||
![]() |
33aa4ca4b7 | ||
![]() |
05658cafc7 | ||
![]() |
ff3710de66 | ||
![]() |
db8dd9f186 | ||
![]() |
e8b73ba6d1 | ||
![]() |
f1112fdf37 | ||
![]() |
a48c4f9e05 | ||
![]() |
19a521d2e9 | ||
![]() |
dd6e55ac31 | ||
![]() |
b1e63f0f14 | ||
![]() |
b0e49a4cc8 | ||
![]() |
1e94517a72 | ||
![]() |
98f60216ac | ||
![]() |
e29b712108 | ||
![]() |
a462435f2f | ||
![]() |
911b8273fe | ||
![]() |
09935e591a | ||
![]() |
4a212dba35 | ||
![]() |
aac9e85e04 | ||
![]() |
bb67a837d3 | ||
![]() |
6cde695194 | ||
![]() |
a1a1ac0bbb | ||
![]() |
9ec8bc2166 | ||
![]() |
28cd6a75e7 | ||
![]() |
4cc7aced15 | ||
![]() |
1058aeb04f | ||
![]() |
cfec0db947 | ||
![]() |
120bd6cd68 | ||
![]() |
9aef06d1b8 | ||
![]() |
e6e9dd751c | ||
![]() |
5dd677756f | ||
![]() |
b77c590910 | ||
![]() |
7e5f2822ae | ||
![]() |
12bbc7fd6b | ||
![]() |
bf9ac8252b | ||
![]() |
4a3f5dc619 | ||
![]() |
ca156befbd | ||
![]() |
4db41e2ac4 | ||
![]() |
982a43fce1 | ||
![]() |
dd76a74e1c | ||
![]() |
70cb52b2c7 | ||
![]() |
5c7f69acaa | ||
![]() |
f1d9015e5f | ||
![]() |
e8d900c58e | ||
![]() |
a6241ae912 | ||
![]() |
4a697ca2ec | ||
![]() |
58bec7f2c9 | ||
![]() |
213f84985c | ||
![]() |
074b1f8c61 | ||
![]() |
326eee8c83 | ||
![]() |
00bff4912e | ||
![]() |
0ce1720516 | ||
![]() |
ee407472cf | ||
![]() |
f341f3b2dd | ||
![]() |
8513946e09 | ||
![]() |
8ebd9c8927 | ||
![]() |
1d54c5144e | ||
![]() |
e40d4318fa | ||
![]() |
7756e10779 | ||
![]() |
3e58d502d0 | ||
![]() |
1c8846dc57 | ||
![]() |
2f320c7239 | ||
![]() |
e799918ab6 | ||
![]() |
86c4928e0f | ||
![]() |
0293eb5c51 | ||
![]() |
1ee75b6aa6 | ||
![]() |
4b30b224b5 | ||
![]() |
16b232d2a3 | ||
![]() |
3f3b1f5b1d | ||
![]() |
cec017b7bf | ||
![]() |
3123cc1059 | ||
![]() |
caa9df86bc | ||
![]() |
f417389a7a | ||
![]() |
662a5c8ea6 | ||
![]() |
7edfbfb764 | ||
![]() |
c1602d2554 | ||
![]() |
9f8d4e1022 | ||
![]() |
d1dfda405f | ||
![]() |
28efded624 | ||
![]() |
06c86ee267 | ||
![]() |
5892780871 | ||
![]() |
4fcdcd9a8a | ||
![]() |
80d834fb55 | ||
![]() |
4122ebe18f | ||
![]() |
7d87777bf8 | ||
![]() |
4a73d634e0 | ||
![]() |
373dc10a40 | ||
![]() |
ed43ec8ea2 | ||
![]() |
7918fc3528 | ||
![]() |
bf58205b0a | ||
![]() |
c0d1ce96d1 | ||
![]() |
b31d3802eb | ||
![]() |
be1228c3b4 | ||
![]() |
15c94c6b34 | ||
![]() |
202d23426a | ||
![]() |
fc26de48b2 | ||
![]() |
76c88913f9 | ||
![]() |
a3a1aed723 | ||
![]() |
81aa56f60f | ||
![]() |
73bb850209 | ||
![]() |
8dfec12330 | ||
![]() |
ae24397793 | ||
![]() |
3b0f888407 | ||
![]() |
845d1e02b0 | ||
![]() |
5d357bc41f | ||
![]() |
6a54672b13 | ||
![]() |
3d9a15df44 | ||
![]() |
449c7fda2f | ||
![]() |
8b7b05da68 | ||
![]() |
92400ebcab | ||
![]() |
23d3e56967 | ||
![]() |
6785dc4967 | ||
![]() |
dad20f6a2d | ||
![]() |
bb15671046 | ||
![]() |
21984fac8b | ||
![]() |
f392afe87f | ||
![]() |
6a243ec7bc | ||
![]() |
8cd3b603df | ||
![]() |
6e1aefe6d8 | ||
![]() |
1c90b6eca3 | ||
![]() |
c33cf9f878 | ||
![]() |
27cb40eec9 | ||
![]() |
c06081b75d | ||
![]() |
a7eec2f0a0 | ||
![]() |
4fd0fe3194 | ||
![]() |
cc74593ddd | ||
![]() |
fdb7c5dba1 | ||
![]() |
77470c7cfa | ||
![]() |
f0a734fdab | ||
![]() |
75405b2b25 | ||
![]() |
90ed4b3c49 | ||
![]() |
290a17a764 | ||
![]() |
aaabd836e4 | ||
![]() |
076e5cea3b | ||
![]() |
8515971ccf | ||
![]() |
d86fb033ea | ||
![]() |
99d7d8ddbc | ||
![]() |
df78fd2d41 | ||
![]() |
dabe6267b9 | ||
![]() |
0119ebddbe | ||
![]() |
3216ef9f47 | ||
![]() |
b79d1bcded | ||
![]() |
17e234f9d5 | ||
![]() |
ea1f75f80e | ||
![]() |
8c40db5730 | ||
![]() |
80855e89ec | ||
![]() |
0850401dc4 | ||
![]() |
337fda2023 | ||
![]() |
64f238191e | ||
![]() |
eb169cb133 | ||
![]() |
92789c3113 | ||
![]() |
c1c677e161 | ||
![]() |
2fe917ff82 | ||
![]() |
0e6c205732 | ||
![]() |
125ae0a173 | ||
![]() |
0245e13591 | ||
![]() |
d546733287 | ||
![]() |
c275326d59 | ||
![]() |
d4561507b8 | ||
![]() |
2624706c69 | ||
![]() |
d39d885ec2 | ||
![]() |
d83c744725 | ||
![]() |
843995cdb9 | ||
![]() |
9491ba77e0 | ||
![]() |
58a449d437 | ||
![]() |
7f55e0f05b | ||
![]() |
4f9e8d2e8a | ||
![]() |
21be2f46f3 | ||
![]() |
a6e7680212 | ||
![]() |
e79e744e08 | ||
![]() |
7abdac72a4 | ||
![]() |
90d85eaf7d | ||
![]() |
e65f9740fb | ||
![]() |
7538f89b56 | ||
![]() |
7c755a3991 | ||
![]() |
10e903c9fc | ||
![]() |
b018124226 | ||
![]() |
5d632d0d90 | ||
![]() |
4eecaea601 | ||
![]() |
63055818ec | ||
![]() |
0beb08b687 | ||
![]() |
bbc9e60a12 | ||
![]() |
6c975ecc4c | ||
![]() |
23e8a4ce4b | ||
![]() |
50134a2f9b | ||
![]() |
628b37c4fa | ||
![]() |
1b4ae70a43 | ||
![]() |
065051a360 |
7
.gitattributes
vendored
7
.gitattributes
vendored
@@ -11,9 +11,14 @@
|
||||
*.bat text eol=crlf
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
chromeos/** binary
|
||||
tools/** binary
|
||||
*.jar binary
|
||||
*.exe binary
|
||||
*.apk binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.ttf binary
|
||||
|
||||
# Help GitHub detect languages
|
||||
native/jni/external/** linguist-vendored
|
||||
native/jni/systemproperties/** linguist-language=C++
|
||||
|
87
.github/workflows/build.yml
vendored
Normal file
87
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
name: Magisk Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
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: |
|
||||
$oldAndroidPath = $env:ANDROID_SDK_ROOT
|
||||
$sdk_root = "C:\Android"
|
||||
New-Item -Path $sdk_root -ItemType SymbolicLink -Value $oldAndroidPath
|
||||
$ndk_ver = Select-String -Path "gradle.properties" -Pattern "^magisk.fullNdkVersion=" | % { $_ -replace ".*=" }
|
||||
echo "ANDROID_SDK_ROOT=$sdk_root" >> $env:GITHUB_ENV
|
||||
echo "ANDROID_HOME=$sdk_root" >> $env:GITHUB_ENV
|
||||
echo "MAGISK_NDK_VERSION=$ndk_ver" >> $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
|
||||
!~/.gradle/caches/**/*.lock
|
||||
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
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -2,8 +2,8 @@ out
|
||||
*.zip
|
||||
*.jks
|
||||
*.apk
|
||||
config.prop
|
||||
update.sh
|
||||
/config.prop
|
||||
/update.sh
|
||||
|
||||
# Built binaries
|
||||
native/out
|
||||
|
9
.gitmodules
vendored
9
.gitmodules
vendored
@@ -19,3 +19,12 @@
|
||||
[submodule "nanopb"]
|
||||
path = native/jni/external/nanopb
|
||||
url = https://github.com/nanopb/nanopb.git
|
||||
[submodule "mincrypt"]
|
||||
path = native/jni/external/mincrypt
|
||||
url = https://github.com/topjohnwu/mincrypt.git
|
||||
[submodule "pcre"]
|
||||
path = native/jni/external/pcre
|
||||
url = https://android.googlesource.com/platform/external/pcre
|
||||
[submodule "termux-elf-cleaner"]
|
||||
path = tools/termux-elf-cleaner
|
||||
url = https://github.com/termux/termux-elf-cleaner.git
|
||||
|
98
README.MD
98
README.MD
@@ -1,63 +1,77 @@
|
||||
# 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)
|
||||

|
||||

|
||||
|
||||
## 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 hide modifications from nearly any system integrity verifications used in banking apps, corporation monitoring apps, game cheat detections, and most importantly [Google's SafetyNet API](https://developer.android.com/training/safetynet/index.html).
|
||||
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
|
||||
|
||||
[](https://github.com/topjohnwu/Magisk/releases/download/manager-v8.0.3/MagiskManager-v8.0.3.apk)
|
||||
[](https://raw.githubusercontent.com/topjohnwu/magisk_files/canary/app-debug.apk)
|
||||
<br>
|
||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v20.4)
|
||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v21.1)
|
||||
|
||||
## 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
|
||||
- Android 9.0+: Magisk Manager stealth mode
|
||||
|
||||
## Bug Reports
|
||||
|
||||
**Make sure to install the latest [Canary Build](https://forum.xda-developers.com/apps/magisk/dev-magisk-canary-channel-bleeding-edge-t3839337) before reporting any bugs!** **DO NOT** report bugs that is already fixed upstream. Follow the instructions in the [Canary Channel XDA Thread](https://forum.xda-developers.com/apps/magisk/dev-magisk-canary-channel-bleeding-edge-t3839337), and report a bug either by opening an issue on GitHub or directly in the thread.
|
||||
Canary Channels are cutting edge builds for those adventurous. To access canary builds, install the Canary Magisk Manager, switch to the Canary Channel in settings and upgrade.
|
||||
|
||||
## Building Environment Requirements
|
||||
**Only bug reports from Canary builds will be accepted.**
|
||||
|
||||
- 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
|
||||
For installation issues, upload both boot image and install logs.<br>
|
||||
For Magisk issues, upload boot logcat or dmesg.<br>
|
||||
For Magisk Manager crashes, record and upload the logcat when the crash occurs.
|
||||
|
||||
## Building Notes and Instructions
|
||||
## Building and Development
|
||||
|
||||
- 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).
|
||||
- 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 in Android Studio. Both app (Kotlin/Java) and native (C++/C) source code can be properly developed using the IDE, but *always* use `build.py` for building.
|
||||
- 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).
|
||||
|
||||
## Translations
|
||||
## Translation Contributions
|
||||
|
||||
Default string resources for Magisk Manager are scattered throughout
|
||||
Default string resources for Magisk Manager and its stub APK are located here:
|
||||
|
||||
- `app/src/main/res/values/strings.xml`
|
||||
- `stub/src/main/res/values/strings.xml`
|
||||
- `shared/src/main/res/values/strings.xml`
|
||||
|
||||
Translate each and place them in the respective locations (`<module>/src/main/res/values-<lang>/strings.xml`).
|
||||
|
||||
## Signature Verification
|
||||
|
||||
Official release zips and APKs are signed with my personal private key. You can verify the key certificate to make sure the binaries you downloaded are not manipulated in anyway.
|
||||
|
||||
``` bash
|
||||
# Use the keytool command from JDK to print certificates
|
||||
keytool -printcert -jarfile <APK or Magisk zip>
|
||||
|
||||
# The output should contain the following signature
|
||||
Owner: CN=John Wu, L=Taipei, C=TW
|
||||
Issuer: CN=John Wu, L=Taipei, C=TW
|
||||
Serial number: 50514879
|
||||
Valid from: Sun Aug 14 13:23:44 EDT 2016 until: Tue Jul 21 13:23:44 EDT 2116
|
||||
Certificate fingerprints:
|
||||
MD5: CE:DA:68:C1:E1:74:71:0A:EF:58:89:7D:AE:6E:AB:4F
|
||||
SHA1: DC:0F:2B:61:CB:D7:E9:D3:DB:BE:06:0B:2B:87:0D:46:BB:06:02:11
|
||||
SHA256: B4:CB:83:B4:DA:D9:9F:99:7D:BE:87:2F:01:3A:A1:6C:14:EE:C4:1D:16:70:21:F3:71:F7:E1:33:0F:27:3E:E6
|
||||
Signature algorithm name: SHA256withRSA
|
||||
Version: 3
|
||||
```
|
||||
Translate each and place them in the respective locations (`[module]/src/main/res/values-[lang]/strings.xml`).
|
||||
|
||||
## License
|
||||
|
||||
|
@@ -1,83 +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 configProps['appVersion']
|
||||
versionCode configProps['appVersionCode'] as Integer
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
argument('butterknife.debuggable', 'false')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
dataBinding {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude '/META-INF/*.version'
|
||||
exclude '/META-INF/*.kotlin_module'
|
||||
exclude '/META-INF/rxkotlin.properties'
|
||||
exclude '/androidsupportmultidexversion.txt'
|
||||
exclude '/org/**'
|
||||
exclude '/kotlin/**'
|
||||
exclude '/kotlinx/**'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
implementation project(':net')
|
||||
implementation project(':shared')
|
||||
implementation project(':signing')
|
||||
|
||||
implementation 'com.github.topjohnwu:jtar:1.0.0'
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
implementation 'com.github.skoumalcz:teanity:0.3.3'
|
||||
implementation 'com.ncapdevi:frag-nav:3.2.0'
|
||||
|
||||
def markwonVersion = '3.0.1'
|
||||
implementation "ru.noties.markwon:core:${markwonVersion}"
|
||||
implementation "ru.noties.markwon:html:${markwonVersion}"
|
||||
implementation "ru.noties.markwon:image-svg:${markwonVersion}"
|
||||
|
||||
def libsuVersion = '2.5.0'
|
||||
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
|
||||
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
|
||||
|
||||
def koin = "2.0.0-rc-2"
|
||||
implementation "org.koin:koin-core:${koin}"
|
||||
implementation "org.koin:koin-android:${koin}"
|
||||
implementation "org.koin:koin-androidx-viewmodel:${koin}"
|
||||
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.preference:preference:1.0.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha05'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.1.0-alpha06'
|
||||
implementation 'androidx.work:work-runtime:2.0.1'
|
||||
implementation 'androidx.transition:transition:1.2.0-alpha01'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
}
|
183
app/build.gradle.kts
Normal file
183
app/build.gradle.kts
Normal file
@@ -0,0 +1,183 @@
|
||||
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.appVersion
|
||||
versionCode = Config.appVersionCode
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
tasks["preBuild"]?.dependsOn(tasks.register("copyUtils", Copy::class) {
|
||||
from(rootProject.file("scripts/util_functions.sh"))
|
||||
into("src/main/res/raw")
|
||||
})
|
||||
|
||||
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.0"
|
||||
implementation("io.noties.markwon:core:${vMarkwon}")
|
||||
implementation("io.noties.markwon:html:${vMarkwon}")
|
||||
implementation("io.noties.markwon:image:${vMarkwon}")
|
||||
implementation("com.caverock:androidsvg:1.4")
|
||||
|
||||
val vLibsu = "3.0.2"
|
||||
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
|
||||
implementation("com.github.topjohnwu.libsu: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-alpha04"
|
||||
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.0.1")
|
||||
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.2.5")
|
||||
implementation("androidx.work:work-runtime-ktx:2.4.0")
|
||||
implementation("androidx.transition:transition:1.3.1")
|
||||
implementation("androidx.multidex:multidex:2.0.1")
|
||||
implementation("androidx.core:core-ktx:1.3.2")
|
||||
implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.0.0")
|
||||
implementation("com.google.android.material:material:1.2.1")
|
||||
}
|
40
app/proguard-rules.pro
vendored
40
app/proguard-rules.pro
vendored
@@ -16,31 +16,33 @@
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Kotlin
|
||||
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
||||
public static void checkExpressionValueIsNotNull(...);
|
||||
public static void checkNotNullExpressionValue(...);
|
||||
public static void checkReturnedValueIsNotNull(...);
|
||||
public static void checkFieldIsNotNull(...);
|
||||
public static void checkParameterIsNotNull(...);
|
||||
}
|
||||
|
||||
# Stubs
|
||||
-keep class a.* { *; }
|
||||
|
||||
# Snet
|
||||
-keepclassmembers class com.topjohnwu.magisk.utils.ISafetyNetHelper { *; }
|
||||
-keep,allowobfuscation interface com.topjohnwu.magisk.utils.ISafetyNetHelper$Callback
|
||||
-keepclassmembers class * implements com.topjohnwu.magisk.utils.ISafetyNetHelper$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);
|
||||
}
|
||||
|
||||
# Keep all fragment constructors
|
||||
-keepclassmembers class * extends androidx.fragment.app.Fragment {
|
||||
public <init>(...);
|
||||
}
|
||||
|
||||
# DelegateWorker
|
||||
-keep,allowobfuscation class * extends com.topjohnwu.magisk.model.worker.DelegateWorker
|
||||
|
||||
# BootSigner
|
||||
-keepclassmembers class com.topjohnwu.signing.BootSigner { *; }
|
||||
|
||||
# Strip logging
|
||||
-assumenosideeffects class com.topjohnwu.magisk.utils.Logger {
|
||||
public *** debug(...);
|
||||
# Strip Timber verbose and debug logging
|
||||
-assumenosideeffects class timber.log.Timber.Tree {
|
||||
public void v(**);
|
||||
public void d(**);
|
||||
}
|
||||
|
||||
# Excessive obfuscation
|
||||
-repackageclasses 'a'
|
||||
-repackageclasses
|
||||
-allowaccessmodification
|
||||
|
||||
# QOL
|
||||
|
0
net/.gitignore → app/shared/.gitignore
vendored
0
net/.gitignore → app/shared/.gitignore
vendored
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"))))
|
||||
}
|
@@ -19,3 +19,7 @@
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-keepclassmembers class * extends javax.net.ssl.SSLSocketFactory {
|
||||
** delegate;
|
||||
}
|
17
app/shared/src/main/AndroidManifest.xml
Normal file
17
app/shared/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.topjohnwu.shared">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:label="Magisk Manager"
|
||||
android:installLocation="internalOnly"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
tools:ignore="UnusedAttribute">
|
||||
</application>
|
||||
|
||||
</manifest>
|
68
app/shared/src/main/java/com/topjohnwu/magisk/DynAPK.java
Normal file
68
app/shared/src/main/java/com/topjohnwu/magisk/DynAPK.java
Normal file
@@ -0,0 +1,68 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
|
||||
public class DynAPK {
|
||||
|
||||
// Indices of the object array
|
||||
private static final int STUB_VERSION_ENTRY = 0;
|
||||
private static final int CLASS_COMPONENT_MAP = 1;
|
||||
|
||||
private static File dynDir;
|
||||
private static Method addAssetPath;
|
||||
|
||||
private static File getDynDir(Context c) {
|
||||
if (dynDir == null) {
|
||||
if (SDK_INT >= 24) {
|
||||
// Use protected context to allow directBootAware
|
||||
c = c.createDeviceProtectedStorageContext();
|
||||
}
|
||||
dynDir = new File(c.getFilesDir().getParent(), "dyn");
|
||||
dynDir.mkdir();
|
||||
}
|
||||
return dynDir;
|
||||
}
|
||||
|
||||
public static File current(Context c) {
|
||||
return new File(getDynDir(c), "current.apk");
|
||||
}
|
||||
|
||||
public static File update(Context c) {
|
||||
return new File(getDynDir(c), "update.apk");
|
||||
}
|
||||
|
||||
public static Data load(Object o) {
|
||||
Object[] arr = (Object[]) o;
|
||||
Data data = new Data();
|
||||
data.version = (int) arr[STUB_VERSION_ENTRY];
|
||||
data.classToComponent = (Map<String, String>) arr[CLASS_COMPONENT_MAP];
|
||||
return data;
|
||||
}
|
||||
|
||||
public static Object pack(Data data) {
|
||||
Object[] arr = new Object[2];
|
||||
arr[STUB_VERSION_ENTRY] = data.version;
|
||||
arr[CLASS_COMPONENT_MAP] = data.classToComponent;
|
||||
return arr;
|
||||
}
|
||||
|
||||
public static void addAssetPath(AssetManager asset, String path) {
|
||||
try {
|
||||
if (addAssetPath == null)
|
||||
addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
|
||||
addAssetPath.invoke(asset, path);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
public static class Data {
|
||||
public int version;
|
||||
public Map<String, String> classToComponent;
|
||||
}
|
||||
}
|
@@ -1,11 +1,9 @@
|
||||
package com.topjohnwu.magisk.utils;
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ProviderInfo;
|
||||
import android.content.res.XmlResourceParser;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.net.Uri;
|
||||
@@ -16,17 +14,12 @@ import android.provider.OpenableColumns;
|
||||
import android.text.TextUtils;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
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
|
||||
*/
|
||||
@@ -34,33 +27,17 @@ public class FileProvider extends ContentProvider {
|
||||
private static final String[] COLUMNS = {
|
||||
OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE };
|
||||
|
||||
private static final String
|
||||
META_DATA_FILE_PROVIDER_PATHS = "android.support.FILE_PROVIDER_PATHS";
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void attachInfo(Context context, ProviderInfo info) {
|
||||
super.attachInfo(context, info);
|
||||
@@ -83,7 +60,6 @@ public class FileProvider extends ContentProvider {
|
||||
return strategy.getUriForFile(file);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String selection,
|
||||
String[] selectionArgs,
|
||||
@@ -116,7 +92,6 @@ public class FileProvider extends ContentProvider {
|
||||
return cursor;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getType(Uri uri) {
|
||||
|
||||
@@ -134,20 +109,17 @@ public class FileProvider extends ContentProvider {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues values) {
|
||||
throw new UnsupportedOperationException("No external inserts");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String selection,
|
||||
String[] selectionArgs) {
|
||||
throw new UnsupportedOperationException("No external updates");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int delete(Uri uri, String selection,
|
||||
String[] selectionArgs) {
|
||||
@@ -156,7 +128,6 @@ public class FileProvider extends ContentProvider {
|
||||
return file.delete() ? 1 : 0;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(Uri uri, String mode)
|
||||
throws FileNotFoundException {
|
||||
@@ -166,95 +137,54 @@ public class FileProvider extends ContentProvider {
|
||||
return ParcelFileDescriptor.open(file, fileMode);
|
||||
}
|
||||
|
||||
|
||||
private static PathStrategy getPathStrategy(Context context, String authority) {
|
||||
PathStrategy strat;
|
||||
synchronized (sCache) {
|
||||
strat = sCache.get(authority);
|
||||
if (strat == null) {
|
||||
try {
|
||||
strat = parsePathStrategy(context, authority);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException(
|
||||
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
|
||||
} catch (XmlPullParserException e) {
|
||||
throw new IllegalArgumentException(
|
||||
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", 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 ProviderInfo info = context.getPackageManager()
|
||||
.resolveContentProvider(authority, PackageManager.GET_META_DATA);
|
||||
final XmlResourceParser in = info.loadXmlMetaData(
|
||||
context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);
|
||||
if (in == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Missing " + META_DATA_FILE_PROVIDER_PATHS + " meta-data");
|
||||
strat.addRoot("root_files", buildPath(DEVICE_ROOT, "."));
|
||||
strat.addRoot("internal_files", buildPath(context.getFilesDir(), "."));
|
||||
strat.addRoot("cache_files", buildPath(context.getCacheDir(), "."));
|
||||
strat.addRoot("external_files", buildPath(Environment.getExternalStorageDirectory(), "."));
|
||||
{
|
||||
File[] externalFilesDirs = getExternalFilesDirs(context, null);
|
||||
if (externalFilesDirs.length > 0) {
|
||||
strat.addRoot("external_file_files", buildPath(externalFilesDirs[0], "."));
|
||||
}
|
||||
}
|
||||
|
||||
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)) {
|
||||
File[] externalFilesDirs = getExternalFilesDirs(context, null);
|
||||
if (externalFilesDirs.length > 0) {
|
||||
target = externalFilesDirs[0];
|
||||
}
|
||||
} else if (TAG_EXTERNAL_CACHE.equals(tag)) {
|
||||
File[] externalCacheDirs = getExternalCacheDirs(context);
|
||||
if (externalCacheDirs.length > 0) {
|
||||
target = externalCacheDirs[0];
|
||||
}
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
|
||||
&& TAG_EXTERNAL_MEDIA.equals(tag)) {
|
||||
File[] externalMediaDirs = context.getExternalMediaDirs();
|
||||
if (externalMediaDirs.length > 0) {
|
||||
target = externalMediaDirs[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (target != null) {
|
||||
strat.addRoot(name, buildPath(target, path));
|
||||
}
|
||||
{
|
||||
File[] externalCacheDirs = getExternalCacheDirs(context);
|
||||
if (externalCacheDirs.length > 0) {
|
||||
strat.addRoot("external_cache_files", buildPath(externalCacheDirs[0], "."));
|
||||
}
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
File[] externalMediaDirs = context.getExternalMediaDirs();
|
||||
if (externalMediaDirs.length > 0) {
|
||||
strat.addRoot("external_media_files", buildPath(externalMediaDirs[0], "."));
|
||||
}
|
||||
}
|
||||
|
||||
return strat;
|
||||
}
|
||||
|
||||
|
||||
interface PathStrategy {
|
||||
|
||||
Uri getUriForFile(File file);
|
||||
|
||||
|
||||
File getFileForUri(Uri uri);
|
||||
}
|
||||
|
||||
|
||||
static class SimplePathStrategy implements PathStrategy {
|
||||
private final String mAuthority;
|
||||
private final HashMap<String, File> mRoots = new HashMap<>();
|
||||
@@ -263,7 +193,6 @@ public class FileProvider extends ContentProvider {
|
||||
mAuthority = authority;
|
||||
}
|
||||
|
||||
|
||||
void addRoot(String name, File root) {
|
||||
if (TextUtils.isEmpty(name)) {
|
||||
throw new IllegalArgumentException("Name must not be empty");
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.net;
|
||||
package com.topjohnwu.magisk.net;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.net;
|
||||
package com.topjohnwu.magisk.net;
|
||||
|
||||
import java.net.HttpURLConnection;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.net;
|
||||
package com.topjohnwu.magisk.net;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
@@ -35,7 +35,7 @@ public class Networking {
|
||||
return request(url, "GET");
|
||||
}
|
||||
|
||||
public static void init(Context context) {
|
||||
public static boolean init(Context context) {
|
||||
try {
|
||||
// Try installing new SSL provider from Google Play Service
|
||||
Context gms = context.createPackageContext("com.google.android.gms",
|
||||
@@ -45,10 +45,13 @@ public class Networking {
|
||||
.getMethod("insertProvider", Context.class)
|
||||
.invoke(null, gms);
|
||||
} catch (Exception e) {
|
||||
// Failed to update SSL provider, use NoSSLv3SocketFactory on SDK < 21
|
||||
if (Build.VERSION.SDK_INT < 21)
|
||||
if (Build.VERSION.SDK_INT < 21) {
|
||||
// Failed to update SSL provider, use NoSSLv3SocketFactory on SDK < 21
|
||||
HttpsURLConnection.setDefaultSSLSocketFactory(new NoSSLv3SocketFactory());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean checkNetworkStatus(Context context) {
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.net;
|
||||
package com.topjohnwu.magisk.net;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
@@ -11,18 +11,18 @@ import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
class NoSSLv3SocketFactory extends SSLSocketFactory {
|
||||
public class NoSSLv3SocketFactory extends SSLSocketFactory {
|
||||
|
||||
private final static SSLSocketFactory base = HttpsURLConnection.getDefaultSSLSocketFactory();
|
||||
private final static SSLSocketFactory delegate = HttpsURLConnection.getDefaultSSLSocketFactory();
|
||||
|
||||
@Override
|
||||
public String[] getDefaultCipherSuites() {
|
||||
return base.getDefaultCipherSuites();
|
||||
return delegate.getDefaultCipherSuites();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedCipherSuites() {
|
||||
return base.getSupportedCipherSuites();
|
||||
return delegate.getSupportedCipherSuites();
|
||||
}
|
||||
|
||||
private Socket createSafeSocket(Socket socket) {
|
||||
@@ -40,31 +40,31 @@ class NoSSLv3SocketFactory extends SSLSocketFactory {
|
||||
|
||||
@Override
|
||||
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
|
||||
return createSafeSocket(base.createSocket(s, host, port, autoClose));
|
||||
return createSafeSocket(delegate.createSocket(s, host, port, autoClose));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket() throws IOException {
|
||||
return createSafeSocket(base.createSocket());
|
||||
return createSafeSocket(delegate.createSocket());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port) throws IOException {
|
||||
return createSafeSocket(base.createSocket(host, port));
|
||||
return createSafeSocket(delegate.createSocket(host, port));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
|
||||
return createSafeSocket(base.createSocket(host, port, localHost, localPort));
|
||||
return createSafeSocket(delegate.createSocket(host, port, localHost, localPort));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(InetAddress host, int port) throws IOException {
|
||||
return createSafeSocket(base.createSocket(host, port));
|
||||
return createSafeSocket(delegate.createSocket(host, port));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
|
||||
return createSafeSocket(base.createSocket(address, port, localAddress, localPort));
|
||||
return createSafeSocket(delegate.createSocket(address, port, localAddress, localPort));
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.net;
|
||||
package com.topjohnwu.magisk.net;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
|
||||
@@ -23,7 +23,6 @@ import java.util.concurrent.Executor;
|
||||
public class Request implements Closeable {
|
||||
private HttpURLConnection conn;
|
||||
private Executor executor = null;
|
||||
private DownloadProgressListener progress = null;
|
||||
private int code = -1;
|
||||
|
||||
ErrorHandler err = null;
|
||||
@@ -66,11 +65,6 @@ public class Request implements Closeable {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Request setDownloadProgressListener(DownloadProgressListener listener) {
|
||||
progress = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Request setErrorHandler(ErrorHandler handler) {
|
||||
err = handler;
|
||||
return this;
|
||||
@@ -169,24 +163,13 @@ public class Request implements Closeable {
|
||||
|
||||
private BufferedInputStream getInputStream() throws IOException {
|
||||
connect0();
|
||||
InputStream in = conn.getInputStream();
|
||||
if (progress != null) {
|
||||
in = new ProgressInputStream(in, conn.getContentLength(), progress) {
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
super.close();
|
||||
conn.disconnect();
|
||||
}
|
||||
};
|
||||
} else {
|
||||
in = new FilterInputStream(in) {
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
super.close();
|
||||
conn.disconnect();
|
||||
}
|
||||
};
|
||||
}
|
||||
InputStream in = new FilterInputStream(conn.getInputStream()) {
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
super.close();
|
||||
conn.disconnect();
|
||||
}
|
||||
};
|
||||
return new BufferedInputStream(in);
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.net;
|
||||
package com.topjohnwu.magisk.net;
|
||||
|
||||
public interface ResponseListener<T> {
|
||||
void onResponse(T response);
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.net;
|
||||
package com.topjohnwu.magisk.net;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
@@ -5,10 +5,16 @@ import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
|
||||
import com.topjohnwu.magisk.FileProvider;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class APKInstall {
|
||||
public static void install(Context c, File apk) {
|
||||
c.startActivity(installIntent(c, apk));
|
||||
}
|
||||
|
||||
public static Intent installIntent(Context c, File apk) {
|
||||
Intent install = new Intent(Intent.ACTION_INSTALL_PACKAGE);
|
||||
install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
@@ -18,6 +24,6 @@ public class APKInstall {
|
||||
apk.setReadable(true, false);
|
||||
install.setData(Uri.fromFile(apk));
|
||||
}
|
||||
c.startActivity(install);
|
||||
return install;
|
||||
}
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
package com.topjohnwu.magisk.utils;
|
||||
|
||||
import java.util.Enumeration;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
public class CompoundEnumeration<E> implements Enumeration<E> {
|
||||
private Enumeration<E>[] enums;
|
||||
private int index = 0;
|
||||
|
||||
@SafeVarargs
|
||||
public CompoundEnumeration(Enumeration<E> ...enums) {
|
||||
this.enums = enums;
|
||||
}
|
||||
|
||||
private boolean next() {
|
||||
while (index < enums.length) {
|
||||
if (enums[index] != null && enums[index].hasMoreElements()) {
|
||||
return true;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasMoreElements() {
|
||||
return next();
|
||||
}
|
||||
|
||||
public E nextElement() {
|
||||
if (!next()) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
return enums[index].nextElement();
|
||||
}
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
package com.topjohnwu.magisk.utils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.Enumeration;
|
||||
|
||||
import dalvik.system.DexClassLoader;
|
||||
|
||||
public class DynamicClassLoader extends DexClassLoader {
|
||||
|
||||
private ClassLoader base = Object.class.getClassLoader();
|
||||
|
||||
public DynamicClassLoader(File apk, ClassLoader parent) {
|
||||
super(apk.getPath(), apk.getParent(), null, parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||
// First check if already loaded
|
||||
Class cls = findLoadedClass(name);
|
||||
if (cls != null)
|
||||
return cls;
|
||||
|
||||
try {
|
||||
// Then check boot classpath
|
||||
return base.loadClass(name);
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
try {
|
||||
// Next try current dex
|
||||
return findClass(name);
|
||||
} catch (ClassNotFoundException fromSuper) {
|
||||
try {
|
||||
// Finally try parent
|
||||
return getParent().loadClass(name);
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw fromSuper;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getResource(String name) {
|
||||
URL resource = base.getResource(name);
|
||||
if (resource != null)
|
||||
return resource;
|
||||
resource = findResource(name);
|
||||
if (resource != null)
|
||||
return resource;
|
||||
resource = getParent().getResource(name);
|
||||
return resource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enumeration<URL> getResources(String name) throws IOException {
|
||||
return new CompoundEnumeration<>(base.getResources(name),
|
||||
findResources(name), getParent().getResources(name));
|
||||
}
|
||||
}
|
@@ -3,56 +3,54 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.topjohnwu.magisk">
|
||||
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="29" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:name="a.e"
|
||||
android:theme="@style/MagiskTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:allowBackup="false"
|
||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
|
||||
|
||||
<!-- Activities -->
|
||||
|
||||
<activity
|
||||
android:name="a.b"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:exported="true" />
|
||||
<!-- Splash -->
|
||||
<activity
|
||||
android:name="a.c"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:exported="true"
|
||||
android:theme="@style/SplashTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="a.f"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:screenOrientation="nosensor"
|
||||
android:theme="@style/MagiskTheme.Flashing" />
|
||||
|
||||
<!-- Main -->
|
||||
<activity android:name="a.b" />
|
||||
|
||||
<!-- Superuser -->
|
||||
|
||||
<activity
|
||||
android:name="a.m"
|
||||
android:exported="false"
|
||||
android:directBootAware="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:theme="@style/MagiskTheme.SU" />
|
||||
android:exported="false"
|
||||
tools:ignore="AppLinkUrlError">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Receiver -->
|
||||
|
||||
<receiver
|
||||
android:name="a.h"
|
||||
android:directBootAware="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.REBOOT" />
|
||||
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
@@ -63,15 +61,39 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- Service -->
|
||||
|
||||
<!-- DownloadService -->
|
||||
<service android:name="a.j" />
|
||||
|
||||
<!-- FileProvider -->
|
||||
<provider
|
||||
android:name="a.p"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:directBootAware="true"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
</provider>
|
||||
|
||||
<!-- Hardcode GMS version -->
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.version"
|
||||
android:value="12451000" />
|
||||
|
||||
<!-- Initialize WorkManager on-demand -->
|
||||
<provider
|
||||
android:name="androidx.work.impl.WorkManagerInitializer"
|
||||
android:authorities="${applicationId}.workmanager-init"
|
||||
tools:node="remove" />
|
||||
|
||||
<!-- We don't invalidate Room -->
|
||||
<service
|
||||
android:name="androidx.room.MultiInstanceInvalidationService"
|
||||
tools:node="remove"/>
|
||||
|
||||
<!-- We don't use Device Credentials -->
|
||||
<activity
|
||||
android:name="androidx.biometric.DeviceCredentialHandlerActivity"
|
||||
tools:node="remove" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
@@ -1,13 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.utils.PatchAPK;
|
||||
import com.topjohnwu.signing.BootSigner;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
@Keep
|
||||
public class a extends BootSigner {
|
||||
public static boolean patchAPK(String in, String out, String pkg) {
|
||||
return PatchAPK.patch(in, out, pkg);
|
||||
}
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.ui.MainActivity;
|
||||
|
||||
public class b extends MainActivity {
|
||||
/* stub */
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.ui.SplashActivity;
|
||||
|
||||
public class c extends SplashActivity {
|
||||
/* stub */
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.App;
|
||||
|
||||
public class e extends App {
|
||||
/* stub */
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.ui.flash.FlashActivity;
|
||||
|
||||
public class f extends FlashActivity {
|
||||
/* stub */
|
||||
}
|
@@ -1,15 +0,0 @@
|
||||
package a;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.topjohnwu.magisk.model.update.UpdateCheckService;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
public class g extends w<UpdateCheckService> {
|
||||
/* Stub */
|
||||
public g(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
||||
super(context, workerParams);
|
||||
}
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.model.receiver.GeneralReceiver;
|
||||
|
||||
public class h extends GeneralReceiver {
|
||||
/* stub */
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.model.download.DownloadModuleService;
|
||||
|
||||
public class j extends DownloadModuleService {
|
||||
/* stub */
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity;
|
||||
|
||||
public class m extends SuRequestActivity {
|
||||
/* stub */
|
||||
}
|
32
app/src/main/java/a/stubs.kt
Normal file
32
app/src/main/java/a/stubs.kt
Normal file
@@ -0,0 +1,32 @@
|
||||
@file:JvmName("a")
|
||||
package a
|
||||
|
||||
import com.topjohnwu.magisk.core.App
|
||||
import com.topjohnwu.magisk.core.Provider
|
||||
import com.topjohnwu.magisk.core.Receiver
|
||||
import com.topjohnwu.magisk.core.SplashActivity
|
||||
import com.topjohnwu.magisk.core.download.DownloadService
|
||||
import com.topjohnwu.magisk.ui.MainActivity
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
||||
import com.topjohnwu.signing.SignBoot
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
SignBoot.main(args)
|
||||
}
|
||||
|
||||
class b : MainActivity()
|
||||
|
||||
class c : SplashActivity()
|
||||
|
||||
class e : App {
|
||||
constructor() : super()
|
||||
constructor(o: Any) : super(o)
|
||||
}
|
||||
|
||||
class h : Receiver()
|
||||
|
||||
class j : DownloadService()
|
||||
|
||||
class m : SuRequestActivity()
|
||||
|
||||
class p : Provider()
|
@@ -1,42 +0,0 @@
|
||||
package a;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.topjohnwu.magisk.model.worker.DelegateWorker;
|
||||
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
public abstract class w<T extends DelegateWorker> extends Worker {
|
||||
|
||||
/* Wrapper class to workaround Proguard -keep class * extends Worker */
|
||||
|
||||
private T base;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
w(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
||||
super(context, workerParams);
|
||||
try {
|
||||
base = ((Class<T>) ((ParameterizedType) getClass().getGenericSuperclass())
|
||||
.getActualTypeArguments()[0]).newInstance();
|
||||
base.setActualWorker(this);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Result doWork() {
|
||||
if (base == null)
|
||||
return Result.failure();
|
||||
return base.doWork();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopped() {
|
||||
if (base != null)
|
||||
base.onStopped();
|
||||
}
|
||||
}
|
@@ -1,130 +0,0 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Configuration
|
||||
import android.os.AsyncTask
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.multidex.MultiDex
|
||||
import com.topjohnwu.magisk.data.database.MagiskDB
|
||||
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper
|
||||
import com.topjohnwu.magisk.di.koinModules
|
||||
import com.topjohnwu.magisk.utils.LocaleManager
|
||||
import com.topjohnwu.magisk.utils.RootUtils
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
import com.topjohnwu.net.Networking
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.context.startKoin
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.ThreadPoolExecutor
|
||||
|
||||
open class App : Application(), Application.ActivityLifecycleCallbacks {
|
||||
|
||||
lateinit var protectedContext: Context
|
||||
|
||||
@Deprecated("Use dependency injection")
|
||||
val prefs: SharedPreferences by inject()
|
||||
@Deprecated("Use dependency injection")
|
||||
val DB: MagiskDB by inject()
|
||||
@Deprecated("Use dependency injection")
|
||||
val repoDB: RepoDatabaseHelper by inject()
|
||||
|
||||
@Volatile
|
||||
private var foreground: Activity? = null
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base)
|
||||
if (BuildConfig.DEBUG)
|
||||
MultiDex.install(base)
|
||||
Timber.plant(Timber.DebugTree())
|
||||
|
||||
startKoin {
|
||||
androidContext(this@App)
|
||||
modules(koinModules)
|
||||
}
|
||||
|
||||
protectedContext = baseContext
|
||||
self = this
|
||||
deContext = base
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
protectedContext = base.createDeviceProtectedStorageContext()
|
||||
deContext = protectedContext
|
||||
deContext.moveSharedPreferencesFrom(base, base.defaultPrefsName)
|
||||
}
|
||||
|
||||
registerActivityLifecycleCallbacks(this)
|
||||
|
||||
Networking.init(base)
|
||||
LocaleManager.setLocale(this)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
LocaleManager.setLocale(this)
|
||||
}
|
||||
|
||||
//region ActivityLifecycleCallbacks
|
||||
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
||||
|
||||
override fun onActivityStarted(activity: Activity) {}
|
||||
|
||||
@Synchronized
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
foreground = activity
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
foreground = null
|
||||
}
|
||||
|
||||
override fun onActivityStopped(activity: Activity) {}
|
||||
|
||||
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {}
|
||||
//endregion
|
||||
|
||||
private val Context.defaultPrefsName get() = "${packageName}_preferences"
|
||||
|
||||
companion object {
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Deprecated("Use dependency injection")
|
||||
@JvmStatic
|
||||
lateinit var self: App
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Deprecated("Use dependency injection; replace with protectedContext")
|
||||
@JvmStatic
|
||||
lateinit var deContext: Context
|
||||
|
||||
@Deprecated("Use Rx or similar")
|
||||
@JvmField
|
||||
var THREAD_POOL: ThreadPoolExecutor
|
||||
|
||||
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(RootUtils::class.java)
|
||||
Shell.Config.setTimeout(2)
|
||||
THREAD_POOL = AsyncTask.THREAD_POOL_EXECUTOR as ThreadPoolExecutor
|
||||
}
|
||||
|
||||
@Deprecated("")
|
||||
@JvmStatic
|
||||
fun foreground(): Activity? {
|
||||
val app: App by inject()
|
||||
return app.foreground
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,31 +0,0 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import com.topjohnwu.magisk.model.download.DownloadModuleService;
|
||||
import com.topjohnwu.magisk.model.receiver.GeneralReceiver;
|
||||
import com.topjohnwu.magisk.model.update.UpdateCheckService;
|
||||
import com.topjohnwu.magisk.ui.MainActivity;
|
||||
import com.topjohnwu.magisk.ui.SplashActivity;
|
||||
import com.topjohnwu.magisk.ui.flash.FlashActivity;
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ClassMap {
|
||||
private static Map<Class, Class> classMap = new HashMap<>();
|
||||
|
||||
static {
|
||||
classMap.put(App.class, a.e.class);
|
||||
classMap.put(MainActivity.class, a.b.class);
|
||||
classMap.put(SplashActivity.class, a.c.class);
|
||||
classMap.put(FlashActivity.class, a.f.class);
|
||||
classMap.put(UpdateCheckService.class, a.g.class);
|
||||
classMap.put(GeneralReceiver.class, a.h.class);
|
||||
classMap.put(DownloadModuleService.class, a.j.class);
|
||||
classMap.put(SuRequestActivity.class, a.m.class);
|
||||
}
|
||||
|
||||
public static <T> Class<T> get(Class c) {
|
||||
return classMap.get(c);
|
||||
}
|
||||
}
|
@@ -1,396 +0,0 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Xml;
|
||||
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.ShellUtils;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import androidx.collection.ArrayMap;
|
||||
|
||||
public class Config {
|
||||
|
||||
// Current status
|
||||
public static String magiskVersionString;
|
||||
public static int magiskVersionCode = -1;
|
||||
private static boolean magiskHide;
|
||||
|
||||
// Update Info
|
||||
public static String remoteMagiskVersionString;
|
||||
public static int remoteMagiskVersionCode = -1;
|
||||
public static String magiskLink;
|
||||
public static String magiskNoteLink;
|
||||
public static String magiskMD5;
|
||||
public static String remoteManagerVersionString;
|
||||
public static int remoteManagerVersionCode = -1;
|
||||
public static String managerLink;
|
||||
public static String managerNoteLink;
|
||||
public static String uninstallerLink;
|
||||
|
||||
// Install flags
|
||||
public static boolean keepVerity = false;
|
||||
public static boolean keepEnc = false;
|
||||
public static boolean recovery = false;
|
||||
|
||||
public static int suLogTimeout = 14;
|
||||
|
||||
public static class Key {
|
||||
// su configs
|
||||
public static final String ROOT_ACCESS = "root_access";
|
||||
public static final String SU_MULTIUSER_MODE = "multiuser_mode";
|
||||
public static final String SU_MNT_NS = "mnt_ns";
|
||||
public static final String SU_MANAGER = "requester";
|
||||
public static final String SU_REQUEST_TIMEOUT = "su_request_timeout";
|
||||
public static final String SU_AUTO_RESPONSE = "su_auto_response";
|
||||
public static final String SU_NOTIFICATION = "su_notification";
|
||||
public static final String SU_REAUTH = "su_reauth";
|
||||
public static final String SU_FINGERPRINT = "su_fingerprint";
|
||||
|
||||
// prefs
|
||||
public static final String CHECK_UPDATES = "check_update";
|
||||
public static final String UPDATE_CHANNEL = "update_channel";
|
||||
public static final String CUSTOM_CHANNEL = "custom_channel";
|
||||
public static final String LOCALE = "locale";
|
||||
public static final String DARK_THEME = "dark_theme";
|
||||
public static final String ETAG_KEY = "ETag";
|
||||
public static final String REPO_ORDER = "repo_order";
|
||||
public static final String SHOW_SYSTEM_APP = "show_system";
|
||||
|
||||
// system state
|
||||
public static final String UPDATE_SERVICE_VER = "update_service_version";
|
||||
public static final String MAGISKHIDE = "magiskhide";
|
||||
public static final String COREONLY = "disable";
|
||||
}
|
||||
|
||||
public static class Value {
|
||||
public static final int DEFAULT_CHANNEL = -1;
|
||||
public static final int STABLE_CHANNEL = 0;
|
||||
public static final int BETA_CHANNEL = 1;
|
||||
public static final int CUSTOM_CHANNEL = 2;
|
||||
public static final int CANARY_CHANNEL = 3;
|
||||
public static final int CANARY_DEBUG_CHANNEL = 4;
|
||||
public static final int ROOT_ACCESS_DISABLED = 0;
|
||||
public static final int ROOT_ACCESS_APPS_ONLY = 1;
|
||||
public static final int ROOT_ACCESS_ADB_ONLY = 2;
|
||||
public static final int ROOT_ACCESS_APPS_AND_ADB = 3;
|
||||
public static final int MULTIUSER_MODE_OWNER_ONLY = 0;
|
||||
public static final int MULTIUSER_MODE_OWNER_MANAGED = 1;
|
||||
public static final int MULTIUSER_MODE_USER = 2;
|
||||
public static final int NAMESPACE_MODE_GLOBAL = 0;
|
||||
public static final int NAMESPACE_MODE_REQUESTER = 1;
|
||||
public static final int NAMESPACE_MODE_ISOLATE = 2;
|
||||
public static final int NO_NOTIFICATION = 0;
|
||||
public static final int NOTIFICATION_TOAST = 1;
|
||||
public static final int SU_PROMPT = 0;
|
||||
public static final int SU_AUTO_DENY = 1;
|
||||
public static final int SU_AUTO_ALLOW = 2;
|
||||
public static final int[] TIMEOUT_LIST = {0, -1, 10, 20, 30, 60};
|
||||
public static final int ORDER_NAME = 0;
|
||||
public static final int ORDER_DATE = 1;
|
||||
}
|
||||
|
||||
public static void loadMagiskInfo() {
|
||||
try {
|
||||
magiskVersionString = ShellUtils.fastCmd("magisk -v").split(":")[0];
|
||||
magiskVersionCode = Integer.parseInt(ShellUtils.fastCmd("magisk -V"));
|
||||
magiskHide = Shell.su("magiskhide --status").exec().isSuccess();
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
public static void export() {
|
||||
// Flush prefs to disk
|
||||
App app = App.self;
|
||||
app.getPrefs().edit().commit();
|
||||
File xml = new File(App.deContext.getFilesDir().getParent() + "/shared_prefs",
|
||||
app.getPackageName() + "_preferences.xml");
|
||||
Shell.su(Utils.fmt("cat %s > /data/adb/%s", xml, Const.MANAGER_CONFIGS)).exec();
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
SharedPreferences pref = App.self.getPrefs();
|
||||
SharedPreferences.Editor editor = pref.edit();
|
||||
File config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS);
|
||||
if (config.exists()) {
|
||||
try {
|
||||
SuFileInputStream is = new SuFileInputStream(config);
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||
parser.setInput(is, "UTF-8");
|
||||
parser.nextTag();
|
||||
parser.require(XmlPullParser.START_TAG, null, "map");
|
||||
while (parser.next() != XmlPullParser.END_TAG) {
|
||||
if (parser.getEventType() != XmlPullParser.START_TAG)
|
||||
continue;
|
||||
String key = parser.getAttributeValue(null, "name");
|
||||
String value = parser.getAttributeValue(null, "value");
|
||||
switch (parser.getName()) {
|
||||
case "string":
|
||||
parser.require(XmlPullParser.START_TAG, null, "string");
|
||||
editor.putString(key, parser.nextText());
|
||||
parser.require(XmlPullParser.END_TAG, null, "string");
|
||||
break;
|
||||
case "boolean":
|
||||
parser.require(XmlPullParser.START_TAG, null, "boolean");
|
||||
editor.putBoolean(key, Boolean.parseBoolean(value));
|
||||
parser.nextTag();
|
||||
parser.require(XmlPullParser.END_TAG, null, "boolean");
|
||||
break;
|
||||
case "int":
|
||||
parser.require(XmlPullParser.START_TAG, null, "int");
|
||||
editor.putInt(key, Integer.parseInt(value));
|
||||
parser.nextTag();
|
||||
parser.require(XmlPullParser.END_TAG, null, "int");
|
||||
break;
|
||||
case "long":
|
||||
parser.require(XmlPullParser.START_TAG, null, "long");
|
||||
editor.putLong(key, Long.parseLong(value));
|
||||
parser.nextTag();
|
||||
parser.require(XmlPullParser.END_TAG, null, "long");
|
||||
break;
|
||||
case "float":
|
||||
parser.require(XmlPullParser.START_TAG, null, "int");
|
||||
editor.putFloat(key, Float.parseFloat(value));
|
||||
parser.nextTag();
|
||||
parser.require(XmlPullParser.END_TAG, null, "int");
|
||||
break;
|
||||
default:
|
||||
parser.next();
|
||||
}
|
||||
}
|
||||
} catch (IOException | XmlPullParserException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
editor.remove(Key.ETAG_KEY);
|
||||
editor.apply();
|
||||
editor = pref.edit();
|
||||
config.delete();
|
||||
}
|
||||
|
||||
// Set defaults if not set
|
||||
setDefs(pref, editor);
|
||||
|
||||
// These settings are from actual device state
|
||||
editor.putBoolean(Key.MAGISKHIDE, magiskHide)
|
||||
.putBoolean(Key.COREONLY, Const.MAGISK_DISABLE_FILE.exists())
|
||||
.putInt(Key.UPDATE_SERVICE_VER, Const.UPDATE_SERVICE_VER)
|
||||
.apply();
|
||||
}
|
||||
|
||||
private static final int PREF_INT = 0;
|
||||
private static final int PREF_STR_INT = 1;
|
||||
private static final int PREF_BOOL = 2;
|
||||
private static final int PREF_STR = 3;
|
||||
private static final int DB_INT = 4;
|
||||
private static final int DB_BOOL = 5;
|
||||
private static final int DB_STR = 6;
|
||||
|
||||
private static int getConfigType(String key) {
|
||||
switch (key) {
|
||||
case Key.REPO_ORDER:
|
||||
return PREF_INT;
|
||||
|
||||
case Key.SU_REQUEST_TIMEOUT:
|
||||
case Key.SU_AUTO_RESPONSE:
|
||||
case Key.SU_NOTIFICATION:
|
||||
case Key.UPDATE_CHANNEL:
|
||||
return PREF_STR_INT;
|
||||
|
||||
case Key.DARK_THEME:
|
||||
case Key.SU_REAUTH:
|
||||
case Key.CHECK_UPDATES:
|
||||
case Key.MAGISKHIDE:
|
||||
case Key.COREONLY:
|
||||
case Key.SHOW_SYSTEM_APP:
|
||||
return PREF_BOOL;
|
||||
|
||||
case Key.CUSTOM_CHANNEL:
|
||||
case Key.LOCALE:
|
||||
case Key.ETAG_KEY:
|
||||
return PREF_STR;
|
||||
|
||||
case Key.ROOT_ACCESS:
|
||||
case Key.SU_MNT_NS:
|
||||
case Key.SU_MULTIUSER_MODE:
|
||||
return DB_INT;
|
||||
|
||||
case Key.SU_FINGERPRINT:
|
||||
return DB_BOOL;
|
||||
|
||||
case Key.SU_MANAGER:
|
||||
return DB_STR;
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T get(String key) {
|
||||
App app = App.self;
|
||||
switch (getConfigType(key)) {
|
||||
case PREF_INT:
|
||||
return (T) (Integer) app.getPrefs().getInt(key, getDef(key));
|
||||
case PREF_STR_INT:
|
||||
return (T) (Integer) Utils.getPrefsInt(app.getPrefs(), key, getDef(key));
|
||||
case PREF_BOOL:
|
||||
return (T) (Boolean) app.getPrefs().getBoolean(key, getDef(key));
|
||||
case PREF_STR:
|
||||
return (T) app.getPrefs().getString(key, getDef(key));
|
||||
case DB_INT:
|
||||
return (T) (Integer) app.getDB().getSettings(key, getDef(key));
|
||||
case DB_BOOL:
|
||||
return (T) (Boolean) (app.getDB().getSettings(key, getDef(key) ? 1 : 0) != 0);
|
||||
case DB_STR:
|
||||
return (T) app.getDB().getStrings(key, getDef(key));
|
||||
}
|
||||
/* Will never get here (IllegalArgumentException in getConfigType) */
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void set(String key, Object val) {
|
||||
App app = App.self;
|
||||
switch (getConfigType(key)) {
|
||||
case PREF_INT:
|
||||
app.getPrefs().edit().putInt(key, (int) val).apply();
|
||||
break;
|
||||
case PREF_STR_INT:
|
||||
app.getPrefs().edit().putString(key, String.valueOf(val)).apply();
|
||||
break;
|
||||
case PREF_BOOL:
|
||||
app.getPrefs().edit().putBoolean(key, (boolean) val).apply();
|
||||
break;
|
||||
case PREF_STR:
|
||||
app.getPrefs().edit().putString(key, (String) val).apply();
|
||||
break;
|
||||
case DB_INT:
|
||||
app.getDB().setSettings(key, (int) val);
|
||||
break;
|
||||
case DB_BOOL:
|
||||
app.getDB().setSettings(key, (boolean) val ? 1 : 0);
|
||||
break;
|
||||
case DB_STR:
|
||||
app.getDB().setStrings(key, (String) val);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void remove(String key) {
|
||||
App app = App.self;
|
||||
switch (getConfigType(key)) {
|
||||
case PREF_INT:
|
||||
case PREF_STR_INT:
|
||||
case PREF_BOOL:
|
||||
case PREF_STR:
|
||||
app.getPrefs().edit().remove(key).apply();
|
||||
break;
|
||||
case DB_BOOL:
|
||||
case DB_INT:
|
||||
app.getDB().rmSettings(key);
|
||||
break;
|
||||
case DB_STR:
|
||||
app.getDB().setStrings(key, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static ArrayMap<String, Object> defs = new ArrayMap<>();
|
||||
|
||||
static {
|
||||
/* Set default configurations */
|
||||
|
||||
// prefs int
|
||||
defs.put(Key.REPO_ORDER, Value.ORDER_DATE);
|
||||
|
||||
// prefs string int
|
||||
defs.put(Key.SU_REQUEST_TIMEOUT, 10);
|
||||
defs.put(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT);
|
||||
defs.put(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST);
|
||||
defs.put(Key.UPDATE_CHANNEL, Utils.isCanary() ?
|
||||
Value.CANARY_DEBUG_CHANNEL : Value.DEFAULT_CHANNEL);
|
||||
|
||||
// prefs bool
|
||||
defs.put(Key.CHECK_UPDATES, true);
|
||||
defs.put(Key.DARK_THEME, true);
|
||||
//defs.put(Key.SU_REAUTH, false);
|
||||
//defs.put(Key.SHOW_SYSTEM_APP, false);
|
||||
|
||||
// prefs string
|
||||
defs.put(Key.CUSTOM_CHANNEL, "");
|
||||
defs.put(Key.LOCALE, "");
|
||||
//defs.put(Key.ETAG_KEY, null);
|
||||
|
||||
// db int
|
||||
defs.put(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB);
|
||||
defs.put(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER);
|
||||
defs.put(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY);
|
||||
|
||||
// db bool
|
||||
//defs.put(Key.SU_FINGERPRINT, false);
|
||||
|
||||
// db strings
|
||||
//defs.put(Key.SU_MANAGER, null);
|
||||
}
|
||||
|
||||
private static <T> T getDef(String key) {
|
||||
Object val = defs.get(key);
|
||||
switch (getConfigType(key)) {
|
||||
case PREF_INT:
|
||||
case DB_INT:
|
||||
case PREF_STR_INT:
|
||||
return val != null ? (T) val : (T) (Integer) 0;
|
||||
case DB_BOOL:
|
||||
case PREF_BOOL:
|
||||
return val != null ? (T) val : (T) (Boolean) false;
|
||||
case DB_STR:
|
||||
case PREF_STR:
|
||||
return (T) val;
|
||||
}
|
||||
/* Will never get here (IllegalArgumentException in getConfigType) */
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void setDefs(SharedPreferences pref, SharedPreferences.Editor editor) {
|
||||
App app = App.self;
|
||||
for (String key : defs.keySet()) {
|
||||
int type = getConfigType(key);
|
||||
switch (type) {
|
||||
case DB_INT:
|
||||
editor.putString(key, String.valueOf(
|
||||
app.getDB().getSettings(key, (Integer) defs.get(key))));
|
||||
continue;
|
||||
case DB_STR:
|
||||
editor.putString(key, app.getDB().getStrings(key, (String) defs.get(key)));
|
||||
continue;
|
||||
case DB_BOOL:
|
||||
int bs = app.getDB().getSettings(key, -1);
|
||||
editor.putBoolean(key, bs < 0 ? (Boolean) defs.get(key) : bs != 0);
|
||||
continue;
|
||||
}
|
||||
if (pref.contains(key))
|
||||
continue;
|
||||
switch (type) {
|
||||
case PREF_INT:
|
||||
editor.putInt(key, (Integer) defs.get(key));
|
||||
break;
|
||||
case PREF_STR_INT:
|
||||
editor.putString(key, String.valueOf(defs.get(key)));
|
||||
break;
|
||||
case PREF_STR:
|
||||
editor.putString(key, (String) defs.get(key));
|
||||
break;
|
||||
case PREF_BOOL:
|
||||
editor.putBoolean(key, (Boolean) defs.get(key));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,102 +0,0 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import android.os.Environment;
|
||||
import android.os.Process;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class Const {
|
||||
|
||||
public static final String DEBUG_TAG = "MagiskManager";
|
||||
|
||||
// APK content
|
||||
public static final String ANDROID_MANIFEST = "AndroidManifest.xml";
|
||||
|
||||
public static final String SU_KEYSTORE_KEY = "su_key";
|
||||
|
||||
// Paths
|
||||
public static final String MAGISK_PATH = "/sbin/.magisk/img";
|
||||
public static final File EXTERNAL_PATH;
|
||||
public static File MAGISK_DISABLE_FILE;
|
||||
|
||||
static {
|
||||
MAGISK_DISABLE_FILE = new File("xxx");
|
||||
EXTERNAL_PATH = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||
EXTERNAL_PATH.mkdirs();
|
||||
}
|
||||
|
||||
public static final String TMP_FOLDER_PATH = "/dev/tmp";
|
||||
public static final String MAGISK_LOG = "/cache/magisk.log";
|
||||
public static final String MANAGER_CONFIGS = ".tmp.magisk.config";
|
||||
|
||||
// Versions
|
||||
public static final int UPDATE_SERVICE_VER = 1;
|
||||
public static final int SNET_EXT_VER = 12;
|
||||
|
||||
public static final int USER_ID = Process.myUid() / 100000;
|
||||
|
||||
// Generic
|
||||
public static final String MAGISK_INSTALL_LOG_FILENAME = "magisk_install_log_%s.log";
|
||||
|
||||
public static final class MAGISK_VER {
|
||||
public static final int MIN_SUPPORT = 18000;
|
||||
}
|
||||
|
||||
public static class ID {
|
||||
public static final int FETCH_ZIP = 2;
|
||||
public static final int SELECT_BOOT = 3;
|
||||
|
||||
// notifications
|
||||
public static final int MAGISK_UPDATE_NOTIFICATION_ID = 4;
|
||||
public static final int APK_UPDATE_NOTIFICATION_ID = 5;
|
||||
public static final int DTBO_NOTIFICATION_ID = 7;
|
||||
public static final int HIDE_MANAGER_NOTIFICATION_ID = 8;
|
||||
public static final String UPDATE_NOTIFICATION_CHANNEL = "update";
|
||||
public static final String PROGRESS_NOTIFICATION_CHANNEL = "progress";
|
||||
public static final String CHECK_MAGISK_UPDATE_WORKER_ID = "magisk_update";
|
||||
}
|
||||
|
||||
public static class Url {
|
||||
private static String getRaw(String where, String name) {
|
||||
return String.format("https://raw.githubusercontent.com/topjohnwu/magisk_files/%s/%s", where, name);
|
||||
}
|
||||
public static final String STABLE_URL = getRaw("master", "stable.json");
|
||||
public static final String BETA_URL = getRaw("master", "beta.json");
|
||||
public static final String CANARY_URL = getRaw("master", "canary_builds/release.json");
|
||||
public static final String CANARY_DEBUG_URL = getRaw("master", "canary_builds/canary.json");
|
||||
public static final String REPO_URL = "https://api.github.com/users/Magisk-Modules-Repo/repos?per_page=100&sort=pushed&page=%d";
|
||||
public static final String FILE_URL = "https://raw.githubusercontent.com/Magisk-Modules-Repo/%s/master/%s";
|
||||
public static final String ZIP_URL = "https://github.com/Magisk-Modules-Repo/%s/archive/master.zip";
|
||||
public static final String MODULE_INSTALLER = "https://raw.githubusercontent.com/topjohnwu/Magisk/master/scripts/module_installer.sh";
|
||||
public static final String PAYPAL_URL = "https://www.paypal.me/topjohnwu";
|
||||
public static final String PATREON_URL = "https://www.patreon.com/topjohnwu";
|
||||
public static final String TWITTER_URL = "https://twitter.com/topjohnwu";
|
||||
public static final String XDA_THREAD = "http://forum.xda-developers.com/showthread.php?t=3432382";
|
||||
public static final String SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk";
|
||||
public static final String SNET_URL = getRaw("b66b1a914978e5f4c4bbfd74a59f4ad371bac107", "snet.apk");
|
||||
public static final String BOOTCTL_URL = getRaw("9c5dfc1b8245c0b5b524901ef0ff0f8335757b77", "bootctl");
|
||||
}
|
||||
|
||||
public static class Key {
|
||||
// others
|
||||
public static final String LINK_KEY = "Link";
|
||||
public static final String IF_NONE_MATCH = "If-None-Match";
|
||||
// intents
|
||||
public static final String OPEN_SECTION = "section";
|
||||
public static final String INTENT_SET_NAME = "filename";
|
||||
public static final String INTENT_SET_LINK = "link";
|
||||
public static final String FLASH_ACTION = "action";
|
||||
public static final String BROADCAST_MANAGER_UPDATE = "manager_update";
|
||||
public static final String BROADCAST_REBOOT = "reboot";
|
||||
}
|
||||
|
||||
public static class Value {
|
||||
public static final String FLASH_ZIP = "flash";
|
||||
public static final String PATCH_FILE = "patch";
|
||||
public static final String FLASH_MAGISK = "magisk";
|
||||
public static final String FLASH_INACTIVE_SLOT = "slot";
|
||||
public static final String UNINSTALL = "uninstall";
|
||||
}
|
||||
|
||||
|
||||
}
|
140
app/src/main/java/com/topjohnwu/magisk/arch/BaseUIActivity.kt
Normal file
140
app/src/main/java/com/topjohnwu/magisk/arch/BaseUIActivity.kt
Normal file
@@ -0,0 +1,140 @@
|
||||
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.lifecycle.MutableLiveData
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDirections
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||
import com.topjohnwu.magisk.ui.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(navHost) as? NavHostFragment
|
||||
}
|
||||
private val topFragment get() = navHostFragment?.childFragmentManager?.fragments?.getOrNull(0)
|
||||
protected val currentFragment get() = topFragment as? BaseUIFragment<*, *>
|
||||
|
||||
override val viewRoot: View get() = binding.root
|
||||
open val navigation: NavController? get() = navHostFragment?.navController
|
||||
|
||||
open val navHost: Int = 0
|
||||
open val snackbarView get() = binding.root
|
||||
|
||||
init {
|
||||
val theme = Config.darkTheme
|
||||
AppCompatDelegate.setDefaultNightMode(theme)
|
||||
}
|
||||
|
||||
override fun 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) }
|
||||
|
||||
directionsDispatcher.observe(this) {
|
||||
it?.navigate()
|
||||
// we don't want the directions to be re-dispatched, so we preemptively set them to null
|
||||
if (it != null) {
|
||||
directionsDispatcher.value = null
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val directionsDispatcher = MutableLiveData<NavDirections?>()
|
||||
|
||||
fun postDirections(navDirections: NavDirections) =
|
||||
directionsDispatcher.postValue(navDirections)
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -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) {}
|
||||
}
|
@@ -0,0 +1,87 @@
|
||||
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.ktx.startAnimations
|
||||
|
||||
abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
|
||||
Fragment(), BaseUIComponent<VM> {
|
||||
|
||||
val activity get() = requireActivity() as BaseUIActivity<*, *>
|
||||
protected lateinit var binding: Binding
|
||||
protected abstract val layoutRes: Int
|
||||
|
||||
override val viewRoot: View get() = binding.root
|
||||
private val navigation get() = activity.navigation
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
startObserveEvents()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
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) = 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
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.requestRefresh()
|
||||
}
|
||||
|
||||
protected open fun onPreBind(binding: Binding) {
|
||||
(binding.root as? ViewGroup)?.startAnimations()
|
||||
}
|
||||
|
||||
fun NavDirections.navigate() {
|
||||
navigation?.navigate(this)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface ReselectionTarget {
|
||||
|
||||
fun onReselected()
|
||||
|
||||
}
|
114
app/src/main/java/com/topjohnwu/magisk/arch/BaseViewModel.kt
Normal file
114
app/src/main/java/com/topjohnwu/magisk/arch/BaseViewModel.kt
Normal file
@@ -0,0 +1,114 @@
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
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.publish() {
|
||||
_viewEvents.postValue(NavigationEvent(this))
|
||||
}
|
||||
|
||||
}
|
48
app/src/main/java/com/topjohnwu/magisk/arch/Helpers.kt
Normal file
48
app/src/main/java/com/topjohnwu/magisk/arch/Helpers.kt
Normal file
@@ -0,0 +1,48 @@
|
||||
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
|
||||
|
||||
fun <T : ComparableRvItem<*>> diffListOf(
|
||||
vararg newItems: T
|
||||
) = diffListOf(newItems.toList())
|
||||
|
||||
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) }
|
||||
|
||||
fun <T : ComparableRvItem<*>> filterableListOf(
|
||||
vararg newItems: T
|
||||
) = FilterableDiffObservableList(object : DiffObservableList.Callback<T> {
|
||||
override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.genericItemSameAs(newItem)
|
||||
override fun areContentsTheSame(oldItem: T, newItem: T) = oldItem.genericContentSameAs(newItem)
|
||||
}).also { it.update(newItems.toList()) }
|
||||
|
||||
fun <T : RvItem> adapterOf() = object : BindingRecyclerViewAdapter<T>() {
|
||||
override fun onBindBinding(
|
||||
binding: ViewDataBinding,
|
||||
variableId: Int,
|
||||
layoutRes: Int,
|
||||
position: Int,
|
||||
item: T
|
||||
) {
|
||||
super.onBindBinding(binding, variableId, layoutRes, position, item)
|
||||
item.onBindingBound(binding)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T : RvItem> itemBindingOf(
|
||||
crossinline body: (ItemBinding<*>) -> Unit = {}
|
||||
) = OnItemBind<T> { itemBinding, _, item ->
|
||||
item.bind(itemBinding)
|
||||
body(itemBinding)
|
||||
}
|
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<*, *>)
|
||||
}
|
106
app/src/main/java/com/topjohnwu/magisk/core/App.kt
Normal file
106
app/src/main/java/com/topjohnwu/magisk/core/App.kt
Normal file
@@ -0,0 +1,106 @@
|
||||
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.core.utils.IODispatcherExecutor
|
||||
import com.topjohnwu.magisk.core.utils.RootInit
|
||||
import com.topjohnwu.magisk.core.utils.updateConfig
|
||||
import com.topjohnwu.magisk.di.koinModules
|
||||
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 kotlin.system.exitProcess
|
||||
|
||||
open class App() : Application() {
|
||||
|
||||
constructor(o: Any) : this() {
|
||||
Info.stub = DynAPK.load(o)
|
||||
}
|
||||
|
||||
init {
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
Shell.setDefaultBuilder(Shell.Builder.create()
|
||||
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
||||
.setInitializers(RootInit::class.java)
|
||||
.setTimeout(2))
|
||||
Shell.EXECUTOR = IODispatcherExecutor()
|
||||
|
||||
// Always log full stack trace with Timber
|
||||
Timber.plant(Timber.DebugTree())
|
||||
Thread.setDefaultUncaughtExceptionHandler { _, e ->
|
||||
Timber.e(e)
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
// Basic setup
|
||||
if (BuildConfig.DEBUG)
|
||||
MultiDex.install(base)
|
||||
|
||||
// Some context magic
|
||||
val app: Application
|
||||
val impl: Context
|
||||
if (base is Application) {
|
||||
app = base
|
||||
impl = base.baseContext
|
||||
} else {
|
||||
app = this
|
||||
impl = base
|
||||
}
|
||||
val wrapped = impl.wrap()
|
||||
super.attachBaseContext(wrapped)
|
||||
|
||||
// Normal startup
|
||||
startKoin {
|
||||
androidContext(wrapped)
|
||||
modules(koinModules)
|
||||
}
|
||||
ResMgr.init(impl)
|
||||
app.registerActivityLifecycleCallbacks(ForegroundTracker)
|
||||
WorkManager.initialize(impl.wrapJob(), androidx.work.Configuration.Builder().build())
|
||||
}
|
||||
|
||||
// This is required as some platforms expect ContextImpl
|
||||
override fun getBaseContext(): Context {
|
||||
return super.getBaseContext().unwrap()
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
resources.updateConfig(newConfig)
|
||||
if (!isRunningAsStub)
|
||||
super.onConfigurationChanged(newConfig)
|
||||
}
|
||||
}
|
||||
|
||||
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) {}
|
||||
}
|
236
app/src/main/java/com/topjohnwu/magisk/core/Config.kt
Normal file
236
app/src/main/java/com/topjohnwu/magisk/core/Config.kt
Normal file
@@ -0,0 +1,236 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Xml
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.edit
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
|
||||
import com.topjohnwu.magisk.core.magiskdb.StringDao
|
||||
import com.topjohnwu.magisk.core.utils.BiometricHelper
|
||||
import com.topjohnwu.magisk.core.utils.refreshLocale
|
||||
import com.topjohnwu.magisk.data.preference.PreferenceModel
|
||||
import com.topjohnwu.magisk.data.repository.DBConfig
|
||||
import com.topjohnwu.magisk.di.Protected
|
||||
import com.topjohnwu.magisk.ktx.inject
|
||||
import com.topjohnwu.magisk.ui.theme.Theme
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
object Config : PreferenceModel, DBConfig {
|
||||
|
||||
override val stringDao: StringDao by inject()
|
||||
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"
|
||||
const val SU_MULTIUSER_MODE = "multiuser_mode"
|
||||
const val SU_MNT_NS = "mnt_ns"
|
||||
const val SU_BIOMETRIC = "su_biometric"
|
||||
const val SU_MANAGER = "requester"
|
||||
const val KEYSTORE = "keystore"
|
||||
|
||||
// prefs
|
||||
const val SU_REQUEST_TIMEOUT = "su_request_timeout"
|
||||
const val SU_AUTO_RESPONSE = "su_auto_response"
|
||||
const val SU_NOTIFICATION = "su_notification"
|
||||
const val SU_REAUTH = "su_reauth"
|
||||
const val 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_extended"
|
||||
const val REPO_ORDER = "repo_order"
|
||||
const val SHOW_SYSTEM_APP = "show_system"
|
||||
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 ASKED_HOME = "asked_home"
|
||||
const val DOH = "doh"
|
||||
|
||||
// system state
|
||||
const val MAGISKHIDE = "magiskhide"
|
||||
}
|
||||
|
||||
object Value {
|
||||
// Update channels
|
||||
const val DEFAULT_CHANNEL = -1
|
||||
const val STABLE_CHANNEL = 0
|
||||
const val BETA_CHANNEL = 1
|
||||
const val CUSTOM_CHANNEL = 2
|
||||
const val CANARY_CHANNEL = 3
|
||||
|
||||
// root access mode
|
||||
const val ROOT_ACCESS_DISABLED = 0
|
||||
const val ROOT_ACCESS_APPS_ONLY = 1
|
||||
const val ROOT_ACCESS_ADB_ONLY = 2
|
||||
const val ROOT_ACCESS_APPS_AND_ADB = 3
|
||||
|
||||
// su multiuser
|
||||
const val MULTIUSER_MODE_OWNER_ONLY = 0
|
||||
const val MULTIUSER_MODE_OWNER_MANAGED = 1
|
||||
const val MULTIUSER_MODE_USER = 2
|
||||
|
||||
// su mnt ns
|
||||
const val NAMESPACE_MODE_GLOBAL = 0
|
||||
const val NAMESPACE_MODE_REQUESTER = 1
|
||||
const val NAMESPACE_MODE_ISOLATE = 2
|
||||
|
||||
// su notification
|
||||
const val NO_NOTIFICATION = 0
|
||||
const val NOTIFICATION_TOAST = 1
|
||||
|
||||
// su auto response
|
||||
const val SU_PROMPT = 0
|
||||
const val SU_AUTO_DENY = 1
|
||||
const val SU_AUTO_ALLOW = 2
|
||||
|
||||
// su timeout
|
||||
val TIMEOUT_LIST = intArrayOf(0, -1, 10, 20, 30, 60)
|
||||
|
||||
// repo order
|
||||
const val ORDER_NAME = 0
|
||||
const val ORDER_DATE = 1
|
||||
}
|
||||
|
||||
private val defaultChannel =
|
||||
if (BuildConfig.DEBUG)
|
||||
Value.CANARY_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 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 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 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)
|
||||
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
|
||||
|
||||
var customChannelUrl by preference(Key.CUSTOM_CHANNEL, "")
|
||||
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)
|
||||
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
|
||||
var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
|
||||
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
||||
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
|
||||
|
||||
private const val SU_FINGERPRINT = "su_fingerprint"
|
||||
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
||||
prefs.edit {
|
||||
// Settings migration
|
||||
if (prefs.getBoolean(SU_FINGERPRINT, false))
|
||||
suBiometric = true
|
||||
remove(SU_FINGERPRINT)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
private fun SharedPreferences.Editor.parsePrefs(input: InputStream) {
|
||||
runCatching {
|
||||
val parser = Xml.newPullParser()
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
|
||||
parser.setInput(input, "UTF-8")
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.START_TAG, null, "map")
|
||||
while (parser.next() != XmlPullParser.END_TAG) {
|
||||
if (parser.eventType != XmlPullParser.START_TAG)
|
||||
continue
|
||||
val key: String = parser.getAttributeValue(null, "name")
|
||||
fun value() = parser.getAttributeValue(null, "value")!!
|
||||
when (parser.name) {
|
||||
"string" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "string")
|
||||
putString(key, parser.nextText())
|
||||
parser.require(XmlPullParser.END_TAG, null, "string")
|
||||
}
|
||||
"boolean" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "boolean")
|
||||
putBoolean(key, value().toBoolean())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "boolean")
|
||||
}
|
||||
"int" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "int")
|
||||
putInt(key, value().toInt())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "int")
|
||||
}
|
||||
"long" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "long")
|
||||
putLong(key, value().toLong())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "long")
|
||||
}
|
||||
"float" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "int")
|
||||
putFloat(key, value().toFloat())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "int")
|
||||
}
|
||||
else -> parser.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
77
app/src/main/java/com/topjohnwu/magisk/core/Const.kt
Normal file
77
app/src/main/java/com/topjohnwu/magisk/core/Const.kt
Normal file
@@ -0,0 +1,77 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.os.Process
|
||||
|
||||
object Const {
|
||||
|
||||
// Paths
|
||||
lateinit var MAGISKTMP: String
|
||||
val MAGISK_PATH get() = "$MAGISKTMP/modules"
|
||||
const val TMP_FOLDER_PATH = "/dev/tmp"
|
||||
const val MAGISK_LOG = "/cache/magisk.log"
|
||||
|
||||
// Versions
|
||||
const val SNET_EXT_VER = 15
|
||||
const val SNET_REVISION = "18ab78817087c337ae0edd1ecac38aec49217880"
|
||||
const val BOOTCTL_REVISION = "18ab78817087c337ae0edd1ecac38aec49217880"
|
||||
|
||||
// Misc
|
||||
val USER_ID = Process.myUid() / 100000
|
||||
|
||||
object Version {
|
||||
const val MIN_VERSION = "v19.0"
|
||||
const val MIN_VERCODE = 19000
|
||||
|
||||
fun atLeast_20_2() = Info.env.magiskVersionCode >= 20200 || isCanary()
|
||||
fun atLeast_20_4() = Info.env.magiskVersionCode >= 20400 || isCanary()
|
||||
fun atLeast_21_0() = Info.env.magiskVersionCode >= 21000 || isCanary()
|
||||
fun isCanary() = Info.env.magiskVersionCode % 100 != 0
|
||||
}
|
||||
|
||||
object ID {
|
||||
const val FETCH_ZIP = 2
|
||||
const val SELECT_FILE = 3
|
||||
const val MAX_ACTIVITY_RESULT = 10
|
||||
|
||||
// notifications
|
||||
const val MAGISK_UPDATE_NOTIFICATION_ID = 4
|
||||
const val APK_UPDATE_NOTIFICATION_ID = 5
|
||||
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 PATREON_URL = "https://www.patreon.com/topjohnwu"
|
||||
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
|
||||
|
||||
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com/"
|
||||
const val GITHUB_API_URL = "https://api.github.com/"
|
||||
const val GITHUB_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 {
|
||||
// intents
|
||||
const val OPEN_SECTION = "section"
|
||||
const val PREV_PKG = "prev_pkg"
|
||||
}
|
||||
|
||||
object Value {
|
||||
const val FLASH_ZIP = "flash"
|
||||
const val PATCH_FILE = "patch"
|
||||
const val FLASH_MAGISK = "magisk"
|
||||
const val FLASH_INACTIVE_SLOT = "slot"
|
||||
const val UNINSTALL = "uninstall"
|
||||
}
|
||||
|
||||
object Nav {
|
||||
const val HOME = "home"
|
||||
const val SETTINGS = "settings"
|
||||
const val HIDE = "hide"
|
||||
const val MODULES = "modules"
|
||||
const val SUPERUSER = "superuser"
|
||||
}
|
||||
}
|
173
app/src/main/java/com/topjohnwu/magisk/core/Hacks.kt
Normal file
173
app/src/main/java/com/topjohnwu/magisk/core/Hacks.kt
Normal file
@@ -0,0 +1,173 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.job.JobInfo
|
||||
import android.app.job.JobScheduler
|
||||
import android.app.job.JobWorkItem
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.content.res.AssetManager
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
import com.topjohnwu.magisk.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.ktx.forceGetDeclaredField
|
||||
import com.topjohnwu.magisk.ui.MainActivity
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
||||
|
||||
fun AssetManager.addAssetPath(path: String) {
|
||||
DynAPK.addAssetPath(this, path)
|
||||
}
|
||||
|
||||
fun Context.wrap(global: Boolean = true): Context =
|
||||
if (global) GlobalResContext(this) else ResContext(this)
|
||||
|
||||
fun Context.wrapJob(): Context = object : GlobalResContext(this) {
|
||||
|
||||
override fun getApplicationContext(): Context {
|
||||
return this
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
override fun getSystemService(name: String): Any? {
|
||||
return if (!isRunningAsStub) super.getSystemService(name) else
|
||||
when (name) {
|
||||
Context.JOB_SCHEDULER_SERVICE ->
|
||||
JobSchedulerWrapper(super.getSystemService(name) as JobScheduler)
|
||||
else -> super.getSystemService(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Class<*>.cmp(pkg: String): ComponentName {
|
||||
val name = ClassMap[this].name
|
||||
return ComponentName(pkg, Info.stub?.classToComponent?.get(name) ?: name)
|
||||
}
|
||||
|
||||
inline fun <reified T> 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!!
|
||||
}
|
||||
|
||||
override fun createConfigurationContext(config: Configuration): Context {
|
||||
return ResContext(super.createConfigurationContext(config))
|
||||
}
|
||||
}
|
||||
|
||||
private class ResContext(base: Context) : GlobalResContext(base) {
|
||||
override val mRes by lazy { base.resources.patch() }
|
||||
|
||||
private fun Resources.patch(): Resources {
|
||||
updateConfig()
|
||||
if (isRunningAsStub)
|
||||
assets.addAssetPath(ResMgr.apk)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
object ResMgr {
|
||||
|
||||
lateinit var resource: Resources
|
||||
lateinit var apk: String
|
||||
|
||||
fun init(context: Context) {
|
||||
resource = context.resources
|
||||
refreshLocale()
|
||||
if (isRunningAsStub) {
|
||||
apk = DynAPK.current(context).path
|
||||
resource.assets.addAssetPath(apk)
|
||||
} else {
|
||||
apk = context.packageResourcePath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(28)
|
||||
private class JobSchedulerWrapper(private val base: JobScheduler) : JobScheduler() {
|
||||
|
||||
override fun schedule(job: JobInfo): Int {
|
||||
return base.schedule(job.patch())
|
||||
}
|
||||
|
||||
override fun enqueue(job: JobInfo, work: JobWorkItem): Int {
|
||||
return base.enqueue(job.patch(), work)
|
||||
}
|
||||
|
||||
override fun cancel(jobId: Int) {
|
||||
base.cancel(jobId)
|
||||
}
|
||||
|
||||
override fun cancelAll() {
|
||||
base.cancelAll()
|
||||
}
|
||||
|
||||
override fun getAllPendingJobs(): List<JobInfo> {
|
||||
return base.allPendingJobs
|
||||
}
|
||||
|
||||
override fun getPendingJob(jobId: Int): JobInfo? {
|
||||
return base.getPendingJob(jobId)
|
||||
}
|
||||
|
||||
private fun JobInfo.patch(): JobInfo {
|
||||
// We need to swap out the service of JobInfo
|
||||
val name = service.className
|
||||
val component = ComponentName(
|
||||
service.packageName,
|
||||
Info.stubChk.classToComponent[name] ?: name
|
||||
)
|
||||
|
||||
javaClass.forceGetDeclaredField("service")?.set(this, component)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
private 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,
|
||||
Receiver::class.java to a.h::class.java,
|
||||
DownloadService::class.java to a.j::class.java,
|
||||
SuRequestActivity::class.java to a.m::class.java
|
||||
)
|
||||
|
||||
operator fun get(c: Class<*>) = map.getOrElse(c) { c }
|
||||
}
|
||||
|
||||
// Keep a reference to these resources to prevent it from
|
||||
// being removed when running "remove unused resources"
|
||||
val shouldKeepResources = listOf(
|
||||
R.string.no_info_provided,
|
||||
R.string.release_notes,
|
||||
R.string.invalid_update_channel,
|
||||
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
|
||||
)
|
87
app/src/main/java/com/topjohnwu/magisk/core/Info.kt
Normal file
87
app/src/main/java/com/topjohnwu/magisk/core/Info.kt
Normal file
@@ -0,0 +1,87 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import androidx.databinding.ObservableBoolean
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
import com.topjohnwu.magisk.core.model.UpdateInfo
|
||||
import com.topjohnwu.magisk.core.utils.net.NetworkObserver
|
||||
import com.topjohnwu.magisk.ktx.get
|
||||
import com.topjohnwu.magisk.utils.CachedValue
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils.fastCmd
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
val isRunningAsStub get() = Info.stub != null
|
||||
|
||||
object Info {
|
||||
|
||||
val envRef = CachedValue { loadState() }
|
||||
|
||||
@JvmStatic val env by envRef
|
||||
|
||||
var stub: DynAPK.Data? = null
|
||||
val stubChk: DynAPK.Data
|
||||
get() = stub as DynAPK.Data
|
||||
|
||||
var remote = UpdateInfo()
|
||||
|
||||
// Device state
|
||||
var crypto = ""
|
||||
@JvmStatic var isSAR = false
|
||||
@JvmStatic var isAB = false
|
||||
@JvmStatic val isFDE get() = crypto == "block"
|
||||
@JvmStatic var ramdisk = false
|
||||
@JvmStatic var hasGMS = true
|
||||
@JvmStatic var isPixel = false
|
||||
@JvmStatic val cryptoText get() = crypto.capitalize(Locale.US)
|
||||
|
||||
val isConnected by lazy {
|
||||
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()
|
||||
if (id != Config.bootId) {
|
||||
Config.bootId = id
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadState() = Env(
|
||||
fastCmd("magisk -v").split(":".toRegex())[0],
|
||||
runCatching { fastCmd("magisk -V").toInt() }.getOrDefault(-1),
|
||||
Shell.su("magiskhide --status").exec().isSuccess
|
||||
)
|
||||
|
||||
class Env(
|
||||
val magiskVersionString: String = "",
|
||||
code: Int = -1,
|
||||
hide: Boolean = false
|
||||
) {
|
||||
val magiskHide get() = Config.magiskHide
|
||||
val magiskVersionCode = when (code) {
|
||||
in Int.MIN_VALUE..Const.Version.MIN_VERCODE -> -1
|
||||
else -> if (Shell.rootAccess()) code else -1
|
||||
}
|
||||
val isUnsupported = code > 0 && code < Const.Version.MIN_VERCODE
|
||||
val isActive = magiskVersionCode >= 0
|
||||
|
||||
init {
|
||||
Config.magiskHide = hide
|
||||
}
|
||||
}
|
||||
}
|
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()
|
||||
}
|
||||
}
|
46
app/src/main/java/com/topjohnwu/magisk/core/Receiver.kt
Normal file
46
app/src/main/java/com/topjohnwu/magisk/core/Receiver.kt
Normal file
@@ -0,0 +1,46 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import com.topjohnwu.magisk.core.base.BaseReceiver
|
||||
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.inject
|
||||
|
||||
open class Receiver : BaseReceiver() {
|
||||
|
||||
private val policyDB: PolicyDao by inject()
|
||||
|
||||
private fun getPkg(intent: Intent): String {
|
||||
return intent.data?.encodedSchemeSpecificPart.orEmpty()
|
||||
}
|
||||
|
||||
override fun onReceive(context: ContextWrapper, intent: Intent?) {
|
||||
intent ?: return
|
||||
|
||||
fun rmPolicy(pkg: String) = GlobalScope.launch {
|
||||
policyDB.delete(pkg)
|
||||
}
|
||||
|
||||
when (intent.action ?: return) {
|
||||
Intent.ACTION_REBOOT -> {
|
||||
SuCallbackHandler(context, intent.getStringExtra("action"), intent.extras)
|
||||
}
|
||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
||||
// This will only work pre-O
|
||||
if (Config.suReAuth)
|
||||
rmPolicy(getPkg(intent))
|
||||
}
|
||||
Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
|
||||
val pkg = getPkg(intent)
|
||||
rmPolicy(pkg)
|
||||
Shell.su("magiskhide --rm $pkg").submit()
|
||||
}
|
||||
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setupDynamic(context)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||
import com.topjohnwu.magisk.ktx.get
|
||||
import com.topjohnwu.magisk.ui.MainActivity
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
open class SplashActivity : Activity() {
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base.wrap())
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTheme(R.style.SplashTheme)
|
||||
super.onCreate(savedInstanceState)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
initAndStart()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRepackage(pkg: String?) {
|
||||
if (packageName != APPLICATION_ID) {
|
||||
runCatching {
|
||||
// Hidden, remove com.topjohnwu.magisk if exist as it could be malware
|
||||
packageManager.getApplicationInfo(APPLICATION_ID, 0)
|
||||
Shell.su("(pm uninstall $APPLICATION_ID)& >/dev/null 2>&1").exec()
|
||||
}
|
||||
} else {
|
||||
if (Config.suManager.isNotEmpty())
|
||||
Config.suManager = ""
|
||||
pkg ?: return
|
||||
Shell.su("(pm uninstall $pkg)& >/dev/null 2>&1").exec()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initAndStart() {
|
||||
// Pre-initialize root shell
|
||||
Shell.getShell()
|
||||
|
||||
val prevPkg = intent.getStringExtra(Const.Key.PREV_PKG)
|
||||
|
||||
Config.load(prevPkg)
|
||||
handleRepackage(prevPkg)
|
||||
Notifications.setup(this)
|
||||
UpdateCheckService.schedule(this)
|
||||
Shortcuts.setupDynamic(this)
|
||||
|
||||
// Pre-fetch network services
|
||||
get<NetworkService>()
|
||||
|
||||
DONE = true
|
||||
|
||||
redirect<MainActivity>().also { startActivity(it) }
|
||||
finish()
|
||||
}
|
||||
|
||||
companion object {
|
||||
var DONE = false
|
||||
}
|
||||
}
|
@@ -0,0 +1,53 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.*
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.inject
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class UpdateCheckService(context: Context, workerParams: WorkerParameters)
|
||||
: CoroutineWorker(context, workerParams), KoinComponent {
|
||||
|
||||
private val svc: NetworkService by inject()
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
// Make sure shell initializer was ran
|
||||
withContext(Dispatchers.IO) {
|
||||
Shell.getShell()
|
||||
}
|
||||
return svc.fetchUpdate()?.let {
|
||||
if (BuildConfig.VERSION_CODE < it.app.versionCode)
|
||||
Notifications.managerUpdate(applicationContext)
|
||||
else if (Info.env.isActive && Info.env.magiskVersionCode < it.magisk.versionCode)
|
||||
Notifications.magiskUpdate(applicationContext)
|
||||
Result.success()
|
||||
} ?: Result.failure()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun schedule(context: Context) {
|
||||
if (Config.checkUpdate) {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.setRequiresDeviceIdle(true)
|
||||
.build()
|
||||
val request = PeriodicWorkRequestBuilder<UpdateCheckService>(12, TimeUnit.HOURS)
|
||||
.setConstraints(constraints)
|
||||
.build()
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
Const.ID.CHECK_MAGISK_UPDATE_WORKER_ID,
|
||||
ExistingPeriodicWorkPolicy.REPLACE, request)
|
||||
} else {
|
||||
WorkManager.getInstance(context)
|
||||
.cancelUniqueWork(Const.ID.CHECK_MAGISK_UPDATE_WORKER_ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
109
app/src/main/java/com/topjohnwu/magisk/core/base/BaseActivity.kt
Normal file
109
app/src/main/java/com/topjohnwu/magisk/core/base/BaseActivity.kt
Normal file
@@ -0,0 +1,109 @@
|
||||
package com.topjohnwu.magisk.core.base
|
||||
|
||||
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.Const
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import com.topjohnwu.magisk.ktx.set
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import kotlin.random.Random
|
||||
|
||||
typealias ActivityResultCallback = BaseActivity.(Int, Intent?) -> Unit
|
||||
|
||||
abstract class BaseActivity : AppCompatActivity() {
|
||||
|
||||
private val resultCallbacks by lazy { SparseArrayCompat<ActivityResultCallback>() }
|
||||
|
||||
override fun applyOverrideConfiguration(config: Configuration?) {
|
||||
// Force applying our preferred local
|
||||
config?.setLocale(currentLocale)
|
||||
super.applyOverrideConfiguration(config)
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base.wrap(false))
|
||||
}
|
||||
|
||||
fun withPermission(permission: String, builder: PermissionRequestBuilder.() -> Unit) {
|
||||
val request = PermissionRequestBuilder().apply(builder).build()
|
||||
|
||||
if (permission == WRITE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= 30) {
|
||||
// We do not need external rw on 30+
|
||||
request.onSuccess()
|
||||
return
|
||||
}
|
||||
|
||||
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
|
||||
request.onSuccess()
|
||||
} else {
|
||||
var requestCode: Int
|
||||
do {
|
||||
requestCode = Random.nextInt(Const.ID.MAX_ACTIVITY_RESULT + 1, 1 shl 15)
|
||||
} while (resultCallbacks.containsKey(requestCode))
|
||||
resultCallbacks[requestCode] = { result, _ ->
|
||||
if (result > 0)
|
||||
request.onSuccess()
|
||||
else
|
||||
request.onFailure()
|
||||
}
|
||||
ActivityCompat.requestPermissions(this, arrayOf(permission), requestCode)
|
||||
}
|
||||
}
|
||||
|
||||
fun withExternalRW(builder: PermissionRequestBuilder.() -> Unit) {
|
||||
withPermission(WRITE_EXTERNAL_STORAGE, builder = builder)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
var success = true
|
||||
for (res in grantResults) {
|
||||
if (res != PackageManager.PERMISSION_GRANTED) {
|
||||
success = false
|
||||
break
|
||||
}
|
||||
}
|
||||
resultCallbacks[requestCode]?.also {
|
||||
resultCallbacks.remove(requestCode)
|
||||
it(this, if (success) 1 else -1, null)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
resultCallbacks[requestCode]?.also { callback ->
|
||||
resultCallbacks.remove(requestCode)
|
||||
callback(this, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
fun startActivityForResult(intent: Intent, requestCode: Int, callback: ActivityResultCallback) {
|
||||
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().setComponent(intent.component))
|
||||
finish()
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package com.topjohnwu.magisk.core.base
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import org.koin.core.KoinComponent
|
||||
|
||||
abstract class BaseReceiver : BroadcastReceiver(), KoinComponent {
|
||||
|
||||
final override fun onReceive(context: Context, intent: Intent?) {
|
||||
onReceive(context.wrap() as ContextWrapper, intent)
|
||||
}
|
||||
|
||||
abstract fun onReceive(context: ContextWrapper, intent: Intent?)
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
package com.topjohnwu.magisk.core.base
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import org.koin.core.KoinComponent
|
||||
|
||||
abstract class BaseService : Service(), KoinComponent {
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base.wrap())
|
||||
}
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
package com.topjohnwu.magisk.core.base
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Network
|
||||
import android.net.Uri
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.work.Data
|
||||
import androidx.work.ListenableWorker
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import java.util.*
|
||||
|
||||
abstract class BaseWorkerWrapper {
|
||||
|
||||
private lateinit var worker: ListenableWorker
|
||||
|
||||
val applicationContext: Context
|
||||
get() = worker.applicationContext
|
||||
|
||||
val id: UUID
|
||||
get() = worker.id
|
||||
|
||||
val inputData: Data
|
||||
get() = worker.inputData
|
||||
|
||||
val tags: Set<String>
|
||||
get() = worker.tags
|
||||
|
||||
val triggeredContentUris: List<Uri>
|
||||
@RequiresApi(24)
|
||||
get() = worker.triggeredContentUris
|
||||
|
||||
val triggeredContentAuthorities: List<String>
|
||||
@RequiresApi(24)
|
||||
get() = worker.triggeredContentAuthorities
|
||||
|
||||
val network: Network?
|
||||
@RequiresApi(28)
|
||||
get() = worker.network
|
||||
|
||||
val runAttemptCount: Int
|
||||
get() = worker.runAttemptCount
|
||||
|
||||
val isStopped: Boolean
|
||||
get() = worker.isStopped
|
||||
|
||||
abstract fun doWork(): ListenableWorker.Result
|
||||
|
||||
fun onStopped() {}
|
||||
|
||||
fun attachWorker(w: ListenableWorker) {
|
||||
worker = w
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun startWork(): ListenableFuture<ListenableWorker.Result> {
|
||||
return worker.startWork()
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.model.permissions
|
||||
package com.topjohnwu.magisk.core.base
|
||||
|
||||
typealias SimpleCallback = () -> Unit
|
||||
typealias PermissionRationaleCallback = (List<String>) -> Unit
|
||||
@@ -37,4 +37,4 @@ class PermissionRequest(
|
||||
fun onFailure() = onFailureCallback()
|
||||
fun onShowRationale(permissions: List<String>) = onShowRationaleCallback(permissions)
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
sealed class Action : Parcelable {
|
||||
|
||||
sealed class Flash : Action() {
|
||||
|
||||
@Parcelize
|
||||
object Primary : Flash()
|
||||
|
||||
@Parcelize
|
||||
object Secondary : Flash()
|
||||
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
object Download : Action()
|
||||
|
||||
@Parcelize
|
||||
object Uninstall : Action()
|
||||
|
||||
@Parcelize
|
||||
object EnvFix : Action()
|
||||
|
||||
@Parcelize
|
||||
data class Patch(val fileUri: Uri) : Action()
|
||||
|
||||
}
|
@@ -0,0 +1,204 @@
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.ForegroundTracker
|
||||
import com.topjohnwu.magisk.core.base.BaseService
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.checkSum
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||
import com.topjohnwu.magisk.ktx.withStreams
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.ResponseBody
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.core.KoinComponent
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
import kotlin.random.Random.Default.nextInt
|
||||
|
||||
abstract class BaseDownloader : BaseService(), KoinComponent {
|
||||
|
||||
private val hasNotifications get() = notifications.isNotEmpty()
|
||||
private val notifications = Collections.synchronizedMap(HashMap<Int, Notification.Builder>())
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
val service: 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 skip = this is Subject.Magisk && file.checkSum("MD5", magisk.md5)
|
||||
if (!skip) {
|
||||
val stream = service.fetchFile(url).toProgressStream(this)
|
||||
when (this) {
|
||||
is Subject.Module -> // Download and process on-the-fly
|
||||
stream.toModule(file, service.fetchInstaller().byteStream())
|
||||
else -> {
|
||||
withStreams(stream, file.outputStream()) { it, out -> it.copyTo(out) }
|
||||
if (this is Subject.Manager)
|
||||
handleAPK(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
val newId = notifyFinish(this)
|
||||
if (ForegroundTracker.hasForeground)
|
||||
onFinish(this, newId)
|
||||
if (!hasNotifications)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun ResponseBody.toProgressStream(subject: Subject): InputStream {
|
||||
val max = contentLength()
|
||||
val total = max.toFloat() / 1048576
|
||||
val id = subject.notifyID()
|
||||
|
||||
update(id) { it.setContentTitle(subject.title) }
|
||||
|
||||
return ProgressInputStream(byteStream()) {
|
||||
val progress = it.toFloat() / 1048576
|
||||
update(id) { notification ->
|
||||
if (max > 0) {
|
||||
broadcast(progress / total, subject)
|
||||
notification
|
||||
.setProgress(max.toInt(), it.toInt(), false)
|
||||
.setContentText("%.2f / %.2f MB".format(progress, total))
|
||||
} else {
|
||||
broadcast(-1f, subject)
|
||||
notification.setContentText("%.2f MB / ??".format(progress))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Notification managements
|
||||
|
||||
fun Subject.notifyID() = hashCode()
|
||||
|
||||
private fun notifyFail(subject: Subject) = lastNotify(subject.notifyID()) {
|
||||
broadcast(-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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,110 @@
|
||||
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 androidx.core.net.toFile
|
||||
import com.topjohnwu.magisk.core.download.Action.*
|
||||
import com.topjohnwu.magisk.core.download.Action.Flash.Secondary
|
||||
import com.topjohnwu.magisk.core.download.Subject.*
|
||||
import com.topjohnwu.magisk.core.intent
|
||||
import com.topjohnwu.magisk.core.tasks.EnvFixTask
|
||||
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import kotlin.random.Random.Default.nextInt
|
||||
|
||||
@SuppressLint("Registered")
|
||||
open class DownloadService : BaseDownloader() {
|
||||
|
||||
private val context get() = this
|
||||
|
||||
override suspend fun onFinish(subject: Subject, id: Int) = when (subject) {
|
||||
is Magisk -> subject.onFinish(id)
|
||||
is Module -> subject.onFinish(id)
|
||||
is Manager -> subject.onFinish(id)
|
||||
}
|
||||
|
||||
private suspend fun Magisk.onFinish(id: Int) = when (val action = action) {
|
||||
Uninstall -> FlashFragment.uninstall(file, id)
|
||||
EnvFix -> {
|
||||
remove(id)
|
||||
EnvFixTask(file).exec()
|
||||
Unit
|
||||
}
|
||||
is Patch -> FlashFragment.patch(file, action.fileUri, id)
|
||||
is Flash -> FlashFragment.flash(file, action is Secondary, id)
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
private fun Module.onFinish(id: Int) = when (action) {
|
||||
is Flash -> FlashFragment.install(file, id)
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
private fun Manager.onFinish(id: Int) {
|
||||
remove(id)
|
||||
APKInstall.install(context, file.toFile())
|
||||
}
|
||||
|
||||
// --- Customize finish notification
|
||||
|
||||
override fun Notification.Builder.setIntent(subject: Subject)
|
||||
= when (subject) {
|
||||
is Magisk -> setIntent(subject)
|
||||
is Module -> setIntent(subject)
|
||||
is Manager -> setIntent(subject)
|
||||
}
|
||||
|
||||
private fun Notification.Builder.setIntent(subject: Magisk)
|
||||
= when (val action = subject.action) {
|
||||
Uninstall -> setContentIntent(FlashFragment.uninstallIntent(context, subject.file))
|
||||
is Flash -> setContentIntent(FlashFragment.flashIntent(context, subject.file, action is Secondary))
|
||||
is Patch -> setContentIntent(FlashFragment.patchIntent(context, subject.file, action.fileUri))
|
||||
else -> setContentIntent(Intent())
|
||||
}
|
||||
|
||||
private fun Notification.Builder.setIntent(subject: Module)
|
||||
= when (subject.action) {
|
||||
is Flash -> setContentIntent(FlashFragment.installIntent(context, subject.file))
|
||||
else -> setContentIntent(Intent())
|
||||
}
|
||||
|
||||
private fun Notification.Builder.setIntent(subject: Manager)
|
||||
= setContentIntent(APKInstall.installIntent(context, subject.file.toFile()))
|
||||
|
||||
private fun Notification.Builder.setContentIntent(intent: Intent) =
|
||||
setContentIntent(
|
||||
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
||||
)
|
||||
|
||||
// ---
|
||||
|
||||
companion object {
|
||||
|
||||
private fun intent(context: Context, subject: Subject) =
|
||||
context.intent<DownloadService>().putExtra(ACTION_KEY, subject)
|
||||
|
||||
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 {
|
||||
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,51 @@
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.net.toFile
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||
import com.topjohnwu.magisk.ktx.relaunchApp
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import java.io.File
|
||||
|
||||
private fun Context.patch(apk: File) {
|
||||
val patched = File(apk.parent, "patched.apk")
|
||||
HideAPK.patch(this, apk.path, patched.path, packageName, applicationInfo.nonLocalizedLabel)
|
||||
apk.delete()
|
||||
patched.renameTo(apk)
|
||||
}
|
||||
|
||||
private fun BaseDownloader.notifyHide(id: Int) {
|
||||
update(id) {
|
||||
it.setProgress(0, 0, true)
|
||||
.setContentTitle(getString(R.string.hide_manager_title))
|
||||
.setContentText("")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun BaseDownloader.handleAPK(subject: Subject.Manager) {
|
||||
val apk = subject.file.toFile()
|
||||
val id = subject.notifyID()
|
||||
if (isRunningAsStub) {
|
||||
// Move to upgrade location
|
||||
apk.copyTo(DynAPK.update(this), overwrite = true)
|
||||
apk.delete()
|
||||
if (Info.stubChk.version < subject.stub.versionCode) {
|
||||
notifyHide(id)
|
||||
// Also upgrade stub
|
||||
service.fetchFile(subject.stub.link).byteStream().use { it.writeTo(apk) }
|
||||
patch(apk)
|
||||
} else {
|
||||
// Simply relaunch the app
|
||||
stopSelf()
|
||||
relaunchApp(this)
|
||||
}
|
||||
} else if (packageName != BuildConfig.APPLICATION_ID) {
|
||||
notifyHide(id)
|
||||
patch(apk)
|
||||
}
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.net.Uri
|
||||
import com.topjohnwu.magisk.ktx.withStreams
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import java.io.InputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
fun InputStream.toModule(file: Uri, installer: InputStream) {
|
||||
|
||||
val input = ZipInputStream(buffered())
|
||||
val output = ZipOutputStream(file.outputStream().buffered())
|
||||
|
||||
withStreams(input, output) { zin, zout ->
|
||||
zout.putNextEntry(ZipEntry("META-INF/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
|
||||
installer.copyTo(zout)
|
||||
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
|
||||
zout.write("#MAGISK\n".toByteArray(charset("UTF-8")))
|
||||
|
||||
var off = -1
|
||||
var entry: ZipEntry? = zin.nextEntry
|
||||
while (entry != null) {
|
||||
if (off < 0) {
|
||||
off = entry.name.indexOf('/') + 1
|
||||
}
|
||||
|
||||
val path = entry.name.substring(off)
|
||||
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
|
||||
zout.putNextEntry(ZipEntry(path))
|
||||
if (!entry.isDirectory) {
|
||||
zin.copyTo(zout)
|
||||
}
|
||||
}
|
||||
|
||||
entry = zin.nextEntry
|
||||
}
|
||||
}
|
||||
}
|
109
app/src/main/java/com/topjohnwu/magisk/core/download/Subject.kt
Normal file
109
app/src/main/java/com/topjohnwu/magisk/core/download/Subject.kt
Normal file
@@ -0,0 +1,109 @@
|
||||
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.ManagerJson
|
||||
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 app: ManagerJson = Info.remote.app,
|
||||
val stub: StubJson = Info.remote.stub
|
||||
) : Subject() {
|
||||
override val action get() = Action.Download
|
||||
override val title: String get() = "MagiskManager-${app.version}(${app.versionCode})"
|
||||
override val url: String get() = app.link
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
cachedFile("manager.apk")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
abstract class Magisk : Subject() {
|
||||
|
||||
val magisk: MagiskJson = Info.remote.magisk
|
||||
|
||||
@Parcelize
|
||||
private class Internal(
|
||||
override val action: Action
|
||||
) : Magisk() {
|
||||
override val url: String get() = magisk.link
|
||||
override val title: String get() = "Magisk-${magisk.version}(${magisk.versionCode})"
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
cachedFile("magisk.zip")
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
private class Uninstall : Magisk() {
|
||||
override val action get() = Action.Uninstall
|
||||
override val url: String get() = Info.remote.uninstaller.link
|
||||
override val title: String get() = "uninstall.zip"
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
cachedFile(title)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
private class Download : Magisk() {
|
||||
override val action get() = Action.Download
|
||||
override val url: String get() = magisk.link
|
||||
override val title: String get() = "Magisk-${magisk.version}(${magisk.versionCode}).zip"
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
MediaStoreUtils.getFile(title).uri
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
operator fun invoke(config: Action) = when (config) {
|
||||
Action.Download -> Download()
|
||||
Action.Uninstall -> Uninstall()
|
||||
Action.EnvFix, is Action.Flash, is Action.Patch -> Internal(config)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
import androidx.annotation.StringDef
|
||||
|
||||
abstract class BaseDao {
|
||||
|
||||
object Table {
|
||||
const val POLICY = "policies"
|
||||
const val LOG = "logs"
|
||||
const val SETTINGS = "settings"
|
||||
const val STRINGS = "strings"
|
||||
}
|
||||
|
||||
@StringDef(Table.POLICY, Table.LOG, Table.SETTINGS, Table.STRINGS)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class TableStrict
|
||||
|
||||
@TableStrict
|
||||
abstract val table: String
|
||||
|
||||
inline fun <reified Builder : Query.Builder> buildQuery(builder: Builder.() -> Unit = {}) =
|
||||
Builder::class.java.newInstance()
|
||||
.apply { table = this@BaseDao.table }
|
||||
.apply(builder)
|
||||
.toString()
|
||||
.let { Query(it) }
|
||||
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||
import com.topjohnwu.magisk.core.model.su.toMap
|
||||
import com.topjohnwu.magisk.core.model.su.toPolicy
|
||||
import com.topjohnwu.magisk.ktx.now
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
class PolicyDao(
|
||||
private val context: Context
|
||||
) : BaseDao() {
|
||||
|
||||
override val table: String = Table.POLICY
|
||||
|
||||
suspend fun deleteOutdated() = buildQuery<Delete> {
|
||||
condition {
|
||||
greaterThan("until", "0")
|
||||
and {
|
||||
lessThan("until", TimeUnit.MILLISECONDS.toSeconds(now).toString())
|
||||
}
|
||||
or {
|
||||
lessThan("until", "0")
|
||||
}
|
||||
}
|
||||
}.commit()
|
||||
|
||||
suspend fun delete(packageName: String) = buildQuery<Delete> {
|
||||
condition {
|
||||
equals("package_name", packageName)
|
||||
}
|
||||
}.commit()
|
||||
|
||||
suspend fun delete(uid: Int) = buildQuery<Delete> {
|
||||
condition {
|
||||
equals("uid", uid)
|
||||
}
|
||||
}.commit()
|
||||
|
||||
suspend fun fetch(uid: Int) = buildQuery<Select> {
|
||||
condition {
|
||||
equals("uid", uid)
|
||||
}
|
||||
}.query().first().toPolicyOrNull()
|
||||
|
||||
suspend fun update(policy: SuPolicy) = buildQuery<Replace> {
|
||||
values(policy.toMap())
|
||||
}.commit()
|
||||
|
||||
suspend fun <R: Any> fetchAll(mapper: (SuPolicy) -> R) = buildQuery<Select> {
|
||||
condition {
|
||||
equals("uid/100000", Const.USER_ID)
|
||||
}
|
||||
}.query {
|
||||
it.toPolicyOrNull()?.let(mapper)
|
||||
}
|
||||
|
||||
private fun Map<String, String>.toPolicyOrNull(): SuPolicy? {
|
||||
return runCatching { toPolicy(context.packageManager) }.getOrElse {
|
||||
Timber.e(it)
|
||||
if (it is PackageManager.NameNotFoundException) {
|
||||
val uid = getOrElse("uid") { null } ?: return null
|
||||
GlobalScope.launch {
|
||||
delete(uid.toInt())
|
||||
}
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
161
app/src/main/java/com/topjohnwu/magisk/core/magiskdb/Query.kt
Normal file
161
app/src/main/java/com/topjohnwu/magisk/core/magiskdb/Query.kt
Normal file
@@ -0,0 +1,161 @@
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
import androidx.annotation.StringDef
|
||||
import com.topjohnwu.magisk.ktx.await
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class Query(private val _query: String) {
|
||||
val query get() = "magisk --sqlite '$_query'"
|
||||
|
||||
interface Builder {
|
||||
val requestType: String
|
||||
var table: String
|
||||
}
|
||||
|
||||
suspend inline fun <R : Any> query(crossinline mapper: (Map<String, String>) -> R?): List<R> =
|
||||
withContext(Dispatchers.Default) {
|
||||
Shell.su(query).await().out.map { line ->
|
||||
async {
|
||||
line.split("\\|".toRegex())
|
||||
.map { it.split("=", limit = 2) }
|
||||
.filter { it.size == 2 }
|
||||
.map { it[0] to it[1] }
|
||||
.toMap()
|
||||
.let(mapper)
|
||||
}
|
||||
}.awaitAll().filterNotNull()
|
||||
}
|
||||
|
||||
suspend inline fun query() = query { it }
|
||||
|
||||
suspend inline fun commit() = Shell.su(query).to(null).await()
|
||||
}
|
||||
|
||||
class Delete : Query.Builder {
|
||||
override val requestType: String = "DELETE FROM"
|
||||
override var table = ""
|
||||
|
||||
private var condition = ""
|
||||
|
||||
fun condition(builder: Condition.() -> Unit) {
|
||||
condition = Condition().apply(builder).toString()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return listOf(requestType, table, condition).joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
class Select : Query.Builder {
|
||||
override val requestType: String get() = "SELECT $fields FROM"
|
||||
override lateinit var table: String
|
||||
|
||||
private var fields = "*"
|
||||
private var condition = ""
|
||||
private var orderField = ""
|
||||
|
||||
fun fields(vararg newFields: String) {
|
||||
if (newFields.isEmpty()) {
|
||||
fields = "*"
|
||||
return
|
||||
}
|
||||
fields = newFields.joinToString(", ")
|
||||
}
|
||||
|
||||
fun condition(builder: Condition.() -> Unit) {
|
||||
condition = Condition().apply(builder).toString()
|
||||
}
|
||||
|
||||
fun orderBy(field: String, @OrderStrict order: String) {
|
||||
orderField = "ORDER BY $field $order"
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return listOf(requestType, table, condition, orderField).joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
class Replace : Insert() {
|
||||
override val requestType: String = "REPLACE INTO"
|
||||
}
|
||||
|
||||
open class Insert : Query.Builder {
|
||||
override val requestType: String = "INSERT INTO"
|
||||
override lateinit var table: String
|
||||
|
||||
private val keys get() = _values.keys.joinToString(",")
|
||||
private val values get() = _values.values.joinToString(",") {
|
||||
when (it) {
|
||||
is Boolean -> if (it) "1" else "0"
|
||||
is Number -> it.toString()
|
||||
else -> "\"$it\""
|
||||
}
|
||||
}
|
||||
private var _values: Map<String, Any> = mapOf()
|
||||
|
||||
fun values(vararg pairs: Pair<String, Any>) {
|
||||
_values = pairs.toMap()
|
||||
}
|
||||
|
||||
fun values(values: Map<String, Any>) {
|
||||
_values = values
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return listOf(requestType, table, "($keys) VALUES($values)").joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
class Condition {
|
||||
|
||||
private val conditionWord = "WHERE %s"
|
||||
private var condition: String = ""
|
||||
|
||||
fun equals(field: String, value: Any) {
|
||||
condition = when (value) {
|
||||
is String -> "$field=\"$value\""
|
||||
else -> "$field=$value"
|
||||
}
|
||||
}
|
||||
|
||||
fun greaterThan(field: String, value: String) {
|
||||
condition = "$field > $value"
|
||||
}
|
||||
|
||||
fun lessThan(field: String, value: String) {
|
||||
condition = "$field < $value"
|
||||
}
|
||||
|
||||
fun greaterOrEqualTo(field: String, value: String) {
|
||||
condition = "$field >= $value"
|
||||
}
|
||||
|
||||
fun lessOrEqualTo(field: String, value: String) {
|
||||
condition = "$field <= $value"
|
||||
}
|
||||
|
||||
fun and(builder: Condition.() -> Unit) {
|
||||
condition = "($condition AND ${Condition().apply(builder).condition})"
|
||||
}
|
||||
|
||||
fun or(builder: Condition.() -> Unit) {
|
||||
condition = "($condition OR ${Condition().apply(builder).condition})"
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return conditionWord.format(condition)
|
||||
}
|
||||
}
|
||||
|
||||
object Order {
|
||||
const val ASC = "ASC"
|
||||
const val DESC = "DESC"
|
||||
}
|
||||
|
||||
@StringDef(Order.ASC, Order.DESC)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class OrderStrict
|
@@ -0,0 +1,22 @@
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
class SettingsDao : BaseDao() {
|
||||
|
||||
override val table = Table.SETTINGS
|
||||
|
||||
suspend fun delete(key: String) = buildQuery<Delete> {
|
||||
condition { equals("key", key) }
|
||||
}.commit()
|
||||
|
||||
suspend fun put(key: String, value: Int) = buildQuery<Replace> {
|
||||
values("key" to key, "value" to value)
|
||||
}.commit()
|
||||
|
||||
suspend fun fetch(key: String, default: Int = -1) = buildQuery<Select> {
|
||||
fields("value")
|
||||
condition { equals("key", key) }
|
||||
}.query {
|
||||
it["value"]?.toIntOrNull()
|
||||
}.firstOrNull() ?: default
|
||||
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
class StringDao : BaseDao() {
|
||||
|
||||
override val table = Table.STRINGS
|
||||
|
||||
suspend fun delete(key: String) = buildQuery<Delete> {
|
||||
condition { equals("key", key) }
|
||||
}.commit()
|
||||
|
||||
suspend fun put(key: String, value: String) = buildQuery<Replace> {
|
||||
values("key" to key, "value" to value)
|
||||
}.commit()
|
||||
|
||||
suspend fun fetch(key: String, default: String = "") = buildQuery<Select> {
|
||||
fields("value")
|
||||
condition { equals("key", key) }
|
||||
}.query {
|
||||
it["value"]
|
||||
}.firstOrNull() ?: default
|
||||
|
||||
}
|
@@ -0,0 +1,69 @@
|
||||
package com.topjohnwu.magisk.core.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.squareup.moshi.JsonClass
|
||||
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(
|
||||
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
|
||||
)
|
@@ -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.isCanary())
|
||||
Shell.su("copy_sepolicy_rules").submit()
|
||||
else
|
||||
Shell.su("mkdir -p $dir", "cp -af $ruleFile $dir").submit()
|
||||
} else {
|
||||
!disableFile.createNewFile()
|
||||
if (Const.Version.isCanary())
|
||||
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.isCanary())
|
||||
Shell.su("copy_sepolicy_rules").submit()
|
||||
else
|
||||
Shell.su("rm -rf $PERSIST/$id").submit()
|
||||
} else {
|
||||
!removeFile.delete()
|
||||
if (Const.Version.isCanary())
|
||||
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() }
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
package com.topjohnwu.magisk.core.model.module
|
||||
|
||||
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
|
||||
|
||||
@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: 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)
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
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.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 SuLog(
|
||||
val fromUid: Int,
|
||||
val toUid: Int,
|
||||
val fromPid: Int,
|
||||
val packageName: String,
|
||||
val appName: String,
|
||||
val command: String,
|
||||
val action: Boolean,
|
||||
val time: Long = -1
|
||||
) {
|
||||
@PrimaryKey(autoGenerate = true) var id: Int = 0
|
||||
@Ignore val timeString = time.toTime(timeFormatTime)
|
||||
}
|
||||
|
||||
fun SuPolicy.toLog(
|
||||
toUid: Int,
|
||||
fromPid: Int,
|
||||
command: String
|
||||
) = SuLog(uid, toUid, fromPid, packageName, appName, command, policy == ALLOW, now)
|
@@ -0,0 +1,70 @@
|
||||
package com.topjohnwu.magisk.core.model.su
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Drawable
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.INTERACTIVE
|
||||
import com.topjohnwu.magisk.ktx.getLabel
|
||||
|
||||
|
||||
data class SuPolicy(
|
||||
var uid: Int,
|
||||
val packageName: String,
|
||||
val appName: String,
|
||||
val icon: Drawable,
|
||||
var policy: Int = INTERACTIVE,
|
||||
var until: Long = -1L,
|
||||
val logging: Boolean = true,
|
||||
val notification: Boolean = true
|
||||
) {
|
||||
|
||||
companion object {
|
||||
const val INTERACTIVE = 0
|
||||
const val DENY = 1
|
||||
const val ALLOW = 2
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun SuPolicy.toMap() = mapOf(
|
||||
"uid" to uid,
|
||||
"package_name" to packageName,
|
||||
"policy" to policy,
|
||||
"until" to until,
|
||||
"logging" to logging,
|
||||
"notification" to notification
|
||||
)
|
||||
|
||||
@Throws(PackageManager.NameNotFoundException::class)
|
||||
fun Map<String, String>.toPolicy(pm: PackageManager): SuPolicy {
|
||||
val uid = get("uid")?.toIntOrNull() ?: -1
|
||||
val packageName = get("package_name").orEmpty()
|
||||
val info = pm.getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES)
|
||||
|
||||
if (info.uid != uid)
|
||||
throw PackageManager.NameNotFoundException()
|
||||
|
||||
return SuPolicy(
|
||||
uid = uid,
|
||||
packageName = packageName,
|
||||
appName = info.getLabel(pm),
|
||||
icon = info.loadIcon(pm),
|
||||
policy = get("policy")?.toIntOrNull() ?: INTERACTIVE,
|
||||
until = get("until")?.toLongOrNull() ?: -1L,
|
||||
logging = get("logging")?.toIntOrNull() != 0,
|
||||
notification = get("notification")?.toIntOrNull() != 0
|
||||
)
|
||||
}
|
||||
|
||||
@Throws(PackageManager.NameNotFoundException::class)
|
||||
fun Int.toPolicy(pm: PackageManager, policy: Int = INTERACTIVE): SuPolicy {
|
||||
val pkg = pm.getPackagesForUid(this)?.firstOrNull()
|
||||
?: throw PackageManager.NameNotFoundException()
|
||||
val info = pm.getApplicationInfo(pkg, PackageManager.GET_UNINSTALLED_PACKAGES)
|
||||
return SuPolicy(
|
||||
uid = info.uid,
|
||||
packageName = pkg,
|
||||
appName = info.getLabel(pm),
|
||||
icon = info.loadIcon(pm),
|
||||
policy = policy
|
||||
)
|
||||
}
|
@@ -0,0 +1,138 @@
|
||||
package com.topjohnwu.magisk.core.su
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Process
|
||||
import android.widget.Toast
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.intent
|
||||
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.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 {
|
||||
|
||||
const val REQUEST = "request"
|
||||
const val LOG = "log"
|
||||
const val NOTIFY = "notify"
|
||||
const val TEST = "test"
|
||||
|
||||
operator fun invoke(context: Context, action: String?, data: Bundle?) {
|
||||
data ?: return
|
||||
|
||||
// Debug messages
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.d(action)
|
||||
data.let { bundle ->
|
||||
bundle.keySet().forEach {
|
||||
Timber.d("[%s]=[%s]", it, bundle[it])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (action) {
|
||||
REQUEST -> handleRequest(context, data)
|
||||
LOG -> handleLogging(context, data)
|
||||
NOTIFY -> handleNotify(context, data)
|
||||
TEST -> {
|
||||
val mode = data.getInt("mode", 2)
|
||||
Shell.su(
|
||||
"magisk --connect-mode $mode",
|
||||
"magisk --use-broadcast"
|
||||
).submit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Any?.toInt(): Int? {
|
||||
return when (this) {
|
||||
is Number -> this.toInt()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
val pm = context.packageManager
|
||||
|
||||
val notify = data.getBoolean("notify", true)
|
||||
val allow = data["policy"].toInt() ?: return
|
||||
|
||||
val policy = runCatching { fromUid.toPolicy(pm, allow) }.getOrElse { return }
|
||||
|
||||
if (notify)
|
||||
notify(context, policy)
|
||||
|
||||
val toUid = data["to.uid"].toInt() ?: return
|
||||
val pid = data["pid"].toInt() ?: return
|
||||
|
||||
val command = data.getString("command") ?: return
|
||||
val log = policy.toLog(
|
||||
toUid = toUid,
|
||||
fromPid = pid,
|
||||
command = command
|
||||
)
|
||||
|
||||
val logRepo = get<LogRepository>()
|
||||
GlobalScope.launch {
|
||||
logRepo.insert(log)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNotify(context: Context, data: Bundle) {
|
||||
val fromUid = data["from.uid"].toInt() ?: return
|
||||
if (fromUid == Process.myUid())
|
||||
return
|
||||
|
||||
val pm = context.packageManager
|
||||
val allow = data["policy"].toInt() ?: return
|
||||
|
||||
runCatching {
|
||||
val policy = fromUid.toPolicy(pm, allow)
|
||||
if (policy.policy >= 0)
|
||||
notify(context, policy)
|
||||
}
|
||||
}
|
||||
|
||||
private fun notify(context: Context, policy: SuPolicy) {
|
||||
if (policy.notification && Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
|
||||
val resId = if (policy.policy == SuPolicy.ALLOW)
|
||||
R.string.su_allow_toast
|
||||
else
|
||||
R.string.su_deny_toast
|
||||
|
||||
Utils.toast(context.getString(resId, policy.appName), Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,139 @@
|
||||
package com.topjohnwu.magisk.core.su
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.LocalSocket
|
||||
import android.net.LocalSocketAddress
|
||||
import androidx.collection.ArrayMap
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
||||
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
|
||||
|
||||
class SuRequestHandler(
|
||||
private val pm: PackageManager,
|
||||
private val policyDB: PolicyDao
|
||||
) : Closeable {
|
||||
|
||||
private lateinit var output: DataOutputStream
|
||||
lateinit var policy: SuPolicy
|
||||
private set
|
||||
|
||||
// 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.suAutoResponse) {
|
||||
Config.Value.SU_AUTO_DENY -> {
|
||||
respond(SuPolicy.DENY, 0)
|
||||
return false
|
||||
}
|
||||
Config.Value.SU_AUTO_ALLOW -> {
|
||||
respond(SuPolicy.ALLOW, 0)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
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) {
|
||||
val until = if (time > 0)
|
||||
TimeUnit.MILLISECONDS.toSeconds(now) + TimeUnit.MINUTES.toSeconds(time.toLong())
|
||||
else
|
||||
time.toLong()
|
||||
|
||||
policy.policy = action
|
||||
policy.until = until
|
||||
policy.uid = policy.uid % 100000 + Const.USER_ID * 100000
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
|
||||
}
|
118
app/src/main/java/com/topjohnwu/magisk/core/tasks/FlashZip.kt
Normal file
118
app/src/main/java/com/topjohnwu/magisk/core/tasks/FlashZip.kt
Normal file
@@ -0,0 +1,118 @@
|
||||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.os.postDelayed
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
||||
import com.topjohnwu.magisk.core.utils.unzip
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
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
|
||||
|
||||
open class FlashZip(
|
||||
private val mUri: Uri,
|
||||
private val console: MutableList<String>,
|
||||
private val logs: MutableList<String>
|
||||
): KoinComponent {
|
||||
|
||||
val context: Context by inject()
|
||||
private val installFolder = File(context.cacheDir, "flash").apply {
|
||||
if (!exists()) mkdirs()
|
||||
}
|
||||
private val tmpFile: File = File(installFolder, "install.zip")
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun unzipAndCheck(): Boolean {
|
||||
val parentFile = tmpFile.parentFile ?: return false
|
||||
tmpFile.unzip(parentFile, "META-INF/com/google/android", true)
|
||||
|
||||
val updaterScript = File(parentFile, "updater-script")
|
||||
return Shell
|
||||
.su("grep -q '#MAGISK' $updaterScript")
|
||||
.exec()
|
||||
.isSuccess
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun flash(): Boolean {
|
||||
console.add("- Copying zip to temp directory")
|
||||
|
||||
runCatching {
|
||||
mUri.inputStream().writeTo(tmpFile)
|
||||
}.getOrElse {
|
||||
when (it) {
|
||||
is FileNotFoundException -> console.add("! Invalid Uri")
|
||||
is IOException -> console.add("! Cannot copy to cache")
|
||||
}
|
||||
throw it
|
||||
}
|
||||
|
||||
val isMagiskModule = runCatching {
|
||||
unzipAndCheck()
|
||||
}.getOrElse {
|
||||
console.add("! Unzip error")
|
||||
throw it
|
||||
}
|
||||
|
||||
if (!isMagiskModule) {
|
||||
console.add("! This zip is not a Magisk Module!")
|
||||
return false
|
||||
}
|
||||
|
||||
console.add("- Installing ${mUri.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
|
||||
}
|
||||
|
||||
open suspend fun exec() = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
if (!flash()) {
|
||||
console.add("! Installation failed")
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
false
|
||||
} finally {
|
||||
Shell.su("cd /", "rm -rf ${tmpFile.parent} ${Const.TMP_FOLDER_PATH}").submit()
|
||||
}
|
||||
}
|
||||
|
||||
class Uninstall(
|
||||
uri: Uri,
|
||||
console: MutableList<String>,
|
||||
log: MutableList<String>
|
||||
) : FlashZip(uri, console, log) {
|
||||
|
||||
override suspend fun exec(): Boolean {
|
||||
val success = super.exec()
|
||||
if (success) {
|
||||
UiThreadHandler.handler.postDelayed(3000) {
|
||||
Shell.su("pm uninstall " + context.packageName).exec()
|
||||
}
|
||||
}
|
||||
return success
|
||||
}
|
||||
}
|
||||
|
||||
}
|
192
app/src/main/java/com/topjohnwu/magisk/core/tasks/HideAPK.kt
Normal file
192
app/src/main/java/com/topjohnwu/magisk/core/tasks/HideAPK.kt
Normal file
@@ -0,0 +1,192 @@
|
||||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import android.app.ProgressDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
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.*
|
||||
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.Utils
|
||||
import com.topjohnwu.signing.JarMap
|
||||
import com.topjohnwu.signing.SignApk
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.security.SecureRandom
|
||||
|
||||
object HideAPK {
|
||||
|
||||
private const val ALPHA = "abcdefghijklmnopqrstuvwxyz"
|
||||
private const val ALPHADOTS = "$ALPHA....."
|
||||
private const val APP_NAME = "Magisk Manager"
|
||||
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 == '.' || prev == 0.toChar() || 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 - 1)
|
||||
builder[idx] = '.'
|
||||
}
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
fun patch(
|
||||
context: Context,
|
||||
apk: String, out: String,
|
||||
pkg: String, label: CharSequence
|
||||
): Boolean {
|
||||
try {
|
||||
val jar = JarMap.open(apk)
|
||||
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 suspend fun patchAndHide(context: Context, label: String): Boolean {
|
||||
val dlStub = !isRunningAsStub && SDK_INT >= 28 && Const.Version.atLeast_20_2()
|
||||
val src = if (dlStub) {
|
||||
val stub = File(context.cacheDir, "stub.apk")
|
||||
try {
|
||||
svc.fetchFile(Info.remote.stub.link).byteStream().use {
|
||||
it.writeTo(stub)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
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()
|
||||
Config.keyStoreRaw = ""
|
||||
|
||||
if (!patch(context, src, repack.path, pkg, label))
|
||||
return false
|
||||
|
||||
// Install the application
|
||||
if (!Shell.su("adb_pm_install $repack").exec().isSuccess)
|
||||
return false
|
||||
|
||||
context.apply {
|
||||
val intent = packageManager.getLaunchIntentForPackage(pkg) ?: return false
|
||||
Config.suManager = 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)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun hide(context: Context, label: String) {
|
||||
val dialog = ProgressDialog.show(context, context.getString(R.string.hide_manager_title), "", true)
|
||||
GlobalScope.launch {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
patchAndHide(context, label)
|
||||
}
|
||||
if (!result) {
|
||||
Utils.toast(R.string.hide_manager_fail_toast, Toast.LENGTH_LONG)
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun downloadAndRestore(context: Context): Boolean {
|
||||
val apk = if (isRunningAsStub) {
|
||||
DynAPK.current(context)
|
||||
} else {
|
||||
File(context.cacheDir, "manager.apk").also { apk ->
|
||||
try {
|
||||
svc.fetchFile(Info.remote.app.link).byteStream().use {
|
||||
it.writeTo(apk)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Shell.su("adb_pm_install $apk").exec().isSuccess)
|
||||
return false
|
||||
|
||||
context.apply {
|
||||
val intent = packageManager.getLaunchIntentForPackage(APPLICATION_ID) ?: return false
|
||||
Config.suManager = ""
|
||||
grantUriPermission(APPLICATION_ID, APK_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
grantUriPermission(APPLICATION_ID, PREFS_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
intent.putExtra(Const.Key.PREV_PKG, packageName)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun restore(context: Context) {
|
||||
val dialog = ProgressDialog.show(context, context.getString(R.string.restore_img_msg), "", true)
|
||||
GlobalScope.launch {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
downloadAndRestore(context)
|
||||
}
|
||||
if (!result) {
|
||||
Utils.toast(R.string.restore_manager_fail_toast, Toast.LENGTH_LONG)
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,482 @@
|
||||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
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.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
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.events.dialog.EnvFixDialog
|
||||
import com.topjohnwu.magisk.ktx.reboot
|
||||
import com.topjohnwu.magisk.ktx.withStreams
|
||||
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 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.get
|
||||
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.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
abstract class MagiskInstallImpl : KoinComponent {
|
||||
|
||||
protected lateinit var installDir: File
|
||||
private lateinit var srcBoot: String
|
||||
private lateinit var zipUri: Uri
|
||||
|
||||
protected val console: MutableList<String>
|
||||
private val logs: MutableList<String>
|
||||
private var tarOut: TarOutputStream? = null
|
||||
|
||||
private val service: NetworkService by inject()
|
||||
protected val context: Context by inject()
|
||||
|
||||
protected constructor() {
|
||||
console = NOPList.getInstance()
|
||||
logs = NOPList.getInstance()
|
||||
}
|
||||
|
||||
protected 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
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun extractZip(): Boolean {
|
||||
val arch = if (Build.VERSION.SDK_INT >= 21) {
|
||||
val abis = listOf(*Build.SUPPORTED_ABIS)
|
||||
if (abis.contains("x86")) "x86" else "arm"
|
||||
} else {
|
||||
if (Build.CPU_ABI == "x86") "x86" else "arm"
|
||||
}
|
||||
|
||||
console.add("- Device platform: " + Build.CPU_ABI)
|
||||
console.add("- Magisk Manager: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
||||
console.add("- Install target: ${Info.remote.magisk.version} (${Info.remote.magisk.versionCode})")
|
||||
|
||||
try {
|
||||
ZipInputStream(zipUri.inputStream().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
|
||||
name?.also {
|
||||
val dest = if (installDir is SuFile)
|
||||
SuFile(installDir, it)
|
||||
else
|
||||
File(installDir, it)
|
||||
dest.parentFile!!.mkdirs()
|
||||
SuFileOutputStream(dest).use { s -> zi.copyTo(s) }
|
||||
} ?: continue
|
||||
}
|
||||
}
|
||||
} 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, output: OutputStream): OutputStream {
|
||||
console.add("- Processing tar file")
|
||||
val tarOut = TarOutputStream(output)
|
||||
TarInputStream(input).use { tarIn ->
|
||||
lateinit var entry: TarEntry
|
||||
|
||||
fun decompressedStream() =
|
||||
if (entry.name.contains(".lz4")) LZ4FrameInputStream(tarIn) else tarIn
|
||||
|
||||
while (tarIn.nextEntry?.let { entry = it } != null) {
|
||||
if (entry.name.contains("boot.img") ||
|
||||
(Config.recovery && entry.name.contains("recovery.img"))) {
|
||||
val name = entry.name.replace(".lz4", "")
|
||||
console.add("-- Extracting: $name")
|
||||
|
||||
val extract = File(installDir, name)
|
||||
FileOutputStream(extract).use { decompressedStream().copyTo(it) }
|
||||
} else if (entry.name.contains("vbmeta.img")) {
|
||||
val rawData = ByteArrayOutputStream().let {
|
||||
decompressedStream().copyTo(it)
|
||||
it.toByteArray()
|
||||
}
|
||||
// Valid vbmeta.img should be at least 256 bytes
|
||||
if (rawData.size < 256)
|
||||
continue
|
||||
|
||||
// Patch flags to AVB_VBMETA_IMAGE_FLAGS_VERIFICATION_DISABLED
|
||||
console.add("-- Patching: vbmeta.img")
|
||||
ByteBuffer.wrap(rawData).putInt(120, 2)
|
||||
tarOut.putNextEntry(newEntry("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 = SuFile.open(installDir, "boot.img")
|
||||
val recovery = SuFile.open(installDir, "recovery.img")
|
||||
if (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
|
||||
}
|
||||
}
|
||||
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 suffix = StringBuilder()
|
||||
for (i in 1..5) {
|
||||
suffix.append(alphaNum[random.nextInt(alphaNum.length)])
|
||||
}
|
||||
|
||||
val filename = "magisk_patched_$suffix"
|
||||
outStream = if (magic.contentEquals("ustar".toByteArray())) {
|
||||
outFile = MediaStoreUtils.getFile("$filename.tar", true)
|
||||
handleTar(src, outFile!!.uri.outputStream())
|
||||
} else {
|
||||
// Raw image
|
||||
srcBoot = File(installDir, "boot.img").path
|
||||
console.add("- Copying image to cache")
|
||||
FileOutputStream(srcBoot).use { src.copyTo(it) }
|
||||
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 patched = SuFile.open(installDir, "new-boot.img")
|
||||
if (outStream is TarOutputStream) {
|
||||
val name = if (srcBoot.contains("recovery")) "recovery.img" else "boot.img"
|
||||
outStream.putNextEntry(newEntry(name, patched.length()))
|
||||
}
|
||||
withStreams(SuFileInputStream(patched), outStream) { src, out -> src.copyTo(out) }
|
||||
patched.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
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun patchBoot(): Boolean {
|
||||
var srcNand = ""
|
||||
if ("[ -c $srcBoot ] && nanddump -f boot.img $srcBoot".sh().isSuccess) {
|
||||
srcNand = srcBoot
|
||||
srcBoot = File(installDir, "boot.img").path
|
||||
}
|
||||
|
||||
var isSigned: Boolean
|
||||
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=${Config.keepEnc} KEEPVERITY=${Config.keepVerity} " +
|
||||
"RECOVERYMODE=${Config.recovery} sh update-binary " +
|
||||
"sh boot_patch.sh $srcBoot").sh().isSuccess) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (srcNand.isNotEmpty()) {
|
||||
srcBoot = srcNand
|
||||
}
|
||||
|
||||
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(null, null, input, out, "/boot")
|
||||
}
|
||||
} 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 copySepolicyRules(): Boolean {
|
||||
if (Info.remote.magisk.versionCode >= 21100) return true
|
||||
// Copy existing rules for migration
|
||||
"copy_sepolicy_rules".sh()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun flashBoot(): Boolean {
|
||||
if (!"direct_install $installDir $srcBoot".sh().isSuccess)
|
||||
return false
|
||||
"run_migrations".sh()
|
||||
return true
|
||||
}
|
||||
|
||||
private suspend fun postOTA(): Boolean {
|
||||
val bootctl = SuFile("/data/adb/bootctl")
|
||||
try {
|
||||
withStreams(service.fetchBootctl().byteStream(), SuFileOutputStream(bootctl)) {
|
||||
it, out -> it.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)
|
||||
|
||||
protected fun direct() = findImage() && extractZip() && patchBoot() &&
|
||||
copySepolicyRules() && flashBoot()
|
||||
|
||||
protected suspend fun secondSlot() = findSecondaryImage() && extractZip() &&
|
||||
patchBoot() && copySepolicyRules() && flashBoot() && postOTA()
|
||||
|
||||
protected fun fixEnv(zip: Uri): Boolean {
|
||||
installDir = SuFile("/data/adb/magisk")
|
||||
Shell.su("rm -rf /data/adb/magisk/*").exec()
|
||||
zipUri = zip
|
||||
return extractZip() && Shell.su("fix_env").exec().isSuccess
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
protected abstract suspend fun operations(): Boolean
|
||||
|
||||
open suspend fun exec() = withContext(Dispatchers.IO) { operations() }
|
||||
}
|
||||
|
||||
sealed class MagiskInstaller(
|
||||
file: Uri,
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>
|
||||
) : MagiskInstallImpl(file, 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")
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
class Patch(
|
||||
file: Uri,
|
||||
private val uri: Uri,
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>
|
||||
) : MagiskInstaller(file, console, logs) {
|
||||
override suspend fun operations() = doPatchFile(uri)
|
||||
}
|
||||
|
||||
class SecondSlot(
|
||||
file: Uri,
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>
|
||||
) : MagiskInstaller(file, console, logs) {
|
||||
override suspend fun operations() = secondSlot()
|
||||
}
|
||||
|
||||
class Direct(
|
||||
file: Uri,
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>
|
||||
) : MagiskInstaller(file, console, logs) {
|
||||
override suspend fun operations() = direct()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class EnvFixTask(
|
||||
private val zip: Uri
|
||||
) : MagiskInstallImpl() {
|
||||
override suspend fun operations() = fixEnv(zip)
|
||||
|
||||
override suspend fun exec(): Boolean {
|
||||
val success = super.exec()
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(Intent(EnvFixDialog.DISMISS))
|
||||
Utils.toast(
|
||||
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
if (success)
|
||||
UiThreadHandler.handler.postDelayed(5000) { reboot() }
|
||||
return success
|
||||
}
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||
import com.topjohnwu.magisk.data.database.RepoDao
|
||||
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.util.*
|
||||
|
||||
class RepoUpdater(
|
||||
private val svc: NetworkService,
|
||||
private val repoDB: RepoDao
|
||||
) {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
repoDB.removeModules(cachedMap.keys)
|
||||
}
|
||||
}
|
||||
}
|
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,60 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.get
|
||||
|
||||
object BiometricHelper: KoinComponent {
|
||||
|
||||
private val mgr by lazy { BiometricManager.from(get()) }
|
||||
|
||||
val isSupported get() = when (mgr.canAuthenticate()) {
|
||||
BiometricManager.BIOMETRIC_SUCCESS -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
val isEnabled: Boolean get() {
|
||||
val enabled = Config.suBiometric
|
||||
if (enabled && !isSupported) {
|
||||
Config.suBiometric = false
|
||||
return false
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
|
||||
fun authenticate(
|
||||
activity: FragmentActivity,
|
||||
onError: () -> Unit = {},
|
||||
onSuccess: () -> Unit): BiometricPrompt {
|
||||
val prompt = BiometricPrompt(activity,
|
||||
ContextCompat.getMainExecutor(activity),
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
onError()
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
onError()
|
||||
}
|
||||
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
)
|
||||
val info = BiometricPrompt.PromptInfo.Builder()
|
||||
.setConfirmationRequired(true)
|
||||
.setDeviceCredentialAllowed(false)
|
||||
.setTitle(activity.getString(R.string.authenticate))
|
||||
.setNegativeButtonText(activity.getString(android.R.string.cancel))
|
||||
.build()
|
||||
prompt.authenticate(info)
|
||||
return prompt
|
||||
}
|
||||
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
133
app/src/main/java/com/topjohnwu/magisk/core/utils/Keygen.kt
Normal file
133
app/src/main/java/com/topjohnwu/magisk/core/utils/Keygen.kt
Normal file
@@ -0,0 +1,133 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Base64
|
||||
import android.util.Base64OutputStream
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.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
|
||||
import java.security.KeyStore
|
||||
import java.security.MessageDigest
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
import java.util.zip.GZIPInputStream
|
||||
import java.util.zip.GZIPOutputStream
|
||||
|
||||
private interface CertKeyProvider {
|
||||
val cert: X509Certificate
|
||||
val key: PrivateKey
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
class Keygen(context: Context) : CertKeyProvider {
|
||||
|
||||
companion object {
|
||||
private const val ALIAS = "magisk"
|
||||
private val PASSWORD get() = "magisk".toCharArray()
|
||||
private const val TESTKEY_CERT = "61ed377e85d386a8dfee6b864bd85b0bfaa5af81"
|
||||
private const val 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().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
|
||||
|
||||
private val provider: CertKeyProvider
|
||||
|
||||
inner class KeyStoreProvider :
|
||||
CertKeyProvider {
|
||||
private val ks by lazy { init() }
|
||||
override val cert by lazy { ks.getCertificate(ALIAS) as X509Certificate }
|
||||
override val key by lazy { ks.getKey(
|
||||
ALIAS,
|
||||
PASSWORD
|
||||
) as PrivateKey }
|
||||
}
|
||||
|
||||
class TestProvider : CertKeyProvider {
|
||||
override val cert by lazy {
|
||||
readCertificate(ByteArrayInputStream(KeyData.testCert()))
|
||||
}
|
||||
override val key by lazy {
|
||||
readPrivateKey(ByteArrayInputStream(KeyData.testKey()))
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val pm = context.packageManager
|
||||
val info = pm.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES)
|
||||
val sig = info.signatures[0]
|
||||
val digest = MessageDigest.getInstance("SHA1")
|
||||
val chksum = digest.digest(sig.toByteArray())
|
||||
|
||||
val sb = StringBuilder()
|
||||
for (b in chksum) {
|
||||
sb.append("%02x".format(0xFF and b.toInt()))
|
||||
}
|
||||
|
||||
provider = if (sb.toString() == TESTKEY_CERT) {
|
||||
// The app was signed by the test key, continue to use it (legacy mode)
|
||||
TestProvider()
|
||||
} else {
|
||||
KeyStoreProvider()
|
||||
}
|
||||
}
|
||||
|
||||
private fun init(): KeyStore {
|
||||
val raw = Config.keyStoreRaw
|
||||
val ks = KeyStore.getInstance("PKCS12")
|
||||
if (raw.isEmpty()) {
|
||||
ks.load(null)
|
||||
} else {
|
||||
GZIPInputStream(Base64.decode(raw,
|
||||
BASE64_FLAG
|
||||
).inputStream()).use {
|
||||
ks.load(it,
|
||||
PASSWORD
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Keys already exist
|
||||
if (ks.containsAlias(ALIAS))
|
||||
return ks
|
||||
|
||||
// Generate new private key and certificate
|
||||
val kp = KeyPairGenerator.getInstance("RSA").apply { initialize(4096) }.genKeyPair()
|
||||
val dname = X500Name(DNAME)
|
||||
val builder = JcaX509v3CertificateBuilder(dname, BigInteger(160, Random()),
|
||||
start.time, end.time, dname, kp.public)
|
||||
val signer = JcaContentSignerBuilder("SHA1WithRSA").build(kp.private)
|
||||
val cert = JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
||||
|
||||
// Store them into keystore
|
||||
ks.setKeyEntry(
|
||||
ALIAS, kp.private,
|
||||
PASSWORD, arrayOf(cert))
|
||||
val bytes = ByteArrayOutputStream()
|
||||
GZIPOutputStream(Base64OutputStream(bytes,
|
||||
BASE64_FLAG
|
||||
)).use {
|
||||
ks.store(it,
|
||||
PASSWORD
|
||||
)
|
||||
}
|
||||
Config.keyStoreRaw = bytes.toString("UTF-8")
|
||||
|
||||
return ks
|
||||
}
|
||||
}
|
90
app/src/main/java/com/topjohnwu/magisk/core/utils/Locales.kt
Normal file
90
app/src/main/java/com/topjohnwu/magisk/core/utils/Locales.kt
Normal file
@@ -0,0 +1,90 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.AssetManager
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.util.DisplayMetrics
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.ResMgr
|
||||
import com.topjohnwu.magisk.core.addAssetPath
|
||||
import com.topjohnwu.magisk.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()
|
||||
|
||||
@SuppressLint("ConstantLocale")
|
||||
val defaultLocale: Locale = Locale.getDefault()
|
||||
|
||||
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)
|
||||
|
||||
val locales = ArrayList<String>().apply {
|
||||
// Add default locale
|
||||
add("en")
|
||||
|
||||
// Add some special locales
|
||||
add("zh-TW")
|
||||
add("pt-BR")
|
||||
|
||||
// Then add all supported locales
|
||||
addAll(res.assets.locales)
|
||||
}.map {
|
||||
it.langTagToLocale()
|
||||
}.distinctBy {
|
||||
config.setLocale(it)
|
||||
res.updateConfiguration(config, metrics)
|
||||
res.getString(compareId)
|
||||
}.sortedWith(Comparator { a, b ->
|
||||
a.getDisplayName(a).compareTo(b.getDisplayName(b), true)
|
||||
})
|
||||
|
||||
config.setLocale(defaultLocale)
|
||||
res.updateConfiguration(config, metrics)
|
||||
val defName = res.getString(R.string.system_default)
|
||||
|
||||
val names = ArrayList<String>(locales.size + 1)
|
||||
val values = ArrayList<String>(locales.size + 1)
|
||||
|
||||
names.add(defName)
|
||||
values.add("")
|
||||
|
||||
locales.forEach { locale ->
|
||||
names.add(locale.getDisplayName(locale))
|
||||
values.add(locale.toLangTag())
|
||||
}
|
||||
|
||||
(names.toTypedArray() to values.toTypedArray()).also { cachedLocales = it }
|
||||
}
|
||||
|
||||
fun Resources.updateConfig(config: Configuration = configuration) {
|
||||
config.setLocale(currentLocale)
|
||||
updateConfiguration(config, displayMetrics)
|
||||
}
|
||||
|
||||
fun refreshLocale() {
|
||||
val localeConfig = Config.locale
|
||||
currentLocale = when {
|
||||
localeConfig.isEmpty() -> defaultLocale
|
||||
else -> localeConfig.langTagToLocale()
|
||||
}
|
||||
Locale.setDefault(currentLocale)
|
||||
ResMgr.resource.updateConfig()
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import java.io.FilterInputStream
|
||||
import java.io.InputStream
|
||||
|
||||
class ProgressInputStream(
|
||||
base: InputStream,
|
||||
val progressEmitter: (Long) -> Unit = {}
|
||||
) : FilterInputStream(base) {
|
||||
|
||||
private var bytesRead = 0L
|
||||
private var lastUpdate = 0L
|
||||
|
||||
private fun emitProgress() {
|
||||
val cur = System.currentTimeMillis()
|
||||
if (cur - lastUpdate > 1000) {
|
||||
lastUpdate = cur
|
||||
progressEmitter(bytesRead)
|
||||
}
|
||||
}
|
||||
|
||||
override fun read(): Int {
|
||||
val b = read()
|
||||
if (b >= 0) {
|
||||
bytesRead++
|
||||
emitProgress()
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
override fun read(b: ByteArray): Int {
|
||||
return read(b, 0, b.size)
|
||||
}
|
||||
|
||||
override fun read(b: ByteArray, off: Int, len: Int): Int {
|
||||
val sz = super.read(b, off, len)
|
||||
if (sz > 0) {
|
||||
bytesRead += sz
|
||||
emitProgress()
|
||||
}
|
||||
return sz
|
||||
}
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
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.wrap
|
||||
import com.topjohnwu.magisk.ktx.rawResource
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
|
||||
class RootInit : Shell.Initializer() {
|
||||
|
||||
override fun onInit(context: Context, shell: Shell): Boolean {
|
||||
return init(context.wrap(), shell)
|
||||
}
|
||||
|
||||
fun init(context: Context, shell: Shell): Boolean {
|
||||
shell.newJob().apply {
|
||||
add("export SDK_INT=${Build.VERSION.SDK_INT}")
|
||||
if (Const.Version.atLeast_20_4()) {
|
||||
add("export MAGISKTMP=\$(magisk --path)/.magisk")
|
||||
} else {
|
||||
add("export MAGISKTMP=/sbin/.magisk")
|
||||
}
|
||||
if (Const.Version.atLeast_21_0()) {
|
||||
add("export ASH_STANDALONE=1")
|
||||
add("[ -x /data/adb/magisk/busybox ] && exec /data/adb/magisk/busybox sh")
|
||||
} else {
|
||||
add("export PATH=\"\$MAGISKTMP/busybox:\$PATH\"")
|
||||
}
|
||||
add(context.rawResource(R.raw.manager))
|
||||
if (shell.isRoot) {
|
||||
add(context.rawResource(R.raw.util_functions))
|
||||
}
|
||||
add("mm_init")
|
||||
}.exec()
|
||||
|
||||
fun fastCmd(cmd: String) = ShellUtils.fastCmd(shell, cmd)
|
||||
fun getVar(name: String) = fastCmd("echo \$$name")
|
||||
fun getBool(name: String) = getVar(name).toBoolean()
|
||||
|
||||
Const.MAGISKTMP = getVar("MAGISKTMP")
|
||||
Info.isSAR = getBool("SYSTEM_ROOT")
|
||||
Info.ramdisk = getBool("RAMDISKEXIST")
|
||||
Info.isAB = getBool("ISAB")
|
||||
Info.crypto = getVar("CRYPTOTYPE")
|
||||
Info.isPixel = fastCmd("getprop ro.product.brand") == "google"
|
||||
|
||||
// Default presets
|
||||
Config.recovery = getBool("RECOVERYMODE")
|
||||
Config.keepVerity = getBool("KEEPVERITY")
|
||||
Config.keepEnc = getBool("KEEPFORCEENCRYPT")
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
@@ -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,190 +0,0 @@
|
||||
package com.topjohnwu.magisk.data.database;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.Const;
|
||||
import com.topjohnwu.magisk.model.entity.Policy;
|
||||
import com.topjohnwu.magisk.model.entity.SuLogEntry;
|
||||
import com.topjohnwu.magisk.utils.LocaleManager;
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class MagiskDB {
|
||||
|
||||
private static final String POLICY_TABLE = "policies";
|
||||
private static final String LOG_TABLE = "logs";
|
||||
private static final String SETTINGS_TABLE = "settings";
|
||||
private static final String STRINGS_TABLE = "strings";
|
||||
|
||||
private PackageManager pm;
|
||||
|
||||
public MagiskDB(Context context) {
|
||||
pm = context.getPackageManager();
|
||||
}
|
||||
|
||||
public void deletePolicy(Policy policy) {
|
||||
deletePolicy(policy.uid);
|
||||
}
|
||||
|
||||
private List<String> rawSQL(String fmt, Object... args) {
|
||||
return Shell.su("magisk --sqlite '" + Utils.fmt(fmt, args) + "'").exec().getOut();
|
||||
}
|
||||
|
||||
private List<ContentValues> SQL(String fmt, Object... args) {
|
||||
List<ContentValues> list = new ArrayList<>();
|
||||
for (String raw : rawSQL(fmt, args)) {
|
||||
ContentValues values = new ContentValues();
|
||||
String[] cols = raw.split("\\|");
|
||||
for (String col : cols) {
|
||||
String[] pair = col.split("=", 2);
|
||||
if (pair.length != 2)
|
||||
continue;
|
||||
values.put(pair[0], pair[1]);
|
||||
}
|
||||
list.add(values);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private String toSQL(ContentValues values) {
|
||||
StringBuilder keys = new StringBuilder(), vals = new StringBuilder();
|
||||
keys.append('(');
|
||||
vals.append("VALUES(");
|
||||
boolean first = true;
|
||||
for (Map.Entry<String, Object> entry : values.valueSet()) {
|
||||
if (!first) {
|
||||
keys.append(',');
|
||||
vals.append(',');
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
keys.append(entry.getKey());
|
||||
vals.append('"');
|
||||
vals.append(entry.getValue());
|
||||
vals.append('"');
|
||||
}
|
||||
keys.append(')');
|
||||
vals.append(')');
|
||||
keys.append(vals);
|
||||
return keys.toString();
|
||||
}
|
||||
|
||||
public void clearOutdated() {
|
||||
rawSQL(
|
||||
"DELETE FROM %s WHERE until > 0 AND until < %d;" +
|
||||
"DELETE FROM %s WHERE time < %d",
|
||||
POLICY_TABLE, System.currentTimeMillis() / 1000,
|
||||
LOG_TABLE, System.currentTimeMillis() - Config.suLogTimeout * 86400000
|
||||
);
|
||||
}
|
||||
|
||||
public void deletePolicy(String pkg) {
|
||||
rawSQL("DELETE FROM %s WHERE package_name=\"%s\"", POLICY_TABLE, pkg);
|
||||
}
|
||||
|
||||
public void deletePolicy(int uid) {
|
||||
rawSQL("DELETE FROM %s WHERE uid=%d", POLICY_TABLE, uid);
|
||||
}
|
||||
|
||||
public Policy getPolicy(int uid) {
|
||||
List<ContentValues> res =
|
||||
SQL("SELECT * FROM %s WHERE uid=%d", POLICY_TABLE, uid);
|
||||
if (!res.isEmpty()) {
|
||||
try {
|
||||
return new Policy(res.get(0), pm);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
deletePolicy(uid);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void updatePolicy(Policy policy) {
|
||||
rawSQL("REPLACE INTO %s %s", POLICY_TABLE, toSQL(policy.getContentValues()));
|
||||
}
|
||||
|
||||
public List<Policy> getPolicyList() {
|
||||
List<Policy> list = new ArrayList<>();
|
||||
for (ContentValues values : SQL("SELECT * FROM %s WHERE uid/100000=%d", POLICY_TABLE, Const.USER_ID)) {
|
||||
try {
|
||||
list.add(new Policy(values, pm));
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
deletePolicy(values.getAsInteger("uid"));
|
||||
}
|
||||
}
|
||||
Collections.sort(list);
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<List<SuLogEntry>> getLogs() {
|
||||
List<List<SuLogEntry>> ret = new ArrayList<>();
|
||||
List<SuLogEntry> list = null;
|
||||
String dateString = null, newString;
|
||||
for (ContentValues values : SQL("SELECT * FROM %s ORDER BY time DESC", LOG_TABLE)) {
|
||||
Date date = new Date(values.getAsLong("time"));
|
||||
newString = DateFormat.getDateInstance(DateFormat.MEDIUM, LocaleManager.locale).format(date);
|
||||
if (!TextUtils.equals(dateString, newString)) {
|
||||
dateString = newString;
|
||||
list = new ArrayList<>();
|
||||
ret.add(list);
|
||||
}
|
||||
list.add(new SuLogEntry(values));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public void addLog(SuLogEntry log) {
|
||||
rawSQL("INSERT INTO %s %s", LOG_TABLE, toSQL(log.getContentValues()));
|
||||
}
|
||||
|
||||
public void clearLogs() {
|
||||
rawSQL("DELETE FROM %s", LOG_TABLE);
|
||||
}
|
||||
|
||||
public void rmSettings(String key) {
|
||||
rawSQL("DELETE FROM %s WHERE key=\"%s\"", SETTINGS_TABLE, key);
|
||||
}
|
||||
|
||||
public void setSettings(String key, int value) {
|
||||
ContentValues data = new ContentValues();
|
||||
data.put("key", key);
|
||||
data.put("value", value);
|
||||
rawSQL("REPLACE INTO %s %s", SETTINGS_TABLE, toSQL(data));
|
||||
}
|
||||
|
||||
public int getSettings(String key, int defaultValue) {
|
||||
List<ContentValues> res = SQL("SELECT value FROM %s WHERE key=\"%s\"", SETTINGS_TABLE, key);
|
||||
if (res.isEmpty())
|
||||
return defaultValue;
|
||||
return res.get(0).getAsInteger("value");
|
||||
}
|
||||
|
||||
public void setStrings(String key, String value) {
|
||||
if (value == null) {
|
||||
rawSQL("DELETE FROM %s WHERE key=\"%s\"", STRINGS_TABLE, key);
|
||||
return;
|
||||
}
|
||||
ContentValues data = new ContentValues();
|
||||
data.put("key", key);
|
||||
data.put("value", value);
|
||||
rawSQL("REPLACE INTO %s %s", STRINGS_TABLE, toSQL(data));
|
||||
}
|
||||
|
||||
public String getStrings(String key, String defaultValue) {
|
||||
List<ContentValues> res = SQL("SELECT value FROM %s WHERE key=\"%s\"", STRINGS_TABLE, key);
|
||||
if (res.isEmpty())
|
||||
return defaultValue;
|
||||
return res.get(0).getAsString("value");
|
||||
}
|
||||
}
|
@@ -0,0 +1,90 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import androidx.room.*
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Database(version = 8, entities = [OnlineModule::class], exportSchema = false)
|
||||
abstract class RepoDatabase : RoomDatabase() {
|
||||
abstract fun repoDao() : RepoDao
|
||||
}
|
||||
|
||||
@Dao
|
||||
abstract class RepoDao(private val db: RepoDatabase) {
|
||||
|
||||
suspend fun clear() = withContext(Dispatchers.IO) { db.clearAllTables() }
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract fun addModule(repo: OnlineModule)
|
||||
|
||||
@Delete
|
||||
abstract fun removeModule(repo: OnlineModule)
|
||||
|
||||
@Query("DELETE FROM modules WHERE id = :id")
|
||||
abstract fun removeModule(id: String)
|
||||
|
||||
@Query("DELETE FROM modules WHERE id IN (:idList)")
|
||||
abstract fun removeModules(idList: Collection<String>)
|
||||
|
||||
@Query("SELECT * FROM modules WHERE id = :id")
|
||||
abstract fun getModule(id: String): OnlineModule?
|
||||
|
||||
@Query("SELECT id, last_update FROM modules")
|
||||
abstract fun getModuleStubs(): List<ModuleStub>
|
||||
|
||||
fun getModules(offset: Int, limit: Int = LIMIT) = when (Config.repoOrder) {
|
||||
Config.Value.ORDER_NAME -> getNameOrder(offset, limit)
|
||||
else -> getDateOrder(offset, limit)
|
||||
}
|
||||
|
||||
fun searchModules(query: String, offset: Int, limit: Int = LIMIT) = when (Config.repoOrder) {
|
||||
Config.Value.ORDER_NAME -> searchNameOrder(query, offset, limit)
|
||||
else -> searchDateOrder(query, offset, limit)
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM modules WHERE id = :id AND versionCode > :versionCode LIMIT 1")
|
||||
abstract fun getUpdatableModule(id: String, versionCode: Int): OnlineModule?
|
||||
|
||||
@Query("SELECT * FROM modules ORDER BY last_update DESC LIMIT :limit OFFSET :offset")
|
||||
protected abstract fun getDateOrder(offset: Int, limit: Int): List<OnlineModule>
|
||||
|
||||
@Query("SELECT * FROM modules ORDER BY name COLLATE NOCASE LIMIT :limit OFFSET :offset")
|
||||
protected abstract fun getNameOrder(offset: Int, limit: Int): List<OnlineModule>
|
||||
|
||||
@Query(
|
||||
"""SELECT *
|
||||
FROM modules
|
||||
WHERE
|
||||
(author LIKE '%' || :query || '%') ||
|
||||
(name LIKE '%' || :query || '%') ||
|
||||
(description LIKE '%' || :query || '%')
|
||||
ORDER BY last_update DESC
|
||||
LIMIT :limit
|
||||
OFFSET :offset"""
|
||||
)
|
||||
protected abstract fun searchDateOrder(query: String, offset: Int, limit: Int): List<OnlineModule>
|
||||
|
||||
@Query(
|
||||
"""SELECT *
|
||||
FROM modules
|
||||
WHERE
|
||||
(author LIKE '%' || :query || '%') ||
|
||||
(name LIKE '%' || :query || '%') ||
|
||||
(description LIKE '%' || :query || '%')
|
||||
ORDER BY name COLLATE NOCASE
|
||||
LIMIT :limit
|
||||
OFFSET :offset"""
|
||||
)
|
||||
protected abstract fun searchNameOrder(query: String, offset: Int, limit: Int): List<OnlineModule>
|
||||
|
||||
companion object {
|
||||
const val LIMIT = 10
|
||||
}
|
||||
}
|
||||
|
||||
data class ModuleStub(
|
||||
@PrimaryKey val id: String,
|
||||
val last_update: Long
|
||||
)
|
@@ -1,107 +0,0 @@
|
||||
package com.topjohnwu.magisk.data.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.model.entity.Repo;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class RepoDatabaseHelper extends SQLiteOpenHelper {
|
||||
|
||||
private static final int DATABASE_VER = 5;
|
||||
private static final String TABLE_NAME = "repos";
|
||||
|
||||
private SQLiteDatabase mDb;
|
||||
|
||||
public RepoDatabaseHelper(Context context) {
|
||||
super(context, "repo.db", null, DATABASE_VER);
|
||||
mDb = getWritableDatabase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
onUpgrade(db, 0, DATABASE_VER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
if (oldVersion != newVersion) {
|
||||
// Nuke old DB and create new table
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
||||
db.execSQL(
|
||||
"CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " " +
|
||||
"(id TEXT, name TEXT, version TEXT, versionCode INT, " +
|
||||
"author TEXT, description TEXT, last_update INT, PRIMARY KEY(id))");
|
||||
Config.remove(Config.Key.ETAG_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
onUpgrade(db, 0, DATABASE_VER);
|
||||
}
|
||||
|
||||
public void clearRepo() {
|
||||
mDb.delete(TABLE_NAME, null, null);
|
||||
}
|
||||
|
||||
|
||||
public void removeRepo(String id) {
|
||||
mDb.delete(TABLE_NAME, "id=?", new String[] { id });
|
||||
}
|
||||
|
||||
public void removeRepo(Repo repo) {
|
||||
removeRepo(repo.getId());
|
||||
}
|
||||
|
||||
public void removeRepo(Iterable<String> list) {
|
||||
for (String id : list) {
|
||||
if (id == null) continue;
|
||||
mDb.delete(TABLE_NAME, "id=?", new String[] { id });
|
||||
}
|
||||
}
|
||||
|
||||
public void addRepo(Repo repo) {
|
||||
mDb.replace(TABLE_NAME, null, repo.getContentValues());
|
||||
}
|
||||
|
||||
public Repo getRepo(String id) {
|
||||
try (Cursor c = mDb.query(TABLE_NAME, null, "id=?", new String[] { id }, null, null, null)) {
|
||||
if (c.moveToNext()) {
|
||||
return new Repo(c);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Cursor getRawCursor() {
|
||||
return mDb.query(TABLE_NAME, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
public Cursor getRepoCursor() {
|
||||
String orderBy = null;
|
||||
switch ((int) Config.get(Config.Key.REPO_ORDER)) {
|
||||
case Config.Value.ORDER_NAME:
|
||||
orderBy = "name COLLATE NOCASE";
|
||||
break;
|
||||
case Config.Value.ORDER_DATE:
|
||||
orderBy = "last_update DESC";
|
||||
}
|
||||
return mDb.query(TABLE_NAME, null, null, null, null, null, orderBy);
|
||||
}
|
||||
|
||||
public Set<String> getRepoIDSet() {
|
||||
HashSet<String> set = new HashSet<>(300);
|
||||
try (Cursor c = mDb.query(TABLE_NAME, null, null, null, null, null, null)) {
|
||||
while (c.moveToNext()) {
|
||||
set.add(c.getString(c.getColumnIndex("id")));
|
||||
}
|
||||
}
|
||||
return set;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user