mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-12-06 04:41:23 -08:00
Compare commits
1906 Commits
manager-v7
...
manager-v8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
6fe03d2795 | ||
|
|
c595a87ccf | ||
|
|
fac07c3913 | ||
|
|
c63fdbbc6b | ||
|
|
2ff5d9606b | ||
|
|
ed43452c1a | ||
|
|
8f28d4028f | ||
|
|
b54543b18c | ||
|
|
966d6593ca | ||
|
|
ad95b1c9d1 | ||
|
|
3bfa38c60a | ||
|
|
0bdbcad8be | ||
|
|
80855e89ec | ||
|
|
0850401dc4 | ||
|
|
337fda2023 | ||
|
|
64f238191e | ||
|
|
eb169cb133 | ||
|
|
80cd85b061 | ||
|
|
89275270f3 | ||
|
|
e7339ba619 | ||
|
|
d9ad7d522c | ||
|
|
92789c3113 | ||
|
|
c1c677e161 | ||
|
|
2fe917ff82 | ||
|
|
0e6c205732 | ||
|
|
125ae0a173 | ||
|
|
0245e13591 | ||
|
|
d546733287 | ||
|
|
c275326d59 | ||
|
|
d4561507b8 | ||
|
|
ef0e22cc41 | ||
|
|
62db65bf18 | ||
|
|
d5371f752c | ||
|
|
a5f5e94115 | ||
|
|
2624706c69 | ||
|
|
d39d885ec2 | ||
|
|
d83c744725 | ||
|
|
843995cdb9 | ||
|
|
9491ba77e0 | ||
|
|
58a449d437 | ||
|
|
7f55e0f05b | ||
|
|
67c3f40adb | ||
|
|
ff7a0ba599 | ||
|
|
b152c63102 | ||
|
|
415ff23be5 | ||
|
|
b0d6de783e | ||
|
|
ac28e6e5ca | ||
|
|
4f9e8d2e8a | ||
|
|
21be2f46f3 | ||
|
|
a6e7680212 | ||
|
|
e79e744e08 | ||
|
|
7abdac72a4 | ||
|
|
90d85eaf7d | ||
|
|
e65f9740fb | ||
|
|
7538f89b56 | ||
|
|
7c755a3991 | ||
|
|
10e903c9fc | ||
|
|
b018124226 | ||
|
|
a9350f50c9 | ||
|
|
ed7babcbf1 | ||
|
|
61ebc335c4 | ||
|
|
0167bd76f1 | ||
|
|
79d704008b | ||
|
|
0a703585b0 | ||
|
|
5d632d0d90 | ||
|
|
4eecaea601 | ||
|
|
63055818ec | ||
|
|
0beb08b687 | ||
|
|
b27801a27c | ||
|
|
a0cfce7cbc | ||
|
|
8b7144c986 | ||
|
|
d3f5f5ee59 | ||
|
|
a2a3c7f438 | ||
|
|
4496f82d5b | ||
|
|
09d531557d | ||
|
|
7fee82f731 | ||
|
|
475054c48a | ||
|
|
a743d05751 | ||
|
|
d1ed502e03 | ||
|
|
37744c7ab6 | ||
|
|
bbc9e60a12 | ||
|
|
6c975ecc4c | ||
|
|
23e8a4ce4b | ||
|
|
50134a2f9b | ||
|
|
628b37c4fa | ||
|
|
1b4ae70a43 | ||
|
|
b25c49725f | ||
|
|
b245782c7e | ||
|
|
a9f32baae0 | ||
|
|
e7ef71865d | ||
|
|
88c4f72b37 | ||
|
|
abbcdf91a5 | ||
|
|
b876df6e21 | ||
|
|
4bb81f35d7 | ||
|
|
ff20267b3f | ||
|
|
2c9586d811 | ||
|
|
2813d2031a | ||
|
|
4040a0242f | ||
|
|
781ec810d9 | ||
|
|
9e90a71c04 | ||
|
|
5571714b26 | ||
|
|
e0d1f02ef5 | ||
|
|
1b729e5ff2 | ||
|
|
51e587d4e8 | ||
|
|
ac9c55dbc1 | ||
|
|
065051a360 | ||
|
|
0893ac3141 | ||
|
|
fb40e96917 | ||
|
|
4ca25f74c6 | ||
|
|
7fda917b86 | ||
|
|
e6bd5f2c40 | ||
|
|
8a904ee384 | ||
|
|
00a9f18a1e | ||
|
|
8d68ebb074 | ||
|
|
5f53cfb4a9 | ||
|
|
a2fa8d8be1 | ||
|
|
70a3c78ebb | ||
|
|
db218407b0 | ||
|
|
d52210dd90 | ||
|
|
f3cd9a096a | ||
|
|
e426090a18 | ||
|
|
cbe64fd559 | ||
|
|
63ea7a70bd | ||
|
|
fb0998f7a2 | ||
|
|
a9b00dd537 | ||
|
|
52eb059515 | ||
|
|
7640246255 | ||
|
|
52c83b2916 | ||
|
|
d9cded0fc9 | ||
|
|
750c42caf1 | ||
|
|
bbf650c6cf | ||
|
|
a25dace7e0 | ||
|
|
14ff22fbcd | ||
|
|
07eb7dda2d | ||
|
|
54d1207f92 | ||
|
|
003e44fb84 | ||
|
|
515f346dcc | ||
|
|
d4058175b4 | ||
|
|
2de984ae24 | ||
|
|
761a8bf2a9 | ||
|
|
6df7006b36 | ||
|
|
aceb3ee863 | ||
|
|
11d716a3c8 | ||
|
|
7cc8c014eb | ||
|
|
f21241d944 | ||
|
|
a181fa0652 | ||
|
|
3f748b4d2a | ||
|
|
683450f9c6 | ||
|
|
6050c4e8ba | ||
|
|
158af8819a | ||
|
|
7787bb31fa | ||
|
|
a1fe3e7ccd | ||
|
|
4316028b23 | ||
|
|
f2b52755d6 | ||
|
|
adbd47a36c | ||
|
|
ce693aa5e9 | ||
|
|
ad80804461 | ||
|
|
2d55632430 | ||
|
|
e81f00ef1a | ||
|
|
93fb0e3d74 | ||
|
|
71ce0de606 | ||
|
|
0407062c1d | ||
|
|
f315c4416b | ||
|
|
cda14af208 | ||
|
|
258f170cd7 | ||
|
|
f76015d714 | ||
|
|
7e5e14163c | ||
|
|
bcd1064e94 | ||
|
|
8a8441c875 | ||
|
|
15aa813416 | ||
|
|
605faccffd | ||
|
|
79f2d08c81 | ||
|
|
0568ae5391 | ||
|
|
5330dda9f8 | ||
|
|
ebab126579 | ||
|
|
0e5417a13e | ||
|
|
9a968e0584 | ||
|
|
ffec64d209 | ||
|
|
f332746188 | ||
|
|
b2fa5b551e | ||
|
|
36e83edddc | ||
|
|
6b045eadef | ||
|
|
147264822c | ||
|
|
36e4ccd800 | ||
|
|
796c16237d | ||
|
|
861ad9881c | ||
|
|
3101c538e9 | ||
|
|
42adc7382f | ||
|
|
9bb4dfad13 | ||
|
|
4e7dafb0e4 | ||
|
|
bd00ae8ede | ||
|
|
f309522268 | ||
|
|
a6395d35db | ||
|
|
a028cd5cec | ||
|
|
540000d26e | ||
|
|
888c656aa8 | ||
|
|
0efaddff23 | ||
|
|
94ba7cb0c5 | ||
|
|
2d58c725e0 | ||
|
|
e035523eb8 | ||
|
|
bea5308ab7 | ||
|
|
f006a85fec | ||
|
|
ea93013ebc | ||
|
|
8d4c407201 | ||
|
|
fdeede23f7 | ||
|
|
53c5ca59b6 | ||
|
|
679db97209 | ||
|
|
fbdd72273e | ||
|
|
0165602515 | ||
|
|
96127f8bd1 | ||
|
|
0dbdf336d6 | ||
|
|
48879df2da | ||
|
|
b067a5bb13 | ||
|
|
4b54cf1288 | ||
|
|
6128c24f96 | ||
|
|
d9c58f307f | ||
|
|
b521fbeeda | ||
|
|
d00a3b89f2 | ||
|
|
3d15518191 | ||
|
|
9b6535fdf5 | ||
|
|
e0424fdba3 | ||
|
|
7481c53451 | ||
|
|
7219947237 | ||
|
|
b72004e9cc | ||
|
|
f187213568 | ||
|
|
fc0df84edd | ||
|
|
f24df4f43d | ||
|
|
dab32e1599 | ||
|
|
bc286fd4d3 | ||
|
|
befe1a83b5 | ||
|
|
82ea9db9fd | ||
|
|
c5758b3f2d | ||
|
|
ace3708c9c | ||
|
|
fc5026d268 | ||
|
|
77fd0e54be | ||
|
|
24490e0ff5 | ||
|
|
da3937ff4e | ||
|
|
ebe1ab982e | ||
|
|
98590cb00d | ||
|
|
ff95f634f0 | ||
|
|
ced9b4a8ee | ||
|
|
7af7910e78 | ||
|
|
a4f5d47e72 | ||
|
|
6953cc2411 | ||
|
|
6a0b2ddee9 | ||
|
|
24f5bc98d8 | ||
|
|
5203886f0b | ||
|
|
c10b376575 | ||
|
|
ceb21ced2b | ||
|
|
86789a8694 | ||
|
|
ca2235aee7 | ||
|
|
a385e5cd92 | ||
|
|
0c7a95bdf6 | ||
|
|
036b5acf42 | ||
|
|
056dafc59f | ||
|
|
a9c90718d6 | ||
|
|
cc77a24502 | ||
|
|
71a91ac7a7 | ||
|
|
08a70f033a | ||
|
|
1b0c36dbd5 | ||
|
|
91da1cf817 | ||
|
|
c577a9525d | ||
|
|
0149b1368d | ||
|
|
cd6bcb97ef | ||
|
|
df4161ffcc | ||
|
|
7a133eaf03 | ||
|
|
1cd45b53b1 | ||
|
|
5b30c77403 | ||
|
|
8248480d56 | ||
|
|
345d992d39 | ||
|
|
a7f6afa4bc | ||
|
|
d22c7de79a |
8
.gitattributes
vendored
8
.gitattributes
vendored
@@ -11,8 +11,14 @@
|
||||
*.bat text eol=crlf
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
chromeos/** binary
|
||||
tools/** binary
|
||||
*.jar binary
|
||||
*.exe binary
|
||||
*.apk binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.ttf binary
|
||||
|
||||
# Help GitHub detect languages
|
||||
native/jni/external/** linguist-vendored
|
||||
native/jni/systemproperties/** linguist-language=C++
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -2,8 +2,8 @@ out
|
||||
*.zip
|
||||
*.jks
|
||||
*.apk
|
||||
config.prop
|
||||
update.sh
|
||||
/config.prop
|
||||
/update.sh
|
||||
|
||||
# Built binaries
|
||||
native/out
|
||||
|
||||
9
.gitmodules
vendored
9
.gitmodules
vendored
@@ -19,3 +19,12 @@
|
||||
[submodule "nanopb"]
|
||||
path = native/jni/external/nanopb
|
||||
url = https://github.com/nanopb/nanopb.git
|
||||
[submodule "mincrypt"]
|
||||
path = native/jni/external/mincrypt
|
||||
url = https://github.com/topjohnwu/mincrypt.git
|
||||
[submodule "pcre"]
|
||||
path = native/jni/external/pcre
|
||||
url = https://android.googlesource.com/platform/external/pcre
|
||||
[submodule "termux-elf-cleaner"]
|
||||
path = tools/termux-elf-cleaner
|
||||
url = https://github.com/termux/termux-elf-cleaner.git
|
||||
|
||||
77
README.MD
77
README.MD
@@ -1,42 +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.2/MagiskManager-v8.0.2.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.0)
|
||||
|
||||
## 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
|
||||
- Set configurations in `config.prop`. A sample `config.prop.sample` is provided.
|
||||
- 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.
|
||||
- `build.py` builds in debug mode by default. If you want release builds (with `-r, --release`), you need a Java Keystore to sign APKs and zips. For more information, 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`).
|
||||
Translate each and place them in the respective locations (`[module]/src/main/res/values-[lang]/strings.xml`).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
applicationId 'com.topjohnwu.magisk'
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
versionName rootProject.ext.configProps['appVersion']
|
||||
versionCode rootProject.ext.configProps['appVersionCode'] as Integer
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
argument('butterknife.debuggable', 'false')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
implementation project(':net')
|
||||
implementation project(':shared')
|
||||
implementation project(':signing')
|
||||
implementation 'ru.noties:markwon:2.0.1'
|
||||
implementation 'com.caverock:androidsvg-aar:1.3'
|
||||
implementation 'org.kamranzafar:jtar:2.3'
|
||||
implementation 'net.sourceforge.streamsupport:android-retrostreams:1.7.0'
|
||||
implementation 'com.github.sevar83:indeterminate-checkbox:1.0.5'
|
||||
|
||||
def androidXVersion = "1.0.0"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||
implementation "androidx.preference:preference:${androidXVersion}"
|
||||
implementation "androidx.recyclerview:recyclerview:${androidXVersion}"
|
||||
implementation "androidx.cardview:cardview:${androidXVersion}"
|
||||
implementation "com.google.android.material:material:${androidXVersion}"
|
||||
implementation 'androidx.work:work-runtime:2.0.0'
|
||||
implementation 'androidx.transition:transition:1.1.0-alpha02'
|
||||
|
||||
def libsuVersion = '2.3.2'
|
||||
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
|
||||
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
|
||||
|
||||
def butterKnifeVersion = '10.1.0'
|
||||
implementation "com.jakewharton:butterknife-runtime:${butterKnifeVersion}"
|
||||
annotationProcessor "com.jakewharton:butterknife-compiler:${butterKnifeVersion}"
|
||||
}
|
||||
149
app/build.gradle.kts
Normal file
149
app/build.gradle.kts
Normal file
@@ -0,0 +1,149 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
kotlin("android")
|
||||
kotlin("android.extensions")
|
||||
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"]?.toInt()
|
||||
buildConfigField("int", "LATEST_MAGISK", Config["versionCode"] ?: "Integer.MAX_VALUE")
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
isExperimental = true
|
||||
}
|
||||
|
||||
val copyUtils = tasks.register("copyUtils", Copy::class) {
|
||||
from(rootProject.file("scripts/util_functions.sh"))
|
||||
into("src/main/res/raw")
|
||||
}
|
||||
|
||||
tasks["preBuild"]?.dependsOn(copyUtils)
|
||||
|
||||
dependencies {
|
||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
|
||||
implementation(kotlin("stdlib"))
|
||||
implementation(project(":app:shared"))
|
||||
implementation(project(":app:signing"))
|
||||
|
||||
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 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-alpha03"
|
||||
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.2.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")
|
||||
}
|
||||
48
app/proguard-rules.pro
vendored
48
app/proguard-rules.pro
vendored
@@ -16,34 +16,36 @@
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# BouncyCastle
|
||||
-keep,allowoptimization class org.bouncycastle.jcajce.provider.asymmetric.rsa.**SHA1** { *; }
|
||||
-keep,allowoptimization class org.bouncycastle.jcajce.provider.asymmetric.RSA** { *; }
|
||||
-keep,allowoptimization class org.bouncycastle.jcajce.provider.digest.SHA1** { *; }
|
||||
-dontwarn javax.naming.**
|
||||
|
||||
# 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);
|
||||
# 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(...);
|
||||
}
|
||||
|
||||
# BootSigner
|
||||
-keepclassmembers class com.topjohnwu.signing.BootSigner { *; }
|
||||
# Stubs
|
||||
-keep class a.* { *; }
|
||||
|
||||
# SVG
|
||||
-dontwarn com.caverock.androidsvg.SVGAndroidRenderer
|
||||
# Snet
|
||||
-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);
|
||||
}
|
||||
|
||||
# RetroStreams
|
||||
-dontwarn java9.**
|
||||
|
||||
# Strip logging
|
||||
-assumenosideeffects class com.topjohnwu.magisk.utils.Logger {
|
||||
public *** debug(...);
|
||||
# Strip Timber verbose and debug logging
|
||||
-assumenosideeffects class timber.log.Timber.Tree {
|
||||
public void v(**);
|
||||
public void d(**);
|
||||
}
|
||||
|
||||
# Excessive obfuscation
|
||||
-repackageclasses 'a'
|
||||
-repackageclasses
|
||||
-allowaccessmodification
|
||||
-optimizationpasses 6
|
||||
|
||||
# QOL
|
||||
-dontnote **
|
||||
-dontwarn com.caverock.androidsvg.**
|
||||
-dontwarn ru.noties.markwon.**
|
||||
|
||||
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",
|
||||
@@ -43,12 +43,15 @@ public class Networking {
|
||||
gms.getClassLoader()
|
||||
.loadClass("com.google.android.gms.common.security.ProviderInstallerImpl")
|
||||
.getMethod("insertProvider", Context.class)
|
||||
.invoke(null, context);
|
||||
.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;
|
||||
|
||||
@@ -15,16 +15,14 @@ import java.io.FileOutputStream;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Reader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.util.Scanner;
|
||||
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;
|
||||
@@ -67,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;
|
||||
@@ -170,37 +163,21 @@ 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);
|
||||
}
|
||||
|
||||
private String dlString() throws IOException {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
try (Reader reader = new InputStreamReader(getInputStream())) {
|
||||
int len;
|
||||
char[] buf = new char[4096];
|
||||
while ((len = reader.read(buf)) != -1) {
|
||||
builder.append(buf, 0, len);
|
||||
}
|
||||
try (Scanner s = new Scanner(getInputStream(), "UTF-8")) {
|
||||
s.useDelimiter("\\A");
|
||||
return s.next();
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private JSONObject dlJSONObject() throws IOException, JSONException {
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
35
app/signing/build.gradle.kts
Normal file
35
app/signing/build.gradle.kts
Normal file
@@ -0,0 +1,35 @@
|
||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||
|
||||
plugins {
|
||||
id("java-library")
|
||||
id("java")
|
||||
id("com.github.johnrengelman.shadow") version "6.0.0"
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
val jar by tasks.getting(Jar::class) {
|
||||
manifest {
|
||||
attributes["Main-Class"] = "com.topjohnwu.signing.ZipSigner"
|
||||
}
|
||||
}
|
||||
|
||||
val shadowJar by tasks.getting(ShadowJar::class) {
|
||||
archiveBaseName.set("zipsigner")
|
||||
archiveClassifier.set(null as String?)
|
||||
archiveVersion.set("4.0")
|
||||
}
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
|
||||
|
||||
api("org.bouncycastle:bcprov-jdk15on:1.67")
|
||||
api("org.bouncycastle:bcpkix-jdk15on:1.67")
|
||||
}
|
||||
772
app/signing/src/main/java/com/topjohnwu/signing/ApkSignerV2.java
Normal file
772
app/signing/src/main/java/com/topjohnwu/signing/ApkSignerV2.java
Normal file
@@ -0,0 +1,772 @@
|
||||
package com.topjohnwu.signing;
|
||||
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.security.DigestException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.MGF1ParameterSpec;
|
||||
import java.security.spec.PSSParameterSpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* APK Signature Scheme v2 signer.
|
||||
*
|
||||
* <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
|
||||
* bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
|
||||
* uncompressed contents of ZIP entries.
|
||||
*/
|
||||
public abstract class ApkSignerV2 {
|
||||
/*
|
||||
* The two main goals of APK Signature Scheme v2 are:
|
||||
* 1. Detect any unauthorized modifications to the APK. This is achieved by making the signature
|
||||
* cover every byte of the APK being signed.
|
||||
* 2. Enable much faster signature and integrity verification. This is achieved by requiring
|
||||
* only a minimal amount of APK parsing before the signature is verified, thus completely
|
||||
* bypassing ZIP entry decompression and by making integrity verification parallelizable by
|
||||
* employing a hash tree.
|
||||
*
|
||||
* The generated signature block is wrapped into an APK Signing Block and inserted into the
|
||||
* original APK immediately before the start of ZIP Central Directory. This is to ensure that
|
||||
* JAR and ZIP parsers continue to work on the signed APK. The APK Signing Block is designed for
|
||||
* extensibility. For example, a future signature scheme could insert its signatures there as
|
||||
* well. The contract of the APK Signing Block is that all contents outside of the block must be
|
||||
* protected by signatures inside the block.
|
||||
*/
|
||||
|
||||
public static final int SIGNATURE_RSA_PSS_WITH_SHA256 = 0x0101;
|
||||
public static final int SIGNATURE_RSA_PSS_WITH_SHA512 = 0x0102;
|
||||
public static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256 = 0x0103;
|
||||
public static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512 = 0x0104;
|
||||
public static final int SIGNATURE_ECDSA_WITH_SHA256 = 0x0201;
|
||||
public static final int SIGNATURE_ECDSA_WITH_SHA512 = 0x0202;
|
||||
public static final int SIGNATURE_DSA_WITH_SHA256 = 0x0301;
|
||||
public static final int SIGNATURE_DSA_WITH_SHA512 = 0x0302;
|
||||
|
||||
/**
|
||||
* {@code .SF} file header section attribute indicating that the APK is signed not just with
|
||||
* JAR signature scheme but also with APK Signature Scheme v2 or newer. This attribute
|
||||
* facilitates v2 signature stripping detection.
|
||||
*
|
||||
* <p>The attribute contains a comma-separated set of signature scheme IDs.
|
||||
*/
|
||||
public static final String SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME = "X-Android-APK-Signed";
|
||||
public static final String SF_ATTRIBUTE_ANDROID_APK_SIGNED_VALUE = "2";
|
||||
|
||||
private static final int CONTENT_DIGEST_CHUNKED_SHA256 = 0;
|
||||
private static final int CONTENT_DIGEST_CHUNKED_SHA512 = 1;
|
||||
|
||||
private static final int CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024;
|
||||
|
||||
private static final byte[] APK_SIGNING_BLOCK_MAGIC =
|
||||
new byte[] {
|
||||
0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20,
|
||||
0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32,
|
||||
};
|
||||
private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
|
||||
|
||||
private ApkSignerV2() {}
|
||||
|
||||
/**
|
||||
* Signer configuration.
|
||||
*/
|
||||
public static final class SignerConfig {
|
||||
/** Private key. */
|
||||
public PrivateKey privateKey;
|
||||
|
||||
/**
|
||||
* Certificates, with the first certificate containing the public key corresponding to
|
||||
* {@link #privateKey}.
|
||||
*/
|
||||
public List<X509Certificate> certificates;
|
||||
|
||||
/**
|
||||
* List of signature algorithms with which to sign (see {@code SIGNATURE_...} constants).
|
||||
*/
|
||||
public List<Integer> signatureAlgorithms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs the provided APK using APK Signature Scheme v2 and returns the signed APK as a list of
|
||||
* consecutive chunks.
|
||||
*
|
||||
* <p>NOTE: To enable APK signature verifier to detect v2 signature stripping, header sections
|
||||
* of META-INF/*.SF files of APK being signed must contain the
|
||||
* {@code X-Android-APK-Signed: true} attribute.
|
||||
*
|
||||
* @param inputApk contents of the APK to be signed. The APK starts at the current position
|
||||
* of the buffer and ends at the limit of the buffer.
|
||||
* @param signerConfigs signer configurations, one for each signer.
|
||||
*
|
||||
* @throws ApkParseException if the APK cannot be parsed.
|
||||
* @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
|
||||
* cannot be used in general.
|
||||
* @throws SignatureException if an error occurs when computing digests of generating
|
||||
* signatures.
|
||||
*/
|
||||
public static ByteBuffer[] sign(
|
||||
ByteBuffer inputApk,
|
||||
List<SignerConfig> signerConfigs)
|
||||
throws ApkParseException, InvalidKeyException, SignatureException {
|
||||
// Slice/create a view in the inputApk to make sure that:
|
||||
// 1. inputApk is what's between position and limit of the original inputApk, and
|
||||
// 2. changes to position, limit, and byte order are not reflected in the original.
|
||||
ByteBuffer originalInputApk = inputApk;
|
||||
inputApk = originalInputApk.slice();
|
||||
inputApk.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
// Locate ZIP End of Central Directory (EoCD), Central Directory, and check that Central
|
||||
// Directory is immediately followed by the ZIP End of Central Directory.
|
||||
int eocdOffset = ZipUtils.findZipEndOfCentralDirectoryRecord(inputApk);
|
||||
if (eocdOffset == -1) {
|
||||
throw new ApkParseException("Failed to locate ZIP End of Central Directory");
|
||||
}
|
||||
if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(inputApk, eocdOffset)) {
|
||||
throw new ApkParseException("ZIP64 format not supported");
|
||||
}
|
||||
inputApk.position(eocdOffset);
|
||||
long centralDirSizeLong = ZipUtils.getZipEocdCentralDirectorySizeBytes(inputApk);
|
||||
if (centralDirSizeLong > Integer.MAX_VALUE) {
|
||||
throw new ApkParseException(
|
||||
"ZIP Central Directory size out of range: " + centralDirSizeLong);
|
||||
}
|
||||
int centralDirSize = (int) centralDirSizeLong;
|
||||
long centralDirOffsetLong = ZipUtils.getZipEocdCentralDirectoryOffset(inputApk);
|
||||
if (centralDirOffsetLong > Integer.MAX_VALUE) {
|
||||
throw new ApkParseException(
|
||||
"ZIP Central Directory offset in file out of range: " + centralDirOffsetLong);
|
||||
}
|
||||
int centralDirOffset = (int) centralDirOffsetLong;
|
||||
int expectedEocdOffset = centralDirOffset + centralDirSize;
|
||||
if (expectedEocdOffset < centralDirOffset) {
|
||||
throw new ApkParseException(
|
||||
"ZIP Central Directory extent too large. Offset: " + centralDirOffset
|
||||
+ ", size: " + centralDirSize);
|
||||
}
|
||||
if (eocdOffset != expectedEocdOffset) {
|
||||
throw new ApkParseException(
|
||||
"ZIP Central Directory not immeiately followed by ZIP End of"
|
||||
+ " Central Directory. CD end: " + expectedEocdOffset
|
||||
+ ", EoCD start: " + eocdOffset);
|
||||
}
|
||||
|
||||
// Create ByteBuffers holding the contents of everything before ZIP Central Directory,
|
||||
// ZIP Central Directory, and ZIP End of Central Directory.
|
||||
inputApk.clear();
|
||||
ByteBuffer beforeCentralDir = getByteBuffer(inputApk, centralDirOffset);
|
||||
ByteBuffer centralDir = getByteBuffer(inputApk, eocdOffset - centralDirOffset);
|
||||
// Create a copy of End of Central Directory because we'll need modify its contents later.
|
||||
byte[] eocdBytes = new byte[inputApk.remaining()];
|
||||
inputApk.get(eocdBytes);
|
||||
ByteBuffer eocd = ByteBuffer.wrap(eocdBytes);
|
||||
eocd.order(inputApk.order());
|
||||
|
||||
// Figure which which digests to use for APK contents.
|
||||
Set<Integer> contentDigestAlgorithms = new HashSet<>();
|
||||
for (SignerConfig signerConfig : signerConfigs) {
|
||||
for (int signatureAlgorithm : signerConfig.signatureAlgorithms) {
|
||||
contentDigestAlgorithms.add(
|
||||
getSignatureAlgorithmContentDigestAlgorithm(signatureAlgorithm));
|
||||
}
|
||||
}
|
||||
|
||||
// Compute digests of APK contents.
|
||||
Map<Integer, byte[]> contentDigests; // digest algorithm ID -> digest
|
||||
try {
|
||||
contentDigests =
|
||||
computeContentDigests(
|
||||
contentDigestAlgorithms,
|
||||
new ByteBuffer[] {beforeCentralDir, centralDir, eocd});
|
||||
} catch (DigestException e) {
|
||||
throw new SignatureException("Failed to compute digests of APK", e);
|
||||
}
|
||||
|
||||
// Sign the digests and wrap the signatures and signer info into an APK Signing Block.
|
||||
ByteBuffer apkSigningBlock =
|
||||
ByteBuffer.wrap(generateApkSigningBlock(signerConfigs, contentDigests));
|
||||
|
||||
// Update Central Directory Offset in End of Central Directory Record. Central Directory
|
||||
// follows the APK Signing Block and thus is shifted by the size of the APK Signing Block.
|
||||
centralDirOffset += apkSigningBlock.remaining();
|
||||
eocd.clear();
|
||||
ZipUtils.setZipEocdCentralDirectoryOffset(eocd, centralDirOffset);
|
||||
|
||||
// Follow the Java NIO pattern for ByteBuffer whose contents have been consumed.
|
||||
originalInputApk.position(originalInputApk.limit());
|
||||
|
||||
// Reset positions (to 0) and limits (to capacity) in the ByteBuffers below to follow the
|
||||
// Java NIO pattern for ByteBuffers which are ready for their contents to be read by caller.
|
||||
// Contrary to the name, this does not clear the contents of these ByteBuffer.
|
||||
beforeCentralDir.clear();
|
||||
centralDir.clear();
|
||||
eocd.clear();
|
||||
|
||||
// Insert APK Signing Block immediately before the ZIP Central Directory.
|
||||
return new ByteBuffer[] {
|
||||
beforeCentralDir,
|
||||
apkSigningBlock,
|
||||
centralDir,
|
||||
eocd,
|
||||
};
|
||||
}
|
||||
|
||||
private static Map<Integer, byte[]> computeContentDigests(
|
||||
Set<Integer> digestAlgorithms,
|
||||
ByteBuffer[] contents) throws DigestException {
|
||||
// For each digest algorithm the result is computed as follows:
|
||||
// 1. Each segment of contents is split into consecutive chunks of 1 MB in size.
|
||||
// The final chunk will be shorter iff the length of segment is not a multiple of 1 MB.
|
||||
// No chunks are produced for empty (zero length) segments.
|
||||
// 2. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk's
|
||||
// length in bytes (uint32 little-endian) and the chunk's contents.
|
||||
// 3. The output digest is computed over the concatenation of the byte 0x5a, the number of
|
||||
// chunks (uint32 little-endian) and the concatenation of digests of chunks of all
|
||||
// segments in-order.
|
||||
|
||||
int chunkCount = 0;
|
||||
for (ByteBuffer input : contents) {
|
||||
chunkCount += getChunkCount(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
|
||||
}
|
||||
|
||||
final Map<Integer, byte[]> digestsOfChunks = new HashMap<>(digestAlgorithms.size());
|
||||
for (int digestAlgorithm : digestAlgorithms) {
|
||||
int digestOutputSizeBytes = getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
|
||||
byte[] concatenationOfChunkCountAndChunkDigests =
|
||||
new byte[5 + chunkCount * digestOutputSizeBytes];
|
||||
concatenationOfChunkCountAndChunkDigests[0] = 0x5a;
|
||||
setUnsignedInt32LittleEngian(
|
||||
chunkCount, concatenationOfChunkCountAndChunkDigests, 1);
|
||||
digestsOfChunks.put(digestAlgorithm, concatenationOfChunkCountAndChunkDigests);
|
||||
}
|
||||
|
||||
int chunkIndex = 0;
|
||||
byte[] chunkContentPrefix = new byte[5];
|
||||
chunkContentPrefix[0] = (byte) 0xa5;
|
||||
// Optimization opportunity: digests of chunks can be computed in parallel.
|
||||
for (ByteBuffer input : contents) {
|
||||
while (input.hasRemaining()) {
|
||||
int chunkSize =
|
||||
Math.min(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
|
||||
final ByteBuffer chunk = getByteBuffer(input, chunkSize);
|
||||
for (int digestAlgorithm : digestAlgorithms) {
|
||||
String jcaAlgorithmName =
|
||||
getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
|
||||
MessageDigest md;
|
||||
try {
|
||||
md = MessageDigest.getInstance(jcaAlgorithmName);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new DigestException(
|
||||
jcaAlgorithmName + " MessageDigest not supported", e);
|
||||
}
|
||||
// Reset position to 0 and limit to capacity. Position would've been modified
|
||||
// by the preceding iteration of this loop. NOTE: Contrary to the method name,
|
||||
// this does not modify the contents of the chunk.
|
||||
chunk.clear();
|
||||
setUnsignedInt32LittleEngian(chunk.remaining(), chunkContentPrefix, 1);
|
||||
md.update(chunkContentPrefix);
|
||||
md.update(chunk);
|
||||
byte[] concatenationOfChunkCountAndChunkDigests =
|
||||
digestsOfChunks.get(digestAlgorithm);
|
||||
int expectedDigestSizeBytes =
|
||||
getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
|
||||
int actualDigestSizeBytes =
|
||||
md.digest(
|
||||
concatenationOfChunkCountAndChunkDigests,
|
||||
5 + chunkIndex * expectedDigestSizeBytes,
|
||||
expectedDigestSizeBytes);
|
||||
if (actualDigestSizeBytes != expectedDigestSizeBytes) {
|
||||
throw new DigestException(
|
||||
"Unexpected output size of " + md.getAlgorithm()
|
||||
+ " digest: " + actualDigestSizeBytes);
|
||||
}
|
||||
}
|
||||
chunkIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
Map<Integer, byte[]> result = new HashMap<>(digestAlgorithms.size());
|
||||
for (Map.Entry<Integer, byte[]> entry : digestsOfChunks.entrySet()) {
|
||||
int digestAlgorithm = entry.getKey();
|
||||
byte[] concatenationOfChunkCountAndChunkDigests = entry.getValue();
|
||||
String jcaAlgorithmName = getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
|
||||
MessageDigest md;
|
||||
try {
|
||||
md = MessageDigest.getInstance(jcaAlgorithmName);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new DigestException(jcaAlgorithmName + " MessageDigest not supported", e);
|
||||
}
|
||||
result.put(digestAlgorithm, md.digest(concatenationOfChunkCountAndChunkDigests));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int getChunkCount(int inputSize, int chunkSize) {
|
||||
return (inputSize + chunkSize - 1) / chunkSize;
|
||||
}
|
||||
|
||||
private static void setUnsignedInt32LittleEngian(int value, byte[] result, int offset) {
|
||||
result[offset] = (byte) (value & 0xff);
|
||||
result[offset + 1] = (byte) ((value >> 8) & 0xff);
|
||||
result[offset + 2] = (byte) ((value >> 16) & 0xff);
|
||||
result[offset + 3] = (byte) ((value >> 24) & 0xff);
|
||||
}
|
||||
|
||||
private static byte[] generateApkSigningBlock(
|
||||
List<SignerConfig> signerConfigs,
|
||||
Map<Integer, byte[]> contentDigests) throws InvalidKeyException, SignatureException {
|
||||
byte[] apkSignatureSchemeV2Block =
|
||||
generateApkSignatureSchemeV2Block(signerConfigs, contentDigests);
|
||||
return generateApkSigningBlock(apkSignatureSchemeV2Block);
|
||||
}
|
||||
|
||||
private static byte[] generateApkSigningBlock(byte[] apkSignatureSchemeV2Block) {
|
||||
// FORMAT:
|
||||
// uint64: size (excluding this field)
|
||||
// repeated ID-value pairs:
|
||||
// uint64: size (excluding this field)
|
||||
// uint32: ID
|
||||
// (size - 4) bytes: value
|
||||
// uint64: size (same as the one above)
|
||||
// uint128: magic
|
||||
|
||||
int resultSize =
|
||||
8 // size
|
||||
+ 8 + 4 + apkSignatureSchemeV2Block.length // v2Block as ID-value pair
|
||||
+ 8 // size
|
||||
+ 16 // magic
|
||||
;
|
||||
ByteBuffer result = ByteBuffer.allocate(resultSize);
|
||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||
long blockSizeFieldValue = resultSize - 8;
|
||||
result.putLong(blockSizeFieldValue);
|
||||
|
||||
long pairSizeFieldValue = 4 + apkSignatureSchemeV2Block.length;
|
||||
result.putLong(pairSizeFieldValue);
|
||||
result.putInt(APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
|
||||
result.put(apkSignatureSchemeV2Block);
|
||||
|
||||
result.putLong(blockSizeFieldValue);
|
||||
result.put(APK_SIGNING_BLOCK_MAGIC);
|
||||
|
||||
return result.array();
|
||||
}
|
||||
|
||||
private static byte[] generateApkSignatureSchemeV2Block(
|
||||
List<SignerConfig> signerConfigs,
|
||||
Map<Integer, byte[]> contentDigests) throws InvalidKeyException, SignatureException {
|
||||
// FORMAT:
|
||||
// * length-prefixed sequence of length-prefixed signer blocks.
|
||||
|
||||
List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size());
|
||||
int signerNumber = 0;
|
||||
for (SignerConfig signerConfig : signerConfigs) {
|
||||
signerNumber++;
|
||||
byte[] signerBlock;
|
||||
try {
|
||||
signerBlock = generateSignerBlock(signerConfig, contentDigests);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidKeyException("Signer #" + signerNumber + " failed", e);
|
||||
} catch (SignatureException e) {
|
||||
throw new SignatureException("Signer #" + signerNumber + " failed", e);
|
||||
}
|
||||
signerBlocks.add(signerBlock);
|
||||
}
|
||||
|
||||
return encodeAsSequenceOfLengthPrefixedElements(
|
||||
new byte[][] {
|
||||
encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
|
||||
});
|
||||
}
|
||||
|
||||
private static byte[] generateSignerBlock(
|
||||
SignerConfig signerConfig,
|
||||
Map<Integer, byte[]> contentDigests) throws InvalidKeyException, SignatureException {
|
||||
if (signerConfig.certificates.isEmpty()) {
|
||||
throw new SignatureException("No certificates configured for signer");
|
||||
}
|
||||
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
|
||||
|
||||
byte[] encodedPublicKey = encodePublicKey(publicKey);
|
||||
|
||||
V2SignatureSchemeBlock.SignedData signedData = new V2SignatureSchemeBlock.SignedData();
|
||||
try {
|
||||
signedData.certificates = encodeCertificates(signerConfig.certificates);
|
||||
} catch (CertificateEncodingException e) {
|
||||
throw new SignatureException("Failed to encode certificates", e);
|
||||
}
|
||||
|
||||
List<Pair<Integer, byte[]>> digests =
|
||||
new ArrayList<>(signerConfig.signatureAlgorithms.size());
|
||||
for (int signatureAlgorithm : signerConfig.signatureAlgorithms) {
|
||||
int contentDigestAlgorithm =
|
||||
getSignatureAlgorithmContentDigestAlgorithm(signatureAlgorithm);
|
||||
byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
|
||||
if (contentDigest == null) {
|
||||
throw new RuntimeException(
|
||||
getContentDigestAlgorithmJcaDigestAlgorithm(contentDigestAlgorithm)
|
||||
+ " content digest for "
|
||||
+ getSignatureAlgorithmJcaSignatureAlgorithm(signatureAlgorithm)
|
||||
+ " not computed");
|
||||
}
|
||||
digests.add(Pair.create(signatureAlgorithm, contentDigest));
|
||||
}
|
||||
signedData.digests = digests;
|
||||
|
||||
V2SignatureSchemeBlock.Signer signer = new V2SignatureSchemeBlock.Signer();
|
||||
// FORMAT:
|
||||
// * length-prefixed sequence of length-prefixed digests:
|
||||
// * uint32: signature algorithm ID
|
||||
// * length-prefixed bytes: digest of contents
|
||||
// * length-prefixed sequence of certificates:
|
||||
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
|
||||
// * length-prefixed sequence of length-prefixed additional attributes:
|
||||
// * uint32: ID
|
||||
// * (length - 4) bytes: value
|
||||
signer.signedData = encodeAsSequenceOfLengthPrefixedElements(new byte[][] {
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(signedData.digests),
|
||||
encodeAsSequenceOfLengthPrefixedElements(signedData.certificates),
|
||||
// additional attributes
|
||||
new byte[0],
|
||||
});
|
||||
signer.publicKey = encodedPublicKey;
|
||||
signer.signatures = new ArrayList<>();
|
||||
for (int signatureAlgorithm : signerConfig.signatureAlgorithms) {
|
||||
Pair<String, ? extends AlgorithmParameterSpec> signatureParams =
|
||||
getSignatureAlgorithmJcaSignatureAlgorithm(signatureAlgorithm);
|
||||
String jcaSignatureAlgorithm = signatureParams.getFirst();
|
||||
AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureParams.getSecond();
|
||||
byte[] signatureBytes;
|
||||
try {
|
||||
Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
|
||||
signature.initSign(signerConfig.privateKey);
|
||||
if (jcaSignatureAlgorithmParams != null) {
|
||||
signature.setParameter(jcaSignatureAlgorithmParams);
|
||||
}
|
||||
signature.update(signer.signedData);
|
||||
signatureBytes = signature.sign();
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidKeyException("Failed sign using " + jcaSignatureAlgorithm, e);
|
||||
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException
|
||||
| SignatureException e) {
|
||||
throw new SignatureException("Failed sign using " + jcaSignatureAlgorithm, e);
|
||||
}
|
||||
|
||||
try {
|
||||
Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
|
||||
signature.initVerify(publicKey);
|
||||
if (jcaSignatureAlgorithmParams != null) {
|
||||
signature.setParameter(jcaSignatureAlgorithmParams);
|
||||
}
|
||||
signature.update(signer.signedData);
|
||||
if (!signature.verify(signatureBytes)) {
|
||||
throw new SignatureException("Signature did not verify");
|
||||
}
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidKeyException("Failed to verify generated " + jcaSignatureAlgorithm
|
||||
+ " signature using public key from certificate", e);
|
||||
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException
|
||||
| SignatureException e) {
|
||||
throw new SignatureException("Failed to verify generated " + jcaSignatureAlgorithm
|
||||
+ " signature using public key from certificate", e);
|
||||
}
|
||||
|
||||
signer.signatures.add(Pair.create(signatureAlgorithm, signatureBytes));
|
||||
}
|
||||
|
||||
// FORMAT:
|
||||
// * length-prefixed signed data
|
||||
// * length-prefixed sequence of length-prefixed signatures:
|
||||
// * uint32: signature algorithm ID
|
||||
// * length-prefixed bytes: signature of signed data
|
||||
// * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
|
||||
return encodeAsSequenceOfLengthPrefixedElements(
|
||||
new byte[][] {
|
||||
signer.signedData,
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||
signer.signatures),
|
||||
signer.publicKey,
|
||||
});
|
||||
}
|
||||
|
||||
private static final class V2SignatureSchemeBlock {
|
||||
private static final class Signer {
|
||||
public byte[] signedData;
|
||||
public List<Pair<Integer, byte[]>> signatures;
|
||||
public byte[] publicKey;
|
||||
}
|
||||
|
||||
private static final class SignedData {
|
||||
public List<Pair<Integer, byte[]>> digests;
|
||||
public List<byte[]> certificates;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] encodePublicKey(PublicKey publicKey) throws InvalidKeyException {
|
||||
byte[] encodedPublicKey = null;
|
||||
if ("X.509".equals(publicKey.getFormat())) {
|
||||
encodedPublicKey = publicKey.getEncoded();
|
||||
}
|
||||
if (encodedPublicKey == null) {
|
||||
try {
|
||||
encodedPublicKey =
|
||||
KeyFactory.getInstance(publicKey.getAlgorithm())
|
||||
.getKeySpec(publicKey, X509EncodedKeySpec.class)
|
||||
.getEncoded();
|
||||
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
|
||||
throw new InvalidKeyException(
|
||||
"Failed to obtain X.509 encoded form of public key " + publicKey
|
||||
+ " of class " + publicKey.getClass().getName(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
if ((encodedPublicKey == null) || (encodedPublicKey.length == 0)) {
|
||||
throw new InvalidKeyException(
|
||||
"Failed to obtain X.509 encoded form of public key " + publicKey
|
||||
+ " of class " + publicKey.getClass().getName());
|
||||
}
|
||||
return encodedPublicKey;
|
||||
}
|
||||
|
||||
public static List<byte[]> encodeCertificates(List<X509Certificate> certificates)
|
||||
throws CertificateEncodingException {
|
||||
List<byte[]> result = new ArrayList<>();
|
||||
for (X509Certificate certificate : certificates) {
|
||||
result.add(certificate.getEncoded());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static byte[] encodeAsSequenceOfLengthPrefixedElements(List<byte[]> sequence) {
|
||||
return encodeAsSequenceOfLengthPrefixedElements(
|
||||
sequence.toArray(new byte[sequence.size()][]));
|
||||
}
|
||||
|
||||
private static byte[] encodeAsSequenceOfLengthPrefixedElements(byte[][] sequence) {
|
||||
int payloadSize = 0;
|
||||
for (byte[] element : sequence) {
|
||||
payloadSize += 4 + element.length;
|
||||
}
|
||||
ByteBuffer result = ByteBuffer.allocate(payloadSize);
|
||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||
for (byte[] element : sequence) {
|
||||
result.putInt(element.length);
|
||||
result.put(element);
|
||||
}
|
||||
return result.array();
|
||||
}
|
||||
|
||||
private static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||
List<Pair<Integer, byte[]>> sequence) {
|
||||
int resultSize = 0;
|
||||
for (Pair<Integer, byte[]> element : sequence) {
|
||||
resultSize += 12 + element.getSecond().length;
|
||||
}
|
||||
ByteBuffer result = ByteBuffer.allocate(resultSize);
|
||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||
for (Pair<Integer, byte[]> element : sequence) {
|
||||
byte[] second = element.getSecond();
|
||||
result.putInt(8 + second.length);
|
||||
result.putInt(element.getFirst());
|
||||
result.putInt(second.length);
|
||||
result.put(second);
|
||||
}
|
||||
return result.array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Relative <em>get</em> method for reading {@code size} number of bytes from the current
|
||||
* position of this buffer.
|
||||
*
|
||||
* <p>This method reads the next {@code size} bytes at this buffer's current position,
|
||||
* returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to
|
||||
* {@code size}, byte order set to this buffer's byte order; and then increments the position by
|
||||
* {@code size}.
|
||||
*/
|
||||
private static ByteBuffer getByteBuffer(ByteBuffer source, int size) {
|
||||
if (size < 0) {
|
||||
throw new IllegalArgumentException("size: " + size);
|
||||
}
|
||||
int originalLimit = source.limit();
|
||||
int position = source.position();
|
||||
int limit = position + size;
|
||||
if ((limit < position) || (limit > originalLimit)) {
|
||||
throw new BufferUnderflowException();
|
||||
}
|
||||
source.limit(limit);
|
||||
try {
|
||||
ByteBuffer result = source.slice();
|
||||
result.order(source.order());
|
||||
source.position(limit);
|
||||
return result;
|
||||
} finally {
|
||||
source.limit(originalLimit);
|
||||
}
|
||||
}
|
||||
|
||||
private static Pair<String, ? extends AlgorithmParameterSpec>
|
||||
getSignatureAlgorithmJcaSignatureAlgorithm(int sigAlgorithm) {
|
||||
switch (sigAlgorithm) {
|
||||
case SIGNATURE_RSA_PSS_WITH_SHA256:
|
||||
return Pair.create(
|
||||
"SHA256withRSA/PSS",
|
||||
new PSSParameterSpec(
|
||||
"SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1));
|
||||
case SIGNATURE_RSA_PSS_WITH_SHA512:
|
||||
return Pair.create(
|
||||
"SHA512withRSA/PSS",
|
||||
new PSSParameterSpec(
|
||||
"SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1));
|
||||
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
|
||||
return Pair.create("SHA256withRSA", null);
|
||||
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
|
||||
return Pair.create("SHA512withRSA", null);
|
||||
case SIGNATURE_ECDSA_WITH_SHA256:
|
||||
return Pair.create("SHA256withECDSA", null);
|
||||
case SIGNATURE_ECDSA_WITH_SHA512:
|
||||
return Pair.create("SHA512withECDSA", null);
|
||||
case SIGNATURE_DSA_WITH_SHA256:
|
||||
return Pair.create("SHA256withDSA", null);
|
||||
case SIGNATURE_DSA_WITH_SHA512:
|
||||
return Pair.create("SHA512withDSA", null);
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Unknown signature algorithm: 0x"
|
||||
+ Long.toHexString(sigAlgorithm & 0xffffffff));
|
||||
}
|
||||
}
|
||||
|
||||
private static int getSignatureAlgorithmContentDigestAlgorithm(int sigAlgorithm) {
|
||||
switch (sigAlgorithm) {
|
||||
case SIGNATURE_RSA_PSS_WITH_SHA256:
|
||||
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
|
||||
case SIGNATURE_ECDSA_WITH_SHA256:
|
||||
case SIGNATURE_DSA_WITH_SHA256:
|
||||
return CONTENT_DIGEST_CHUNKED_SHA256;
|
||||
case SIGNATURE_RSA_PSS_WITH_SHA512:
|
||||
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
|
||||
case SIGNATURE_ECDSA_WITH_SHA512:
|
||||
case SIGNATURE_DSA_WITH_SHA512:
|
||||
return CONTENT_DIGEST_CHUNKED_SHA512;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Unknown signature algorithm: 0x"
|
||||
+ Long.toHexString(sigAlgorithm & 0xffffffff));
|
||||
}
|
||||
}
|
||||
|
||||
private static String getContentDigestAlgorithmJcaDigestAlgorithm(int digestAlgorithm) {
|
||||
switch (digestAlgorithm) {
|
||||
case CONTENT_DIGEST_CHUNKED_SHA256:
|
||||
return "SHA-256";
|
||||
case CONTENT_DIGEST_CHUNKED_SHA512:
|
||||
return "SHA-512";
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Unknown content digest algorthm: " + digestAlgorithm);
|
||||
}
|
||||
}
|
||||
|
||||
private static int getContentDigestAlgorithmOutputSizeBytes(int digestAlgorithm) {
|
||||
switch (digestAlgorithm) {
|
||||
case CONTENT_DIGEST_CHUNKED_SHA256:
|
||||
return 256 / 8;
|
||||
case CONTENT_DIGEST_CHUNKED_SHA512:
|
||||
return 512 / 8;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Unknown content digest algorthm: " + digestAlgorithm);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that APK file could not be parsed.
|
||||
*/
|
||||
public static class ApkParseException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ApkParseException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ApkParseException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pair of two elements.
|
||||
*/
|
||||
private static class Pair<A, B> {
|
||||
private final A mFirst;
|
||||
private final B mSecond;
|
||||
|
||||
private Pair(A first, B second) {
|
||||
mFirst = first;
|
||||
mSecond = second;
|
||||
}
|
||||
|
||||
public static <A, B> Pair<A, B> create(A first, B second) {
|
||||
return new Pair<>(first, second);
|
||||
}
|
||||
|
||||
public A getFirst() {
|
||||
return mFirst;
|
||||
}
|
||||
|
||||
public B getSecond() {
|
||||
return mSecond;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode());
|
||||
result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
@SuppressWarnings("rawtypes")
|
||||
Pair other = (Pair) obj;
|
||||
if (mFirst == null) {
|
||||
if (other.mFirst != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!mFirst.equals(other.mFirst)) {
|
||||
return false;
|
||||
}
|
||||
if (mSecond == null) {
|
||||
return other.mSecond == null;
|
||||
} else return mSecond.equals(other.mSecond);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,13 +18,19 @@ public class BootSigner {
|
||||
} else if (args.length > 0 && "-sign".equals(args[0])) {
|
||||
InputStream cert = null;
|
||||
InputStream key = null;
|
||||
String name = "/boot";
|
||||
|
||||
if (args.length >= 3) {
|
||||
cert = new FileInputStream(args[1]);
|
||||
key = new FileInputStream(args[2]);
|
||||
}
|
||||
if (args.length == 2) {
|
||||
name = args[1];
|
||||
} else if (args.length >= 4) {
|
||||
name = args[3];
|
||||
}
|
||||
|
||||
boolean success = SignBoot.doSignature("/boot", System.in, System.out, cert, key);
|
||||
boolean success = SignBoot.doSignature(name, System.in, System.out, cert, key);
|
||||
System.exit(success ? 0 : 1);
|
||||
} else {
|
||||
System.err.println(
|
||||
@@ -34,8 +40,9 @@ public class BootSigner {
|
||||
"Actions:\n" +
|
||||
" -verify [x509.pem]\n" +
|
||||
" verify image, cert is optional\n" +
|
||||
" -sign [x509.pem] [pk8]\n" +
|
||||
" sign image, cert and key pair is optional\n"
|
||||
" -sign [x509.pem] [pk8] [name]\n" +
|
||||
" sign image, name, cert and key pair are optional\n" +
|
||||
" name should be /boot (default) or /recovery\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
class CryptoUtils {
|
||||
public class CryptoUtils {
|
||||
|
||||
static final Map<String, String> ID_TO_ALG;
|
||||
static final Map<String, String> ALG_TO_ID;
|
||||
@@ -81,7 +81,7 @@ class CryptoUtils {
|
||||
return new AlgorithmIdentifier(new ASN1ObjectIdentifier(id));
|
||||
}
|
||||
|
||||
static X509Certificate readCertificate(InputStream input)
|
||||
public static X509Certificate readCertificate(InputStream input)
|
||||
throws IOException, GeneralSecurityException {
|
||||
try {
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
@@ -92,7 +92,7 @@ class CryptoUtils {
|
||||
}
|
||||
|
||||
/** Read a PKCS#8 format private key. */
|
||||
static PrivateKey readPrivateKey(InputStream input)
|
||||
public static PrivateKey readPrivateKey(InputStream input)
|
||||
throws IOException, GeneralSecurityException {
|
||||
try {
|
||||
ByteArrayStream buf = new ByteArrayStream();
|
||||
174
app/signing/src/main/java/com/topjohnwu/signing/JarMap.java
Normal file
174
app/signing/src/main/java/com/topjohnwu/signing/JarMap.java
Normal file
@@ -0,0 +1,174 @@
|
||||
package com.topjohnwu.signing;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.JarInputStream;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
public abstract class JarMap implements Closeable {
|
||||
|
||||
LinkedHashMap<String, JarEntry> entryMap;
|
||||
|
||||
public static JarMap open(String file) throws IOException {
|
||||
return new FileMap(new File(file), true, ZipFile.OPEN_READ);
|
||||
}
|
||||
|
||||
public static JarMap open(File file, boolean verify) throws IOException {
|
||||
return new FileMap(file, verify, ZipFile.OPEN_READ);
|
||||
}
|
||||
|
||||
public static JarMap open(String file, boolean verify) throws IOException {
|
||||
return new FileMap(new File(file), verify, ZipFile.OPEN_READ);
|
||||
}
|
||||
|
||||
public static JarMap open(InputStream is, boolean verify) throws IOException {
|
||||
return new StreamMap(is, verify);
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public abstract Manifest getManifest() throws IOException;
|
||||
|
||||
public InputStream getInputStream(ZipEntry ze) throws IOException {
|
||||
JarMapEntry e = getMapEntry(ze.getName());
|
||||
return e != null ? e.data.getInputStream() : null;
|
||||
}
|
||||
|
||||
public OutputStream getOutputStream(ZipEntry ze) {
|
||||
if (entryMap == null)
|
||||
entryMap = new LinkedHashMap<>();
|
||||
JarMapEntry e = new JarMapEntry(ze.getName());
|
||||
entryMap.put(ze.getName(), e);
|
||||
return e.data;
|
||||
}
|
||||
|
||||
public byte[] getRawData(ZipEntry ze) throws IOException {
|
||||
JarMapEntry e = getMapEntry(ze.getName());
|
||||
return e != null ? e.data.toByteArray() : null;
|
||||
}
|
||||
|
||||
public abstract Enumeration<JarEntry> entries();
|
||||
|
||||
public final ZipEntry getEntry(String name) {
|
||||
return getJarEntry(name);
|
||||
}
|
||||
|
||||
public JarEntry getJarEntry(String name) {
|
||||
return getMapEntry(name);
|
||||
}
|
||||
|
||||
JarMapEntry getMapEntry(String name) {
|
||||
JarMapEntry e = null;
|
||||
if (entryMap != null)
|
||||
e = (JarMapEntry) entryMap.get(name);
|
||||
return e;
|
||||
}
|
||||
|
||||
private static class FileMap extends JarMap {
|
||||
|
||||
private JarFile jarFile;
|
||||
|
||||
FileMap(File file, boolean verify, int mode) throws IOException {
|
||||
jarFile = new JarFile(file, verify, mode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getFile() {
|
||||
return new File(jarFile.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Manifest getManifest() throws IOException {
|
||||
return jarFile.getManifest();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream(ZipEntry ze) throws IOException {
|
||||
InputStream is = super.getInputStream(ze);
|
||||
return is != null ? is : jarFile.getInputStream(ze);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getRawData(ZipEntry ze) throws IOException {
|
||||
byte[] b = super.getRawData(ze);
|
||||
if (b != null)
|
||||
return b;
|
||||
ByteArrayStream bytes = new ByteArrayStream();
|
||||
bytes.readFrom(jarFile.getInputStream(ze));
|
||||
return bytes.toByteArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enumeration<JarEntry> entries() {
|
||||
return jarFile.entries();
|
||||
}
|
||||
|
||||
@Override
|
||||
public JarEntry getJarEntry(String name) {
|
||||
JarEntry e = getMapEntry(name);
|
||||
return e != null ? e : jarFile.getJarEntry(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
jarFile.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static class StreamMap extends JarMap {
|
||||
|
||||
private JarInputStream jis;
|
||||
|
||||
StreamMap(InputStream is, boolean verify) throws IOException {
|
||||
jis = new JarInputStream(is, verify);
|
||||
entryMap = new LinkedHashMap<>();
|
||||
JarEntry entry;
|
||||
while ((entry = jis.getNextJarEntry()) != null) {
|
||||
entryMap.put(entry.getName(), new JarMapEntry(entry, jis));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Manifest getManifest() {
|
||||
return jis.getManifest();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enumeration<JarEntry> entries() {
|
||||
return Collections.enumeration(entryMap.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
jis.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static class JarMapEntry extends JarEntry {
|
||||
|
||||
ByteArrayStream data;
|
||||
|
||||
JarMapEntry(JarEntry je, InputStream is) {
|
||||
super(je);
|
||||
data = new ByteArrayStream();
|
||||
data.readFrom(is);
|
||||
}
|
||||
|
||||
JarMapEntry(String s) {
|
||||
super(s);
|
||||
data = new ByteArrayStream();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
package com.topjohnwu.signing;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1Encoding;
|
||||
import org.bouncycastle.asn1.ASN1InputStream;
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||
import org.bouncycastle.asn1.DEROutputStream;
|
||||
import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
|
||||
import org.bouncycastle.asn1.ASN1OutputStream;
|
||||
import org.bouncycastle.cert.jcajce.JcaCertStore;
|
||||
import org.bouncycastle.cms.CMSException;
|
||||
import org.bouncycastle.cms.CMSProcessableByteArray;
|
||||
@@ -18,31 +17,31 @@ import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.DigestOutputStream;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.Provider;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Security;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
import java.util.TreeMap;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarEntry;
|
||||
@@ -52,93 +51,28 @@ import java.util.jar.Manifest;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/*
|
||||
* Modified from from AOSP(Marshmallow) SignAPK.java
|
||||
* */
|
||||
|
||||
public class SignAPK {
|
||||
* Modified from from AOSP
|
||||
* https://android.googlesource.com/platform/build/+/refs/tags/android-7.1.2_r39/tools/signapk/src/com/android/signapk/SignApk.java
|
||||
* */
|
||||
|
||||
public class SignApk {
|
||||
private static final String CERT_SF_NAME = "META-INF/CERT.SF";
|
||||
private static final String CERT_SIG_NAME = "META-INF/CERT.%s";
|
||||
private static final String CERT_SF_MULTI_NAME = "META-INF/CERT%d.SF";
|
||||
private static final String CERT_SIG_MULTI_NAME = "META-INF/CERT%d.%s";
|
||||
|
||||
private static Provider sBouncyCastleProvider;
|
||||
// bitmasks for which hash algorithms we need the manifest to include.
|
||||
private static final int USE_SHA1 = 1;
|
||||
private static final int USE_SHA256 = 2;
|
||||
|
||||
static {
|
||||
sBouncyCastleProvider = new BouncyCastleProvider();
|
||||
Security.insertProviderAt(sBouncyCastleProvider, 1);
|
||||
}
|
||||
|
||||
public static void sign(JarMap input, OutputStream output) throws Exception {
|
||||
sign(SignAPK.class.getResourceAsStream("/keys/testkey.x509.pem"),
|
||||
SignAPK.class.getResourceAsStream("/keys/testkey.pk8"), input, output);
|
||||
}
|
||||
|
||||
public static void sign(InputStream certIs, InputStream keyIs,
|
||||
JarMap input, OutputStream output) throws Exception {
|
||||
X509Certificate cert = CryptoUtils.readCertificate(certIs);
|
||||
PrivateKey key = CryptoUtils.readPrivateKey(keyIs);
|
||||
sign(cert, key, input, output);
|
||||
}
|
||||
|
||||
public static void sign(InputStream jks, String keyStorePass, String alias, String keyPass,
|
||||
JarMap input, OutputStream output) throws Exception {
|
||||
KeyStore ks = KeyStore.getInstance("JKS");
|
||||
ks.load(jks, keyStorePass.toCharArray());
|
||||
X509Certificate cert = (X509Certificate) ks.getCertificate(alias);
|
||||
PrivateKey key = (PrivateKey) ks.getKey(alias, keyPass.toCharArray());
|
||||
sign(cert, key, input, output);
|
||||
}
|
||||
|
||||
private static void sign(X509Certificate cert, PrivateKey key,
|
||||
JarMap input, OutputStream output) throws Exception {
|
||||
File temp1 = File.createTempFile("signAPK", null);
|
||||
File temp2 = File.createTempFile("signAPK", null);
|
||||
|
||||
try {
|
||||
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(temp1))) {
|
||||
sign(cert, key, input, out, false);
|
||||
}
|
||||
|
||||
ZipAdjust.adjust(temp1, temp2);
|
||||
|
||||
try (JarMap map = new JarMap(temp2, false)) {
|
||||
sign(cert, key, map, output, true);
|
||||
}
|
||||
} finally {
|
||||
temp1.delete();
|
||||
temp2.delete();
|
||||
}
|
||||
}
|
||||
|
||||
private static void sign(X509Certificate cert, PrivateKey key,
|
||||
JarMap input, OutputStream output, boolean minSign) throws Exception {
|
||||
int hashes = 0;
|
||||
hashes |= getDigestAlgorithm(cert);
|
||||
|
||||
// Set the ZIP file timestamp to the starting valid time
|
||||
// of the 0th certificate plus one hour (to match what
|
||||
// we've historically done).
|
||||
long timestamp = cert.getNotBefore().getTime() + 3600L * 1000;
|
||||
|
||||
if (minSign) {
|
||||
signWholeFile(input.getFile(), cert, key, output);
|
||||
} else {
|
||||
JarOutputStream outputJar = new JarOutputStream(output);
|
||||
// For signing .apks, use the maximum compression to make
|
||||
// them as small as possible (since they live forever on
|
||||
// the system partition). For OTA packages, use the
|
||||
// default compression level, which is much much faster
|
||||
// and produces output that is only a tiny bit larger
|
||||
// (~0.1% on full OTA packages I tested).
|
||||
outputJar.setLevel(9);
|
||||
Manifest manifest = addDigestsToManifest(input, hashes);
|
||||
copyFiles(manifest, input, outputJar, timestamp, 4);
|
||||
signFile(manifest, input, cert, key, outputJar);
|
||||
outputJar.close();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Digest algorithm used when signing the APK using APK Signature Scheme v2.
|
||||
*/
|
||||
private static final String APK_SIG_SCHEME_V2_DIGEST_ALGORITHM = "SHA-256";
|
||||
// Files matching this pattern are not copied to the output.
|
||||
private static final Pattern stripPattern =
|
||||
Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" +
|
||||
Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
|
||||
|
||||
/**
|
||||
* Return one of USE_SHA1 or USE_SHA256 according to the signature
|
||||
@@ -146,8 +80,7 @@ public class SignAPK {
|
||||
*/
|
||||
private static int getDigestAlgorithm(X509Certificate cert) {
|
||||
String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
|
||||
if ("SHA1WITHRSA".equals(sigAlg) ||
|
||||
"MD5WITHRSA".equals(sigAlg)) { // see "HISTORICAL NOTE" above.
|
||||
if ("SHA1WITHRSA".equals(sigAlg) || "MD5WITHRSA".equals(sigAlg)) {
|
||||
return USE_SHA1;
|
||||
} else if (sigAlg.startsWith("SHA256WITH")) {
|
||||
return USE_SHA256;
|
||||
@@ -156,9 +89,11 @@ public class SignAPK {
|
||||
"\" in cert [" + cert.getSubjectDN());
|
||||
}
|
||||
}
|
||||
/** Returns the expected signature algorithm for this key type. */
|
||||
|
||||
/**
|
||||
* Returns the expected signature algorithm for this key type.
|
||||
*/
|
||||
private static String getSignatureAlgorithm(X509Certificate cert) {
|
||||
String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
|
||||
String keyType = cert.getPublicKey().getAlgorithm().toUpperCase(Locale.US);
|
||||
if ("RSA".equalsIgnoreCase(keyType)) {
|
||||
if (getDigestAlgorithm(cert) == USE_SHA256) {
|
||||
@@ -172,10 +107,6 @@ public class SignAPK {
|
||||
throw new IllegalArgumentException("unsupported key type: " + keyType);
|
||||
}
|
||||
}
|
||||
// Files matching this pattern are not copied to the output.
|
||||
private static Pattern stripPattern =
|
||||
Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" +
|
||||
Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
|
||||
|
||||
/**
|
||||
* Add the hash(es) of every file to the manifest, creating it if
|
||||
@@ -192,6 +123,7 @@ public class SignAPK {
|
||||
main.putValue("Manifest-Version", "1.0");
|
||||
main.putValue("Created-By", "1.0 (Android SignApk)");
|
||||
}
|
||||
|
||||
MessageDigest md_sha1 = null;
|
||||
MessageDigest md_sha256 = null;
|
||||
if ((hashes & USE_SHA1) != 0) {
|
||||
@@ -200,32 +132,51 @@ public class SignAPK {
|
||||
if ((hashes & USE_SHA256) != 0) {
|
||||
md_sha256 = MessageDigest.getInstance("SHA256");
|
||||
}
|
||||
|
||||
byte[] buffer = new byte[4096];
|
||||
int num;
|
||||
|
||||
// We sort the input entries by name, and add them to the
|
||||
// output manifest in sorted order. We expect that the output
|
||||
// map will be deterministic.
|
||||
TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
|
||||
|
||||
TreeMap<String, JarEntry> byName = new TreeMap<>();
|
||||
|
||||
for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
|
||||
JarEntry entry = e.nextElement();
|
||||
byName.put(entry.getName(), entry);
|
||||
}
|
||||
for (JarEntry entry: byName.values()) {
|
||||
|
||||
for (JarEntry entry : byName.values()) {
|
||||
String name = entry.getName();
|
||||
if (!entry.isDirectory() &&
|
||||
(stripPattern == null || !stripPattern.matcher(name).matches())) {
|
||||
if (!entry.isDirectory() && !stripPattern.matcher(name).matches()) {
|
||||
InputStream data = jar.getInputStream(entry);
|
||||
while ((num = data.read(buffer)) > 0) {
|
||||
if (md_sha1 != null) md_sha1.update(buffer, 0, num);
|
||||
if (md_sha256 != null) md_sha256.update(buffer, 0, num);
|
||||
}
|
||||
|
||||
Attributes attr = null;
|
||||
if (input != null) attr = input.getAttributes(name);
|
||||
attr = attr != null ? new Attributes(attr) : new Attributes();
|
||||
// Remove any previously computed digests from this entry's attributes.
|
||||
for (Iterator<Object> i = attr.keySet().iterator(); i.hasNext(); ) {
|
||||
Object key = i.next();
|
||||
if (!(key instanceof Attributes.Name)) {
|
||||
continue;
|
||||
}
|
||||
String attributeNameLowerCase =
|
||||
key.toString().toLowerCase(Locale.US);
|
||||
if (attributeNameLowerCase.endsWith("-digest")) {
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
// Add SHA-1 digest if requested
|
||||
if (md_sha1 != null) {
|
||||
attr.putValue("SHA1-Digest",
|
||||
new String(Base64.encode(md_sha1.digest()), "ASCII"));
|
||||
}
|
||||
// Add SHA-256 digest if requested
|
||||
if (md_sha256 != null) {
|
||||
attr.putValue("SHA-256-Digest",
|
||||
new String(Base64.encode(md_sha256.digest()), "ASCII"));
|
||||
@@ -233,33 +184,13 @@ public class SignAPK {
|
||||
output.getEntries().put(name, attr);
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/** Write to another stream and track how many bytes have been
|
||||
* written.
|
||||
/**
|
||||
* Write a .SF file with a digest of the specified manifest.
|
||||
*/
|
||||
private static class CountOutputStream extends FilterOutputStream {
|
||||
private int mCount;
|
||||
public CountOutputStream(OutputStream out) {
|
||||
super(out);
|
||||
mCount = 0;
|
||||
}
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
super.write(b);
|
||||
mCount++;
|
||||
}
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
super.write(b, off, len);
|
||||
mCount += len;
|
||||
}
|
||||
public int size() {
|
||||
return mCount;
|
||||
}
|
||||
}
|
||||
/** Write a .SF file with a digest of the specified manifest. */
|
||||
private static void writeSignatureFile(Manifest manifest, OutputStream out,
|
||||
int hash)
|
||||
throws IOException, GeneralSecurityException {
|
||||
@@ -267,16 +198,25 @@ public class SignAPK {
|
||||
Attributes main = sf.getMainAttributes();
|
||||
main.putValue("Signature-Version", "1.0");
|
||||
main.putValue("Created-By", "1.0 (Android SignApk)");
|
||||
MessageDigest md = MessageDigest.getInstance(
|
||||
hash == USE_SHA256 ? "SHA256" : "SHA1");
|
||||
PrintStream print = new PrintStream(
|
||||
new DigestOutputStream(new ByteArrayOutputStream(), md),
|
||||
// Add APK Signature Scheme v2 signature stripping protection.
|
||||
// This attribute indicates that this APK is supposed to have been signed using one or
|
||||
// more APK-specific signature schemes in addition to the standard JAR signature scheme
|
||||
// used by this code. APK signature verifier should reject the APK if it does not
|
||||
// contain a signature for the signature scheme the verifier prefers out of this set.
|
||||
main.putValue(
|
||||
ApkSignerV2.SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME,
|
||||
ApkSignerV2.SF_ATTRIBUTE_ANDROID_APK_SIGNED_VALUE);
|
||||
|
||||
MessageDigest md = MessageDigest.getInstance(hash == USE_SHA256 ? "SHA256" : "SHA1");
|
||||
PrintStream print = new PrintStream(new DigestOutputStream(new ByteArrayOutputStream(), md),
|
||||
true, "UTF-8");
|
||||
|
||||
// Digest of the entire manifest
|
||||
manifest.write(print);
|
||||
print.flush();
|
||||
main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",
|
||||
new String(Base64.encode(md.digest()), "ASCII"));
|
||||
|
||||
Map<String, Attributes> entries = manifest.getEntries();
|
||||
for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
|
||||
// Digest of the manifest stanza for this entry.
|
||||
@@ -286,13 +226,16 @@ public class SignAPK {
|
||||
}
|
||||
print.print("\r\n");
|
||||
print.flush();
|
||||
|
||||
Attributes sfAttr = new Attributes();
|
||||
sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest-Manifest",
|
||||
sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest",
|
||||
new String(Base64.encode(md.digest()), "ASCII"));
|
||||
sf.getEntries().put(entry.getKey(), sfAttr);
|
||||
}
|
||||
|
||||
CountOutputStream cout = new CountOutputStream(out);
|
||||
sf.write(cout);
|
||||
|
||||
// A bug in the java.util.jar implementation of Android platforms
|
||||
// up to version 1.6 will cause a spurious IOException to be thrown
|
||||
// if the length of the signature file is a multiple of 1024 bytes.
|
||||
@@ -302,10 +245,12 @@ public class SignAPK {
|
||||
cout.write('\n');
|
||||
}
|
||||
}
|
||||
/** Sign data and write the digital signature to 'out'. */
|
||||
|
||||
/**
|
||||
* Sign data and write the digital signature to 'out'.
|
||||
*/
|
||||
private static void writeSignatureBlock(
|
||||
CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
|
||||
OutputStream out)
|
||||
CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, OutputStream out)
|
||||
throws IOException,
|
||||
CertificateEncodingException,
|
||||
OperatorCreationException,
|
||||
@@ -313,23 +258,24 @@ public class SignAPK {
|
||||
ArrayList<X509Certificate> certList = new ArrayList<>(1);
|
||||
certList.add(publicKey);
|
||||
JcaCertStore certs = new JcaCertStore(certList);
|
||||
|
||||
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
|
||||
ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(publicKey))
|
||||
.setProvider(sBouncyCastleProvider)
|
||||
.build(privateKey);
|
||||
gen.addSignerInfoGenerator(
|
||||
new JcaSignerInfoGeneratorBuilder(
|
||||
new JcaDigestCalculatorProviderBuilder()
|
||||
.setProvider(sBouncyCastleProvider)
|
||||
.build())
|
||||
new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build())
|
||||
.setDirectSignature(true)
|
||||
.build(signer, publicKey));
|
||||
.build(signer, publicKey)
|
||||
);
|
||||
gen.addCertificates(certs);
|
||||
CMSSignedData sigData = gen.generate(data, false);
|
||||
ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded());
|
||||
DEROutputStream dos = new DEROutputStream(out);
|
||||
dos.writeObject(asn1.readObject());
|
||||
|
||||
try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) {
|
||||
ASN1OutputStream dos = ASN1OutputStream.create(out, ASN1Encoding.DER);
|
||||
dos.writeObject(asn1.readObject());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy all the files in a manifest from input to output. We set
|
||||
* the modification times in the output to a fixed time, so as to
|
||||
@@ -337,27 +283,37 @@ public class SignAPK {
|
||||
* more efficient.
|
||||
*/
|
||||
private static void copyFiles(Manifest manifest, JarMap in, JarOutputStream out,
|
||||
long timestamp, int alignment) throws IOException {
|
||||
long timestamp, int defaultAlignment) throws IOException {
|
||||
byte[] buffer = new byte[4096];
|
||||
int num;
|
||||
|
||||
Map<String, Attributes> entries = manifest.getEntries();
|
||||
ArrayList<String> names = new ArrayList<>(entries.keySet());
|
||||
Collections.sort(names);
|
||||
|
||||
boolean firstEntry = true;
|
||||
long offset = 0L;
|
||||
|
||||
// We do the copy in two passes -- first copying all the
|
||||
// entries that are STORED, then copying all the entries that
|
||||
// have any other compression flag (which in practice means
|
||||
// DEFLATED). This groups all the stored entries together at
|
||||
// the start of the file and makes it easier to do alignment
|
||||
// on them (since only stored entries are aligned).
|
||||
|
||||
for (String name : names) {
|
||||
JarEntry inEntry = in.getJarEntry(name);
|
||||
JarEntry outEntry = null;
|
||||
JarEntry outEntry;
|
||||
if (inEntry.getMethod() != JarEntry.STORED) continue;
|
||||
// Preserve the STORED method of the input entry.
|
||||
outEntry = new JarEntry(inEntry);
|
||||
outEntry.setTime(timestamp);
|
||||
// Discard comment and extra fields of this entry to
|
||||
// simplify alignment logic below and for consistency with
|
||||
// how compressed entries are handled later.
|
||||
outEntry.setComment(null);
|
||||
outEntry.setExtra(null);
|
||||
|
||||
// 'offset' is the offset into the file at which we expect
|
||||
// the file data to begin. This is the value we need to
|
||||
// make a multiple of 'alignement'.
|
||||
@@ -371,15 +327,18 @@ public class SignAPK {
|
||||
offset += 4;
|
||||
firstEntry = false;
|
||||
}
|
||||
int alignment = getStoredEntryDataAlignment(name, defaultAlignment);
|
||||
if (alignment > 0 && (offset % alignment != 0)) {
|
||||
// Set the "extra data" of the entry to between 1 and
|
||||
// alignment-1 bytes, to make the file data begin at
|
||||
// an aligned offset.
|
||||
int needed = alignment - (int)(offset % alignment);
|
||||
int needed = alignment - (int) (offset % alignment);
|
||||
outEntry.setExtra(new byte[needed]);
|
||||
offset += needed;
|
||||
}
|
||||
|
||||
out.putNextEntry(outEntry);
|
||||
|
||||
InputStream data = in.getInputStream(inEntry);
|
||||
while ((num = data.read(buffer)) > 0) {
|
||||
out.write(buffer, 0, num);
|
||||
@@ -387,17 +346,20 @@ public class SignAPK {
|
||||
}
|
||||
out.flush();
|
||||
}
|
||||
|
||||
// Copy all the non-STORED entries. We don't attempt to
|
||||
// maintain the 'offset' variable past this point; we don't do
|
||||
// alignment on these entries.
|
||||
|
||||
for (String name : names) {
|
||||
JarEntry inEntry = in.getJarEntry(name);
|
||||
JarEntry outEntry = null;
|
||||
JarEntry outEntry;
|
||||
if (inEntry.getMethod() == JarEntry.STORED) continue;
|
||||
// Create a new entry so that the compressed len is recomputed.
|
||||
outEntry = new JarEntry(name);
|
||||
outEntry.setTime(timestamp);
|
||||
out.putNextEntry(outEntry);
|
||||
|
||||
InputStream data = in.getInputStream(inEntry);
|
||||
while ((num = data.read(buffer)) > 0) {
|
||||
out.write(buffer, 0, num);
|
||||
@@ -406,134 +368,203 @@ public class SignAPK {
|
||||
}
|
||||
}
|
||||
|
||||
// This class is to provide a file's content, but trimming out the last two bytes
|
||||
// Used for signWholeFile
|
||||
private static class CMSProcessableFile implements CMSTypedData {
|
||||
|
||||
private ASN1ObjectIdentifier type;
|
||||
private RandomAccessFile file;
|
||||
|
||||
CMSProcessableFile(File file) throws FileNotFoundException {
|
||||
this.file = new RandomAccessFile(file, "r");
|
||||
type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
|
||||
/**
|
||||
* Returns the multiple (in bytes) at which the provided {@code STORED} entry's data must start
|
||||
* relative to start of file or {@code 0} if alignment of this entry's data is not important.
|
||||
*/
|
||||
private static int getStoredEntryDataAlignment(String entryName, int defaultAlignment) {
|
||||
if (defaultAlignment <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ASN1ObjectIdentifier getContentType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(OutputStream out) throws IOException, CMSException {
|
||||
file.seek(0);
|
||||
int read;
|
||||
byte buffer[] = new byte[4096];
|
||||
int len = (int) file.length() - 2;
|
||||
while ((read = file.read(buffer, 0, len < buffer.length ? len : buffer.length)) > 0) {
|
||||
out.write(buffer, 0, read);
|
||||
len -= read;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getContent() {
|
||||
return file;
|
||||
}
|
||||
|
||||
byte[] getTail() throws IOException {
|
||||
byte tail[] = new byte[22];
|
||||
file.seek(file.length() - 22);
|
||||
file.readFully(tail);
|
||||
return tail;
|
||||
if (entryName.endsWith(".so")) {
|
||||
// Align .so contents to memory page boundary to enable memory-mapped
|
||||
// execution.
|
||||
return 4096;
|
||||
} else {
|
||||
return defaultAlignment;
|
||||
}
|
||||
}
|
||||
|
||||
private static void signWholeFile(File input, X509Certificate publicKey,
|
||||
PrivateKey privateKey, OutputStream outputStream)
|
||||
throws Exception {
|
||||
ByteArrayOutputStream temp = new ByteArrayOutputStream();
|
||||
// put a readable message and a null char at the start of the
|
||||
// archive comment, so that tools that display the comment
|
||||
// (hopefully) show something sensible.
|
||||
// TODO: anything more useful we can put in this message?
|
||||
byte[] message = "signed by SignApk".getBytes("UTF-8");
|
||||
temp.write(message);
|
||||
temp.write(0);
|
||||
|
||||
CMSProcessableFile cmsFile = new CMSProcessableFile(input);
|
||||
writeSignatureBlock(cmsFile, publicKey, privateKey, temp);
|
||||
|
||||
// For a zip with no archive comment, the
|
||||
// end-of-central-directory record will be 22 bytes long, so
|
||||
// we expect to find the EOCD marker 22 bytes from the end.
|
||||
byte[] zipData = cmsFile.getTail();
|
||||
if (zipData[zipData.length-22] != 0x50 ||
|
||||
zipData[zipData.length-21] != 0x4b ||
|
||||
zipData[zipData.length-20] != 0x05 ||
|
||||
zipData[zipData.length-19] != 0x06) {
|
||||
throw new IllegalArgumentException("zip data already has an archive comment");
|
||||
}
|
||||
int total_size = temp.size() + 6;
|
||||
if (total_size > 0xffff) {
|
||||
throw new IllegalArgumentException("signature is too big for ZIP file comment");
|
||||
}
|
||||
// signature starts this many bytes from the end of the file
|
||||
int signature_start = total_size - message.length - 1;
|
||||
temp.write(signature_start & 0xff);
|
||||
temp.write((signature_start >> 8) & 0xff);
|
||||
// Why the 0xff bytes? In a zip file with no archive comment,
|
||||
// bytes [-6:-2] of the file are the little-endian offset from
|
||||
// the start of the file to the central directory. So for the
|
||||
// two high bytes to be 0xff 0xff, the archive would have to
|
||||
// be nearly 4GB in size. So it's unlikely that a real
|
||||
// commentless archive would have 0xffs here, and lets us tell
|
||||
// an old signed archive from a new one.
|
||||
temp.write(0xff);
|
||||
temp.write(0xff);
|
||||
temp.write(total_size & 0xff);
|
||||
temp.write((total_size >> 8) & 0xff);
|
||||
temp.flush();
|
||||
// Signature verification checks that the EOCD header is the
|
||||
// last such sequence in the file (to avoid minzip finding a
|
||||
// fake EOCD appended after the signature in its scan). The
|
||||
// odds of producing this sequence by chance are very low, but
|
||||
// let's catch it here if it does.
|
||||
byte[] b = temp.toByteArray();
|
||||
for (int i = 0; i < b.length-3; ++i) {
|
||||
if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {
|
||||
throw new IllegalArgumentException("found spurious EOCD header at " + i);
|
||||
}
|
||||
}
|
||||
cmsFile.write(outputStream);
|
||||
outputStream.write(total_size & 0xff);
|
||||
outputStream.write((total_size >> 8) & 0xff);
|
||||
temp.writeTo(outputStream);
|
||||
outputStream.close();
|
||||
}
|
||||
private static void signFile(Manifest manifest, JarMap inputJar,
|
||||
X509Certificate publicKey, PrivateKey privateKey,
|
||||
JarOutputStream outputJar)
|
||||
throws Exception {
|
||||
// Assume the certificate is valid for at least an hour.
|
||||
long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
|
||||
private static void signFile(Manifest manifest,
|
||||
X509Certificate[] publicKey, PrivateKey[] privateKey,
|
||||
long timestamp, JarOutputStream outputJar) throws Exception {
|
||||
// MANIFEST.MF
|
||||
JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
|
||||
je.setTime(timestamp);
|
||||
outputJar.putNextEntry(je);
|
||||
manifest.write(outputJar);
|
||||
je = new JarEntry(CERT_SF_NAME);
|
||||
je.setTime(timestamp);
|
||||
outputJar.putNextEntry(je);
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey));
|
||||
byte[] signedData = baos.toByteArray();
|
||||
outputJar.write(signedData);
|
||||
// CERT.{EC,RSA} / CERT#.{EC,RSA}
|
||||
final String keyType = publicKey.getPublicKey().getAlgorithm();
|
||||
je = new JarEntry(String.format(CERT_SIG_NAME, keyType));
|
||||
je.setTime(timestamp);
|
||||
outputJar.putNextEntry(je);
|
||||
writeSignatureBlock(new CMSProcessableByteArray(signedData),
|
||||
publicKey, privateKey, outputJar);
|
||||
|
||||
int numKeys = publicKey.length;
|
||||
for (int k = 0; k < numKeys; ++k) {
|
||||
// CERT.SF / CERT#.SF
|
||||
je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
|
||||
(String.format(Locale.US, CERT_SF_MULTI_NAME, k)));
|
||||
je.setTime(timestamp);
|
||||
outputJar.putNextEntry(je);
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey[k]));
|
||||
byte[] signedData = baos.toByteArray();
|
||||
outputJar.write(signedData);
|
||||
|
||||
// CERT.{EC,RSA} / CERT#.{EC,RSA}
|
||||
final String keyType = publicKey[k].getPublicKey().getAlgorithm();
|
||||
je = new JarEntry(numKeys == 1 ? (String.format(CERT_SIG_NAME, keyType)) :
|
||||
(String.format(Locale.US, CERT_SIG_MULTI_NAME, k, keyType)));
|
||||
je.setTime(timestamp);
|
||||
outputJar.putNextEntry(je);
|
||||
writeSignatureBlock(new CMSProcessableByteArray(signedData),
|
||||
publicKey[k], privateKey[k], outputJar);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the provided lists of private keys, their X.509 certificates, and digest algorithms
|
||||
* into a list of APK Signature Scheme v2 {@code SignerConfig} instances.
|
||||
*/
|
||||
private static List<ApkSignerV2.SignerConfig> createV2SignerConfigs(
|
||||
PrivateKey[] privateKeys, X509Certificate[] certificates, String[] digestAlgorithms)
|
||||
throws InvalidKeyException {
|
||||
if (privateKeys.length != certificates.length) {
|
||||
throw new IllegalArgumentException(
|
||||
"The number of private keys must match the number of certificates: "
|
||||
+ privateKeys.length + " vs" + certificates.length);
|
||||
}
|
||||
List<ApkSignerV2.SignerConfig> result = new ArrayList<>(privateKeys.length);
|
||||
for (int i = 0; i < privateKeys.length; i++) {
|
||||
PrivateKey privateKey = privateKeys[i];
|
||||
X509Certificate certificate = certificates[i];
|
||||
PublicKey publicKey = certificate.getPublicKey();
|
||||
String keyAlgorithm = privateKey.getAlgorithm();
|
||||
if (!keyAlgorithm.equalsIgnoreCase(publicKey.getAlgorithm())) {
|
||||
throw new InvalidKeyException(
|
||||
"Key algorithm of private key #" + (i + 1) + " does not match key"
|
||||
+ " algorithm of public key #" + (i + 1) + ": " + keyAlgorithm
|
||||
+ " vs " + publicKey.getAlgorithm());
|
||||
}
|
||||
ApkSignerV2.SignerConfig signerConfig = new ApkSignerV2.SignerConfig();
|
||||
signerConfig.privateKey = privateKey;
|
||||
signerConfig.certificates = Collections.singletonList(certificate);
|
||||
List<Integer> signatureAlgorithms = new ArrayList<>(digestAlgorithms.length);
|
||||
for (String digestAlgorithm : digestAlgorithms) {
|
||||
try {
|
||||
signatureAlgorithms.add(getV2SignatureAlgorithm(keyAlgorithm, digestAlgorithm));
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new InvalidKeyException(
|
||||
"Unsupported key and digest algorithm combination for signer #"
|
||||
+ (i + 1), e);
|
||||
}
|
||||
}
|
||||
signerConfig.signatureAlgorithms = signatureAlgorithms;
|
||||
result.add(signerConfig);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int getV2SignatureAlgorithm(String keyAlgorithm, String digestAlgorithm) {
|
||||
if ("SHA-256".equalsIgnoreCase(digestAlgorithm)) {
|
||||
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||
// Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
|
||||
// deterministic signatures which make life easier for OTA updates (fewer files
|
||||
// changed when deterministic signature schemes are used).
|
||||
return ApkSignerV2.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256;
|
||||
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
|
||||
return ApkSignerV2.SIGNATURE_ECDSA_WITH_SHA256;
|
||||
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||
return ApkSignerV2.SIGNATURE_DSA_WITH_SHA256;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm);
|
||||
}
|
||||
} else if ("SHA-512".equalsIgnoreCase(digestAlgorithm)) {
|
||||
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||
// Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
|
||||
// deterministic signatures which make life easier for OTA updates (fewer files
|
||||
// changed when deterministic signature schemes are used).
|
||||
return ApkSignerV2.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512;
|
||||
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
|
||||
return ApkSignerV2.SIGNATURE_ECDSA_WITH_SHA512;
|
||||
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||
return ApkSignerV2.SIGNATURE_DSA_WITH_SHA512;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported digest algorithm: " + digestAlgorithm);
|
||||
}
|
||||
}
|
||||
|
||||
public static void sign(X509Certificate cert, PrivateKey key,
|
||||
JarMap inputJar, FileOutputStream outputFile) throws Exception {
|
||||
int alignment = 4;
|
||||
int hashes = 0;
|
||||
|
||||
X509Certificate[] publicKey = new X509Certificate[1];
|
||||
publicKey[0] = cert;
|
||||
hashes |= getDigestAlgorithm(publicKey[0]);
|
||||
|
||||
// Set all ZIP file timestamps to Jan 1 2009 00:00:00.
|
||||
long timestamp = 1230768000000L;
|
||||
// The Java ZipEntry API we're using converts milliseconds since epoch into MS-DOS
|
||||
// timestamp using the current timezone. We thus adjust the milliseconds since epoch
|
||||
// value to end up with MS-DOS timestamp of Jan 1 2009 00:00:00.
|
||||
timestamp -= TimeZone.getDefault().getOffset(timestamp);
|
||||
|
||||
PrivateKey[] privateKey = new PrivateKey[1];
|
||||
privateKey[0] = key;
|
||||
|
||||
// Generate, in memory, an APK signed using standard JAR Signature Scheme.
|
||||
ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream();
|
||||
JarOutputStream outputJar = new JarOutputStream(v1SignedApkBuf);
|
||||
// Use maximum compression for compressed entries because the APK lives forever on
|
||||
// the system partition.
|
||||
outputJar.setLevel(9);
|
||||
Manifest manifest = addDigestsToManifest(inputJar, hashes);
|
||||
copyFiles(manifest, inputJar, outputJar, timestamp, alignment);
|
||||
signFile(manifest, publicKey, privateKey, timestamp, outputJar);
|
||||
outputJar.close();
|
||||
ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray());
|
||||
v1SignedApkBuf.reset();
|
||||
|
||||
ByteBuffer[] outputChunks;
|
||||
List<ApkSignerV2.SignerConfig> signerConfigs = createV2SignerConfigs(privateKey, publicKey,
|
||||
new String[]{APK_SIG_SCHEME_V2_DIGEST_ALGORITHM});
|
||||
outputChunks = ApkSignerV2.sign(v1SignedApk, signerConfigs);
|
||||
|
||||
// This assumes outputChunks are array-backed. To avoid this assumption, the
|
||||
// code could be rewritten to use FileChannel.
|
||||
for (ByteBuffer outputChunk : outputChunks) {
|
||||
outputFile.write(outputChunk.array(),
|
||||
outputChunk.arrayOffset() + outputChunk.position(), outputChunk.remaining());
|
||||
outputChunk.position(outputChunk.limit());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write to another stream and track how many bytes have been
|
||||
* written.
|
||||
*/
|
||||
private static class CountOutputStream extends FilterOutputStream {
|
||||
private int mCount;
|
||||
|
||||
public CountOutputStream(OutputStream out) {
|
||||
super(out);
|
||||
mCount = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
super.write(b);
|
||||
mCount++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
super.write(b, off, len);
|
||||
mCount += len;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return mCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import org.bouncycastle.asn1.DEROctetString;
|
||||
import org.bouncycastle.asn1.DERPrintableString;
|
||||
import org.bouncycastle.asn1.DERSequence;
|
||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.FilterInputStream;
|
||||
@@ -23,7 +22,6 @@ import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Security;
|
||||
import java.security.Signature;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
@@ -32,9 +30,14 @@ import java.util.Arrays;
|
||||
|
||||
public class SignBoot {
|
||||
|
||||
static {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
private static final int BOOT_IMAGE_HEADER_V1_RECOVERY_DTBO_SIZE_OFFSET = 1632;
|
||||
private static final int BOOT_IMAGE_HEADER_V2_DTB_SIZE_OFFSET = 1648;
|
||||
|
||||
// Arbitrary maximum header version value; when greater assume the field is dt/extra size
|
||||
private static final int BOOT_IMAGE_HEADER_VERSION_MAXIMUM = 8;
|
||||
|
||||
// Maximum header size byte value to read (currently the bootimg minimum page size)
|
||||
private static final int BOOT_IMAGE_HEADER_SIZE_MAXIMUM = 2048;
|
||||
|
||||
private static class PushBackRWStream extends FilterInputStream {
|
||||
private OutputStream out;
|
||||
@@ -85,7 +88,7 @@ public class SignBoot {
|
||||
InputStream cert, InputStream key) {
|
||||
try {
|
||||
PushBackRWStream in = new PushBackRWStream(imgIn, imgOut);
|
||||
byte[] hdr = new byte[1024];
|
||||
byte[] hdr = new byte[BOOT_IMAGE_HEADER_SIZE_MAXIMUM];
|
||||
// First read the header
|
||||
in.read(hdr);
|
||||
int signableSize = getSignableImageSize(hdr);
|
||||
@@ -93,12 +96,12 @@ public class SignBoot {
|
||||
in.unread(hdr);
|
||||
BootSignature bootsig = new BootSignature(target, signableSize);
|
||||
if (cert == null) {
|
||||
cert = SignBoot.class.getResourceAsStream("/keys/testkey.x509.pem");
|
||||
cert = SignBoot.class.getResourceAsStream("/keys/verity.x509.pem");
|
||||
}
|
||||
X509Certificate certificate = CryptoUtils.readCertificate(cert);
|
||||
bootsig.setCertificate(certificate);
|
||||
if (key == null) {
|
||||
key = SignBoot.class.getResourceAsStream("/keys/testkey.pk8");
|
||||
key = SignBoot.class.getResourceAsStream("/keys/verity.pk8");
|
||||
}
|
||||
PrivateKey privateKey = CryptoUtils.readPrivateKey(key);
|
||||
byte[] sig = bootsig.sign(privateKey, in, signableSize);
|
||||
@@ -116,22 +119,27 @@ public class SignBoot {
|
||||
public static boolean verifySignature(InputStream imgIn, InputStream certIn) {
|
||||
try {
|
||||
// Read the header for size
|
||||
byte[] hdr = new byte[1024];
|
||||
if (imgIn.read(hdr) != hdr.length)
|
||||
byte[] hdr = new byte[BOOT_IMAGE_HEADER_SIZE_MAXIMUM];
|
||||
if (imgIn.read(hdr) != hdr.length) {
|
||||
System.err.println("Unable to read image header");
|
||||
return false;
|
||||
}
|
||||
int signableSize = getSignableImageSize(hdr);
|
||||
|
||||
// Read the rest of the image
|
||||
byte[] rawImg = Arrays.copyOf(hdr, signableSize);
|
||||
int remain = signableSize - hdr.length;
|
||||
if (imgIn.read(rawImg, hdr.length, remain) != remain) {
|
||||
System.err.println("Invalid image: not signed");
|
||||
System.err.println("Unable to read image");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read footer, which contains the signature
|
||||
byte[] signature = new byte[4096];
|
||||
imgIn.read(signature);
|
||||
if (imgIn.read(signature) == -1 || Arrays.equals(signature, new byte [signature.length])) {
|
||||
System.err.println("Invalid image: not signed");
|
||||
return false;
|
||||
}
|
||||
|
||||
BootSignature bootsig = new BootSignature(signature);
|
||||
if (certIn != null) {
|
||||
@@ -144,7 +152,8 @@ public class SignBoot {
|
||||
System.err.println("Signature is INVALID");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("Invalid image: not signed");
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -168,6 +177,27 @@ public class SignBoot {
|
||||
+ ((kernelSize + pageSize - 1) / pageSize) * pageSize
|
||||
+ ((ramdskSize + pageSize - 1) / pageSize) * pageSize
|
||||
+ ((secondSize + pageSize - 1) / pageSize) * pageSize;
|
||||
int headerVersion = image.getInt(); // boot image header version or dt/extra size
|
||||
if (headerVersion > 0 && headerVersion < BOOT_IMAGE_HEADER_VERSION_MAXIMUM) {
|
||||
image.position(BOOT_IMAGE_HEADER_V1_RECOVERY_DTBO_SIZE_OFFSET);
|
||||
int recoveryDtboLength = image.getInt();
|
||||
length += ((recoveryDtboLength + pageSize - 1) / pageSize) * pageSize;
|
||||
image.getLong(); // recovery_dtbo address
|
||||
int headerSize = image.getInt();
|
||||
if (headerVersion == 2) {
|
||||
image.position(BOOT_IMAGE_HEADER_V2_DTB_SIZE_OFFSET);
|
||||
int dtbLength = image.getInt();
|
||||
length += ((dtbLength + pageSize - 1) / pageSize) * pageSize;
|
||||
image.getLong(); // dtb address
|
||||
}
|
||||
if (image.position() != headerSize) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid image header: invalid header length");
|
||||
}
|
||||
} else {
|
||||
// headerVersion is 0 or actually dt/extra size in this case
|
||||
length += ((headerVersion + pageSize - 1) / pageSize) * pageSize;
|
||||
}
|
||||
length = ((length + pageSize - 1) / pageSize) * pageSize;
|
||||
if (length <= 0) {
|
||||
throw new IllegalArgumentException("Invalid image header: invalid length");
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.topjohnwu.signing;
|
||||
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.Security;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
public class ZipSigner {
|
||||
|
||||
private static void usage() {
|
||||
System.err.println("ZipSigner usage:");
|
||||
System.err.println(" zipsigner.jar input.jar output.jar");
|
||||
System.err.println(" sign jar with AOSP test keys");
|
||||
System.err.println(" zipsigner.jar x509.pem pk8 input.jar output.jar");
|
||||
System.err.println(" sign jar with certificate / private key pair");
|
||||
System.err.println(" zipsigner.jar keyStore keyStorePass alias keyPass input.jar output.jar");
|
||||
System.err.println(" sign jar with Java KeyStore");
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
private static void sign(JarMap input, FileOutputStream output) throws Exception {
|
||||
sign(SignApk.class.getResourceAsStream("/keys/testkey.x509.pem"),
|
||||
SignApk.class.getResourceAsStream("/keys/testkey.pk8"), input, output);
|
||||
}
|
||||
|
||||
private static void sign(InputStream certIs, InputStream keyIs,
|
||||
JarMap input, FileOutputStream output) throws Exception {
|
||||
X509Certificate cert = CryptoUtils.readCertificate(certIs);
|
||||
PrivateKey key = CryptoUtils.readPrivateKey(keyIs);
|
||||
SignApk.sign(cert, key, input, output);
|
||||
}
|
||||
|
||||
private static void sign(String keyStore, String keyStorePass, String alias, String keyPass,
|
||||
JarMap in, FileOutputStream out) throws Exception {
|
||||
KeyStore ks;
|
||||
try {
|
||||
ks = KeyStore.getInstance("JKS");
|
||||
try (InputStream is = new FileInputStream(keyStore)) {
|
||||
ks.load(is, keyStorePass.toCharArray());
|
||||
}
|
||||
} catch (KeyStoreException|IOException|CertificateException|NoSuchAlgorithmException e) {
|
||||
ks = KeyStore.getInstance("PKCS12");
|
||||
try (InputStream is = new FileInputStream(keyStore)) {
|
||||
ks.load(is, keyStorePass.toCharArray());
|
||||
}
|
||||
}
|
||||
X509Certificate cert = (X509Certificate) ks.getCertificate(alias);
|
||||
PrivateKey key = (PrivateKey) ks.getKey(alias, keyPass.toCharArray());
|
||||
SignApk.sign(cert, key, in, out);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
if (args.length != 2 && args.length != 4 && args.length != 6)
|
||||
usage();
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 1);
|
||||
|
||||
try (JarMap in = JarMap.open(args[args.length - 2], false);
|
||||
FileOutputStream out = new FileOutputStream(args[args.length - 1])) {
|
||||
if (args.length == 2) {
|
||||
sign(in, out);
|
||||
} else if (args.length == 4) {
|
||||
try (InputStream cert = new FileInputStream(args[0]);
|
||||
InputStream key = new FileInputStream(args[1])) {
|
||||
sign(cert, key, in, out);
|
||||
}
|
||||
} else if (args.length == 6) {
|
||||
sign(args[0], args[1], args[2], args[3], in, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
136
app/signing/src/main/java/com/topjohnwu/signing/ZipUtils.java
Normal file
136
app/signing/src/main/java/com/topjohnwu/signing/ZipUtils.java
Normal file
@@ -0,0 +1,136 @@
|
||||
package com.topjohnwu.signing;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* Assorted ZIP format helpers.
|
||||
*
|
||||
* <p>NOTE: Most helper methods operating on {@code ByteBuffer} instances expect that the byte
|
||||
* order of these buffers is little-endian.
|
||||
*/
|
||||
public abstract class ZipUtils {
|
||||
|
||||
private static final int ZIP_EOCD_REC_MIN_SIZE = 22;
|
||||
private static final int ZIP_EOCD_REC_SIG = 0x06054b50;
|
||||
private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12;
|
||||
private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16;
|
||||
private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20;
|
||||
|
||||
private static final int ZIP64_EOCD_LOCATOR_SIZE = 20;
|
||||
private static final int ZIP64_EOCD_LOCATOR_SIG = 0x07064b50;
|
||||
|
||||
private static final int UINT16_MAX_VALUE = 0xffff;
|
||||
|
||||
private ZipUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position at which ZIP End of Central Directory record starts in the provided
|
||||
* buffer or {@code -1} if the record is not present.
|
||||
*
|
||||
* <p>NOTE: Byte order of {@code zipContents} must be little-endian.
|
||||
*/
|
||||
public static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) {
|
||||
assertByteOrderLittleEndian(zipContents);
|
||||
|
||||
// ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
|
||||
// The record can be identified by its 4-byte signature/magic which is located at the very
|
||||
// beginning of the record. A complication is that the record is variable-length because of
|
||||
// the comment field.
|
||||
// The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
|
||||
// end of the buffer for the EOCD record signature. Whenever we find a signature, we check
|
||||
// the candidate record's comment length is such that the remainder of the record takes up
|
||||
// exactly the remaining bytes in the buffer. The search is bounded because the maximum
|
||||
// size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
|
||||
|
||||
int archiveSize = zipContents.capacity();
|
||||
if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) {
|
||||
return -1;
|
||||
}
|
||||
int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE);
|
||||
int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE;
|
||||
for (int expectedCommentLength = 0; expectedCommentLength < maxCommentLength; expectedCommentLength++) {
|
||||
int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength;
|
||||
if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) {
|
||||
int actualCommentLength = getUnsignedInt16(zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET);
|
||||
if (actualCommentLength == expectedCommentLength) {
|
||||
return eocdStartPos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the provided buffer contains a ZIP64 End of Central Directory
|
||||
* Locator.
|
||||
*
|
||||
* <p>NOTE: Byte order of {@code zipContents} must be little-endian.
|
||||
*/
|
||||
public static boolean isZip64EndOfCentralDirectoryLocatorPresent(ByteBuffer zipContents, int zipEndOfCentralDirectoryPosition) {
|
||||
assertByteOrderLittleEndian(zipContents);
|
||||
|
||||
// ZIP64 End of Central Directory Locator immediately precedes the ZIP End of Central
|
||||
// Directory Record.
|
||||
|
||||
int locatorPosition = zipEndOfCentralDirectoryPosition - ZIP64_EOCD_LOCATOR_SIZE;
|
||||
if (locatorPosition < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return zipContents.getInt(locatorPosition) == ZIP64_EOCD_LOCATOR_SIG;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the offset of the start of the ZIP Central Directory in the archive.
|
||||
*
|
||||
* <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
|
||||
*/
|
||||
public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) {
|
||||
assertByteOrderLittleEndian(zipEndOfCentralDirectory);
|
||||
return getUnsignedInt32(zipEndOfCentralDirectory, zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the offset of the start of the ZIP Central Directory in the archive.
|
||||
*
|
||||
* <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
|
||||
*/
|
||||
public static void setZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory, long offset) {
|
||||
assertByteOrderLittleEndian(zipEndOfCentralDirectory);
|
||||
setUnsignedInt32(zipEndOfCentralDirectory, zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET, offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size (in bytes) of the ZIP Central Directory.
|
||||
*
|
||||
* <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
|
||||
*/
|
||||
public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) {
|
||||
assertByteOrderLittleEndian(zipEndOfCentralDirectory);
|
||||
return getUnsignedInt32(zipEndOfCentralDirectory, zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET);
|
||||
}
|
||||
|
||||
private static void assertByteOrderLittleEndian(ByteBuffer buffer) {
|
||||
if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
|
||||
throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
|
||||
}
|
||||
}
|
||||
|
||||
private static int getUnsignedInt16(ByteBuffer buffer, int offset) {
|
||||
return buffer.getShort(offset) & 0xffff;
|
||||
}
|
||||
|
||||
private static long getUnsignedInt32(ByteBuffer buffer, int offset) {
|
||||
return buffer.getInt(offset) & 0xffffffffL;
|
||||
}
|
||||
|
||||
private static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) {
|
||||
if ((value < 0) || (value > 0xffffffffL)) {
|
||||
throw new IllegalArgumentException("uint32 value of out range: " + value);
|
||||
}
|
||||
buffer.putInt(buffer.position() + offset, (int) value);
|
||||
}
|
||||
}
|
||||
BIN
app/signing/src/main/resources/keys/verity.pk8
Normal file
BIN
app/signing/src/main/resources/keys/verity.pk8
Normal file
Binary file not shown.
24
app/signing/src/main/resources/keys/verity.x509.pem
Normal file
24
app/signing/src/main/resources/keys/verity.x509.pem
Normal file
@@ -0,0 +1,24 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID/TCCAuWgAwIBAgIJAJcPmDkJqolJMA0GCSqGSIb3DQEBBQUAMIGUMQswCQYD
|
||||
VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4g
|
||||
VmlldzEQMA4GA1UECgwHQW5kcm9pZDEQMA4GA1UECwwHQW5kcm9pZDEQMA4GA1UE
|
||||
AwwHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe
|
||||
Fw0xNDExMDYxOTA3NDBaFw00MjAzMjQxOTA3NDBaMIGUMQswCQYDVQQGEwJVUzET
|
||||
MBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEQMA4G
|
||||
A1UECgwHQW5kcm9pZDEQMA4GA1UECwwHQW5kcm9pZDEQMA4GA1UEAwwHQW5kcm9p
|
||||
ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASIwDQYJKoZI
|
||||
hvcNAQEBBQADggEPADCCAQoCggEBAOjreE0vTVSRenuzO9vnaWfk0eQzYab0gqpi
|
||||
6xAzi6dmD+ugoEKJmbPiuE5Dwf21isZ9uhUUu0dQM46dK4ocKxMRrcnmGxydFn6o
|
||||
fs3ODJMXOkv2gKXL/FdbEPdDbxzdu8z3yk+W67udM/fW7WbaQ3DO0knu+izKak/3
|
||||
T41c5uoXmQ81UNtAzRGzGchNVXMmWuTGOkg6U+0I2Td7K8yvUMWhAWPPpKLtVH9r
|
||||
AL5TzjYNR92izdKcz3AjRsI3CTjtpiVABGeX0TcjRSuZB7K9EK56HV+OFNS6I1NP
|
||||
jdD7FIShyGlqqZdUOkAUZYanbpgeT5N7QL6uuqcGpoTOkalu6kkCAwEAAaNQME4w
|
||||
HQYDVR0OBBYEFH5DM/m7oArf4O3peeKO0ZIEkrQPMB8GA1UdIwQYMBaAFH5DM/m7
|
||||
oArf4O3peeKO0ZIEkrQPMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEB
|
||||
AHO3NSvDE5jFvMehGGtS8BnFYdFKRIglDMc4niWSzhzOVYRH4WajxdtBWc5fx0ix
|
||||
NF/+hVKVhP6AIOQa+++sk+HIi7RvioPPbhjcsVlZe7cUEGrLSSveGouQyc+j0+m6
|
||||
JF84kszIl5GGNMTnx0XRPO+g8t6h5LWfnVydgZfpGRRg+WHewk1U2HlvTjIceb0N
|
||||
dcoJ8WKJAFWdcuE7VIm4w+vF/DYX/A2Oyzr2+QRhmYSv1cusgAeC1tvH4ap+J1Lg
|
||||
UnOu5Kh/FqPLLSwNVQp4Bu7b9QFfqK8Moj84bj88NqRGZgDyqzuTrFxn6FW7dmyA
|
||||
yttuAJAEAymk1mipd9+zp38=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -3,57 +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/AppTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:allowBackup="true"
|
||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
|
||||
|
||||
<!-- Activities -->
|
||||
|
||||
<activity
|
||||
android:name="a.b"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:exported="true" />
|
||||
<!-- Splash -->
|
||||
<activity
|
||||
android:name="a.c"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:exported="true"
|
||||
android:theme="@style/SplashTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="a.f"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:screenOrientation="nosensor"
|
||||
android:theme="@style/AppTheme.NoDrawer" />
|
||||
|
||||
<!-- Main -->
|
||||
<activity android:name="a.b" />
|
||||
|
||||
<!-- Superuser -->
|
||||
|
||||
<activity
|
||||
android:name="a.m"
|
||||
android:directBootAware="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity="a.m"
|
||||
android:theme="@style/SuRequest" />
|
||||
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>
|
||||
@@ -64,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>
|
||||
</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.MainActivity;
|
||||
|
||||
public class b extends MainActivity {
|
||||
/* stub */
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.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.FlashActivity;
|
||||
|
||||
public class f extends FlashActivity {
|
||||
/* stub */
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package a;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.topjohnwu.magisk.components.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.components.GeneralReceiver;
|
||||
|
||||
public class h extends GeneralReceiver {
|
||||
/* stub */
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.components.DownloadModuleService;
|
||||
|
||||
public class j extends DownloadModuleService {
|
||||
/* stub */
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.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.BootSigner
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
BootSigner.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.components.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,62 +0,0 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
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.preference.PreferenceManager;
|
||||
|
||||
import com.topjohnwu.magisk.database.MagiskDB;
|
||||
import com.topjohnwu.magisk.database.RepoDatabaseHelper;
|
||||
import com.topjohnwu.magisk.utils.LocaleManager;
|
||||
import com.topjohnwu.magisk.utils.RootUtils;
|
||||
import com.topjohnwu.net.Networking;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
public class App extends Application {
|
||||
|
||||
public static App self;
|
||||
public static Context deContext;
|
||||
public static ThreadPoolExecutor THREAD_POOL;
|
||||
|
||||
// Global resources
|
||||
public SharedPreferences prefs;
|
||||
public MagiskDB mDB;
|
||||
public RepoDatabaseHelper repoDB;
|
||||
|
||||
static {
|
||||
Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER | Shell.FLAG_USE_MAGISK_BUSYBOX);
|
||||
Shell.Config.verboseLogging(BuildConfig.DEBUG);
|
||||
Shell.Config.addInitializers(RootUtils.class);
|
||||
Shell.Config.setTimeout(2);
|
||||
THREAD_POOL = (ThreadPoolExecutor) AsyncTask.THREAD_POOL_EXECUTOR;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(base);
|
||||
self = this;
|
||||
deContext = base;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
deContext = base.createDeviceProtectedStorageContext();
|
||||
deContext.moveSharedPreferencesFrom(base,
|
||||
PreferenceManager.getDefaultSharedPreferencesName(base));
|
||||
}
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(deContext);
|
||||
mDB = new MagiskDB(base);
|
||||
|
||||
Networking.init(base);
|
||||
LocaleManager.setLocale(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
LocaleManager.setLocale(this);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import com.topjohnwu.magisk.components.DownloadModuleService;
|
||||
import com.topjohnwu.magisk.components.GeneralReceiver;
|
||||
import com.topjohnwu.magisk.components.UpdateCheckService;
|
||||
|
||||
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,398 +0,0 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Xml;
|
||||
|
||||
import androidx.collection.ArrayMap;
|
||||
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.ShellUtils;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
public class Config {
|
||||
|
||||
// Current status
|
||||
public static String magiskVersionString;
|
||||
public static int magiskVersionCode = -1;
|
||||
private static boolean magiskHide;
|
||||
|
||||
// Update Info
|
||||
public static String remoteMagiskVersionString;
|
||||
public static int remoteMagiskVersionCode = -1;
|
||||
public static String magiskLink;
|
||||
public static String magiskNoteLink;
|
||||
public static String magiskMD5;
|
||||
public static String remoteManagerVersionString;
|
||||
public static int remoteManagerVersionCode = -1;
|
||||
public static String managerLink;
|
||||
public static String managerNoteLink;
|
||||
public static String uninstallerLink;
|
||||
|
||||
// Install flags
|
||||
public static boolean keepVerity = false;
|
||||
public static boolean keepEnc = false;
|
||||
public static boolean recovery = false;
|
||||
|
||||
public static int suLogTimeout = 14;
|
||||
|
||||
public static class Key {
|
||||
// su configs
|
||||
public static final String ROOT_ACCESS = "root_access";
|
||||
public static final String SU_MULTIUSER_MODE = "multiuser_mode";
|
||||
public static final String SU_MNT_NS = "mnt_ns";
|
||||
public static final String SU_MANAGER = "requester";
|
||||
public static final String SU_REQUEST_TIMEOUT = "su_request_timeout";
|
||||
public static final String SU_AUTO_RESPONSE = "su_auto_response";
|
||||
public static final String SU_NOTIFICATION = "su_notification";
|
||||
public static final String SU_REAUTH = "su_reauth";
|
||||
public static final String SU_FINGERPRINT = "su_fingerprint";
|
||||
|
||||
// prefs
|
||||
public static final String CHECK_UPDATES = "check_update";
|
||||
public static final String UPDATE_CHANNEL = "update_channel";
|
||||
public static final String CUSTOM_CHANNEL = "custom_channel";
|
||||
public static final String BOOT_FORMAT = "boot_format";
|
||||
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 STABLE_CHANNEL = 0;
|
||||
public static final int BETA_CHANNEL = 1;
|
||||
public static final int CUSTOM_CHANNEL = 2;
|
||||
public static final int CANARY_CHANNEL = 3;
|
||||
public static final int CANARY_DEBUG_CHANNEL = 4;
|
||||
public static final int ROOT_ACCESS_DISABLED = 0;
|
||||
public static final int ROOT_ACCESS_APPS_ONLY = 1;
|
||||
public static final int ROOT_ACCESS_ADB_ONLY = 2;
|
||||
public static final int ROOT_ACCESS_APPS_AND_ADB = 3;
|
||||
public static final int MULTIUSER_MODE_OWNER_ONLY = 0;
|
||||
public static final int MULTIUSER_MODE_OWNER_MANAGED = 1;
|
||||
public static final int MULTIUSER_MODE_USER = 2;
|
||||
public static final int NAMESPACE_MODE_GLOBAL = 0;
|
||||
public static final int NAMESPACE_MODE_REQUESTER = 1;
|
||||
public static final int NAMESPACE_MODE_ISOLATE = 2;
|
||||
public static final int NO_NOTIFICATION = 0;
|
||||
public static final int NOTIFICATION_TOAST = 1;
|
||||
public static final int SU_PROMPT = 0;
|
||||
public static final int SU_AUTO_DENY = 1;
|
||||
public static final int SU_AUTO_ALLOW = 2;
|
||||
public static final int[] TIMEOUT_LIST = {0, -1, 10, 20, 30, 60};
|
||||
public static final int ORDER_NAME = 0;
|
||||
public static final int ORDER_DATE = 1;
|
||||
}
|
||||
|
||||
public static void loadMagiskInfo() {
|
||||
try {
|
||||
magiskVersionString = ShellUtils.fastCmd("magisk -v").split(":")[0];
|
||||
magiskVersionCode = Integer.parseInt(ShellUtils.fastCmd("magisk -V"));
|
||||
magiskHide = Shell.su("magiskhide --status").exec().isSuccess();
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
public static void export() {
|
||||
// Flush prefs to disk
|
||||
App app = App.self;
|
||||
app.prefs.edit().commit();
|
||||
File xml = new File(App.deContext.getFilesDir().getParent() + "/shared_prefs",
|
||||
app.getPackageName() + "_preferences.xml");
|
||||
Shell.su(Utils.fmt("cat %s > /data/adb/%s", xml, Const.MANAGER_CONFIGS)).exec();
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
SharedPreferences pref = App.self.prefs;
|
||||
SharedPreferences.Editor editor = pref.edit();
|
||||
SuFile config = new SuFile("/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.BOOT_FORMAT:
|
||||
case Key.LOCALE:
|
||||
case Key.ETAG_KEY:
|
||||
return PREF_STR;
|
||||
|
||||
case Key.ROOT_ACCESS:
|
||||
case Key.SU_MNT_NS:
|
||||
case Key.SU_MULTIUSER_MODE:
|
||||
return DB_INT;
|
||||
|
||||
case Key.SU_FINGERPRINT:
|
||||
return DB_BOOL;
|
||||
|
||||
case Key.SU_MANAGER:
|
||||
return DB_STR;
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T get(String key) {
|
||||
App app = App.self;
|
||||
switch (getConfigType(key)) {
|
||||
case PREF_INT:
|
||||
return (T) (Integer) app.prefs.getInt(key, getDef(key));
|
||||
case PREF_STR_INT:
|
||||
return (T) (Integer) Utils.getPrefsInt(app.prefs, key, getDef(key));
|
||||
case PREF_BOOL:
|
||||
return (T) (Boolean) app.prefs.getBoolean(key, getDef(key));
|
||||
case PREF_STR:
|
||||
return (T) app.prefs.getString(key, getDef(key));
|
||||
case DB_INT:
|
||||
return (T) (Integer) app.mDB.getSettings(key, getDef(key));
|
||||
case DB_BOOL:
|
||||
return (T) (Boolean) (app.mDB.getSettings(key, getDef(key) ? 1 : 0) != 0);
|
||||
case DB_STR:
|
||||
return (T) app.mDB.getStrings(key, getDef(key));
|
||||
}
|
||||
/* Will never get here (IllegalArgumentException in getConfigType) */
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void set(String key, Object val) {
|
||||
App app = App.self;
|
||||
switch (getConfigType(key)) {
|
||||
case PREF_INT:
|
||||
app.prefs.edit().putInt(key, (int) val).apply();
|
||||
break;
|
||||
case PREF_STR_INT:
|
||||
app.prefs.edit().putString(key, String.valueOf(val)).apply();
|
||||
break;
|
||||
case PREF_BOOL:
|
||||
app.prefs.edit().putBoolean(key, (boolean) val).apply();
|
||||
break;
|
||||
case PREF_STR:
|
||||
app.prefs.edit().putString(key, (String) val).apply();
|
||||
break;
|
||||
case DB_INT:
|
||||
app.mDB.setSettings(key, (int) val);
|
||||
break;
|
||||
case DB_BOOL:
|
||||
app.mDB.setSettings(key, (boolean) val ? 1 : 0);
|
||||
break;
|
||||
case DB_STR:
|
||||
app.mDB.setStrings(key, (String) val);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void remove(String key) {
|
||||
App app = App.self;
|
||||
switch (getConfigType(key)) {
|
||||
case PREF_INT:
|
||||
case PREF_STR_INT:
|
||||
case PREF_BOOL:
|
||||
case PREF_STR:
|
||||
app.prefs.edit().remove(key).apply();
|
||||
break;
|
||||
case DB_BOOL:
|
||||
case DB_INT:
|
||||
app.mDB.rmSettings(key);
|
||||
break;
|
||||
case DB_STR:
|
||||
app.mDB.setStrings(key, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static ArrayMap<String, Object> defs = new ArrayMap<>();
|
||||
|
||||
static {
|
||||
/* Set default configurations */
|
||||
|
||||
// prefs int
|
||||
defs.put(Key.REPO_ORDER, Value.ORDER_DATE);
|
||||
|
||||
// prefs string int
|
||||
defs.put(Key.SU_REQUEST_TIMEOUT, 10);
|
||||
defs.put(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT);
|
||||
defs.put(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST);
|
||||
defs.put(Key.UPDATE_CHANNEL, Utils.isCanary() ?
|
||||
Value.CANARY_DEBUG_CHANNEL : Value.STABLE_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.BOOT_FORMAT, ".img");
|
||||
defs.put(Key.LOCALE, "");
|
||||
//defs.put(Key.ETAG_KEY, null);
|
||||
|
||||
// db int
|
||||
defs.put(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB);
|
||||
defs.put(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER);
|
||||
defs.put(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY);
|
||||
|
||||
// db bool
|
||||
//defs.put(Key.SU_FINGERPRINT, false);
|
||||
|
||||
// db strings
|
||||
//defs.put(Key.SU_MANAGER, null);
|
||||
}
|
||||
|
||||
private static <T> T getDef(String key) {
|
||||
Object val = defs.get(key);
|
||||
switch (getConfigType(key)) {
|
||||
case PREF_INT:
|
||||
case DB_INT:
|
||||
case PREF_STR_INT:
|
||||
return val != null ? (T) val : (T) (Integer) 0;
|
||||
case DB_BOOL:
|
||||
case PREF_BOOL:
|
||||
return val != null ? (T) val : (T) (Boolean) false;
|
||||
case DB_STR:
|
||||
case PREF_STR:
|
||||
return (T) val;
|
||||
}
|
||||
/* Will never get here (IllegalArgumentException in getConfigType) */
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void setDefs(SharedPreferences pref, SharedPreferences.Editor editor) {
|
||||
App app = App.self;
|
||||
for (String key : defs.keySet()) {
|
||||
int type = getConfigType(key);
|
||||
switch (type) {
|
||||
case DB_INT:
|
||||
editor.putString(key, String.valueOf(
|
||||
app.mDB.getSettings(key, (Integer) defs.get(key))));
|
||||
continue;
|
||||
case DB_STR:
|
||||
editor.putString(key, app.mDB.getStrings(key, (String) defs.get(key)));
|
||||
continue;
|
||||
case DB_BOOL:
|
||||
int bs = app.mDB.getSettings(key, -1);
|
||||
editor.putBoolean(key, bs < 0 ? (Boolean) defs.get(key) : bs != 0);
|
||||
continue;
|
||||
}
|
||||
if (pref.contains(key))
|
||||
continue;
|
||||
switch (type) {
|
||||
case PREF_INT:
|
||||
editor.putInt(key, (Integer) defs.get(key));
|
||||
break;
|
||||
case PREF_STR_INT:
|
||||
editor.putString(key, String.valueOf(defs.get(key)));
|
||||
break;
|
||||
case PREF_STR:
|
||||
editor.putString(key, (String) defs.get(key));
|
||||
break;
|
||||
case PREF_BOOL:
|
||||
editor.putBoolean(key, (Boolean) defs.get(key));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,105 +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 BUSYBOX_PATH = "/sbin/.magisk/busybox";
|
||||
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 MIN_MODULE_VER = 1500;
|
||||
public static final int SNET_EXT_VER = 12;
|
||||
|
||||
public static final int USER_ID = Process.myUid() / 100000;
|
||||
|
||||
public static final class MAGISK_VER {
|
||||
public static final int MIN_SUPPORT = 18000;
|
||||
}
|
||||
|
||||
public static class ID {
|
||||
public static final int UPDATE_SERVICE_ID = 1;
|
||||
public static final int FETCH_ZIP = 2;
|
||||
public static final int SELECT_BOOT = 3;
|
||||
public static final int ONBOOT_SERVICE_ID = 6;
|
||||
|
||||
// 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 FROM_SPLASH = "splash";
|
||||
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 FLASH_SET_BOOT = "boot";
|
||||
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_BOOT = "patch";
|
||||
public static final String FLASH_MAGISK = "magisk";
|
||||
public static final String FLASH_INACTIVE_SLOT = "slot";
|
||||
public static final String UNINSTALL = "uninstall";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.topjohnwu.magisk.adapters.StringListAdapter;
|
||||
import com.topjohnwu.magisk.components.BaseActivity;
|
||||
import com.topjohnwu.magisk.tasks.FlashZip;
|
||||
import com.topjohnwu.magisk.tasks.MagiskInstaller;
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
import com.topjohnwu.superuser.CallbackList;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import butterknife.BindColor;
|
||||
import butterknife.BindView;
|
||||
import butterknife.OnClick;
|
||||
|
||||
public class FlashActivity extends BaseActivity {
|
||||
|
||||
@BindView(R.id.toolbar) Toolbar toolbar;
|
||||
@BindView(R.id.button_panel) LinearLayout buttonPanel;
|
||||
@BindView(R.id.reboot) Button reboot;
|
||||
@BindView(R.id.recyclerView) RecyclerView rv;
|
||||
@BindColor(android.R.color.white) int white;
|
||||
|
||||
private List<String> console, logs;
|
||||
|
||||
@OnClick(R.id.reboot)
|
||||
void reboot() {
|
||||
Utils.reboot();
|
||||
}
|
||||
|
||||
@OnClick(R.id.save_logs)
|
||||
void saveLogs() {
|
||||
runWithExternalRW(() -> {
|
||||
Calendar now = Calendar.getInstance();
|
||||
String filename = String.format(Locale.US,
|
||||
"magisk_install_log_%04d%02d%02d_%02d%02d%02d.log",
|
||||
now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1,
|
||||
now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY),
|
||||
now.get(Calendar.MINUTE), now.get(Calendar.SECOND));
|
||||
|
||||
File logFile = new File(Const.EXTERNAL_PATH, filename);
|
||||
try (FileWriter writer = new FileWriter(logFile)) {
|
||||
for (String s : logs) {
|
||||
writer.write(s);
|
||||
writer.write('\n');
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
Utils.toast(logFile.getPath(), Toast.LENGTH_LONG);
|
||||
});
|
||||
}
|
||||
|
||||
@OnClick(R.id.close)
|
||||
public void close() {
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
// Prevent user accidentally press back button
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDarkTheme() {
|
||||
return R.style.AppTheme_NoDrawer_Dark;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_flash);
|
||||
new FlashActivity_ViewBinding(this);
|
||||
|
||||
setSupportActionBar(toolbar);
|
||||
ActionBar ab = getSupportActionBar();
|
||||
if (ab != null) {
|
||||
ab.setTitle(R.string.flashing);
|
||||
}
|
||||
setFloating();
|
||||
setFinishOnTouchOutside(false);
|
||||
if (!Shell.rootAccess())
|
||||
reboot.setVisibility(View.GONE);
|
||||
|
||||
logs = Collections.synchronizedList(new ArrayList<>());
|
||||
console = new ConsoleList();
|
||||
rv.setAdapter(new ConsoleAdapter());
|
||||
|
||||
Intent intent = getIntent();
|
||||
Uri uri = intent.getData();
|
||||
|
||||
switch (intent.getStringExtra(Const.Key.FLASH_ACTION)) {
|
||||
case Const.Value.FLASH_ZIP:
|
||||
new FlashModule(uri).exec();
|
||||
break;
|
||||
case Const.Value.UNINSTALL:
|
||||
new Uninstall(uri).exec();
|
||||
break;
|
||||
case Const.Value.FLASH_MAGISK:
|
||||
new DirectInstall().exec();
|
||||
break;
|
||||
case Const.Value.FLASH_INACTIVE_SLOT:
|
||||
new SecondSlot().exec();
|
||||
break;
|
||||
case Const.Value.PATCH_BOOT:
|
||||
new PatchBoot(uri).exec();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private class ConsoleAdapter extends StringListAdapter<ConsoleAdapter.ViewHolder> {
|
||||
|
||||
ConsoleAdapter() {
|
||||
super(console, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int itemLayoutRes() {
|
||||
return R.layout.list_item_console;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder createViewHolder(@NonNull View v) {
|
||||
return new ViewHolder(v);
|
||||
}
|
||||
|
||||
class ViewHolder extends StringListAdapter.ViewHolder {
|
||||
|
||||
public ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
txt.setTextColor(white);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int textViewResId() {
|
||||
return R.id.txt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ConsoleList extends CallbackList<String> {
|
||||
|
||||
ConsoleList() {
|
||||
super(new ArrayList<>());
|
||||
}
|
||||
|
||||
private void updateUI() {
|
||||
rv.getAdapter().notifyItemChanged(size() - 1);
|
||||
rv.postDelayed(() -> rv.smoothScrollToPosition(size() - 1), 10);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddElement(String s) {
|
||||
logs.add(s);
|
||||
updateUI();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String set(int i, String s) {
|
||||
String ret = super.set(i, s);
|
||||
UiThreadHandler.run(this::updateUI);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
private class FlashModule extends FlashZip {
|
||||
|
||||
FlashModule(Uri uri) {
|
||||
super(uri, console, logs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResult(boolean success) {
|
||||
if (success) {
|
||||
Utils.loadModules();
|
||||
} else {
|
||||
console.add("! Installation failed");
|
||||
reboot.setVisibility(View.GONE);
|
||||
}
|
||||
buttonPanel.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private class Uninstall extends FlashModule {
|
||||
|
||||
Uninstall(Uri uri) {
|
||||
super(uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResult(boolean success) {
|
||||
if (success)
|
||||
UiThreadHandler.handler.postDelayed(Shell.su("pm uninstall " + getPackageName())::exec, 3000);
|
||||
else
|
||||
super.onResult(false);
|
||||
}
|
||||
}
|
||||
|
||||
private abstract class BaseInstaller extends MagiskInstaller {
|
||||
BaseInstaller() {
|
||||
super(console, logs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResult(boolean success) {
|
||||
if (success) {
|
||||
console.add("- All done!");
|
||||
} else {
|
||||
Shell.sh("rm -rf " + installDir).submit();
|
||||
console.add("! Installation failed");
|
||||
reboot.setVisibility(View.GONE);
|
||||
}
|
||||
buttonPanel.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private class DirectInstall extends BaseInstaller {
|
||||
|
||||
@Override
|
||||
protected boolean operations() {
|
||||
return findImage() && extractZip() && patchBoot() && flashBoot();
|
||||
}
|
||||
}
|
||||
|
||||
private class SecondSlot extends BaseInstaller {
|
||||
|
||||
@Override
|
||||
protected boolean operations() {
|
||||
return findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA();
|
||||
}
|
||||
}
|
||||
|
||||
private class PatchBoot extends BaseInstaller {
|
||||
|
||||
private Uri uri;
|
||||
|
||||
PatchBoot(Uri u) {
|
||||
uri = u;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean operations() {
|
||||
return copyBoot(uri) && extractZip() && patchBoot() && storeBoot();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
import com.google.android.material.navigation.NavigationView;
|
||||
import com.topjohnwu.magisk.components.BaseActivity;
|
||||
import com.topjohnwu.magisk.fragments.LogFragment;
|
||||
import com.topjohnwu.magisk.fragments.MagiskFragment;
|
||||
import com.topjohnwu.magisk.fragments.MagiskHideFragment;
|
||||
import com.topjohnwu.magisk.fragments.ModulesFragment;
|
||||
import com.topjohnwu.magisk.fragments.ReposFragment;
|
||||
import com.topjohnwu.magisk.fragments.SettingsFragment;
|
||||
import com.topjohnwu.magisk.fragments.SuperuserFragment;
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
import com.topjohnwu.net.Networking;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
import butterknife.BindView;
|
||||
|
||||
public class MainActivity extends BaseActivity
|
||||
implements NavigationView.OnNavigationItemSelectedListener {
|
||||
|
||||
private final Handler mDrawerHandler = new Handler();
|
||||
private int mDrawerItem;
|
||||
private static boolean fromShortcut = false;
|
||||
|
||||
@BindView(R.id.toolbar) public Toolbar toolbar;
|
||||
@BindView(R.id.drawer_layout) DrawerLayout drawer;
|
||||
@BindView(R.id.nav_view) NavigationView navigationView;
|
||||
|
||||
private float toolbarElevation;
|
||||
|
||||
@Override
|
||||
public int getDarkTheme() {
|
||||
return R.style.AppTheme_Dark;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
if (!getIntent().getBooleanExtra(Const.Key.FROM_SPLASH, false)) {
|
||||
startActivity(new Intent(this, ClassMap.get(SplashActivity.class)));
|
||||
finish();
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
new MainActivity_ViewBinding(this);
|
||||
checkHideSection();
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.magisk, R.string.magisk) {
|
||||
@Override
|
||||
public void onDrawerOpened(View drawerView) {
|
||||
super.onDrawerOpened(drawerView);
|
||||
super.onDrawerSlide(drawerView, 0); // this disables the arrow @ completed tate
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawerSlide(View drawerView, float slideOffset) {
|
||||
super.onDrawerSlide(drawerView, 0); // this disables the animation
|
||||
}
|
||||
};
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
toolbarElevation = toolbar.getElevation();
|
||||
}
|
||||
|
||||
drawer.addDrawerListener(toggle);
|
||||
toggle.syncState();
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
String section = getIntent().getStringExtra(Const.Key.OPEN_SECTION);
|
||||
fromShortcut = section != null;
|
||||
navigate(section);
|
||||
}
|
||||
|
||||
navigationView.setNavigationItemSelectedListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (drawer.isDrawerOpen(navigationView)) {
|
||||
drawer.closeDrawer(navigationView);
|
||||
} else if (mDrawerItem != R.id.magisk && !fromShortcut) {
|
||||
navigate(R.id.magisk);
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onNavigationItemSelected(@NonNull final MenuItem menuItem) {
|
||||
mDrawerHandler.removeCallbacksAndMessages(null);
|
||||
mDrawerHandler.postDelayed(() -> navigate(menuItem.getItemId()), 250);
|
||||
drawer.closeDrawer(navigationView);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void checkHideSection() {
|
||||
Menu menu = navigationView.getMenu();
|
||||
menu.findItem(R.id.magiskhide).setVisible(Shell.rootAccess() &&
|
||||
(boolean) Config.get(Config.Key.MAGISKHIDE));
|
||||
menu.findItem(R.id.modules).setVisible(Shell.rootAccess() && Config.magiskVersionCode >= 0);
|
||||
menu.findItem(R.id.downloads).setVisible(Networking.checkNetworkStatus(this)
|
||||
&& Shell.rootAccess() && Config.magiskVersionCode >= 0);
|
||||
menu.findItem(R.id.log).setVisible(Shell.rootAccess());
|
||||
menu.findItem(R.id.superuser).setVisible(Utils.showSuperUser());
|
||||
}
|
||||
|
||||
public void navigate(String item) {
|
||||
int itemId = R.id.magisk;
|
||||
if (item != null) {
|
||||
switch (item) {
|
||||
case "superuser":
|
||||
itemId = R.id.superuser;
|
||||
break;
|
||||
case "modules":
|
||||
itemId = R.id.modules;
|
||||
break;
|
||||
case "downloads":
|
||||
itemId = R.id.downloads;
|
||||
break;
|
||||
case "magiskhide":
|
||||
itemId = R.id.magiskhide;
|
||||
break;
|
||||
case "log":
|
||||
itemId = R.id.log;
|
||||
break;
|
||||
case "settings":
|
||||
itemId = R.id.settings;
|
||||
break;
|
||||
}
|
||||
}
|
||||
navigate(itemId);
|
||||
}
|
||||
|
||||
public void navigate(int itemId) {
|
||||
mDrawerItem = itemId;
|
||||
navigationView.setCheckedItem(itemId);
|
||||
switch (itemId) {
|
||||
case R.id.magisk:
|
||||
fromShortcut = false;
|
||||
displayFragment(new MagiskFragment(), true);
|
||||
break;
|
||||
case R.id.superuser:
|
||||
displayFragment(new SuperuserFragment(), true);
|
||||
break;
|
||||
case R.id.modules:
|
||||
displayFragment(new ModulesFragment(), true);
|
||||
break;
|
||||
case R.id.downloads:
|
||||
displayFragment(new ReposFragment(), true);
|
||||
break;
|
||||
case R.id.magiskhide:
|
||||
displayFragment(new MagiskHideFragment(), true);
|
||||
break;
|
||||
case R.id.log:
|
||||
displayFragment(new LogFragment(), false);
|
||||
break;
|
||||
case R.id.settings:
|
||||
displayFragment(new SettingsFragment(), true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void displayFragment(@NonNull Fragment navFragment, boolean setElevation) {
|
||||
supportInvalidateOptionsMenu();
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
|
||||
.replace(R.id.content_frame, navFragment)
|
||||
.commitNow();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
toolbar.setElevation(setElevation ? toolbarElevation : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.topjohnwu.magisk.components.BaseActivity;
|
||||
import com.topjohnwu.magisk.database.RepoDatabaseHelper;
|
||||
import com.topjohnwu.magisk.tasks.UpdateRepos;
|
||||
import com.topjohnwu.magisk.uicomponents.Notifications;
|
||||
import com.topjohnwu.magisk.uicomponents.Shortcuts;
|
||||
import com.topjohnwu.magisk.utils.LocaleManager;
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
import com.topjohnwu.net.Networking;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
public class SplashActivity extends BaseActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Shell.getShell(shell -> {
|
||||
if (Config.magiskVersionCode > 0 &&
|
||||
Config.magiskVersionCode < Const.MAGISK_VER.MIN_SUPPORT) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.unsupport_magisk_title)
|
||||
.setMessage(R.string.unsupport_magisk_message)
|
||||
.setNegativeButton(R.string.ok, null)
|
||||
.setOnDismissListener(dialog -> finish())
|
||||
.show();
|
||||
} else {
|
||||
initAndStart();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initAndStart() {
|
||||
String pkg = Config.get(Config.Key.SU_MANAGER);
|
||||
if (pkg != null && getPackageName().equals(BuildConfig.APPLICATION_ID)) {
|
||||
Config.remove(Config.Key.SU_MANAGER);
|
||||
Shell.su("pm uninstall " + pkg).submit();
|
||||
}
|
||||
if (TextUtils.equals(pkg, getPackageName())) {
|
||||
try {
|
||||
// We are the manager, remove com.topjohnwu.magisk as it could be malware
|
||||
getPackageManager().getApplicationInfo(BuildConfig.APPLICATION_ID, 0);
|
||||
Shell.su("pm uninstall " + BuildConfig.APPLICATION_ID).submit();
|
||||
} catch (PackageManager.NameNotFoundException ignored) {}
|
||||
}
|
||||
|
||||
// Dynamic detect all locales
|
||||
LocaleManager.loadAvailableLocales(R.string.app_changelog);
|
||||
|
||||
// Set default configs
|
||||
Config.initialize();
|
||||
|
||||
// Create notification channel on Android O
|
||||
Notifications.setup(this);
|
||||
|
||||
// Schedule periodic update checks
|
||||
Utils.scheduleUpdateCheck();
|
||||
|
||||
// Setup shortcuts
|
||||
Shortcuts.setup(this);
|
||||
|
||||
// Create repo database
|
||||
app.repoDB = new RepoDatabaseHelper(this);
|
||||
|
||||
// Magisk working as expected
|
||||
if (Shell.rootAccess() && Config.magiskVersionCode > 0) {
|
||||
// Load modules
|
||||
Utils.loadModules(false);
|
||||
// Load repos
|
||||
if (Networking.checkNetworkStatus(this))
|
||||
new UpdateRepos().exec();
|
||||
}
|
||||
|
||||
Intent intent = new Intent(this, ClassMap.get(MainActivity.class));
|
||||
intent.putExtra(Const.Key.OPEN_SECTION, getIntent().getStringExtra(Const.Key.OPEN_SECTION));
|
||||
intent.putExtra(Const.Key.FROM_SPLASH, true);
|
||||
intent.putExtra(BaseActivity.INTENT_PERM, getIntent().getStringExtra(BaseActivity.INTENT_PERM));
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.os.Bundle;
|
||||
import android.os.CountDownTimer;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
|
||||
import com.topjohnwu.magisk.components.BaseActivity;
|
||||
import com.topjohnwu.magisk.container.Policy;
|
||||
import com.topjohnwu.magisk.utils.FingerprintHelper;
|
||||
import com.topjohnwu.magisk.utils.SuConnector;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import butterknife.BindView;
|
||||
|
||||
public class SuRequestActivity extends BaseActivity {
|
||||
@BindView(R.id.su_popup) LinearLayout suPopup;
|
||||
@BindView(R.id.timeout) Spinner timeout;
|
||||
@BindView(R.id.app_icon) ImageView appIcon;
|
||||
@BindView(R.id.app_name) TextView appNameView;
|
||||
@BindView(R.id.package_name) TextView packageNameView;
|
||||
@BindView(R.id.grant_btn) Button grant_btn;
|
||||
@BindView(R.id.deny_btn) Button deny_btn;
|
||||
@BindView(R.id.fingerprint) ImageView fingerprintImg;
|
||||
@BindView(R.id.warning) TextView warning;
|
||||
|
||||
private SuConnector connector;
|
||||
private Policy policy;
|
||||
private CountDownTimer timer;
|
||||
private FingerprintHelper fingerprintHelper;
|
||||
private SharedPreferences timeoutPrefs;
|
||||
|
||||
@Override
|
||||
public int getDarkTheme() {
|
||||
return R.style.SuRequest_Dark;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
if (timer != null)
|
||||
timer.cancel();
|
||||
if (fingerprintHelper != null)
|
||||
fingerprintHelper.cancel();
|
||||
super.finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (policy != null) {
|
||||
handleAction(Policy.DENY);
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
|
||||
PackageManager pm = getPackageManager();
|
||||
app.mDB.clearOutdated();
|
||||
timeoutPrefs = App.deContext.getSharedPreferences("su_timeout", 0);
|
||||
|
||||
// Get policy
|
||||
Intent intent = getIntent();
|
||||
try {
|
||||
String socketName = intent.getStringExtra("socket");
|
||||
connector = new SuConnector(socketName) {
|
||||
@Override
|
||||
protected void onResponse() throws IOException {
|
||||
out.writeInt(policy.policy);
|
||||
}
|
||||
};
|
||||
Bundle bundle = connector.readSocketInput();
|
||||
int uid = Integer.parseInt(bundle.getString("uid"));
|
||||
policy = app.mDB.getPolicy(uid);
|
||||
if (policy == null) {
|
||||
policy = new Policy(uid, pm);
|
||||
}
|
||||
} catch (IOException | PackageManager.NameNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Never allow com.topjohnwu.magisk (could be malware)
|
||||
if (TextUtils.equals(policy.packageName, BuildConfig.APPLICATION_ID)) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
switch ((int) Config.get(Config.Key.SU_AUTO_RESPONSE)) {
|
||||
case Config.Value.SU_AUTO_DENY:
|
||||
handleAction(Policy.DENY, 0);
|
||||
return;
|
||||
case Config.Value.SU_AUTO_ALLOW:
|
||||
handleAction(Policy.ALLOW, 0);
|
||||
return;
|
||||
case Config.Value.SU_PROMPT:
|
||||
default:
|
||||
}
|
||||
|
||||
// If not interactive, response directly
|
||||
if (policy.policy != Policy.INTERACTIVE) {
|
||||
handleAction();
|
||||
return;
|
||||
}
|
||||
|
||||
setContentView(R.layout.activity_request);
|
||||
new SuRequestActivity_ViewBinding(this);
|
||||
|
||||
appIcon.setImageDrawable(policy.info.loadIcon(pm));
|
||||
appNameView.setText(policy.appName);
|
||||
packageNameView.setText(policy.packageName);
|
||||
warning.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
AppCompatResources.getDrawable(this, R.drawable.ic_warning), null, null, null);
|
||||
|
||||
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,
|
||||
R.array.allow_timeout, android.R.layout.simple_spinner_item);
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
timeout.setAdapter(adapter);
|
||||
timeout.setSelection(timeoutPrefs.getInt(policy.packageName, 0));
|
||||
|
||||
timer = new CountDownTimer((int) Config.get(Config.Key.SU_REQUEST_TIMEOUT) * 1000, 1000) {
|
||||
@Override
|
||||
public void onTick(long millisUntilFinished) {
|
||||
deny_btn.setText(getString(R.string.deny_with_str, "(" + millisUntilFinished / 1000 + ")"));
|
||||
}
|
||||
@Override
|
||||
public void onFinish() {
|
||||
deny_btn.setText(getString(R.string.deny_with_str, "(0)"));
|
||||
handleAction(Policy.DENY);
|
||||
}
|
||||
};
|
||||
|
||||
boolean useFP = FingerprintHelper.useFingerprint();
|
||||
|
||||
if (useFP) {
|
||||
try {
|
||||
fingerprintHelper = new FingerprintHelper() {
|
||||
@Override
|
||||
public void onAuthenticationError(int errorCode, CharSequence errString) {
|
||||
warning.setText(errString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
|
||||
warning.setText(helpString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
|
||||
handleAction(Policy.ALLOW);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailed() {
|
||||
warning.setText(R.string.auth_fail);
|
||||
}
|
||||
};
|
||||
fingerprintHelper.authenticate();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
useFP = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!useFP) {
|
||||
grant_btn.setOnClickListener(v -> {
|
||||
handleAction(Policy.ALLOW);
|
||||
timer.cancel();
|
||||
});
|
||||
grant_btn.requestFocus();
|
||||
}
|
||||
|
||||
grant_btn.setVisibility(useFP ? View.GONE : View.VISIBLE);
|
||||
fingerprintImg.setVisibility(useFP ? View.VISIBLE : View.GONE);
|
||||
|
||||
deny_btn.setOnClickListener(v -> {
|
||||
handleAction(Policy.DENY);
|
||||
timer.cancel();
|
||||
});
|
||||
suPopup.setOnClickListener(v -> cancelTimeout());
|
||||
timeout.setOnTouchListener((v, event) -> cancelTimeout());
|
||||
timer.start();
|
||||
}
|
||||
|
||||
private boolean cancelTimeout() {
|
||||
timer.cancel();
|
||||
deny_btn.setText(getString(R.string.deny));
|
||||
return false;
|
||||
}
|
||||
|
||||
private void handleAction() {
|
||||
connector.response();
|
||||
finish();
|
||||
}
|
||||
|
||||
private void handleAction(int action) {
|
||||
int pos = timeout.getSelectedItemPosition();
|
||||
timeoutPrefs.edit().putInt(policy.packageName, pos).apply();
|
||||
handleAction(action, Config.Value.TIMEOUT_LIST[pos]);
|
||||
}
|
||||
|
||||
private void handleAction(int action, int time) {
|
||||
policy.policy = action;
|
||||
if (time >= 0) {
|
||||
policy.until = (time == 0) ? 0 : (System.currentTimeMillis() / 1000 + time * 60);
|
||||
app.mDB.updatePolicy(policy);
|
||||
}
|
||||
handleAction();
|
||||
}
|
||||
}
|
||||
@@ -1,427 +0,0 @@
|
||||
package com.topjohnwu.magisk.adapters;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.ComponentInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.AsyncTask;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.collection.ArraySet;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.buildware.widget.indeterm.IndeterminateCheckBox;
|
||||
import com.topjohnwu.magisk.App;
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.R;
|
||||
import com.topjohnwu.magisk.uicomponents.ArrowExpandable;
|
||||
import com.topjohnwu.magisk.uicomponents.Expandable;
|
||||
import com.topjohnwu.magisk.utils.Event;
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import butterknife.BindView;
|
||||
import java9.util.Comparators;
|
||||
import java9.util.Lists;
|
||||
import java9.util.Objects;
|
||||
import java9.util.Sets;
|
||||
import java9.util.stream.Collectors;
|
||||
import java9.util.stream.Stream;
|
||||
import java9.util.stream.StreamSupport;
|
||||
|
||||
public class ApplicationAdapter extends SectionedAdapter
|
||||
<ApplicationAdapter.AppViewHolder, ApplicationAdapter.ProcessViewHolder> {
|
||||
|
||||
private static final String SAFETYNET_PROCESS = "com.google.android.gms.unstable";
|
||||
private static final String GMS_PACKAGE = "com.google.android.gms";
|
||||
private static boolean old_hide = false;
|
||||
|
||||
/* A list of apps that should not be shown as hide-able */
|
||||
private static final List<String> HIDE_BLACKLIST = Lists.of(
|
||||
App.self.getPackageName(),
|
||||
"android",
|
||||
"com.android.chrome",
|
||||
"com.chrome.beta",
|
||||
"com.chrome.dev",
|
||||
"com.chrome.canary",
|
||||
"com.android.webview",
|
||||
"com.google.android.webview"
|
||||
);
|
||||
private static final List<String> DEFAULT_HIDELIST = Lists.of(
|
||||
SAFETYNET_PROCESS
|
||||
);
|
||||
|
||||
private static int BOTTOM_MARGIN = -1;
|
||||
|
||||
private List<HideAppInfo> fullList, showList;
|
||||
private List<HideTarget> hideList;
|
||||
private PackageManager pm;
|
||||
private boolean showSystem;
|
||||
|
||||
public ApplicationAdapter(Context context) {
|
||||
fullList = showList = Collections.emptyList();
|
||||
hideList = Collections.emptyList();
|
||||
pm = context.getPackageManager();
|
||||
showSystem = Config.get(Config.Key.SHOW_SYSTEM_APP);
|
||||
AsyncTask.SERIAL_EXECUTOR.execute(this::loadApps);
|
||||
}
|
||||
|
||||
private static ViewGroup.MarginLayoutParams getMargins(RecyclerView.ViewHolder vh) {
|
||||
return (ViewGroup.MarginLayoutParams) vh.itemView.getLayoutParams();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSectionCount() {
|
||||
return showList.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(int section) {
|
||||
HideAppInfo app = showList.get(section);
|
||||
return app.expanded ? app.processList.size() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AppViewHolder onCreateSectionViewHolder(ViewGroup parent) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_hide_app, parent, false);
|
||||
AppViewHolder vh = new AppViewHolder(v);
|
||||
if (BOTTOM_MARGIN < 0)
|
||||
BOTTOM_MARGIN = getMargins(vh).bottomMargin;
|
||||
return vh;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProcessViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_hide_process, parent, false);
|
||||
return new ProcessViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindSectionViewHolder(AppViewHolder holder, int section) {
|
||||
HideAppInfo app = showList.get(section);
|
||||
holder.app_name.setText(app.name);
|
||||
holder.app_icon.setImageDrawable(app.info.loadIcon(pm));
|
||||
holder.package_name.setText(app.info.packageName);
|
||||
holder.checkBox.setOnStateChangedListener(null);
|
||||
holder.checkBox.setState(app.getState());
|
||||
holder.ex.setExpanded(app.expanded);
|
||||
|
||||
int index = getItemPosition(section, 0);
|
||||
holder.checkBox.setOnStateChangedListener((IndeterminateCheckBox box, @Nullable Boolean status) -> {
|
||||
if (status != null) {
|
||||
setHide(status, app);
|
||||
if (app.expanded)
|
||||
notifyItemRangeChanged(index, app.processList.size());
|
||||
}
|
||||
});
|
||||
|
||||
if (app.processList.size() > 1) {
|
||||
holder.arrow.setVisibility(View.VISIBLE);
|
||||
|
||||
holder.trigger.setOnClickListener((v) -> {
|
||||
if (app.expanded) {
|
||||
app.expanded = false;
|
||||
notifyItemRangeRemoved(index, app.processList.size());
|
||||
holder.ex.collapse();
|
||||
} else {
|
||||
app.expanded = true;
|
||||
notifyItemRangeInserted(index, app.processList.size());
|
||||
holder.ex.expand();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
holder.arrow.setVisibility(View.GONE);
|
||||
holder.trigger.setOnClickListener(null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindItemViewHolder(ProcessViewHolder holder, int section, int position) {
|
||||
HideAppInfo app = showList.get(section);
|
||||
HideProcessInfo target = app.processList.get(position);
|
||||
holder.process.setText(target.name);
|
||||
holder.checkbox.setOnCheckedChangeListener(null);
|
||||
holder.checkbox.setChecked(target.hidden);
|
||||
holder.checkbox.setOnCheckedChangeListener((v, checked) -> {
|
||||
setHide(checked, app, target);
|
||||
notifyItemChanged(getSectionPosition(section));
|
||||
});
|
||||
getMargins(holder).bottomMargin =
|
||||
position == app.processList.size() - 1 ? BOTTOM_MARGIN : 0;
|
||||
}
|
||||
|
||||
public void filter(String constraint) {
|
||||
AsyncTask.SERIAL_EXECUTOR.execute(() -> {
|
||||
Stream<HideAppInfo> s = StreamSupport.stream(fullList)
|
||||
.filter(this::systemFilter)
|
||||
.filter(t -> nameFilter(t, constraint));
|
||||
UiThreadHandler.run(() -> {
|
||||
showList = s.collect(Collectors.toList());
|
||||
notifyDataSetChanged();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void setShowSystem(boolean b) {
|
||||
showSystem = b;
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
AsyncTask.SERIAL_EXECUTOR.execute(this::loadApps);
|
||||
}
|
||||
|
||||
private void setHide(boolean add, HideAppInfo app) {
|
||||
if (add) {
|
||||
StreamSupport.stream(app.processList).forEach(p -> setHide(true, app, p));
|
||||
} else {
|
||||
if (StreamSupport.stream(app.processList)
|
||||
.anyMatch(p -> p.name.equals(SAFETYNET_PROCESS))) {
|
||||
StreamSupport.stream(app.processList).forEach(p -> setHide(false, app, p));
|
||||
} else {
|
||||
// Quick removal
|
||||
Shell.su("magiskhide --rm " + app.info.packageName).submit();
|
||||
StreamSupport.stream(app.processList).forEach(p -> p.hidden = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setHide(boolean add, HideAppInfo app, HideProcessInfo process) {
|
||||
// Don't remove SafetyNet
|
||||
if (!add && DEFAULT_HIDELIST.contains(process.name))
|
||||
return;
|
||||
Shell.su(Utils.fmt("magiskhide --%s %s %s", add ? "add" : "rm",
|
||||
app.info.packageName, process.name)).submit();
|
||||
process.hidden = add;
|
||||
}
|
||||
|
||||
private void addProcesses(Set<String> set, ComponentInfo[] infos) {
|
||||
if (infos != null)
|
||||
for (ComponentInfo info : infos)
|
||||
set.add(info.processName);
|
||||
}
|
||||
|
||||
private PackageInfo getPackageInfo(String pkg) {
|
||||
// Try super hard to get as much info as possible
|
||||
try {
|
||||
return pm.getPackageInfo(pkg,
|
||||
PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES |
|
||||
PackageManager.GET_RECEIVERS | PackageManager.GET_PROVIDERS);
|
||||
} catch (Exception e1) {
|
||||
try {
|
||||
PackageInfo info = pm.getPackageInfo(pkg, PackageManager.GET_ACTIVITIES);
|
||||
info.services = pm.getPackageInfo(pkg, PackageManager.GET_SERVICES).services;
|
||||
info.receivers = pm.getPackageInfo(pkg, PackageManager.GET_RECEIVERS).receivers;
|
||||
info.providers = pm.getPackageInfo(pkg, PackageManager.GET_PROVIDERS).providers;
|
||||
return info;
|
||||
} catch (Exception e2) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void loadApps() {
|
||||
hideList = StreamSupport.stream(Shell.su("magiskhide --ls").exec().getOut())
|
||||
.map(HideTarget::new)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
fullList = StreamSupport.stream(pm.getInstalledApplications(0))
|
||||
.filter(info -> !HIDE_BLACKLIST.contains(info.packageName) && info.enabled)
|
||||
.map(info -> {
|
||||
if (old_hide) {
|
||||
return new HideAppInfo(info, Sets.of(info.packageName));
|
||||
} else {
|
||||
Set<String> set = new ArraySet<>();
|
||||
PackageInfo pkg = getPackageInfo(info.packageName);
|
||||
if (pkg != null) {
|
||||
addProcesses(set, pkg.activities);
|
||||
addProcesses(set, pkg.services);
|
||||
addProcesses(set, pkg.receivers);
|
||||
addProcesses(set, pkg.providers);
|
||||
}
|
||||
if (set.isEmpty())
|
||||
return null;
|
||||
return new HideAppInfo(info, set);
|
||||
}
|
||||
}).filter(Objects::nonNull).sorted()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Event.trigger(false, Event.MAGISK_HIDE_DONE);
|
||||
}
|
||||
|
||||
// True if not system app or user already hidden it
|
||||
private boolean systemFilter(HideAppInfo target) {
|
||||
return showSystem || target.haveHidden() ||
|
||||
(target.info.flags & ApplicationInfo.FLAG_SYSTEM) == 0;
|
||||
}
|
||||
|
||||
private boolean contains(String s, String filter) {
|
||||
return s.toLowerCase().contains(filter);
|
||||
}
|
||||
|
||||
private boolean nameFilter(HideAppInfo target, String filter) {
|
||||
if (filter == null || filter.isEmpty())
|
||||
return true;
|
||||
filter = filter.toLowerCase();
|
||||
if (contains(target.name, filter))
|
||||
return true;
|
||||
for (HideProcessInfo p : target.processList) {
|
||||
if (contains(p.name, filter))
|
||||
return true;
|
||||
}
|
||||
return contains(target.info.packageName, filter);
|
||||
}
|
||||
|
||||
class HideAppInfo implements Comparable<HideAppInfo> {
|
||||
String name;
|
||||
ApplicationInfo info;
|
||||
List<HideProcessInfo> processList;
|
||||
boolean expanded;
|
||||
IndeterminateCheckBox.OnStateChangedListener listener;
|
||||
|
||||
HideAppInfo(ApplicationInfo appInfo, Set<String> set) {
|
||||
info = appInfo;
|
||||
name = Utils.getAppLabel(info, pm);
|
||||
expanded = false;
|
||||
processList = StreamSupport.stream(set)
|
||||
.map(process -> new HideProcessInfo(info.packageName, process))
|
||||
.sorted().collect(Collectors.toList());
|
||||
listener = (IndeterminateCheckBox box, @Nullable Boolean status) -> {
|
||||
if (status != null) {
|
||||
for (HideProcessInfo p : processList) {
|
||||
String cmd = Utils.fmt("magiskhide --%s %s %s",
|
||||
status ? "add" : "rm", info.packageName, p.name);
|
||||
Shell.su(cmd).submit();
|
||||
p.hidden = status;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(HideAppInfo o) {
|
||||
Comparator<HideAppInfo> c;
|
||||
c = Comparators.comparing(HideAppInfo::haveHidden);
|
||||
c = Comparators.reversed(c);
|
||||
c = Comparators.thenComparing(c, t -> t.name, String::compareToIgnoreCase);
|
||||
c = Comparators.thenComparing(c, t -> t.info.packageName);
|
||||
return c.compare(this, o);
|
||||
}
|
||||
|
||||
Boolean getState() {
|
||||
boolean all = true;
|
||||
boolean hidden = false;
|
||||
for (HideProcessInfo p : processList) {
|
||||
if (!p.hidden)
|
||||
all = false;
|
||||
else
|
||||
hidden = true;
|
||||
}
|
||||
if (all)
|
||||
return true;
|
||||
return hidden ? null : false;
|
||||
}
|
||||
|
||||
boolean haveHidden() {
|
||||
Boolean c = getState();
|
||||
return c == null ? true : c;
|
||||
}
|
||||
}
|
||||
|
||||
class HideProcessInfo implements Comparable<HideProcessInfo> {
|
||||
String name;
|
||||
boolean hidden;
|
||||
|
||||
HideProcessInfo(String pkg, String process) {
|
||||
this.name = process;
|
||||
for (HideTarget t : hideList) {
|
||||
if (t.pkg.equals(pkg) && t.process.equals(process)) {
|
||||
hidden = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(HideProcessInfo o) {
|
||||
Comparator<HideProcessInfo> c;
|
||||
c = Comparators.comparing((HideProcessInfo t) -> t.hidden);
|
||||
c = Comparators.reversed(c);
|
||||
c = Comparators.thenComparing(c, t -> t.name);
|
||||
return c.compare(this, o);
|
||||
}
|
||||
}
|
||||
|
||||
class HideTarget {
|
||||
String pkg;
|
||||
String process;
|
||||
|
||||
HideTarget(String line) {
|
||||
String[] split = line.split("\\|", 2);
|
||||
pkg = split[0];
|
||||
if (split.length == 2) {
|
||||
process = split[1];
|
||||
} else {
|
||||
// Backwards compatibility
|
||||
old_hide = true;
|
||||
process = pkg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AppViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
@BindView(R.id.app_icon) ImageView app_icon;
|
||||
@BindView(R.id.app_name) TextView app_name;
|
||||
@BindView(R.id.package_name) TextView package_name;
|
||||
@BindView(R.id.checkbox) IndeterminateCheckBox checkBox;
|
||||
@BindView(R.id.trigger) View trigger;
|
||||
@BindView(R.id.arrow) ImageView arrow;
|
||||
|
||||
Expandable ex;
|
||||
|
||||
AppViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
new ApplicationAdapter$AppViewHolder_ViewBinding(this, itemView);
|
||||
ex = new ArrowExpandable(new Expandable() {
|
||||
@Override
|
||||
protected void onExpand() {
|
||||
getMargins(AppViewHolder.this).bottomMargin = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCollapse() {
|
||||
getMargins(AppViewHolder.this).bottomMargin = BOTTOM_MARGIN;
|
||||
}
|
||||
}, arrow);
|
||||
}
|
||||
}
|
||||
|
||||
class ProcessViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
@BindView(R.id.process) TextView process;
|
||||
@BindView(R.id.checkbox) CheckBox checkbox;
|
||||
|
||||
ProcessViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
new ApplicationAdapter$ProcessViewHolder_ViewBinding(this, itemView);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package com.topjohnwu.magisk.adapters;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.topjohnwu.magisk.R;
|
||||
import com.topjohnwu.magisk.container.Module;
|
||||
import com.topjohnwu.magisk.uicomponents.SnackbarMaker;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import butterknife.BindView;
|
||||
|
||||
public class ModulesAdapter extends RecyclerView.Adapter<ModulesAdapter.ViewHolder> {
|
||||
|
||||
private final List<Module> mList;
|
||||
|
||||
public ModulesAdapter(List<Module> list) {
|
||||
mList = list;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_module, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(final ViewHolder holder, int position) {
|
||||
Context context = holder.itemView.getContext();
|
||||
final Module module = mList.get(position);
|
||||
|
||||
String version = module.getVersion();
|
||||
String author = module.getAuthor();
|
||||
String description = module.getDescription();
|
||||
String noInfo = context.getString(R.string.no_info_provided);
|
||||
|
||||
holder.title.setText(module.getName());
|
||||
holder.versionName.setText(TextUtils.isEmpty(version) ? noInfo : version);
|
||||
holder.author.setText(TextUtils.isEmpty(author) ? noInfo : context.getString(R.string.author, author));
|
||||
holder.description.setText(TextUtils.isEmpty(description) ? noInfo : description);
|
||||
|
||||
holder.checkBox.setOnCheckedChangeListener(null);
|
||||
holder.checkBox.setChecked(module.isEnabled());
|
||||
holder.checkBox.setOnCheckedChangeListener((v, isChecked) -> {
|
||||
int snack;
|
||||
if (isChecked) {
|
||||
module.removeDisableFile();
|
||||
snack = R.string.disable_file_removed;
|
||||
} else {
|
||||
module.createDisableFile();
|
||||
snack = R.string.disable_file_created;
|
||||
}
|
||||
SnackbarMaker.make(holder.itemView, snack, Snackbar.LENGTH_SHORT).show();
|
||||
});
|
||||
|
||||
holder.delete.setOnClickListener(v -> {
|
||||
boolean removed = module.willBeRemoved();
|
||||
int snack;
|
||||
if (removed) {
|
||||
module.deleteRemoveFile();
|
||||
snack = R.string.remove_file_deleted;
|
||||
} else {
|
||||
module.createRemoveFile();
|
||||
snack = R.string.remove_file_created;
|
||||
}
|
||||
SnackbarMaker.make(holder.itemView, snack, Snackbar.LENGTH_SHORT).show();
|
||||
updateDeleteButton(holder, module);
|
||||
});
|
||||
|
||||
if (module.isUpdated()) {
|
||||
holder.notice.setVisibility(View.VISIBLE);
|
||||
holder.notice.setText(R.string.update_file_created);
|
||||
holder.delete.setEnabled(false);
|
||||
} else {
|
||||
updateDeleteButton(holder, module);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDeleteButton(ViewHolder holder, Module module) {
|
||||
holder.notice.setVisibility(module.willBeRemoved() ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (module.willBeRemoved()) {
|
||||
holder.delete.setImageResource(R.drawable.ic_undelete);
|
||||
} else {
|
||||
holder.delete.setImageResource(R.drawable.ic_delete);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mList.size();
|
||||
}
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
@BindView(R.id.title) TextView title;
|
||||
@BindView(R.id.version_name) TextView versionName;
|
||||
@BindView(R.id.description) TextView description;
|
||||
@BindView(R.id.notice) TextView notice;
|
||||
@BindView(R.id.checkbox) CheckBox checkBox;
|
||||
@BindView(R.id.author) TextView author;
|
||||
@BindView(R.id.delete) ImageView delete;
|
||||
|
||||
ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
new ModulesAdapter$ViewHolder_ViewBinding(this, itemView);
|
||||
|
||||
if (!Shell.rootAccess()) {
|
||||
checkBox.setEnabled(false);
|
||||
delete.setEnabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
package com.topjohnwu.magisk.adapters;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.SwitchCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.topjohnwu.magisk.R;
|
||||
import com.topjohnwu.magisk.container.Policy;
|
||||
import com.topjohnwu.magisk.database.MagiskDB;
|
||||
import com.topjohnwu.magisk.dialogs.CustomAlertDialog;
|
||||
import com.topjohnwu.magisk.dialogs.FingerprintAuthDialog;
|
||||
import com.topjohnwu.magisk.uicomponents.ArrowExpandable;
|
||||
import com.topjohnwu.magisk.uicomponents.Expandable;
|
||||
import com.topjohnwu.magisk.uicomponents.ExpandableViewHolder;
|
||||
import com.topjohnwu.magisk.uicomponents.SnackbarMaker;
|
||||
import com.topjohnwu.magisk.utils.FingerprintHelper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import butterknife.BindView;
|
||||
|
||||
public class PolicyAdapter extends RecyclerView.Adapter<PolicyAdapter.ViewHolder> {
|
||||
|
||||
private List<Policy> policyList;
|
||||
private MagiskDB dbHelper;
|
||||
private PackageManager pm;
|
||||
private boolean[] expandList;
|
||||
|
||||
public PolicyAdapter(List<Policy> list, MagiskDB db, PackageManager pm) {
|
||||
policyList = list;
|
||||
expandList = new boolean[policyList.size()];
|
||||
dbHelper = db;
|
||||
this.pm = pm;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_policy, parent, false);
|
||||
return new ViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
Policy policy = policyList.get(position);
|
||||
|
||||
holder.settings.setExpanded(expandList[position]);
|
||||
holder.trigger.setOnClickListener(view -> {
|
||||
if (holder.settings.isExpanded()) {
|
||||
holder.settings.collapse();
|
||||
expandList[position] = false;
|
||||
} else {
|
||||
holder.settings.expand();
|
||||
expandList[position] = true;
|
||||
}
|
||||
});
|
||||
|
||||
holder.appName.setText(policy.appName);
|
||||
holder.packageName.setText(policy.packageName);
|
||||
holder.appIcon.setImageDrawable(policy.info.loadIcon(pm));
|
||||
|
||||
holder.notificationSwitch.setOnCheckedChangeListener(null);
|
||||
holder.loggingSwitch.setOnCheckedChangeListener(null);
|
||||
|
||||
holder.masterSwitch.setChecked(policy.policy == Policy.ALLOW);
|
||||
holder.notificationSwitch.setChecked(policy.notification);
|
||||
holder.loggingSwitch.setChecked(policy.logging);
|
||||
|
||||
holder.masterSwitch.setOnClickListener(v -> {
|
||||
boolean isChecked = holder.masterSwitch.isChecked();
|
||||
Runnable r = () -> {
|
||||
if ((isChecked && policy.policy == Policy.DENY) ||
|
||||
(!isChecked && policy.policy == Policy.ALLOW)) {
|
||||
policy.policy = isChecked ? Policy.ALLOW : Policy.DENY;
|
||||
String message = v.getContext().getString(
|
||||
isChecked ? R.string.su_snack_grant : R.string.su_snack_deny, policy.appName);
|
||||
SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show();
|
||||
dbHelper.updatePolicy(policy);
|
||||
}
|
||||
};
|
||||
if (FingerprintHelper.useFingerprint()) {
|
||||
holder.masterSwitch.setChecked(!isChecked);
|
||||
new FingerprintAuthDialog((Activity) v.getContext(), () -> {
|
||||
holder.masterSwitch.setChecked(isChecked);
|
||||
r.run();
|
||||
}).show();
|
||||
} else {
|
||||
r.run();
|
||||
}
|
||||
});
|
||||
holder.notificationSwitch.setOnCheckedChangeListener((v, isChecked) -> {
|
||||
if ((isChecked && !policy.notification) ||
|
||||
(!isChecked && policy.notification)) {
|
||||
policy.notification = isChecked;
|
||||
String message = v.getContext().getString(
|
||||
isChecked ? R.string.su_snack_notif_on : R.string.su_snack_notif_off, policy.appName);
|
||||
SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show();
|
||||
dbHelper.updatePolicy(policy);
|
||||
}
|
||||
});
|
||||
holder.loggingSwitch.setOnCheckedChangeListener((v, isChecked) -> {
|
||||
if ((isChecked && !policy.logging) ||
|
||||
(!isChecked && policy.logging)) {
|
||||
policy.logging = isChecked;
|
||||
String message = v.getContext().getString(
|
||||
isChecked ? R.string.su_snack_log_on : R.string.su_snack_log_off, policy.appName);
|
||||
SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show();
|
||||
dbHelper.updatePolicy(policy);
|
||||
}
|
||||
});
|
||||
holder.delete.setOnClickListener(v -> {
|
||||
DialogInterface.OnClickListener l = (dialog, which) -> {
|
||||
policyList.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
notifyItemRangeChanged(position, policyList.size());
|
||||
SnackbarMaker.make(holder.itemView, v.getContext().getString(R.string.su_snack_revoke, policy.appName),
|
||||
Snackbar.LENGTH_SHORT).show();
|
||||
dbHelper.deletePolicy(policy);
|
||||
};
|
||||
if (FingerprintHelper.useFingerprint()) {
|
||||
new FingerprintAuthDialog((Activity) v.getContext(),
|
||||
() -> l.onClick(null, 0)).show();
|
||||
} else {
|
||||
new CustomAlertDialog((Activity) v.getContext())
|
||||
.setTitle(R.string.su_revoke_title)
|
||||
.setMessage(v.getContext().getString(R.string.su_revoke_msg, policy.appName))
|
||||
.setPositiveButton(R.string.yes, l)
|
||||
.setNegativeButton(R.string.no_thanks, null)
|
||||
.setCancelable(true)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return policyList.size();
|
||||
}
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
@BindView(R.id.app_name) TextView appName;
|
||||
@BindView(R.id.package_name) TextView packageName;
|
||||
@BindView(R.id.app_icon) ImageView appIcon;
|
||||
@BindView(R.id.master_switch) SwitchCompat masterSwitch;
|
||||
@BindView(R.id.notification_switch) SwitchCompat notificationSwitch;
|
||||
@BindView(R.id.logging_switch) SwitchCompat loggingSwitch;
|
||||
@BindView(R.id.expand_layout) ViewGroup expandLayout;
|
||||
@BindView(R.id.arrow) ImageView arrow;
|
||||
@BindView(R.id.trigger) View trigger;
|
||||
@BindView(R.id.delete) ImageView delete;
|
||||
@BindView(R.id.more_info) ImageView moreInfo;
|
||||
|
||||
Expandable settings;
|
||||
|
||||
public ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
new PolicyAdapter$ViewHolder_ViewBinding(this, itemView);
|
||||
settings = new ArrowExpandable(new ExpandableViewHolder(expandLayout), arrow);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
package com.topjohnwu.magisk.adapters;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.SearchView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.topjohnwu.magisk.App;
|
||||
import com.topjohnwu.magisk.ClassMap;
|
||||
import com.topjohnwu.magisk.R;
|
||||
import com.topjohnwu.magisk.components.BaseActivity;
|
||||
import com.topjohnwu.magisk.components.DownloadModuleService;
|
||||
import com.topjohnwu.magisk.container.Module;
|
||||
import com.topjohnwu.magisk.container.Repo;
|
||||
import com.topjohnwu.magisk.database.RepoDatabaseHelper;
|
||||
import com.topjohnwu.magisk.dialogs.CustomAlertDialog;
|
||||
import com.topjohnwu.magisk.uicomponents.MarkDownWindow;
|
||||
import com.topjohnwu.magisk.utils.Event;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import butterknife.BindView;
|
||||
import java9.util.stream.StreamSupport;
|
||||
|
||||
public class ReposAdapter
|
||||
extends SectionedAdapter<ReposAdapter.SectionHolder, ReposAdapter.RepoHolder>
|
||||
implements Event.AutoListener, SearchView.OnQueryTextListener {
|
||||
|
||||
private static final int UPDATES = 0;
|
||||
private static final int INSTALLED = 1;
|
||||
private static final int OTHERS = 2;
|
||||
|
||||
private Map<String, Module> moduleMap;
|
||||
private RepoDatabaseHelper repoDB;
|
||||
private List<Pair<Integer, List<Repo>>> repoPairs;
|
||||
private List<Repo> fullList;
|
||||
private SearchView mSearch;
|
||||
|
||||
public ReposAdapter() {
|
||||
repoDB = App.self.repoDB;
|
||||
moduleMap = Collections.emptyMap();
|
||||
fullList = Collections.emptyList();
|
||||
repoPairs = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSectionCount() {
|
||||
return repoPairs.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(int section) {
|
||||
return repoPairs.get(section).second.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SectionHolder onCreateSectionViewHolder(ViewGroup parent) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.section, parent, false);
|
||||
return new SectionHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RepoHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_repo, parent, false);
|
||||
return new RepoHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindSectionViewHolder(SectionHolder holder, int section) {
|
||||
switch (repoPairs.get(section).first) {
|
||||
case UPDATES:
|
||||
holder.sectionText.setText(R.string.update_available);
|
||||
break;
|
||||
case INSTALLED:
|
||||
holder.sectionText.setText(R.string.installed);
|
||||
break;
|
||||
case OTHERS:
|
||||
holder.sectionText.setText(R.string.not_installed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindItemViewHolder(RepoHolder holder, int section, int position) {
|
||||
Repo repo = repoPairs.get(section).second.get(position);
|
||||
Context context = holder.itemView.getContext();
|
||||
|
||||
String name = repo.getName();
|
||||
String version = repo.getVersion();
|
||||
String author = repo.getAuthor();
|
||||
String description = repo.getDescription();
|
||||
String noInfo = context.getString(R.string.no_info_provided);
|
||||
|
||||
holder.title.setText(TextUtils.isEmpty(name) ? noInfo : name);
|
||||
holder.versionName.setText(TextUtils.isEmpty(version) ? noInfo : version);
|
||||
holder.author.setText(TextUtils.isEmpty(author) ? noInfo : context.getString(R.string.author, author));
|
||||
holder.description.setText(TextUtils.isEmpty(description) ? noInfo : description);
|
||||
holder.updateTime.setText(context.getString(R.string.updated_on, repo.getLastUpdateString()));
|
||||
|
||||
holder.infoLayout.setOnClickListener(v ->
|
||||
MarkDownWindow.show((BaseActivity) context, null, repo.getDetailUrl()));
|
||||
|
||||
holder.downloadImage.setOnClickListener(v -> {
|
||||
new CustomAlertDialog((BaseActivity) context)
|
||||
.setTitle(context.getString(R.string.repo_install_title, repo.getName()))
|
||||
.setMessage(context.getString(R.string.repo_install_msg, repo.getDownloadFilename()))
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(R.string.install, (d, i) ->
|
||||
startDownload((BaseActivity) context, repo, true))
|
||||
.setNeutralButton(R.string.download, (d, i) ->
|
||||
startDownload((BaseActivity) context, repo, false))
|
||||
.setNegativeButton(R.string.no_thanks, null)
|
||||
.show();
|
||||
});
|
||||
}
|
||||
|
||||
private void startDownload(BaseActivity activity, Repo repo, Boolean install) {
|
||||
activity.runWithExternalRW(() -> {
|
||||
Intent intent = new Intent(activity, ClassMap.get(DownloadModuleService.class))
|
||||
.putExtra("repo", repo).putExtra("install", install);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
activity.startForegroundService(intent);
|
||||
} else {
|
||||
activity.startService(intent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateLists() {
|
||||
if (mSearch != null)
|
||||
onQueryTextChange(mSearch.getQuery().toString());
|
||||
else
|
||||
onQueryTextChange("");
|
||||
}
|
||||
|
||||
private static boolean noCaseContain(String a, String b) {
|
||||
return a.toLowerCase().contains(b.toLowerCase());
|
||||
}
|
||||
|
||||
public void setSearchView(SearchView view) {
|
||||
mSearch = view;
|
||||
mSearch.setOnQueryTextListener(this);
|
||||
}
|
||||
|
||||
public void notifyDBChanged(boolean refresh) {
|
||||
try (Cursor c = repoDB.getRepoCursor()) {
|
||||
fullList = new ArrayList<>(c.getCount());
|
||||
while (c.moveToNext())
|
||||
fullList.add(new Repo(c));
|
||||
}
|
||||
if (refresh)
|
||||
updateLists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEvent(int event) {
|
||||
moduleMap = Event.getResult(event);
|
||||
updateLists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getListeningEvents() {
|
||||
return new int[] {Event.MODULE_LOAD_DONE};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String query) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(String s) {
|
||||
List<Repo> updates = new ArrayList<>();
|
||||
List<Repo> installed = new ArrayList<>();
|
||||
List<Repo> others = new ArrayList<>();
|
||||
|
||||
StreamSupport.stream(fullList)
|
||||
.filter(repo -> noCaseContain(repo.getName(), s)
|
||||
|| noCaseContain(repo.getAuthor(), s)
|
||||
|| noCaseContain(repo.getDescription(), s))
|
||||
.forEach(repo -> {
|
||||
Module module = moduleMap.get(repo.getId());
|
||||
if (module != null) {
|
||||
if (repo.getVersionCode() > module.getVersionCode()) {
|
||||
// Updates
|
||||
updates.add(repo);
|
||||
} else {
|
||||
installed.add(repo);
|
||||
}
|
||||
} else {
|
||||
others.add(repo);
|
||||
}
|
||||
});
|
||||
|
||||
repoPairs.clear();
|
||||
if (!updates.isEmpty())
|
||||
repoPairs.add(new Pair<>(UPDATES, updates));
|
||||
if (!installed.isEmpty())
|
||||
repoPairs.add(new Pair<>(INSTALLED, installed));
|
||||
if (!others.isEmpty())
|
||||
repoPairs.add(new Pair<>(OTHERS, others));
|
||||
|
||||
notifyDataSetChanged();
|
||||
return false;
|
||||
}
|
||||
|
||||
static class SectionHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
@BindView(R.id.section_text) TextView sectionText;
|
||||
|
||||
SectionHolder(View itemView) {
|
||||
super(itemView);
|
||||
new ReposAdapter$SectionHolder_ViewBinding(this, itemView);
|
||||
}
|
||||
}
|
||||
|
||||
static class RepoHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
@BindView(R.id.title) TextView title;
|
||||
@BindView(R.id.version_name) TextView versionName;
|
||||
@BindView(R.id.description) TextView description;
|
||||
@BindView(R.id.author) TextView author;
|
||||
@BindView(R.id.info_layout) View infoLayout;
|
||||
@BindView(R.id.download) ImageView downloadImage;
|
||||
@BindView(R.id.update_time) TextView updateTime;
|
||||
|
||||
RepoHolder(View itemView) {
|
||||
super(itemView);
|
||||
new ReposAdapter$RepoHolder_ViewBinding(this, itemView);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
package com.topjohnwu.magisk.adapters;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public abstract class SectionedAdapter<S extends RecyclerView.ViewHolder, C extends RecyclerView.ViewHolder>
|
||||
extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
|
||||
private static final int SECTION_TYPE = Integer.MIN_VALUE;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
final public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
if (viewType == SECTION_TYPE)
|
||||
return onCreateSectionViewHolder(parent);
|
||||
return onCreateItemViewHolder(parent, viewType);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
final public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
PositionInfo info = getPositionInfo(position);
|
||||
if (info.position == -1)
|
||||
onBindSectionViewHolder((S) holder, info.section);
|
||||
else
|
||||
onBindItemViewHolder((C) holder, info.section, info.position);
|
||||
}
|
||||
|
||||
@Override
|
||||
final public int getItemCount() {
|
||||
int size, sec;
|
||||
size = sec = getSectionCount();
|
||||
for (int i = 0; i < sec; ++i){
|
||||
size += getItemCount(i);
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
final public int getItemViewType(int position) {
|
||||
PositionInfo info = getPositionInfo(position);
|
||||
if (info.position == -1)
|
||||
return SECTION_TYPE;
|
||||
else
|
||||
return getItemViewType(info.section, info.position);
|
||||
}
|
||||
|
||||
public int getItemViewType(int section, int position) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected int getSectionPosition(int section) {
|
||||
return getItemPosition(section, -1);
|
||||
}
|
||||
|
||||
protected int getItemPosition(int section, int position) {
|
||||
int realPosition = 0;
|
||||
// Previous sections
|
||||
for (int i = 0; i < section; ++i) {
|
||||
realPosition += getItemCount(i) + 1;
|
||||
}
|
||||
// Current section
|
||||
realPosition += position + 1;
|
||||
return realPosition;
|
||||
}
|
||||
|
||||
private PositionInfo getPositionInfo(int position) {
|
||||
int section = 0;
|
||||
while (true) {
|
||||
if (position == 0)
|
||||
return new PositionInfo(section, -1);
|
||||
position -= 1;
|
||||
if (position < getItemCount(section))
|
||||
return new PositionInfo(section, position);
|
||||
position -= getItemCount(section++);
|
||||
}
|
||||
}
|
||||
|
||||
private static class PositionInfo {
|
||||
int section;
|
||||
int position;
|
||||
PositionInfo(int section, int position) {
|
||||
this.section = section;
|
||||
this.position = position;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract int getSectionCount();
|
||||
public abstract int getItemCount(int section);
|
||||
public abstract S onCreateSectionViewHolder(ViewGroup parent);
|
||||
public abstract C onCreateItemViewHolder(ViewGroup parent, int viewType);
|
||||
public abstract void onBindSectionViewHolder(S holder, int section);
|
||||
public abstract void onBindItemViewHolder(C holder, int section, int position);
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package com.topjohnwu.magisk.adapters;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public abstract class StringListAdapter<VH extends StringListAdapter.ViewHolder>
|
||||
extends RecyclerView.Adapter<VH> {
|
||||
|
||||
private RecyclerView rv;
|
||||
private boolean dynamic;
|
||||
private int screenWidth;
|
||||
private int txtWidth = -1;
|
||||
private int padding;
|
||||
|
||||
protected List<String> mList;
|
||||
|
||||
public StringListAdapter(List<String> list) {
|
||||
this(list, false);
|
||||
}
|
||||
|
||||
public StringListAdapter(List<String> list, boolean isDynamic) {
|
||||
mList = list;
|
||||
dynamic = isDynamic;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public final VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(itemLayoutRes(), parent, false);
|
||||
VH vh = createViewHolder(v);
|
||||
if (txtWidth < 0)
|
||||
onUpdateTextWidth(vh);
|
||||
return vh;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull VH holder, int position) {
|
||||
holder.txt.setText(mList.get(position));
|
||||
holder.txt.getLayoutParams().width = txtWidth;
|
||||
if (dynamic)
|
||||
onUpdateTextWidth(holder);
|
||||
}
|
||||
|
||||
protected void onUpdateTextWidth(VH vh) {
|
||||
if (txtWidth < 0) {
|
||||
txtWidth = screenWidth - padding;
|
||||
} else {
|
||||
vh.txt.measure(0, 0);
|
||||
int width = vh.txt.getMeasuredWidth();
|
||||
if (width > txtWidth) {
|
||||
txtWidth = width;
|
||||
vh.txt.getLayoutParams().width = txtWidth;
|
||||
}
|
||||
}
|
||||
if (rv.getWidth() != txtWidth + padding)
|
||||
rv.requestLayout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToRecyclerView(@NonNull RecyclerView rv) {
|
||||
DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||
((Activity) rv.getContext()).getWindowManager()
|
||||
.getDefaultDisplay().getMetrics(displayMetrics);
|
||||
screenWidth = displayMetrics.widthPixels;
|
||||
padding = rv.getPaddingStart() + rv.getPaddingEnd();
|
||||
this.rv = rv;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int getItemCount() {
|
||||
return mList.size();
|
||||
}
|
||||
|
||||
@LayoutRes
|
||||
protected abstract int itemLayoutRes();
|
||||
|
||||
@NonNull
|
||||
public abstract VH createViewHolder(@NonNull View v);
|
||||
|
||||
public static abstract class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
public TextView txt;
|
||||
|
||||
public ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
txt = itemView.findViewById(textViewResId());
|
||||
}
|
||||
|
||||
@IdRes
|
||||
protected abstract int textViewResId();
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
package com.topjohnwu.magisk.adapters;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.RotateAnimation;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.topjohnwu.magisk.R;
|
||||
import com.topjohnwu.magisk.container.SuLogEntry;
|
||||
import com.topjohnwu.magisk.database.MagiskDB;
|
||||
import com.topjohnwu.magisk.uicomponents.ExpandableViewHolder;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import butterknife.BindView;
|
||||
|
||||
public class SuLogAdapter extends SectionedAdapter<SuLogAdapter.SectionHolder, SuLogAdapter.LogViewHolder> {
|
||||
|
||||
private List<List<SuLogEntry>> logEntries;
|
||||
private Set<Integer> itemExpanded, sectionExpanded;
|
||||
private MagiskDB suDB;
|
||||
|
||||
public SuLogAdapter(MagiskDB db) {
|
||||
suDB = db;
|
||||
logEntries = Collections.emptyList();
|
||||
sectionExpanded = new HashSet<>();
|
||||
itemExpanded = new HashSet<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSectionCount() {
|
||||
return logEntries.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(int section) {
|
||||
return sectionExpanded.contains(section) ? logEntries.get(section).size() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SectionHolder onCreateSectionViewHolder(ViewGroup parent) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_sulog_group, parent, false);
|
||||
return new SectionHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LogViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_sulog, parent, false);
|
||||
return new LogViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindSectionViewHolder(SectionHolder holder, int section) {
|
||||
SuLogEntry entry = logEntries.get(section).get(0);
|
||||
holder.arrow.setRotation(sectionExpanded.contains(section) ? 180 : 0);
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
RotateAnimation rotate;
|
||||
if (sectionExpanded.contains(section)) {
|
||||
holder.arrow.setRotation(0);
|
||||
rotate = new RotateAnimation(180, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
|
||||
sectionExpanded.remove(section);
|
||||
notifyItemRangeRemoved(getItemPosition(section, 0), logEntries.get(section).size());
|
||||
} else {
|
||||
rotate = new RotateAnimation(0, 180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
|
||||
sectionExpanded.add(section);
|
||||
notifyItemRangeInserted(getItemPosition(section, 0), logEntries.get(section).size());
|
||||
}
|
||||
rotate.setDuration(300);
|
||||
rotate.setFillAfter(true);
|
||||
holder.arrow.setAnimation(rotate);
|
||||
});
|
||||
holder.date.setText(entry.getDateString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindItemViewHolder(LogViewHolder holder, int section, int position) {
|
||||
SuLogEntry entry = logEntries.get(section).get(position);
|
||||
int realIdx = getItemPosition(section, position);
|
||||
holder.expandable.setExpanded(itemExpanded.contains(realIdx));
|
||||
holder.itemView.setOnClickListener(view -> {
|
||||
if (holder.expandable.isExpanded()) {
|
||||
holder.expandable.collapse();
|
||||
itemExpanded.remove(realIdx);
|
||||
} else {
|
||||
holder.expandable.expand();
|
||||
itemExpanded.add(realIdx);
|
||||
}
|
||||
});
|
||||
Context context = holder.itemView.getContext();
|
||||
holder.appName.setText(entry.appName);
|
||||
holder.action.setText(entry.action ? R.string.grant : R.string.deny);
|
||||
holder.pid.setText(context.getString(R.string.pid, entry.fromPid));
|
||||
holder.uid.setText(context.getString(R.string.target_uid, entry.toUid));
|
||||
holder.command.setText(context.getString(R.string.command, entry.command));
|
||||
holder.time.setText(entry.getTimeString());
|
||||
}
|
||||
|
||||
public void notifyDBChanged() {
|
||||
logEntries = suDB.getLogs();
|
||||
itemExpanded.clear();
|
||||
sectionExpanded.clear();
|
||||
sectionExpanded.add(0);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
static class SectionHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
@BindView(R.id.date) TextView date;
|
||||
@BindView(R.id.arrow) ImageView arrow;
|
||||
|
||||
SectionHolder(View itemView) {
|
||||
super(itemView);
|
||||
new SuLogAdapter$SectionHolder_ViewBinding(this, itemView);
|
||||
}
|
||||
}
|
||||
|
||||
static class LogViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
@BindView(R.id.app_name) TextView appName;
|
||||
@BindView(R.id.action) TextView action;
|
||||
@BindView(R.id.time) TextView time;
|
||||
@BindView(R.id.pid) TextView pid;
|
||||
@BindView(R.id.uid) TextView uid;
|
||||
@BindView(R.id.cmd) TextView command;
|
||||
@BindView(R.id.expand_layout) ViewGroup expandLayout;
|
||||
|
||||
ExpandableViewHolder expandable;
|
||||
|
||||
LogViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
new SuLogAdapter$LogViewHolder_ViewBinding(this, itemView);
|
||||
expandable = new ExpandableViewHolder(expandLayout);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package com.topjohnwu.magisk.adapters;
|
||||
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class TabFragmentAdapter extends FragmentPagerAdapter {
|
||||
|
||||
private List<Fragment> fragmentList;
|
||||
private List<String> titleList;
|
||||
|
||||
public TabFragmentAdapter(FragmentManager fm) {
|
||||
super(fm);
|
||||
fragmentList = new ArrayList<>();
|
||||
titleList = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
return fragmentList.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return fragmentList.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
return titleList.get(position);
|
||||
}
|
||||
|
||||
public void addTab(Fragment fragment, String title) {
|
||||
fragmentList.add(fragment);
|
||||
titleList.add(title);
|
||||
}
|
||||
}
|
||||
111
app/src/main/java/com/topjohnwu/magisk/arch/BaseUIActivity.kt
Normal file
111
app/src/main/java/com/topjohnwu/magisk/arch/BaseUIActivity.kt
Normal file
@@ -0,0 +1,111 @@
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
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.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?) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setContentView() {
|
||||
binding = DataBindingUtil.setContentView<Binding>(this, layoutRes).also {
|
||||
it.setVariable(BR.viewModel, viewModel)
|
||||
it.lifecycleOwner = this
|
||||
}
|
||||
|
||||
ensureInsets()
|
||||
}
|
||||
|
||||
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,63 @@
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
|
||||
interface BaseUIComponent<VM : BaseViewModel>: LifecycleOwner {
|
||||
|
||||
val viewRoot: View
|
||||
val viewModel: VM
|
||||
|
||||
fun startObserveEvents() {
|
||||
viewModel.viewEvents.observe(this) {
|
||||
onEventDispatched(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun consumeSystemWindowInsets(insets: Insets): Insets? = null
|
||||
|
||||
/**
|
||||
* Called for all [ViewEvent]s published by associated viewModel.
|
||||
*/
|
||||
fun onEventDispatched(event: ViewEvent) {}
|
||||
|
||||
fun ensureInsets() {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(viewRoot) { _, insets ->
|
||||
insets.asInsets()
|
||||
.also { viewModel.insets = it }
|
||||
.let { consumeSystemWindowInsets(it) }
|
||||
?.subtractBy(insets) ?: insets
|
||||
}
|
||||
if (ViewCompat.isAttachedToWindow(viewRoot)) {
|
||||
ViewCompat.requestApplyInsets(viewRoot)
|
||||
} else {
|
||||
viewRoot.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
|
||||
override fun onViewDetachedFromWindow(v: View) = Unit
|
||||
override fun onViewAttachedToWindow(v: View) {
|
||||
ViewCompat.requestApplyInsets(v)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun WindowInsetsCompat.asInsets() = Insets.of(
|
||||
systemWindowInsetLeft,
|
||||
systemWindowInsetTop,
|
||||
systemWindowInsetRight,
|
||||
systemWindowInsetBottom
|
||||
)
|
||||
|
||||
private fun Insets.subtractBy(insets: WindowInsetsCompat) =
|
||||
WindowInsetsCompat.Builder(insets).setSystemWindowInsets(
|
||||
Insets.of(
|
||||
insets.systemWindowInsetLeft - left,
|
||||
insets.systemWindowInsetTop - top,
|
||||
insets.systemWindowInsetRight - right,
|
||||
insets.systemWindowInsetBottom - bottom
|
||||
)
|
||||
).build()
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
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 consumeSystemWindowInsets(insets: Insets) = insets
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
ensureInsets()
|
||||
}
|
||||
|
||||
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<*, *>)
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
package com.topjohnwu.magisk.components;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StyleRes;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.topjohnwu.magisk.App;
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.Const;
|
||||
import com.topjohnwu.magisk.R;
|
||||
import com.topjohnwu.magisk.utils.Event;
|
||||
import com.topjohnwu.magisk.utils.LocaleManager;
|
||||
|
||||
public abstract class BaseActivity extends AppCompatActivity implements Event.AutoListener {
|
||||
|
||||
public static final String INTENT_PERM = "perm_dialog";
|
||||
private static Runnable grantCallback;
|
||||
|
||||
static int[] EMPTY_INT_ARRAY = new int[0];
|
||||
|
||||
private ActivityResultListener activityResultListener;
|
||||
public App app = App.self;
|
||||
|
||||
static {
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getListeningEvents() {
|
||||
return EMPTY_INT_ARRAY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEvent(int event) {}
|
||||
|
||||
@StyleRes
|
||||
public int getDarkTheme() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(LocaleManager.getLocaleContext(base, LocaleManager.locale));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
Event.register(this);
|
||||
if (getDarkTheme() != -1 && (boolean) Config.get(Config.Key.DARK_THEME)) {
|
||||
setTheme(getDarkTheme());
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
String[] perms = getIntent().getStringArrayExtra(INTENT_PERM);
|
||||
if (perms != null)
|
||||
ActivityCompat.requestPermissions(this, perms, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
Event.unregister(this);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
protected void setFloating() {
|
||||
boolean isTablet = getResources().getBoolean(R.bool.isTablet);
|
||||
if (isTablet) {
|
||||
WindowManager.LayoutParams params = getWindow().getAttributes();
|
||||
params.height = getResources().getDimensionPixelSize(R.dimen.floating_height);
|
||||
params.width = getResources().getDimensionPixelSize(R.dimen.floating_width);
|
||||
params.alpha = 1.0f;
|
||||
params.dimAmount = 0.6f;
|
||||
params.flags |= 2;
|
||||
getWindow().setAttributes(params);
|
||||
setFinishOnTouchOutside(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void runWithExternalRW(Runnable callback) {
|
||||
runWithPermission(new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, callback);
|
||||
}
|
||||
|
||||
public void runWithPermission(String[] permissions, Runnable callback) {
|
||||
runWithPermission(this, permissions, callback);
|
||||
}
|
||||
|
||||
public static void runWithPermission(Context context, String[] permissions, Runnable callback) {
|
||||
boolean granted = true;
|
||||
for (String perm : permissions) {
|
||||
if (ContextCompat.checkSelfPermission(context, perm) != PackageManager.PERMISSION_GRANTED)
|
||||
granted = false;
|
||||
}
|
||||
if (granted) {
|
||||
Const.EXTERNAL_PATH.mkdirs();
|
||||
callback.run();
|
||||
} else {
|
||||
// Passed in context should be an activity if not granted, need to show dialog!
|
||||
if (context instanceof BaseActivity) {
|
||||
grantCallback = callback;
|
||||
ActivityCompat.requestPermissions((BaseActivity) context, permissions, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (activityResultListener != null)
|
||||
activityResultListener.onActivityResult(requestCode, resultCode, data);
|
||||
activityResultListener = null;
|
||||
}
|
||||
|
||||
public void startActivityForResult(Intent intent, int requestCode, ActivityResultListener listener) {
|
||||
activityResultListener = listener;
|
||||
super.startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
boolean grant = true;
|
||||
for (int result : grantResults) {
|
||||
if (result != PackageManager.PERMISSION_GRANTED)
|
||||
grant = false;
|
||||
}
|
||||
if (grant) {
|
||||
if (grantCallback != null) {
|
||||
grantCallback.run();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, R.string.no_rw_storage, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
grantCallback = null;
|
||||
}
|
||||
|
||||
public interface ActivityResultListener {
|
||||
void onActivityResult(int requestCode, int resultCode, Intent data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SharedPreferences getSharedPreferences(String name, int mode) {
|
||||
if (TextUtils.equals(name, getPackageName() + "_preferences"))
|
||||
return app.prefs;
|
||||
return super.getSharedPreferences(name, mode);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package com.topjohnwu.magisk.components;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.topjohnwu.magisk.App;
|
||||
import com.topjohnwu.magisk.utils.Event;
|
||||
|
||||
import butterknife.Unbinder;
|
||||
|
||||
public abstract class BaseFragment extends Fragment implements Event.AutoListener {
|
||||
|
||||
public App app = App.self;
|
||||
protected Unbinder unbinder = null;
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
Event.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
Event.unregister(this);
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
if (unbinder != null)
|
||||
unbinder.unbind();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startActivityForResult(Intent intent, int requestCode) {
|
||||
startActivityForResult(intent, requestCode, this::onActivityResult);
|
||||
}
|
||||
|
||||
public void startActivityForResult(Intent intent, int requestCode, BaseActivity.ActivityResultListener listener) {
|
||||
((BaseActivity) requireActivity()).startActivityForResult(intent, requestCode, listener);
|
||||
}
|
||||
|
||||
public void runWithPermission(String[] permissions, Runnable callback) {
|
||||
((BaseActivity) requireActivity()).runWithPermission(permissions,callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getListeningEvents() {
|
||||
return BaseActivity.EMPTY_INT_ARRAY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEvent(int event) {}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package com.topjohnwu.magisk.components;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceCategory;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceGroupAdapter;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.topjohnwu.magisk.App;
|
||||
import com.topjohnwu.magisk.R;
|
||||
import com.topjohnwu.magisk.utils.Event;
|
||||
|
||||
public abstract class BasePreferenceFragment extends PreferenceFragmentCompat
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener, Event.AutoListener {
|
||||
|
||||
public App app = App.self;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View v = super.onCreateView(inflater, container, savedInstanceState);
|
||||
app.prefs.registerOnSharedPreferenceChangeListener(this);
|
||||
Event.register(this);
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
app.prefs.unregisterOnSharedPreferenceChangeListener(this);
|
||||
Event.unregister(this);
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getListeningEvents() {
|
||||
return BaseActivity.EMPTY_INT_ARRAY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) {
|
||||
return new PreferenceGroupAdapter(preferenceScreen) {
|
||||
@SuppressLint("RestrictedApi")
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder, int position) {
|
||||
super.onBindViewHolder(holder, position);
|
||||
Preference preference = getItem(position);
|
||||
if (preference instanceof PreferenceCategory)
|
||||
setZeroPaddingToLayoutChildren(holder.itemView);
|
||||
else {
|
||||
View iconFrame = holder.itemView.findViewById(R.id.icon_frame);
|
||||
if (iconFrame != null) {
|
||||
iconFrame.setVisibility(preference.getIcon() == null ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void setZeroPaddingToLayoutChildren(View view) {
|
||||
if (!(view instanceof ViewGroup))
|
||||
return;
|
||||
ViewGroup viewGroup = (ViewGroup) view;
|
||||
int childCount = viewGroup.getChildCount();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
setZeroPaddingToLayoutChildren(viewGroup.getChildAt(i));
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
|
||||
viewGroup.setPaddingRelative(0, viewGroup.getPaddingTop(), viewGroup.getPaddingEnd(), viewGroup.getPaddingBottom());
|
||||
else
|
||||
viewGroup.setPadding(0, viewGroup.getPaddingTop(), viewGroup.getPaddingRight(), viewGroup.getPaddingBottom());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package com.topjohnwu.magisk.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Network;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.ListenableWorker;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class DelegateWorker {
|
||||
|
||||
private ListenableWorker worker;
|
||||
|
||||
@NonNull
|
||||
public abstract ListenableWorker.Result doWork();
|
||||
|
||||
public void onStopped() {}
|
||||
|
||||
public void setActualWorker(ListenableWorker w) {
|
||||
worker = w;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Context getApplicationContext() {
|
||||
return worker.getApplicationContext();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public UUID getId() {
|
||||
return worker.getId();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Data getInputData() {
|
||||
return worker.getInputData();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Set<String> getTags() {
|
||||
return worker.getTags();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@RequiresApi(24)
|
||||
public List<Uri> getTriggeredContentUris() {
|
||||
return worker.getTriggeredContentUris();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@RequiresApi(24)
|
||||
public List<String> getTriggeredContentAuthorities() {
|
||||
return worker.getTriggeredContentAuthorities();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@RequiresApi(28)
|
||||
public Network getNetwork() {
|
||||
return worker.getNetwork();
|
||||
}
|
||||
|
||||
public int getRunAttemptCount() {
|
||||
return worker.getRunAttemptCount();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@MainThread
|
||||
public ListenableFuture<ListenableWorker.Result> startWork() {
|
||||
return worker.startWork();
|
||||
}
|
||||
|
||||
public boolean isStopped() {
|
||||
return worker.isStopped();
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package com.topjohnwu.magisk.components;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.IBinder;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.topjohnwu.magisk.ClassMap;
|
||||
import com.topjohnwu.magisk.Const;
|
||||
import com.topjohnwu.magisk.FlashActivity;
|
||||
import com.topjohnwu.magisk.R;
|
||||
import com.topjohnwu.magisk.container.Repo;
|
||||
import com.topjohnwu.magisk.uicomponents.ProgressNotification;
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
import com.topjohnwu.net.Networking;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.ShellUtils;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
public class DownloadModuleService extends Service {
|
||||
|
||||
private boolean running = false;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (flags == 0 && running) {
|
||||
Utils.toast(R.string.dl_one_module, Toast.LENGTH_LONG);
|
||||
} else {
|
||||
running = true;
|
||||
Shell.EXECUTOR.execute(() -> {
|
||||
Repo repo = intent.getParcelableExtra("repo");
|
||||
boolean install = intent.getBooleanExtra("install", false);
|
||||
dlProcessInstall(repo, install);
|
||||
stopSelf();
|
||||
});
|
||||
}
|
||||
return START_REDELIVER_INTENT;
|
||||
}
|
||||
|
||||
private void dlProcessInstall(Repo repo, boolean install) {
|
||||
File output = new File(Const.EXTERNAL_PATH, repo.getDownloadFilename());
|
||||
ProgressNotification progress = new ProgressNotification(output.getName());
|
||||
startForeground(progress.hashCode(), progress.getNotification());
|
||||
try {
|
||||
InputStream in = Networking.get(repo.getZipUrl())
|
||||
.setDownloadProgressListener(progress)
|
||||
.execForInputStream().getResult();
|
||||
OutputStream out = new BufferedOutputStream(new FileOutputStream(output));
|
||||
processZip(in, out, repo.isNewInstaller());
|
||||
if (install) {
|
||||
progress.dismiss();
|
||||
Intent intent = new Intent(this, ClassMap.get(FlashActivity.class));
|
||||
intent.setData(Uri.fromFile(output))
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
progress.dlDone();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
progress.dlFail();
|
||||
}
|
||||
}
|
||||
|
||||
private void processZip(InputStream in, OutputStream out, boolean inject)
|
||||
throws IOException {
|
||||
try (ZipInputStream zin = new ZipInputStream(in);
|
||||
ZipOutputStream zout = new ZipOutputStream(out)) {
|
||||
|
||||
if (inject) {
|
||||
// Inject latest module-installer.sh as update-binary
|
||||
zout.putNextEntry(new ZipEntry("META-INF/"));
|
||||
zout.putNextEntry(new ZipEntry("META-INF/com/"));
|
||||
zout.putNextEntry(new ZipEntry("META-INF/com/google/"));
|
||||
zout.putNextEntry(new ZipEntry("META-INF/com/google/android/"));
|
||||
zout.putNextEntry(new ZipEntry("META-INF/com/google/android/update-binary"));
|
||||
try (InputStream update_bin = Networking.get(Const.Url.MODULE_INSTALLER)
|
||||
.execForInputStream().getResult()) {
|
||||
ShellUtils.pump(update_bin, zout);
|
||||
}
|
||||
zout.putNextEntry(new ZipEntry("META-INF/com/google/android/updater-script"));
|
||||
zout.write("#MAGISK\n".getBytes("UTF-8"));
|
||||
}
|
||||
|
||||
int off = -1;
|
||||
ZipEntry entry;
|
||||
while ((entry = zin.getNextEntry()) != null) {
|
||||
if (off < 0)
|
||||
off = entry.getName().indexOf('/') + 1;
|
||||
String path = entry.getName().substring(off);
|
||||
if (path.isEmpty())
|
||||
continue;
|
||||
if (inject && path.startsWith("META-INF"))
|
||||
continue;
|
||||
zout.putNextEntry(new ZipEntry(path));
|
||||
if (!entry.isDirectory())
|
||||
ShellUtils.pump(zin, zout);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package com.topjohnwu.magisk.components;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import com.topjohnwu.magisk.App;
|
||||
import com.topjohnwu.magisk.ClassMap;
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.Const;
|
||||
import com.topjohnwu.magisk.R;
|
||||
import com.topjohnwu.magisk.SuRequestActivity;
|
||||
import com.topjohnwu.magisk.container.Policy;
|
||||
import com.topjohnwu.magisk.uicomponents.Notifications;
|
||||
import com.topjohnwu.magisk.uicomponents.Shortcuts;
|
||||
import com.topjohnwu.magisk.utils.DownloadApp;
|
||||
import com.topjohnwu.magisk.utils.SuLogger;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
public class GeneralReceiver extends BroadcastReceiver {
|
||||
|
||||
private static SuLogger SU_LOGGER = new SuLogger() {
|
||||
@Override
|
||||
public String getMessage(Policy policy) {
|
||||
return App.self.getString(policy.policy == Policy.ALLOW ?
|
||||
R.string.su_allow_toast : R.string.su_deny_toast, policy.appName);
|
||||
}
|
||||
};
|
||||
|
||||
private String getPkg(Intent i) {
|
||||
return i.getData() == null ? "" : i.getData().getEncodedSchemeSpecificPart();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
App app = App.self;
|
||||
if (intent == null)
|
||||
return;
|
||||
String action = intent.getAction();
|
||||
if (action == null)
|
||||
return;
|
||||
switch (action) {
|
||||
case Intent.ACTION_REBOOT:
|
||||
case Intent.ACTION_BOOT_COMPLETED:
|
||||
String realAction = intent.getStringExtra("action");
|
||||
if (realAction == null)
|
||||
realAction = "boot_complete";
|
||||
switch (realAction) {
|
||||
case "request":
|
||||
Intent i = new Intent(app, ClassMap.get(SuRequestActivity.class))
|
||||
.putExtra("socket", intent.getStringExtra("socket"))
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
app.startActivity(i);
|
||||
break;
|
||||
case "log":
|
||||
SU_LOGGER.handleLogs(intent);
|
||||
break;
|
||||
case "notify":
|
||||
SU_LOGGER.handleNotify(intent);
|
||||
break;
|
||||
case "boot_complete":
|
||||
default:
|
||||
/* Devices with DTBO might want to patch dtbo.img.
|
||||
* However, that is not possible if Magisk is installed by
|
||||
* patching boot image with Magisk Manager and flashed via
|
||||
* fastboot, since at that time we do not have root.
|
||||
* Check for dtbo status every boot time, and prompt user
|
||||
* to reboot if dtbo wasn't patched and patched by Magisk Manager.
|
||||
* */
|
||||
Shell.su("mm_patch_dtbo").submit(result -> {
|
||||
if (result.isSuccess())
|
||||
Notifications.dtboPatched();
|
||||
});
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case Intent.ACTION_PACKAGE_REPLACED:
|
||||
// This will only work pre-O
|
||||
if (Config.get(Config.Key.SU_REAUTH)) {
|
||||
app.mDB.deletePolicy(getPkg(intent));
|
||||
}
|
||||
break;
|
||||
case Intent.ACTION_PACKAGE_FULLY_REMOVED:
|
||||
String pkg = getPkg(intent);
|
||||
app.mDB.deletePolicy(pkg);
|
||||
Shell.su("magiskhide --rm " + pkg).submit();
|
||||
break;
|
||||
case Intent.ACTION_LOCALE_CHANGED:
|
||||
Shortcuts.setup(context);
|
||||
break;
|
||||
case Const.Key.BROADCAST_MANAGER_UPDATE:
|
||||
Config.managerLink = intent.getStringExtra(Const.Key.INTENT_SET_LINK);
|
||||
DownloadApp.upgrade(intent.getStringExtra(Const.Key.INTENT_SET_NAME));
|
||||
break;
|
||||
case Const.Key.BROADCAST_REBOOT:
|
||||
Shell.su("/system/bin/reboot").submit();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.topjohnwu.magisk.components;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.ListenableWorker;
|
||||
|
||||
import com.topjohnwu.magisk.BuildConfig;
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.tasks.CheckUpdates;
|
||||
import com.topjohnwu.magisk.uicomponents.Notifications;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
public class UpdateCheckService extends DelegateWorker {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ListenableWorker.Result doWork() {
|
||||
Shell.getShell();
|
||||
CheckUpdates.checkNow(this::onCheckDone);
|
||||
return ListenableWorker.Result.success();
|
||||
}
|
||||
|
||||
private void onCheckDone() {
|
||||
if (BuildConfig.VERSION_CODE < Config.remoteManagerVersionCode) {
|
||||
Notifications.managerUpdate();
|
||||
} else if (Config.magiskVersionCode < Config.remoteMagiskVersionCode) {
|
||||
Notifications.magiskUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
package com.topjohnwu.magisk.container;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public abstract class BaseModule implements Comparable<BaseModule>, Parcelable {
|
||||
|
||||
private String mId, mName, mVersion, mAuthor, mDescription;
|
||||
private int mVersionCode = -1, minMagiskVersion = -1;
|
||||
|
||||
protected BaseModule() {
|
||||
mId = mName = mVersion = mAuthor = mDescription = "";
|
||||
}
|
||||
|
||||
protected BaseModule(Cursor c) {
|
||||
mId = nonNull(c.getString(c.getColumnIndex("id")));
|
||||
mName = nonNull(c.getString(c.getColumnIndex("name")));
|
||||
mVersion = nonNull(c.getString(c.getColumnIndex("version")));
|
||||
mVersionCode = c.getInt(c.getColumnIndex("versionCode"));
|
||||
mAuthor = nonNull(c.getString(c.getColumnIndex("author")));
|
||||
mDescription = nonNull(c.getString(c.getColumnIndex("description")));
|
||||
minMagiskVersion = c.getInt(c.getColumnIndex("minMagisk"));
|
||||
}
|
||||
|
||||
protected BaseModule(Parcel p) {
|
||||
mId = p.readString();
|
||||
mName = p.readString();
|
||||
mVersion = p.readString();
|
||||
mAuthor = p.readString();
|
||||
mDescription = p.readString();
|
||||
mVersionCode = p.readInt();
|
||||
minMagiskVersion = p.readInt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NonNull BaseModule module) {
|
||||
return this.getName().toLowerCase().compareTo(module.getName().toLowerCase());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(mId);
|
||||
dest.writeString(mName);
|
||||
dest.writeString(mVersion);
|
||||
dest.writeString(mAuthor);
|
||||
dest.writeString(mDescription);
|
||||
dest.writeInt(mVersionCode);
|
||||
dest.writeInt(minMagiskVersion);
|
||||
}
|
||||
|
||||
private String nonNull(String s) {
|
||||
return s == null ? "" : s;
|
||||
}
|
||||
|
||||
public ContentValues getContentValues() {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("id", mId);
|
||||
values.put("name", mName);
|
||||
values.put("version", mVersion);
|
||||
values.put("versionCode", mVersionCode);
|
||||
values.put("author", mAuthor);
|
||||
values.put("description", mDescription);
|
||||
values.put("minMagisk", minMagiskVersion);
|
||||
return values;
|
||||
}
|
||||
|
||||
protected void parseProps(List<String> props) { parseProps(props.toArray(new String[0])); }
|
||||
|
||||
protected void parseProps(String[] props) throws NumberFormatException {
|
||||
for (String line : props) {
|
||||
String[] prop = line.split("=", 2);
|
||||
if (prop.length != 2)
|
||||
continue;
|
||||
|
||||
String key = prop[0].trim();
|
||||
String value = prop[1].trim();
|
||||
if (key.isEmpty() || key.charAt(0) == '#')
|
||||
continue;
|
||||
|
||||
switch (key) {
|
||||
case "id":
|
||||
mId = value;
|
||||
break;
|
||||
case "name":
|
||||
mName = value;
|
||||
break;
|
||||
case "version":
|
||||
mVersion = value;
|
||||
break;
|
||||
case "versionCode":
|
||||
mVersionCode = Integer.parseInt(value);
|
||||
break;
|
||||
case "author":
|
||||
mAuthor = value;
|
||||
break;
|
||||
case "description":
|
||||
mDescription = value;
|
||||
break;
|
||||
case "minMagisk":
|
||||
case "template":
|
||||
minMagiskVersion = Integer.parseInt(value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return mName;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
mName = name;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return mVersion;
|
||||
}
|
||||
|
||||
public String getAuthor() {
|
||||
return mAuthor;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return mId;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
mId = id;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return mDescription;
|
||||
}
|
||||
|
||||
public int getVersionCode() {
|
||||
return mVersionCode;
|
||||
}
|
||||
|
||||
public int getMinMagiskVersion() {
|
||||
return minMagiskVersion;
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package com.topjohnwu.magisk.container;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
public class Module extends BaseModule {
|
||||
|
||||
private SuFile mRemoveFile, mDisableFile, mUpdateFile;
|
||||
private boolean mEnable, mRemove, mUpdated;
|
||||
|
||||
public Module(String path) {
|
||||
|
||||
try {
|
||||
parseProps(Shell.su("dos2unix < " + path + "/module.prop").exec().getOut());
|
||||
} catch (NumberFormatException ignored) {}
|
||||
|
||||
mRemoveFile = new SuFile(path, "remove");
|
||||
mDisableFile = new SuFile(path, "disable");
|
||||
mUpdateFile = new SuFile(path, "update");
|
||||
|
||||
if (getId().isEmpty()) {
|
||||
int sep = path.lastIndexOf('/');
|
||||
setId(path.substring(sep + 1));
|
||||
}
|
||||
|
||||
if (getName().isEmpty()) {
|
||||
setName(getId());
|
||||
}
|
||||
|
||||
mEnable = !mDisableFile.exists();
|
||||
mRemove = mRemoveFile.exists();
|
||||
mUpdated = mUpdateFile.exists();
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<Module> CREATOR = new Creator<Module>() {
|
||||
/* It won't be used at any place */
|
||||
@Override
|
||||
public Module createFromParcel(Parcel source) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Module[] newArray(int size) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
public void createDisableFile() {
|
||||
mEnable = !mDisableFile.createNewFile();
|
||||
}
|
||||
|
||||
public void removeDisableFile() {
|
||||
mEnable = mDisableFile.delete();
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return mEnable;
|
||||
}
|
||||
|
||||
public void createRemoveFile() {
|
||||
mRemove = mRemoveFile.createNewFile();
|
||||
}
|
||||
|
||||
public void deleteRemoveFile() {
|
||||
mRemove = !mRemoveFile.delete();
|
||||
}
|
||||
|
||||
public boolean willBeRemoved() {
|
||||
return mRemove;
|
||||
}
|
||||
|
||||
public boolean isUpdated() {
|
||||
return mUpdated;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package com.topjohnwu.magisk.container;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
|
||||
|
||||
public class Policy implements Comparable<Policy>{
|
||||
public static final int INTERACTIVE = 0;
|
||||
public static final int DENY = 1;
|
||||
public static final int ALLOW = 2;
|
||||
|
||||
public int uid, policy = INTERACTIVE;
|
||||
public long until;
|
||||
public boolean logging = true, notification = true;
|
||||
public String packageName, appName;
|
||||
public ApplicationInfo info;
|
||||
|
||||
public Policy(int uid, PackageManager pm) throws PackageManager.NameNotFoundException {
|
||||
String[] pkgs = pm.getPackagesForUid(uid);
|
||||
if (pkgs == null || pkgs.length == 0)
|
||||
throw new PackageManager.NameNotFoundException();
|
||||
this.uid = uid;
|
||||
packageName = pkgs[0];
|
||||
info = pm.getApplicationInfo(packageName, 0);
|
||||
appName = Utils.getAppLabel(info, pm);
|
||||
}
|
||||
|
||||
public Policy(ContentValues values, PackageManager pm) throws PackageManager.NameNotFoundException {
|
||||
uid = values.getAsInteger("uid");
|
||||
packageName = values.getAsString("package_name");
|
||||
policy = values.getAsInteger("policy");
|
||||
until = values.getAsInteger("until");
|
||||
logging = values.getAsInteger("logging") != 0;
|
||||
notification = values.getAsInteger("notification") != 0;
|
||||
info = pm.getApplicationInfo(packageName, 0);
|
||||
if (info.uid != uid)
|
||||
throw new PackageManager.NameNotFoundException();
|
||||
appName = info.loadLabel(pm).toString();
|
||||
}
|
||||
|
||||
public ContentValues getContentValues() {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("uid", uid);
|
||||
values.put("package_name", packageName);
|
||||
values.put("policy", policy);
|
||||
values.put("until", until);
|
||||
values.put("logging", logging ? 1 : 0);
|
||||
values.put("notification", notification ? 1 : 0);
|
||||
return values;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NonNull Policy policy) {
|
||||
return appName.toLowerCase().compareTo(policy.appName.toLowerCase());
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package com.topjohnwu.magisk.container;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.topjohnwu.magisk.Const;
|
||||
import com.topjohnwu.magisk.utils.Logger;
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
import com.topjohnwu.net.Networking;
|
||||
import com.topjohnwu.net.Request;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
public class Repo extends BaseModule {
|
||||
|
||||
private Date mLastUpdate;
|
||||
|
||||
public Repo(String id) {
|
||||
setId(id);
|
||||
}
|
||||
|
||||
public Repo(Cursor c) {
|
||||
super(c);
|
||||
mLastUpdate = new Date(c.getLong(c.getColumnIndex("last_update")));
|
||||
}
|
||||
|
||||
public Repo(Parcel p) {
|
||||
super(p);
|
||||
mLastUpdate = new Date(p.readLong());
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<Repo> CREATOR = new Parcelable.Creator<Repo>() {
|
||||
|
||||
@Override
|
||||
public Repo createFromParcel(Parcel source) {
|
||||
return new Repo(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Repo[] newArray(int size) {
|
||||
return new Repo[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
super.writeToParcel(dest, flags);
|
||||
dest.writeLong(mLastUpdate.getTime());
|
||||
}
|
||||
|
||||
public void update() throws IllegalRepoException {
|
||||
String props[] = Utils.dlString(getPropUrl()).split("\\n");
|
||||
try {
|
||||
parseProps(props);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalRepoException("Repo [" + getId() + "] parse error: " + e.getMessage());
|
||||
}
|
||||
|
||||
if (getVersionCode() < 0) {
|
||||
throw new IllegalRepoException("Repo [" + getId() + "] does not contain versionCode");
|
||||
}
|
||||
if (getMinMagiskVersion() < Const.MIN_MODULE_VER) {
|
||||
Logger.debug("Repo [" + getId() + "] is outdated");
|
||||
}
|
||||
}
|
||||
|
||||
public void update(Date lastUpdate) throws IllegalRepoException {
|
||||
mLastUpdate = lastUpdate;
|
||||
update();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentValues getContentValues() {
|
||||
ContentValues values = super.getContentValues();
|
||||
values.put("last_update", mLastUpdate.getTime());
|
||||
return values;
|
||||
}
|
||||
|
||||
public String getZipUrl() {
|
||||
return String.format(Const.Url.ZIP_URL, getId());
|
||||
}
|
||||
|
||||
public String getPropUrl() {
|
||||
return getFileUrl("module.prop");
|
||||
}
|
||||
|
||||
public String getDetailUrl() {
|
||||
return getFileUrl("README.md");
|
||||
}
|
||||
|
||||
public String getFileUrl(String file) {
|
||||
return String.format(Const.Url.FILE_URL, getId(), file);
|
||||
}
|
||||
|
||||
public boolean isNewInstaller() {
|
||||
try (Request install = Networking.get(getFileUrl("install.sh"))) {
|
||||
if (install.connect().isSuccess()) {
|
||||
// Double check whether config.sh exists
|
||||
try (Request config = Networking.get(getFileUrl("config.sh"))) {
|
||||
return !config.connect().isSuccess();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public String getLastUpdateString() {
|
||||
return DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(mLastUpdate);
|
||||
}
|
||||
|
||||
public Date getLastUpdate() {
|
||||
return mLastUpdate;
|
||||
}
|
||||
|
||||
public String getDownloadFilename() {
|
||||
return Utils.getLegalFilename(getName() + "-" + getVersion() + ".zip");
|
||||
}
|
||||
|
||||
public class IllegalRepoException extends Exception {
|
||||
IllegalRepoException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package com.topjohnwu.magisk.container;
|
||||
|
||||
import android.content.ContentValues;
|
||||
|
||||
import com.topjohnwu.magisk.utils.LocaleManager;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
public class SuLogEntry {
|
||||
|
||||
public int fromUid, toUid, fromPid;
|
||||
public String packageName, appName, command;
|
||||
public boolean action;
|
||||
public Date date;
|
||||
|
||||
public SuLogEntry(Policy policy) {
|
||||
fromUid = policy.uid;
|
||||
packageName = policy.packageName;
|
||||
appName = policy.appName;
|
||||
action = policy.policy == Policy.ALLOW;
|
||||
}
|
||||
|
||||
public SuLogEntry(ContentValues values) {
|
||||
fromUid = values.getAsInteger("from_uid");
|
||||
packageName = values.getAsString("package_name");
|
||||
appName = values.getAsString("app_name");
|
||||
fromPid = values.getAsInteger("from_pid");
|
||||
command = values.getAsString("command");
|
||||
toUid = values.getAsInteger("to_uid");
|
||||
action = values.getAsInteger("action") != 0;
|
||||
date = new Date(values.getAsLong("time"));
|
||||
}
|
||||
|
||||
public ContentValues getContentValues() {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("from_uid", fromUid);
|
||||
values.put("package_name", packageName);
|
||||
values.put("app_name", appName);
|
||||
values.put("from_pid", fromPid);
|
||||
values.put("command", command);
|
||||
values.put("to_uid", toUid);
|
||||
values.put("action", action ? 1 : 0);
|
||||
values.put("time", date.getTime());
|
||||
return values;
|
||||
}
|
||||
|
||||
public String getDateString() {
|
||||
return DateFormat.getDateInstance(DateFormat.MEDIUM, LocaleManager.locale).format(date);
|
||||
}
|
||||
|
||||
public String getTimeString() {
|
||||
return new SimpleDateFormat("h:mm a", LocaleManager.locale).format(date);
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package com.topjohnwu.magisk.container;
|
||||
|
||||
import org.kamranzafar.jtar.TarHeader;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class TarEntry extends org.kamranzafar.jtar.TarEntry {
|
||||
|
||||
public TarEntry(File file, String entryName) {
|
||||
super(file, entryName);
|
||||
}
|
||||
|
||||
/*
|
||||
* Workaround missing java.nio.file.attribute.PosixFilePermission
|
||||
* Simply just assign a default permission to the file
|
||||
* */
|
||||
|
||||
@Override
|
||||
public void extractTarHeader(String entryName) {
|
||||
int permissions = file.isDirectory() ? 000755 : 000644;
|
||||
header = TarHeader.createHeader(entryName, file.length(), file.lastModified() / 1000, file.isDirectory(), permissions);
|
||||
header.userName = new StringBuffer("");
|
||||
header.groupName = header.userName;
|
||||
}
|
||||
|
||||
/*
|
||||
* Rewrite the header to GNU format
|
||||
* */
|
||||
|
||||
@Override
|
||||
public void writeEntryHeader(byte[] outbuf) {
|
||||
super.writeEntryHeader(outbuf);
|
||||
|
||||
System.arraycopy("ustar \0".getBytes(), 0, outbuf, 257, TarHeader.USTAR_MAGICLEN);
|
||||
getOctalBytes(header.mode, outbuf, 100, TarHeader.MODELEN);
|
||||
getOctalBytes(header.userId, outbuf, 108, TarHeader.UIDLEN);
|
||||
getOctalBytes(header.groupId, outbuf, 116, TarHeader.GIDLEN);
|
||||
getOctalBytes(header.size, outbuf, 124, TarHeader.SIZELEN);
|
||||
getOctalBytes(header.modTime, outbuf, 136, TarHeader.MODTIMELEN);
|
||||
Arrays.fill(outbuf, 148, 148 + TarHeader.CHKSUMLEN, (byte) ' ');
|
||||
Arrays.fill(outbuf, 329, 329 + TarHeader.USTAR_DEVLEN, (byte) '\0');
|
||||
Arrays.fill(outbuf, 337, 337 + TarHeader.USTAR_DEVLEN, (byte) '\0');
|
||||
|
||||
// Recalculate checksum
|
||||
getOctalBytes(computeCheckSum(outbuf), outbuf, 148, TarHeader.CHKSUMLEN);
|
||||
}
|
||||
|
||||
/*
|
||||
* Proper octal to ASCII conversion
|
||||
* */
|
||||
|
||||
private void getOctalBytes(long value, byte[] buf, int offset, int length) {
|
||||
int idx = length - 1;
|
||||
|
||||
buf[offset + idx] = 0;
|
||||
--idx;
|
||||
|
||||
for (long val = value; idx >= 0; --idx) {
|
||||
buf[offset + idx] = (byte) ((byte) '0' + (byte) (val & 7));
|
||||
val = val >> 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package com.topjohnwu.magisk.container;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ValueSortedMap<K, V extends Comparable<? super V>> extends HashMap<K, V> {
|
||||
|
||||
private List<V> sorted = new ArrayList<>();
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Collection<V> values() {
|
||||
if (sorted.isEmpty()) {
|
||||
sorted.addAll(super.values());
|
||||
Collections.sort(sorted);
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V put(K key, V value) {
|
||||
sorted.clear();
|
||||
return super.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putAll(Map<? extends K, ? extends V> m) {
|
||||
sorted.clear();
|
||||
super.putAll(m);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V remove(Object key) {
|
||||
sorted.clear();
|
||||
return super.remove(key);
|
||||
}
|
||||
}
|
||||
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.IOException
|
||||
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) {
|
||||
try {
|
||||
context.contentResolver.openInputStream(Provider.PREFS_URI(pkg))?.use {
|
||||
prefs.edit { parsePrefs(it) }
|
||||
}
|
||||
} catch (e: IOException) {}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
79
app/src/main/java/com/topjohnwu/magisk/core/Const.kt
Normal file
79
app/src/main/java/com/topjohnwu/magisk/core/Const.kt
Normal file
@@ -0,0 +1,79 @@
|
||||
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
|
||||
const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
||||
const val MAGISK_INSTALL_LOG_FILENAME = "magisk_install_log_%s.log"
|
||||
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 HIDDEN_PKG = "hidden_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 hiddenPackage = intent.getStringExtra(Const.Key.HIDDEN_PKG)
|
||||
|
||||
Config.load(hiddenPackage ?: APPLICATION_ID)
|
||||
handleRepackage(hiddenPackage)
|
||||
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()
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user