You've already forked Magisk
mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-09-06 06:36:58 +00:00
Compare commits
2532 Commits
manager-v6
...
v21.1
| 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 | ||
|
|
3eae9494ce | ||
|
|
be7e737253 | ||
|
|
b6eb912dba | ||
|
|
8049b08918 | ||
|
|
d1fa5be210 | ||
|
|
fdbb1af02c | ||
|
|
c85a5cae88 | ||
|
|
649ef53409 | ||
|
|
e784212283 | ||
|
|
66eb1078fe | ||
|
|
1c09b3642f | ||
|
|
d08b1a6639 | ||
|
|
10f50e2401 | ||
|
|
8e9a7b25a1 | ||
|
|
4859ee2da9 | ||
|
|
b45db44ad9 | ||
|
|
e25ce63872 | ||
|
|
162eeaa0a6 | ||
|
|
f36ce905aa | ||
|
|
8ac3aaf36c | ||
|
|
a199b0ace1 | ||
|
|
2f2108e4e8 | ||
|
|
f5f7fd9132 | ||
|
|
f9ae4ab475 | ||
|
|
8de03eef3f | ||
|
|
8df942f96e | ||
|
|
9bb2243b56 | ||
|
|
db06038548 | ||
|
|
ecb33d3176 | ||
|
|
eae1c17738 | ||
|
|
ea55532e33 | ||
|
|
2a40cb60a9 | ||
|
|
d371d017b7 | ||
|
|
1d9359d563 | ||
|
|
945f88105f | ||
|
|
957feca626 | ||
|
|
c0447009db | ||
|
|
8893cbd64a | ||
|
|
f0240b1f06 | ||
|
|
e476c18c99 | ||
|
|
a1b5185ecb | ||
|
|
981e90cc32 | ||
|
|
da0a72e8b0 | ||
|
|
b7e2e972c7 | ||
|
|
650b2ce6b1 | ||
|
|
ecf3d30349 | ||
|
|
15ddd0e284 | ||
|
|
18ac6b270f | ||
|
|
3e35de9b39 | ||
|
|
1e24c72c11 | ||
|
|
217564963d | ||
|
|
f2f4649ab0 | ||
|
|
4395ffec5f | ||
|
|
9a7a26407a | ||
|
|
5072a67807 | ||
|
|
dce0b6c05a | ||
|
|
a4a661bf34 | ||
|
|
771e500468 | ||
|
|
7e3ff03109 | ||
|
|
a1827fd680 | ||
|
|
9ce334feac | ||
|
|
ed11e0bff6 | ||
|
|
5111086637 | ||
|
|
20f204810e | ||
|
|
4581354e7a | ||
|
|
faf4d76388 | ||
|
|
a46e255709 | ||
|
|
63e2bbb4d1 | ||
|
|
c3dabae237 | ||
|
|
f1abcbb7fb | ||
|
|
70efddb90f | ||
|
|
f24a5dfd45 | ||
|
|
081074ad9d | ||
|
|
ab0cc78d2c | ||
|
|
de5c902fdb | ||
|
|
cf65169c99 | ||
|
|
745865ee53 | ||
|
|
c134fb1939 | ||
|
|
0204d05316 | ||
|
|
c345633d80 | ||
|
|
a57a94040e | ||
|
|
1bde78d121 | ||
|
|
bbd014ad1b | ||
|
|
1287372f5a | ||
|
|
d2cb638fcd | ||
|
|
bbe4b69c8d | ||
|
|
7f08c06943 | ||
|
|
8f4a6415cd | ||
|
|
0442d6d509 | ||
|
|
a3fc6d2a27 | ||
|
|
7db05ac927 | ||
|
|
8bed93b3c5 | ||
|
|
915b49014f | ||
|
|
c699f30831 | ||
|
|
3e73e3a906 | ||
|
|
32c65d8a88 | ||
|
|
a49328edd3 | ||
|
|
9a15365a57 | ||
|
|
82c864d57e | ||
|
|
6226f875ff | ||
|
|
370015a853 | ||
|
|
6597b7adc0 | ||
|
|
4e53ebfe44 | ||
|
|
04ef1e6405 | ||
|
|
b278d07b05 | ||
|
|
6c3896079d | ||
|
|
e73fa57d54 | ||
|
|
eaa9c7e2a0 | ||
|
|
14ae29d907 | ||
|
|
e8f35b02ca | ||
|
|
dee3c3e7ba | ||
|
|
d8cd2031c7 | ||
|
|
7203e7df5c | ||
|
|
b51feffe80 | ||
|
|
b1afd554fc | ||
|
|
885e3c574b | ||
|
|
05dd5f3396 | ||
|
|
ec3c43faf1 | ||
|
|
e72c6685ed | ||
|
|
99d6bd8efc | ||
|
|
4c8587a9f2 | ||
|
|
54a8a05dae | ||
|
|
164a99681b | ||
|
|
0eef4eacd6 | ||
|
|
5764f0c839 | ||
|
|
f28e425542 | ||
|
|
d1a4f046e9 | ||
|
|
2ce1dc4afe | ||
|
|
37ac249fd7 | ||
|
|
f152bea8d8 | ||
|
|
7b089b888a | ||
|
|
68f0e1fe39 | ||
|
|
8032bd0bac | ||
|
|
0c227f2917 | ||
|
|
c9fa8118d1 | ||
|
|
63b18246d8 | ||
|
|
16ec37a226 | ||
|
|
bd4e5bfc1a | ||
|
|
621fd0ee29 | ||
|
|
6ca8db2f0c | ||
|
|
ea129fb206 | ||
|
|
3356d7b6ff | ||
|
|
c84023bdc2 | ||
|
|
86f778c0aa | ||
|
|
defbbdfe21 | ||
|
|
0f46493477 | ||
|
|
340bac7e42 | ||
|
|
1d3ce9fef1 | ||
|
|
4a398642b8 | ||
|
|
9c89e56c56 | ||
|
|
267c59b1f1 | ||
|
|
2ab17204c6 | ||
|
|
75939047d1 | ||
|
|
2d7f130d2c | ||
|
|
f7ae72a36c | ||
|
|
391783e268 | ||
|
|
6f12c08204 | ||
|
|
cb8fe70734 | ||
|
|
69d10b747a | ||
|
|
da3394f34e | ||
|
|
b4c2a9f49f | ||
|
|
7cee77f57a | ||
|
|
f28bd1972f | ||
|
|
0f92d1de1b | ||
|
|
e59c5c8780 | ||
|
|
86d8026301 | ||
|
|
d67b827338 | ||
|
|
660e0dc09a | ||
|
|
3ebc886f8a | ||
|
|
5b54ef840a | ||
|
|
c08b0d4974 | ||
|
|
7d652afd87 | ||
|
|
0f61c627b1 | ||
|
|
7126648404 | ||
|
|
4a5e2dc9c7 | ||
|
|
10613686ed | ||
|
|
17ab55115a | ||
|
|
2708c74ebe | ||
|
|
50ff11405f | ||
|
|
31a27838f5 | ||
|
|
2f1b0fe57f | ||
|
|
692f893e1f | ||
|
|
14aa6041ec | ||
|
|
fb55fe184c | ||
|
|
6412bfc7b5 | ||
|
|
3c56f38229 | ||
|
|
f4f2274c60 | ||
|
|
19ee189468 | ||
|
|
a19c7215d2 | ||
|
|
8b84039f1f | ||
|
|
9430dbb96c | ||
|
|
4872df6a46 | ||
|
|
014105f0a0 | ||
|
|
b106d1c501 | ||
|
|
99db0672b4 | ||
|
|
d584360de2 | ||
|
|
4eed6794c7 | ||
|
|
c66cabd80f | ||
|
|
24da3485bd | ||
|
|
7384d2d330 | ||
|
|
e5940168fe | ||
|
|
6855baf0f8 | ||
|
|
dfd16e8fef | ||
|
|
98a36819bc | ||
|
|
de8bc9ca9d | ||
|
|
c137f2de4f | ||
|
|
0f55fcafe8 | ||
|
|
ed027ec3ee | ||
|
|
b3fd79cbb9 | ||
|
|
ed4df87b57 | ||
|
|
1321f097b8 | ||
|
|
cfa28f0c4a | ||
|
|
ab47b717b1 | ||
|
|
65ebb0d2f8 | ||
|
|
49640ce03a | ||
|
|
e05cdc83f3 | ||
|
|
992a9ea2f9 | ||
|
|
228351fc13 | ||
|
|
8a5b6f2b86 | ||
|
|
71ecbb3af3 | ||
|
|
5746614ccf | ||
|
|
3a422c3f15 | ||
|
|
b3242322fd | ||
|
|
9826640ae6 | ||
|
|
1f5267204b | ||
|
|
ed25e1bbd6 | ||
|
|
c8491d008f | ||
|
|
08e3405394 | ||
|
|
4ebfa07186 | ||
|
|
6698c189fc | ||
|
|
f0639390aa | ||
|
|
bbdfed2d5a | ||
|
|
7f4daa2c50 | ||
|
|
baf9b67b35 | ||
|
|
caf73b0b36 | ||
|
|
acf87c2794 | ||
|
|
7f5f6b54fb | ||
|
|
a08eb8a446 | ||
|
|
b31402766e | ||
|
|
9ab3143bf0 | ||
|
|
81a0cddb9e | ||
|
|
f620ac769f | ||
|
|
dc91041edd | ||
|
|
6ee08b6717 | ||
|
|
5a2cd2ac84 | ||
|
|
2bd8448aaa | ||
|
|
2360adb592 | ||
|
|
c7301a5161 | ||
|
|
72270825c1 | ||
|
|
1e94f0a094 | ||
|
|
e39d2567ea | ||
|
|
949136c92a | ||
|
|
9f456a9b19 | ||
|
|
4cf6ba25ca | ||
|
|
093f971896 | ||
|
|
df38a9da71 | ||
|
|
813814c54a | ||
|
|
68cb32f375 | ||
|
|
93c9590b0f | ||
|
|
619d979c39 | ||
|
|
c30faad838 | ||
|
|
bea0de4980 | ||
|
|
ef5a490415 | ||
|
|
fa404285be | ||
|
|
0e526258ff | ||
|
|
56d2fb9a3b | ||
|
|
7c82690852 | ||
|
|
62acc17e42 | ||
|
|
9fbe5895b7 | ||
|
|
6bbe0f07d4 | ||
|
|
bd3e0b9336 | ||
|
|
699debdaca | ||
|
|
70eba568af | ||
|
|
bb7560e441 | ||
|
|
43c0cac52f | ||
|
|
4b4aa148a9 | ||
|
|
c9c90c4e7f | ||
|
|
99093e9a4c | ||
|
|
2cf33d635d | ||
|
|
d6abaf846e | ||
|
|
4b88131977 | ||
|
|
4520f46a57 | ||
|
|
348d47076a | ||
|
|
6e7b90a184 | ||
|
|
28d7a7a6d2 | ||
|
|
da13b5dbf2 | ||
|
|
a60710e3bb | ||
|
|
7d2a2b9983 | ||
|
|
749df5dacd | ||
|
|
af88b7c807 | ||
|
|
4091687733 | ||
|
|
cfb0a3ba2a | ||
|
|
6c4d082f35 | ||
|
|
262185046a | ||
|
|
da9d00be7d | ||
|
|
454abc388b | ||
|
|
3e9174deed | ||
|
|
4df1047b07 | ||
|
|
60f69feaff | ||
|
|
5df426380d | ||
|
|
976c299657 | ||
|
|
18ab6b51fd | ||
|
|
4be8bd4d18 | ||
|
|
075bc4a6d5 | ||
|
|
1c61feb368 | ||
|
|
d32b788988 | ||
|
|
7565ea2787 | ||
|
|
9275975b2c | ||
|
|
b3e0d5ba58 | ||
|
|
841dee94c6 | ||
|
|
71638191ee | ||
|
|
9d6851cbbd | ||
|
|
d633d05803 | ||
|
|
45d7879d7b | ||
|
|
4a8375355c | ||
|
|
d3ebd763a2 | ||
|
|
b7f69238a1 | ||
|
|
118a9f224e | ||
|
|
a44dc8df37 | ||
|
|
abf19aad74 | ||
|
|
d73127b175 | ||
|
|
00f4242fa4 | ||
|
|
f6a4510659 | ||
|
|
33215424d8 | ||
|
|
6094bc9210 | ||
|
|
a8cd9b3aa9 | ||
|
|
a189dec1c8 | ||
|
|
858216796a | ||
|
|
f24342f117 | ||
|
|
50b55a77de | ||
|
|
fdf167db11 | ||
|
|
a4f8bd4ee0 | ||
|
|
3e4c12cf56 | ||
|
|
03c39e692a | ||
|
|
ab63b0e970 | ||
|
|
6ea42a35a9 | ||
|
|
d366dfc72b | ||
|
|
85042fbe25 | ||
|
|
23e5188422 | ||
|
|
93ee0c8798 | ||
|
|
aa88486f59 | ||
|
|
1d9c441038 | ||
|
|
928c56bda2 | ||
|
|
bc6f37eecc | ||
|
|
ffebff8cab | ||
|
|
6d6bd89d6b | ||
|
|
0a64a7e5d4 | ||
|
|
586488af48 | ||
|
|
7b9a45f1a8 | ||
|
|
4ff70aefac | ||
|
|
d63b5d7014 | ||
|
|
ab5f6bf901 | ||
|
|
04088b34a2 | ||
|
|
3edcd2004e | ||
|
|
7bd52d0245 | ||
|
|
1df65940b9 | ||
|
|
d9ace35c3e | ||
|
|
1fe92cee6f | ||
|
|
267868c3b0 | ||
|
|
6d27eb7f64 | ||
|
|
2e10fa494f | ||
|
|
039be65a89 | ||
|
|
570ecd9987 | ||
|
|
a575180475 | ||
|
|
07d1a20f3d | ||
|
|
76491cbb31 | ||
|
|
bf7d6ddcb2 | ||
|
|
44b969e0b6 | ||
|
|
176e470497 | ||
|
|
646a10d9bf | ||
|
|
52137fd64f | ||
|
|
3ccac8c3b8 | ||
|
|
0be158afa1 | ||
|
|
e6942e0122 | ||
|
|
496b22026f | ||
|
|
bb2df02dff | ||
|
|
4c850ecc31 | ||
|
|
da9c6f6e23 | ||
|
|
58ba0b0b4e | ||
|
|
1d0b87246a | ||
|
|
920b60da19 | ||
|
|
523e66294b | ||
|
|
23f8f35098 | ||
|
|
8d210b5e37 | ||
|
|
3c6c0e6700 | ||
|
|
01344c451f | ||
|
|
2c42c79482 | ||
|
|
75c2cfe7bf | ||
|
|
6c6eeb3f28 | ||
|
|
31053e0cd0 | ||
|
|
aad9aced18 | ||
|
|
dd2c9eeafe | ||
|
|
740d76bc42 | ||
|
|
45f4f5afd9 | ||
|
|
e875de3e98 | ||
|
|
fd7786633d | ||
|
|
bce9cfa39a | ||
|
|
ff3d66a661 | ||
|
|
006d28abd5 | ||
|
|
59b1e63bdf | ||
|
|
eab74ef06b | ||
|
|
89837de9b0 | ||
|
|
b245931c79 | ||
|
|
fd5e42698c | ||
|
|
c75512ba6e | ||
|
|
a22e7aa0b1 | ||
|
|
020dd97f99 | ||
|
|
e9882d9702 | ||
|
|
fd4a27dbf2 | ||
|
|
9c63e31da6 | ||
|
|
c91f809eba | ||
|
|
a54eaf5371 | ||
|
|
8032bd4bb9 | ||
|
|
ea1beec2f7 | ||
|
|
05f2f6820e | ||
|
|
0f5f15a5ce | ||
|
|
14ac37e8a5 | ||
|
|
1fae89cbb6 | ||
|
|
8b4008798f | ||
|
|
fd4faf59b8 | ||
|
|
109891d668 | ||
|
|
bdea796121 | ||
|
|
c8813c05c9 | ||
|
|
1cff08ce5d | ||
|
|
a868118f6f | ||
|
|
e5c62f5750 | ||
|
|
4084e8790b | ||
|
|
8d931dd773 | ||
|
|
25d6366297 | ||
|
|
08cd5b81d1 | ||
|
|
5d3a8a5b1a | ||
|
|
79b84da4b8 | ||
|
|
68b07c5913 | ||
|
|
553db9124d | ||
|
|
b495f37299 | ||
|
|
1915547594 | ||
|
|
03de29164a | ||
|
|
86d8b50547 | ||
|
|
7b04386162 | ||
|
|
07bfdf3e4d | ||
|
|
d510224e2a | ||
|
|
e658f9297d | ||
|
|
2b502e9a0f | ||
|
|
59141f9bbe | ||
|
|
3af66b72f2 | ||
|
|
422c24bd68 | ||
|
|
f0f87c8eb9 | ||
|
|
80dad54119 | ||
|
|
56a76df28e | ||
|
|
ee2c801fe0 | ||
|
|
fc314cc248 | ||
|
|
fe231a4c80 | ||
|
|
2e2bbe0a7f | ||
|
|
857e6e8345 | ||
|
|
3402981ada | ||
|
|
f401e577e5 | ||
|
|
0241a50c6f | ||
|
|
2a2e1236fc | ||
|
|
9b170f2b4f | ||
|
|
51e9ff59de | ||
|
|
2977dbcded | ||
|
|
ac60b51035 | ||
|
|
4c2f33a089 | ||
|
|
3b071116ac | ||
|
|
a9f265a591 | ||
|
|
5b62fc8103 | ||
|
|
0598f5f89a | ||
|
|
f723427b8b | ||
|
|
f69a004c1c | ||
|
|
1134b18a8b | ||
|
|
2e4aa507f7 | ||
|
|
5fb96cdcf4 | ||
|
|
e8cba3524e | ||
|
|
7e6b5363f1 | ||
|
|
29457a1d28 | ||
|
|
731455f164 | ||
|
|
b01a8cace6 | ||
|
|
72db5b4fac | ||
|
|
ddfd42994e | ||
|
|
2a9ff9c5ef | ||
|
|
6d49f05356 | ||
|
|
85a5e62e36 | ||
|
|
e67965a381 | ||
|
|
ec4723096f | ||
|
|
762b678d24 | ||
|
|
38fcc57bbf | ||
|
|
c8c57c74cc | ||
|
|
0784448c69 | ||
|
|
de0064af47 | ||
|
|
baae1fc84f | ||
|
|
2ab999f4ca | ||
|
|
c9f390d6e0 | ||
|
|
af05922ecc | ||
|
|
299edbf3ab | ||
|
|
c8abed9d48 | ||
|
|
3622c49ce1 | ||
|
|
0462e9a7d9 | ||
|
|
c3a6091908 | ||
|
|
ab5fedda0b | ||
|
|
ba70269398 | ||
|
|
77fd5fa7de | ||
|
|
ab74290fe3 | ||
|
|
3aad9d8166 | ||
|
|
572e078d87 | ||
|
|
ee4548230b | ||
|
|
96b93bd876 | ||
|
|
927f69fe30 | ||
|
|
7e9ad5927a | ||
|
|
6d6b07865e | ||
|
|
376e7977f0 | ||
|
|
83ae66daea | ||
|
|
89e0be0099 | ||
|
|
ef40c1212e | ||
|
|
3a2a2a4ffa | ||
|
|
9592a69986 | ||
|
|
89be07e1f2 | ||
|
|
c61c3ae0e9 | ||
|
|
817350c8c5 | ||
|
|
3603b7c82b | ||
|
|
5743c72cca | ||
|
|
4cdd66ceff | ||
|
|
d3947d2cfa | ||
|
|
07718b994a | ||
|
|
ef9d463bd7 | ||
|
|
8745c7884e | ||
|
|
b6965105b7 | ||
|
|
3d269fe8be | ||
|
|
be5f00aa1a | ||
|
|
59ba350f34 | ||
|
|
803c5377a6 | ||
|
|
7c12bf7fa1 | ||
|
|
ca35a9681f | ||
|
|
9fe5f37337 | ||
|
|
0742901cd2 | ||
|
|
5e4d2dedbe | ||
|
|
411ea56a3e | ||
|
|
cda57dd4b4 | ||
|
|
4351de503f | ||
|
|
6339ba6bfb | ||
|
|
ef6677f43d | ||
|
|
a7824af5a8 | ||
|
|
1eb7d7b7a8 | ||
|
|
11c33d4447 | ||
|
|
b8a3cc8b60 | ||
|
|
27c688252d | ||
|
|
3e2afd4b1d | ||
|
|
f45b0686d2 | ||
|
|
1f3f881f81 | ||
|
|
ceb51bb14f | ||
|
|
3e22573d8d | ||
|
|
79418a3767 | ||
|
|
40d4683de1 | ||
|
|
79e5b54ec7 | ||
|
|
bd81923f2f | ||
|
|
69560b8ad7 | ||
|
|
dc413e7b73 | ||
|
|
7fc00c446b | ||
|
|
2efc423cf8 | ||
|
|
8ec3086cdd | ||
|
|
5fc7079023 | ||
|
|
bfbd254be7 | ||
|
|
f8ea43466c | ||
|
|
75ab1fa570 | ||
|
|
bf4a46d57c | ||
|
|
1046dd5eda | ||
|
|
f9e32a119a | ||
|
|
dbb8b8a439 | ||
|
|
2a65c3dc8f | ||
|
|
f17ec9e9d7 | ||
|
|
675d6d8328 | ||
|
|
6dc9ccad75 | ||
|
|
6add02702b | ||
|
|
958d6377e3 | ||
|
|
9954154ca2 | ||
|
|
4ecbf8c12c | ||
|
|
fc8a3c5fb4 | ||
|
|
01e7dff1a0 | ||
|
|
018c0064cd | ||
|
|
c2b016370b | ||
|
|
fc791b4371 | ||
|
|
f76bb009f4 | ||
|
|
8a1292b295 | ||
|
|
d7d80d3fc1 | ||
|
|
41b01003fd | ||
|
|
6557070ae1 | ||
|
|
e7e580e177 | ||
|
|
dd9ddd2019 | ||
|
|
74aae523ba | ||
|
|
48c40f9516 | ||
|
|
e0e7674715 | ||
|
|
e1a65276b9 | ||
|
|
469adc85ad | ||
|
|
e1b181ca4e | ||
|
|
a4f0fbf8b7 | ||
|
|
190cdaddf8 | ||
|
|
5c4ba13839 | ||
|
|
e62630cf3e | ||
|
|
36fe7846c0 | ||
|
|
8d150dd67a | ||
|
|
506df00d81 | ||
|
|
a9121fa28f | ||
|
|
d5a56d9e85 | ||
|
|
acf7c0c665 | ||
|
|
619d48c97a | ||
|
|
2cb198c38c | ||
|
|
e8e39e0f3c | ||
|
|
37860181d4 | ||
|
|
d119dd9a0c | ||
|
|
09ef19f7ec | ||
|
|
6a06c92fa6 | ||
|
|
58ae596b0f | ||
|
|
f1ca21678d | ||
|
|
d7eeef2c8a | ||
|
|
4f626897f2 | ||
|
|
b127e01845 | ||
|
|
2118beeb23 | ||
|
|
5020cd1bbf | ||
|
|
cce636224c | ||
|
|
60b3b8ddce | ||
|
|
41446ec9ba | ||
|
|
df8b047bca | ||
|
|
12ced52012 | ||
|
|
1d53335ae5 | ||
|
|
971a50d290 | ||
|
|
36dd9106a8 | ||
|
|
0a4ee3ffc7 |
8
.gitattributes
vendored
8
.gitattributes
vendored
@@ -11,8 +11,14 @@
|
|||||||
*.bat text eol=crlf
|
*.bat text eol=crlf
|
||||||
|
|
||||||
# Denote all files that are truly binary and should not be modified.
|
# Denote all files that are truly binary and should not be modified.
|
||||||
chromeos/** binary
|
tools/** binary
|
||||||
*.jar binary
|
*.jar binary
|
||||||
*.exe binary
|
*.exe binary
|
||||||
*.apk binary
|
*.apk binary
|
||||||
*.png binary
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.ttf binary
|
||||||
|
|
||||||
|
# Help GitHub detect languages
|
||||||
|
native/jni/external/** linguist-vendored
|
||||||
|
native/jni/systemproperties/** linguist-language=C++
|
||||||
|
|||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -2,10 +2,8 @@ out
|
|||||||
*.zip
|
*.zip
|
||||||
*.jks
|
*.jks
|
||||||
*.apk
|
*.apk
|
||||||
config.prop
|
/config.prop
|
||||||
|
/update.sh
|
||||||
# Manually dumped jars
|
|
||||||
snet/libs
|
|
||||||
|
|
||||||
# Built binaries
|
# Built binaries
|
||||||
native/out
|
native/out
|
||||||
@@ -17,4 +15,3 @@ native/out
|
|||||||
/.idea
|
/.idea
|
||||||
/build
|
/build
|
||||||
/captures
|
/captures
|
||||||
.externalNativeBuild
|
|
||||||
|
|||||||
9
.gitmodules
vendored
9
.gitmodules
vendored
@@ -19,3 +19,12 @@
|
|||||||
[submodule "nanopb"]
|
[submodule "nanopb"]
|
||||||
path = native/jni/external/nanopb
|
path = native/jni/external/nanopb
|
||||||
url = https://github.com/nanopb/nanopb.git
|
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
|
||||||
|
|||||||
108
README.MD
108
README.MD
@@ -1,41 +1,89 @@
|
|||||||
# Magisk
|

|
||||||
[XDA Announcement Thread](https://forum.xda-developers.com/apps/magisk/official-magisk-v7-universal-systemless-t3473445)
|
|
||||||
|
|
||||||
## Building Environment Requirements
|

|
||||||
1. Python 3.5+: run `build.py` script
|

|
||||||
2. Java Development Kit (JDK) 8: Compile Magisk Manager and sign zips
|
|
||||||
3. Latest Android SDK: set `ANDROID_HOME` environment variable to the path to Android SDK
|
|
||||||
4. Android NDK: Install NDK along with SDK (`$ANDROID_HOME/ndk-bundle`), or optionally specify a custom path `ANDROID_NDK_HOME`
|
|
||||||
5. (Windows Only) Python package Colorama: Install with `pip install colorama`, used for ANSI color codes
|
|
||||||
|
|
||||||
## Building Notes and Instructions
|
## Introduction
|
||||||
1. Clone sources with submodules: `git clone --recurse-submodules https://github.com/topjohnwu/Magisk.git`
|
|
||||||
2. 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.
|
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.
|
||||||
3. Set configurations in `config.prop`. A sample file `config.prop.sample` is provided as an example.
|
|
||||||
4. 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`
|
Here are some feature highlights:
|
||||||
5. 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).
|
|
||||||
|
- **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
|
## 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!** Please **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.
|
||||||
|
|
||||||
## Documentation
|
**Only bug reports from Canary builds will be accepted.**
|
||||||
[Link to Documentation](docs/README.MD)
|
|
||||||
|
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 and Development
|
||||||
|
|
||||||
|
- 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).
|
||||||
|
|
||||||
|
## Translation Contributions
|
||||||
|
|
||||||
|
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`
|
||||||
|
|
||||||
|
Translate each and place them in the respective locations (`[module]/src/main/res/values-[lang]/strings.xml`).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
```
|
Magisk, including all git submodules are free software:
|
||||||
Magisk, including all git submodules are free software:
|
you can redistribute it and/or modify it under the terms of the
|
||||||
you can redistribute it and/or modify it under the terms of the
|
GNU General Public License as published by the Free Software Foundation,
|
||||||
GNU General Public License as published by the Free Software Foundation,
|
either version 3 of the License, or (at your option) any later version.
|
||||||
either version 3 of the License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
```
|
|
||||||
|
|||||||
1
app/.gitignore
vendored
1
app/.gitignore
vendored
@@ -6,7 +6,6 @@
|
|||||||
app/release
|
app/release
|
||||||
*.hprof
|
*.hprof
|
||||||
.externalNativeBuild/
|
.externalNativeBuild/
|
||||||
src/full/res/raw/util_functions.sh
|
|
||||||
public.certificate.x509.pem
|
public.certificate.x509.pem
|
||||||
private.key.pk8
|
private.key.pk8
|
||||||
*.apk
|
*.apk
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
# Magisk Manager
|
|
||||||
This repo is no longer an independent component. It is merged into the [Magisk Project](https://github.com/topjohnwu/Magisk).
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
The default (English) strings are mainly in `src/full/res/values/strings.xml`; some are scattered in `src/main/res/values/strings.xml` and `src/stub/res/values/strings.xml`.
|
|
||||||
Translations are highly appreciated via pull requests here on Github.
|
|
||||||
Place translated XMLs in the corresponding locale folder.
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
apply plugin: 'com.android.application'
|
|
||||||
|
|
||||||
def configProps = new Properties()
|
|
||||||
def configPath = project.hasProperty('configPath') ? project.configPath : rootProject.file('config.prop')
|
|
||||||
configProps.load(new FileInputStream(configPath))
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
|
||||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "com.topjohnwu.magisk"
|
|
||||||
minSdkVersion 21
|
|
||||||
targetSdkVersion rootProject.ext.compileSdkVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
signingConfigs {
|
|
||||||
config {
|
|
||||||
storeFile rootProject.file('release-key.jks')
|
|
||||||
storePassword configProps['keyStorePass']
|
|
||||||
keyAlias configProps['keyAlias']
|
|
||||||
keyPassword configProps['keyPass']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
debug {
|
|
||||||
// If keystore exists, sign the APK with custom signature
|
|
||||||
if (signingConfigs.config.storeFile.exists())
|
|
||||||
signingConfig signingConfigs.config
|
|
||||||
}
|
|
||||||
release {
|
|
||||||
minifyEnabled true
|
|
||||||
shrinkResources true
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
||||||
signingConfig signingConfigs.config
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
flavorDimensions "mode"
|
|
||||||
|
|
||||||
productFlavors {
|
|
||||||
full {
|
|
||||||
versionName configProps['appVersion']
|
|
||||||
versionCode configProps['appVersionCode'] as Integer
|
|
||||||
javaCompileOptions {
|
|
||||||
annotationProcessorOptions {
|
|
||||||
argument('butterknife.debuggable', 'false')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stub {
|
|
||||||
versionCode 1
|
|
||||||
versionName "stub"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
dexOptions {
|
|
||||||
preDexLibraries true
|
|
||||||
javaMaxHeapSize "2g"
|
|
||||||
}
|
|
||||||
lintOptions {
|
|
||||||
disable 'MissingTranslation'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
|
||||||
fullImplementation project(':utils')
|
|
||||||
implementation "androidx.core:core:${rootProject.ext.androidXVersion}"
|
|
||||||
fullImplementation "androidx.preference:preference:${rootProject.ext.androidXVersion}"
|
|
||||||
fullImplementation "androidx.recyclerview:recyclerview:${rootProject.ext.androidXVersion}"
|
|
||||||
fullImplementation "androidx.cardview:cardview:${rootProject.ext.androidXVersion}"
|
|
||||||
fullImplementation "com.google.android.material:material:${rootProject.ext.androidXVersion}"
|
|
||||||
fullImplementation 'com.github.topjohnwu:libsu:2.0.2'
|
|
||||||
fullImplementation 'com.atlassian.commonmark:commonmark:0.11.0'
|
|
||||||
fullImplementation 'org.kamranzafar:jtar:2.3'
|
|
||||||
}
|
|
||||||
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")
|
||||||
|
}
|
||||||
38
app/proguard-rules.pro
vendored
38
app/proguard-rules.pro
vendored
@@ -16,20 +16,36 @@
|
|||||||
# public *;
|
# public *;
|
||||||
#}
|
#}
|
||||||
|
|
||||||
# BouncyCastle
|
# Kotlin
|
||||||
-keep class org.bouncycastle.jcajce.provider.asymmetric.rsa.**SHA1** { *; }
|
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
||||||
-keep class org.bouncycastle.jcajce.provider.asymmetric.RSA** { *; }
|
public static void checkExpressionValueIsNotNull(...);
|
||||||
-keep class org.bouncycastle.jcajce.provider.digest.SHA1** { *; }
|
public static void checkNotNullExpressionValue(...);
|
||||||
-dontwarn javax.naming.**
|
public static void checkReturnedValueIsNotNull(...);
|
||||||
|
public static void checkFieldIsNotNull(...);
|
||||||
|
public static void checkParameterIsNotNull(...);
|
||||||
|
}
|
||||||
|
|
||||||
# Snet extention
|
# Stubs
|
||||||
-keepclassmembers class com.topjohnwu.magisk.utils.ISafetyNetHelper { *; }
|
-keep class a.* { *; }
|
||||||
|
|
||||||
# Strip logging
|
# Snet
|
||||||
-assumenosideeffects class com.topjohnwu.magisk.utils.Logger {
|
-keepclassmembers class com.topjohnwu.magisk.ui.safetynet.SafetyNetHelper { *; }
|
||||||
public *** debug(...);
|
-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Strip Timber verbose and debug logging
|
||||||
|
-assumenosideeffects class timber.log.Timber.Tree {
|
||||||
|
public void v(**);
|
||||||
|
public void d(**);
|
||||||
}
|
}
|
||||||
|
|
||||||
# Excessive obfuscation
|
# Excessive obfuscation
|
||||||
-repackageclasses 'a'
|
-repackageclasses
|
||||||
-allowaccessmodification
|
-allowaccessmodification
|
||||||
|
|
||||||
|
# QOL
|
||||||
|
-dontnote **
|
||||||
|
-dontwarn com.caverock.androidsvg.**
|
||||||
|
-dontwarn ru.noties.markwon.**
|
||||||
|
|||||||
0
snet/.gitignore → app/shared/.gitignore
vendored
0
snet/.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"))))
|
||||||
|
}
|
||||||
25
app/shared/proguard-rules.pro
vendored
Normal file
25
app/shared/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
341
app/shared/src/main/java/com/topjohnwu/magisk/FileProvider.java
Normal file
341
app/shared/src/main/java/com/topjohnwu/magisk/FileProvider.java
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
package com.topjohnwu.magisk;
|
||||||
|
|
||||||
|
import android.content.ContentProvider;
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.ProviderInfo;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.MatrixCursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Environment;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.provider.OpenableColumns;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modified from androidx.core.content.FileProvider
|
||||||
|
*/
|
||||||
|
public class FileProvider extends ContentProvider {
|
||||||
|
private static final String[] COLUMNS = {
|
||||||
|
OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE };
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
|
||||||
|
if (info.exported) {
|
||||||
|
throw new SecurityException("Provider must not be exported");
|
||||||
|
}
|
||||||
|
if (!info.grantUriPermissions) {
|
||||||
|
throw new SecurityException("Provider must grant uri permissions");
|
||||||
|
}
|
||||||
|
|
||||||
|
mStrategy = getPathStrategy(context, info.authority);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Uri getUriForFile(Context context, String authority,
|
||||||
|
File file) {
|
||||||
|
final PathStrategy strategy = getPathStrategy(context, authority);
|
||||||
|
return strategy.getUriForFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Cursor query(Uri uri, String[] projection, String selection,
|
||||||
|
String[] selectionArgs,
|
||||||
|
String sortOrder) {
|
||||||
|
|
||||||
|
final File file = mStrategy.getFileForUri(uri);
|
||||||
|
|
||||||
|
if (projection == null) {
|
||||||
|
projection = COLUMNS;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] cols = new String[projection.length];
|
||||||
|
Object[] values = new Object[projection.length];
|
||||||
|
int i = 0;
|
||||||
|
for (String col : projection) {
|
||||||
|
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
|
||||||
|
cols[i] = OpenableColumns.DISPLAY_NAME;
|
||||||
|
values[i++] = file.getName();
|
||||||
|
} else if (OpenableColumns.SIZE.equals(col)) {
|
||||||
|
cols[i] = OpenableColumns.SIZE;
|
||||||
|
values[i++] = file.length();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cols = copyOf(cols, i);
|
||||||
|
values = copyOf(values, i);
|
||||||
|
|
||||||
|
final MatrixCursor cursor = new MatrixCursor(cols, 1);
|
||||||
|
cursor.addRow(values);
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getType(Uri uri) {
|
||||||
|
|
||||||
|
final File file = mStrategy.getFileForUri(uri);
|
||||||
|
|
||||||
|
final int lastDot = file.getName().lastIndexOf('.');
|
||||||
|
if (lastDot >= 0) {
|
||||||
|
final String extension = file.getName().substring(lastDot + 1);
|
||||||
|
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||||
|
if (mime != null) {
|
||||||
|
return mime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
|
||||||
|
final File file = mStrategy.getFileForUri(uri);
|
||||||
|
return file.delete() ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ParcelFileDescriptor openFile(Uri uri, String mode)
|
||||||
|
throws FileNotFoundException {
|
||||||
|
|
||||||
|
final File file = mStrategy.getFileForUri(uri);
|
||||||
|
final int fileMode = modeToMode(mode);
|
||||||
|
return ParcelFileDescriptor.open(file, fileMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PathStrategy getPathStrategy(Context context, String authority) {
|
||||||
|
PathStrategy strat;
|
||||||
|
synchronized (sCache) {
|
||||||
|
strat = sCache.get(authority);
|
||||||
|
if (strat == null) {
|
||||||
|
strat = createPathStrategy(context, authority);
|
||||||
|
sCache.put(authority, strat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strat;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PathStrategy createPathStrategy(Context context, String authority) {
|
||||||
|
final SimplePathStrategy strat = new SimplePathStrategy(authority);
|
||||||
|
|
||||||
|
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], "."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
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<>();
|
||||||
|
|
||||||
|
SimplePathStrategy(String authority) {
|
||||||
|
mAuthority = authority;
|
||||||
|
}
|
||||||
|
|
||||||
|
void addRoot(String name, File root) {
|
||||||
|
if (TextUtils.isEmpty(name)) {
|
||||||
|
throw new IllegalArgumentException("Name must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
root = root.getCanonicalFile();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Failed to resolve canonical path for " + root, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
mRoots.put(name, root);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uri getUriForFile(File file) {
|
||||||
|
String path;
|
||||||
|
try {
|
||||||
|
path = file.getCanonicalPath();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Map.Entry<String, File> mostSpecific = null;
|
||||||
|
for (Map.Entry<String, File> root : mRoots.entrySet()) {
|
||||||
|
final String rootPath = root.getValue().getPath();
|
||||||
|
if (path.startsWith(rootPath) && (mostSpecific == null
|
||||||
|
|| rootPath.length() > mostSpecific.getValue().getPath().length())) {
|
||||||
|
mostSpecific = root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mostSpecific == null) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Failed to find configured root that contains " + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
final String rootPath = mostSpecific.getValue().getPath();
|
||||||
|
if (rootPath.endsWith("/")) {
|
||||||
|
path = path.substring(rootPath.length());
|
||||||
|
} else {
|
||||||
|
path = path.substring(rootPath.length() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
|
||||||
|
return new Uri.Builder().scheme("content")
|
||||||
|
.authority(mAuthority).encodedPath(path).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File getFileForUri(Uri uri) {
|
||||||
|
String path = uri.getEncodedPath();
|
||||||
|
|
||||||
|
final int splitIndex = path.indexOf('/', 1);
|
||||||
|
final String tag = Uri.decode(path.substring(1, splitIndex));
|
||||||
|
path = Uri.decode(path.substring(splitIndex + 1));
|
||||||
|
|
||||||
|
final File root = mRoots.get(tag);
|
||||||
|
if (root == null) {
|
||||||
|
throw new IllegalArgumentException("Unable to find configured root for " + uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
File file = new File(root, path);
|
||||||
|
try {
|
||||||
|
file = file.getCanonicalFile();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file.getPath().startsWith(root.getPath())) {
|
||||||
|
throw new SecurityException("Resolved path jumped beyond configured root");
|
||||||
|
}
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static int modeToMode(String mode) {
|
||||||
|
int modeBits;
|
||||||
|
if ("r".equals(mode)) {
|
||||||
|
modeBits = ParcelFileDescriptor.MODE_READ_ONLY;
|
||||||
|
} else if ("w".equals(mode) || "wt".equals(mode)) {
|
||||||
|
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
|
||||||
|
| ParcelFileDescriptor.MODE_CREATE
|
||||||
|
| ParcelFileDescriptor.MODE_TRUNCATE;
|
||||||
|
} else if ("wa".equals(mode)) {
|
||||||
|
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
|
||||||
|
| ParcelFileDescriptor.MODE_CREATE
|
||||||
|
| ParcelFileDescriptor.MODE_APPEND;
|
||||||
|
} else if ("rw".equals(mode)) {
|
||||||
|
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
|
||||||
|
| ParcelFileDescriptor.MODE_CREATE;
|
||||||
|
} else if ("rwt".equals(mode)) {
|
||||||
|
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
|
||||||
|
| ParcelFileDescriptor.MODE_CREATE
|
||||||
|
| ParcelFileDescriptor.MODE_TRUNCATE;
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Invalid mode: " + mode);
|
||||||
|
}
|
||||||
|
return modeBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static File buildPath(File base, String... segments) {
|
||||||
|
File cur = base;
|
||||||
|
for (String segment : segments) {
|
||||||
|
if (segment != null) {
|
||||||
|
cur = new File(cur, segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String[] copyOf(String[] original, int newLength) {
|
||||||
|
final String[] result = new String[newLength];
|
||||||
|
System.arraycopy(original, 0, result, 0, newLength);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object[] copyOf(Object[] original, int newLength) {
|
||||||
|
final Object[] result = new Object[newLength];
|
||||||
|
System.arraycopy(original, 0, result, 0, newLength);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static File[] getExternalFilesDirs(Context context, String type) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 19) {
|
||||||
|
return context.getExternalFilesDirs(type);
|
||||||
|
} else {
|
||||||
|
return new File[] { context.getExternalFilesDir(type) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static File[] getExternalCacheDirs(Context context) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 19) {
|
||||||
|
return context.getExternalCacheDirs();
|
||||||
|
} else {
|
||||||
|
return new File[] { context.getExternalCacheDir() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.topjohnwu.magisk.net;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
class BadRequest extends Request {
|
||||||
|
|
||||||
|
private IOException ex;
|
||||||
|
|
||||||
|
BadRequest(IOException e) { super(null); ex = e; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Request addHeaders(String key, String value) { return this; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<InputStream> execForInputStream() { fail(); return new Result<>(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void getAsFile(File out, ResponseListener<File> rs) { fail(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execForFile(File out) { fail(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void getAsString(ResponseListener<String> rs) { fail(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<String> execForString() { fail(); return new Result<>(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void getAsJSONObject(ResponseListener<JSONObject> rs) { fail(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<JSONObject> execForJSONObject() { fail(); return new Result<>(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void getAsJSONArray(ResponseListener<JSONArray> rs) { fail(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<JSONArray> execForJSONArray() { fail(); return new Result<>(); }
|
||||||
|
|
||||||
|
private void fail() {
|
||||||
|
if (err != null)
|
||||||
|
err.onError(null, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.topjohnwu.magisk.net;
|
||||||
|
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
|
||||||
|
public interface ErrorHandler {
|
||||||
|
void onError(HttpURLConnection conn, Exception e);
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.topjohnwu.magisk.net;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.ConnectivityManager;
|
||||||
|
import android.net.NetworkInfo;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
import javax.net.ssl.HttpsURLConnection;
|
||||||
|
|
||||||
|
public class Networking {
|
||||||
|
|
||||||
|
private static final int READ_TIMEOUT = 15000;
|
||||||
|
private static final int CONNECT_TIMEOUT = 15000;
|
||||||
|
static Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
|
private static Request request(String url, String method) {
|
||||||
|
try {
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
|
||||||
|
conn.setRequestMethod(method);
|
||||||
|
conn.setReadTimeout(READ_TIMEOUT);
|
||||||
|
conn.setConnectTimeout(CONNECT_TIMEOUT);
|
||||||
|
return new Request(conn);
|
||||||
|
} catch (IOException e) {
|
||||||
|
return new BadRequest(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Request get(String url) {
|
||||||
|
return request(url, "GET");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean init(Context context) {
|
||||||
|
try {
|
||||||
|
// Try installing new SSL provider from Google Play Service
|
||||||
|
Context gms = context.createPackageContext("com.google.android.gms",
|
||||||
|
Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
|
||||||
|
gms.getClassLoader()
|
||||||
|
.loadClass("com.google.android.gms.common.security.ProviderInstallerImpl")
|
||||||
|
.getMethod("insertProvider", Context.class)
|
||||||
|
.invoke(null, gms);
|
||||||
|
} catch (Exception e) {
|
||||||
|
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) {
|
||||||
|
ConnectivityManager manager = (ConnectivityManager)
|
||||||
|
context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
|
NetworkInfo networkInfo = manager.getActiveNetworkInfo();
|
||||||
|
return networkInfo != null && networkInfo.isConnected();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.topjohnwu.magisk.net;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.net.ssl.HttpsURLConnection;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
import javax.net.ssl.SSLSocketFactory;
|
||||||
|
|
||||||
|
public class NoSSLv3SocketFactory extends SSLSocketFactory {
|
||||||
|
|
||||||
|
private final static SSLSocketFactory delegate = HttpsURLConnection.getDefaultSSLSocketFactory();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getDefaultCipherSuites() {
|
||||||
|
return delegate.getDefaultCipherSuites();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getSupportedCipherSuites() {
|
||||||
|
return delegate.getSupportedCipherSuites();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Socket createSafeSocket(Socket socket) {
|
||||||
|
if (socket instanceof SSLSocket)
|
||||||
|
return new SSLSocketWrapper((SSLSocket) socket) {
|
||||||
|
@Override
|
||||||
|
public void setEnabledProtocols(String[] protocols) {
|
||||||
|
List<String> proto = new ArrayList<>(Arrays.asList(getSupportedProtocols()));
|
||||||
|
proto.remove("SSLv3");
|
||||||
|
super.setEnabledProtocols(proto.toArray(new String[0]));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
|
||||||
|
return createSafeSocket(delegate.createSocket(s, host, port, autoClose));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Socket createSocket() throws IOException {
|
||||||
|
return createSafeSocket(delegate.createSocket());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Socket createSocket(String host, int port) throws IOException {
|
||||||
|
return createSafeSocket(delegate.createSocket(host, port));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
|
||||||
|
return createSafeSocket(delegate.createSocket(host, port, localHost, localPort));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Socket createSocket(InetAddress host, int port) throws IOException {
|
||||||
|
return createSafeSocket(delegate.createSocket(host, port));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
|
||||||
|
return createSafeSocket(delegate.createSocket(address, port, localAddress, localPort));
|
||||||
|
}
|
||||||
|
}
|
||||||
215
app/shared/src/main/java/com/topjohnwu/magisk/net/Request.java
Normal file
215
app/shared/src/main/java/com/topjohnwu/magisk/net/Request.java
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
package com.topjohnwu.magisk.net;
|
||||||
|
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.BufferedOutputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.FilterInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
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 int code = -1;
|
||||||
|
|
||||||
|
ErrorHandler err = null;
|
||||||
|
|
||||||
|
private interface Requestor<T> {
|
||||||
|
T request() throws Exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Result<T> {
|
||||||
|
T result;
|
||||||
|
|
||||||
|
public T getResult() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSuccess() {
|
||||||
|
return code >= 200 && code <= 299;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpURLConnection getConnection() {
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Request(HttpURLConnection c) {
|
||||||
|
conn = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
conn.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Request addHeaders(String key, String value) {
|
||||||
|
conn.setRequestProperty(key, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Request setErrorHandler(ErrorHandler handler) {
|
||||||
|
err = handler;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Request setExecutor(Executor e) {
|
||||||
|
executor = e;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result<Void> connect() {
|
||||||
|
try {
|
||||||
|
connect0();
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (err != null)
|
||||||
|
err.onError(conn, e);
|
||||||
|
}
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result<InputStream> execForInputStream() {
|
||||||
|
return exec(this::getInputStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void getAsFile(File out, ResponseListener<File> rs) {
|
||||||
|
submit(() -> dlFile(out), rs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void execForFile(File out) {
|
||||||
|
exec(() -> dlFile(out));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void getAsBytes(ResponseListener<byte[]> rs) {
|
||||||
|
submit(this::dlBytes, rs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result<byte[]> execForBytes() {
|
||||||
|
return exec(this::dlBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void getAsString(ResponseListener<String> rs) {
|
||||||
|
submit(this::dlString, rs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result<String> execForString() {
|
||||||
|
return exec(this::dlString);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void getAsJSONObject(ResponseListener<JSONObject> rs) {
|
||||||
|
submit(this::dlJSONObject, rs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result<JSONObject> execForJSONObject() {
|
||||||
|
return exec(this::dlJSONObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void getAsJSONArray(ResponseListener<JSONArray> rs) {
|
||||||
|
submit(this::dlJSONArray, rs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result<JSONArray> execForJSONArray() {
|
||||||
|
return exec(this::dlJSONArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void connect0() throws IOException {
|
||||||
|
conn.connect();
|
||||||
|
code = conn.getResponseCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> Result<T> exec(Requestor<T> req) {
|
||||||
|
Result<T> res = new Result<>();
|
||||||
|
try {
|
||||||
|
res.result = req.request();
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (err != null)
|
||||||
|
err.onError(conn, e);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> void submit(Requestor<T> req, ResponseListener<T> rs) {
|
||||||
|
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
||||||
|
try {
|
||||||
|
T t = req.request();
|
||||||
|
Runnable cb = () -> rs.onResponse(t);
|
||||||
|
if (executor == null)
|
||||||
|
Networking.mainHandler.post(cb);
|
||||||
|
else
|
||||||
|
executor.execute(cb);
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (err != null)
|
||||||
|
err.onError(conn, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private BufferedInputStream getInputStream() throws IOException {
|
||||||
|
connect0();
|
||||||
|
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 {
|
||||||
|
try (Scanner s = new Scanner(getInputStream(), "UTF-8")) {
|
||||||
|
s.useDelimiter("\\A");
|
||||||
|
return s.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONObject dlJSONObject() throws IOException, JSONException {
|
||||||
|
return new JSONObject(dlString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONArray dlJSONArray() throws IOException, JSONException {
|
||||||
|
return new JSONArray(dlString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private File dlFile(File f) throws IOException {
|
||||||
|
try (InputStream in = getInputStream();
|
||||||
|
OutputStream out = new BufferedOutputStream(new FileOutputStream(f))) {
|
||||||
|
int len;
|
||||||
|
byte[] buf = new byte[4096];
|
||||||
|
while ((len = in.read(buf)) != -1) {
|
||||||
|
out.write(buf, 0, len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] dlBytes() throws IOException {
|
||||||
|
int len = conn.getContentLength();
|
||||||
|
len = len > 0 ? len : 32;
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream(len);
|
||||||
|
try (InputStream in = getInputStream()) {
|
||||||
|
byte[] buf = new byte[4096];
|
||||||
|
while ((len = in.read(buf)) != -1) {
|
||||||
|
out.write(buf, 0, len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.topjohnwu.magisk.net;
|
||||||
|
|
||||||
|
public interface ResponseListener<T> {
|
||||||
|
void onResponse(T response);
|
||||||
|
}
|
||||||
@@ -0,0 +1,333 @@
|
|||||||
|
package com.topjohnwu.magisk.net;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.SocketAddress;
|
||||||
|
import java.net.SocketException;
|
||||||
|
import java.nio.channels.SocketChannel;
|
||||||
|
|
||||||
|
import javax.net.ssl.HandshakeCompletedListener;
|
||||||
|
import javax.net.ssl.SSLParameters;
|
||||||
|
import javax.net.ssl.SSLSession;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
|
||||||
|
class SSLSocketWrapper extends SSLSocket {
|
||||||
|
|
||||||
|
private SSLSocket mBase;
|
||||||
|
|
||||||
|
SSLSocketWrapper(SSLSocket socket) {
|
||||||
|
mBase = socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getSupportedCipherSuites() {
|
||||||
|
return mBase.getSupportedCipherSuites();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getEnabledCipherSuites() {
|
||||||
|
return mBase.getEnabledCipherSuites();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setEnabledCipherSuites(String[] suites) {
|
||||||
|
mBase.setEnabledCipherSuites(suites);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getSupportedProtocols() {
|
||||||
|
return mBase.getSupportedProtocols();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getEnabledProtocols() {
|
||||||
|
return mBase.getEnabledProtocols();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setEnabledProtocols(String[] protocols) {
|
||||||
|
mBase.setEnabledProtocols(protocols);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SSLSession getSession() {
|
||||||
|
return mBase.getSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SSLSession getHandshakeSession() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addHandshakeCompletedListener(HandshakeCompletedListener listener) {
|
||||||
|
mBase.addHandshakeCompletedListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeHandshakeCompletedListener(HandshakeCompletedListener listener) {
|
||||||
|
mBase.removeHandshakeCompletedListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startHandshake() throws IOException {
|
||||||
|
mBase.startHandshake();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setUseClientMode(boolean mode) {
|
||||||
|
mBase.setUseClientMode(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getUseClientMode() {
|
||||||
|
return mBase.getUseClientMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setNeedClientAuth(boolean need) {
|
||||||
|
mBase.setNeedClientAuth(need);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getNeedClientAuth() {
|
||||||
|
return mBase.getNeedClientAuth();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setWantClientAuth(boolean want) {
|
||||||
|
mBase.setWantClientAuth(want);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getWantClientAuth() {
|
||||||
|
return mBase.getWantClientAuth();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setEnableSessionCreation(boolean flag) {
|
||||||
|
mBase.setEnableSessionCreation(flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getEnableSessionCreation() {
|
||||||
|
return mBase.getEnableSessionCreation();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SSLParameters getSSLParameters() {
|
||||||
|
return mBase.getSSLParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSSLParameters(SSLParameters params) {
|
||||||
|
mBase.setSSLParameters(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return mBase.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void connect(SocketAddress endpoint) throws IOException {
|
||||||
|
mBase.connect(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void connect(SocketAddress endpoint, int timeout) throws IOException {
|
||||||
|
mBase.connect(endpoint, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void bind(SocketAddress bindpoint) throws IOException {
|
||||||
|
mBase.bind(bindpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InetAddress getInetAddress() {
|
||||||
|
return mBase.getInetAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InetAddress getLocalAddress() {
|
||||||
|
return mBase.getLocalAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPort() {
|
||||||
|
return mBase.getPort();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getLocalPort() {
|
||||||
|
return mBase.getLocalPort();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SocketAddress getRemoteSocketAddress() {
|
||||||
|
return mBase.getRemoteSocketAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SocketAddress getLocalSocketAddress() {
|
||||||
|
return mBase.getLocalSocketAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SocketChannel getChannel() {
|
||||||
|
return mBase.getChannel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream() throws IOException {
|
||||||
|
return mBase.getInputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OutputStream getOutputStream() throws IOException {
|
||||||
|
return mBase.getOutputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTcpNoDelay(boolean on) throws SocketException {
|
||||||
|
mBase.setTcpNoDelay(on);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getTcpNoDelay() throws SocketException {
|
||||||
|
return mBase.getTcpNoDelay();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSoLinger(boolean on, int linger) throws SocketException {
|
||||||
|
mBase.setSoLinger(on, linger);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSoLinger() throws SocketException {
|
||||||
|
return mBase.getSoLinger();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendUrgentData(int data) throws IOException {
|
||||||
|
mBase.sendUrgentData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOOBInline(boolean on) throws SocketException {
|
||||||
|
mBase.setOOBInline(on);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getOOBInline() throws SocketException {
|
||||||
|
return mBase.getOOBInline();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSoTimeout(int timeout) throws SocketException {
|
||||||
|
mBase.setSoTimeout(timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSoTimeout() throws SocketException {
|
||||||
|
return mBase.getSoTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSendBufferSize(int size) throws SocketException {
|
||||||
|
mBase.setSendBufferSize(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSendBufferSize() throws SocketException {
|
||||||
|
return mBase.getSendBufferSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setReceiveBufferSize(int size) throws SocketException {
|
||||||
|
mBase.setReceiveBufferSize(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getReceiveBufferSize() throws SocketException {
|
||||||
|
return mBase.getReceiveBufferSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setKeepAlive(boolean on) throws SocketException {
|
||||||
|
mBase.setKeepAlive(on);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getKeepAlive() throws SocketException {
|
||||||
|
return mBase.getKeepAlive();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTrafficClass(int tc) throws SocketException {
|
||||||
|
mBase.setTrafficClass(tc);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getTrafficClass() throws SocketException {
|
||||||
|
return mBase.getTrafficClass();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setReuseAddress(boolean on) throws SocketException {
|
||||||
|
mBase.setReuseAddress(on);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getReuseAddress() throws SocketException {
|
||||||
|
return mBase.getReuseAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
mBase.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdownInput() throws IOException {
|
||||||
|
mBase.shutdownInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdownOutput() throws IOException {
|
||||||
|
mBase.shutdownOutput();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isConnected() {
|
||||||
|
return mBase.isConnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isBound() {
|
||||||
|
return mBase.isBound();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isClosed() {
|
||||||
|
return mBase.isClosed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isInputShutdown() {
|
||||||
|
return mBase.isInputShutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isOutputShutdown() {
|
||||||
|
return mBase.isOutputShutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPerformancePreferences(int connectionTime, int latency, int bandwidth) {
|
||||||
|
mBase.setPerformancePreferences(connectionTime, latency, bandwidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.topjohnwu.magisk.utils;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
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);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
install.setData(FileProvider.getUriForFile(c, c.getPackageName() + ".provider", apk));
|
||||||
|
} else {
|
||||||
|
apk.setReadable(true, false);
|
||||||
|
install.setData(Uri.fromFile(apk));
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,10 @@
|
|||||||
package com.topjohnwu.magisk.utils;
|
package com.topjohnwu.signing;
|
||||||
|
|
||||||
import com.topjohnwu.utils.SignBoot;
|
|
||||||
|
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
import androidx.annotation.Keep;
|
|
||||||
|
|
||||||
public class BootSigner {
|
public class BootSigner {
|
||||||
|
|
||||||
@Keep
|
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
if (args.length > 0 && "-verify".equals(args[0])) {
|
if (args.length > 0 && "-verify".equals(args[0])) {
|
||||||
String certPath = "";
|
String certPath = "";
|
||||||
@@ -23,13 +18,19 @@ public class BootSigner {
|
|||||||
} else if (args.length > 0 && "-sign".equals(args[0])) {
|
} else if (args.length > 0 && "-sign".equals(args[0])) {
|
||||||
InputStream cert = null;
|
InputStream cert = null;
|
||||||
InputStream key = null;
|
InputStream key = null;
|
||||||
|
String name = "/boot";
|
||||||
|
|
||||||
if (args.length >= 3) {
|
if (args.length >= 3) {
|
||||||
cert = new FileInputStream(args[1]);
|
cert = new FileInputStream(args[1]);
|
||||||
key = new FileInputStream(args[2]);
|
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);
|
System.exit(success ? 0 : 1);
|
||||||
} else {
|
} else {
|
||||||
System.err.println(
|
System.err.println(
|
||||||
@@ -39,8 +40,9 @@ public class BootSigner {
|
|||||||
"Actions:\n" +
|
"Actions:\n" +
|
||||||
" -verify [x509.pem]\n" +
|
" -verify [x509.pem]\n" +
|
||||||
" verify image, cert is optional\n" +
|
" verify image, cert is optional\n" +
|
||||||
" -sign [x509.pem] [pk8]\n" +
|
" -sign [x509.pem] [pk8] [name]\n" +
|
||||||
" sign image, cert and key pair is optional\n"
|
" sign image, name, cert and key pair are optional\n" +
|
||||||
|
" name should be /boot (default) or /recovery\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,23 +1,21 @@
|
|||||||
package com.topjohnwu.utils;
|
package com.topjohnwu.signing;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
public class ByteArrayStream extends ByteArrayOutputStream {
|
public class ByteArrayStream extends ByteArrayOutputStream {
|
||||||
public byte[] getBuf() {
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
public synchronized void readFrom(InputStream is) {
|
public synchronized void readFrom(InputStream is) {
|
||||||
readFrom(is, Integer.MAX_VALUE);
|
readFrom(is, Integer.MAX_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void readFrom(InputStream is, int len) {
|
public synchronized void readFrom(InputStream is, int len) {
|
||||||
int read;
|
int read;
|
||||||
byte buffer[] = new byte[4096];
|
byte buffer[] = new byte[4096];
|
||||||
try {
|
try {
|
||||||
while ((read = is.read(buffer, 0, len < buffer.length ? len : buffer.length)) > 0) {
|
while ((read = is.read(buffer, 0, Math.min(len, buffer.length))) > 0) {
|
||||||
write(buffer, 0, read);
|
write(buffer, 0, read);
|
||||||
len -= read;
|
len -= read;
|
||||||
}
|
}
|
||||||
@@ -25,9 +23,7 @@ public class ByteArrayStream extends ByteArrayOutputStream {
|
|||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public synchronized void writeTo(OutputStream out, int off, int len) throws IOException {
|
|
||||||
out.write(buf, off, len);
|
|
||||||
}
|
|
||||||
public ByteArrayInputStream getInputStream() {
|
public ByteArrayInputStream getInputStream() {
|
||||||
return new ByteArrayInputStream(buf, 0, count);
|
return new ByteArrayInputStream(buf, 0, count);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.topjohnwu.utils;
|
package com.topjohnwu.signing;
|
||||||
|
|
||||||
import org.bouncycastle.asn1.ASN1InputStream;
|
import org.bouncycastle.asn1.ASN1InputStream;
|
||||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||||
@@ -24,7 +24,7 @@ import java.security.spec.PKCS8EncodedKeySpec;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
class CryptoUtils {
|
public class CryptoUtils {
|
||||||
|
|
||||||
static final Map<String, String> ID_TO_ALG;
|
static final Map<String, String> ID_TO_ALG;
|
||||||
static final Map<String, String> ALG_TO_ID;
|
static final Map<String, String> ALG_TO_ID;
|
||||||
@@ -81,7 +81,7 @@ class CryptoUtils {
|
|||||||
return new AlgorithmIdentifier(new ASN1ObjectIdentifier(id));
|
return new AlgorithmIdentifier(new ASN1ObjectIdentifier(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
static X509Certificate readCertificate(InputStream input)
|
public static X509Certificate readCertificate(InputStream input)
|
||||||
throws IOException, GeneralSecurityException {
|
throws IOException, GeneralSecurityException {
|
||||||
try {
|
try {
|
||||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||||
@@ -92,7 +92,7 @@ class CryptoUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Read a PKCS#8 format private key. */
|
/** Read a PKCS#8 format private key. */
|
||||||
static PrivateKey readPrivateKey(InputStream input)
|
public static PrivateKey readPrivateKey(InputStream input)
|
||||||
throws IOException, GeneralSecurityException {
|
throws IOException, GeneralSecurityException {
|
||||||
try {
|
try {
|
||||||
ByteArrayStream buf = new ByteArrayStream();
|
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.utils;
|
package com.topjohnwu.signing;
|
||||||
|
|
||||||
|
import org.bouncycastle.asn1.ASN1Encoding;
|
||||||
import org.bouncycastle.asn1.ASN1InputStream;
|
import org.bouncycastle.asn1.ASN1InputStream;
|
||||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
import org.bouncycastle.asn1.ASN1OutputStream;
|
||||||
import org.bouncycastle.asn1.DEROutputStream;
|
|
||||||
import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
|
|
||||||
import org.bouncycastle.cert.jcajce.JcaCertStore;
|
import org.bouncycastle.cert.jcajce.JcaCertStore;
|
||||||
import org.bouncycastle.cms.CMSException;
|
import org.bouncycastle.cms.CMSException;
|
||||||
import org.bouncycastle.cms.CMSProcessableByteArray;
|
import org.bouncycastle.cms.CMSProcessableByteArray;
|
||||||
@@ -18,31 +17,31 @@ import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
|||||||
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
|
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
|
||||||
import org.bouncycastle.util.encoders.Base64;
|
import org.bouncycastle.util.encoders.Base64;
|
||||||
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.FilterOutputStream;
|
import java.io.FilterOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.PrintStream;
|
import java.io.PrintStream;
|
||||||
import java.io.RandomAccessFile;
|
import java.nio.ByteBuffer;
|
||||||
import java.security.DigestOutputStream;
|
import java.security.DigestOutputStream;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
import java.security.KeyStore;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.Provider;
|
import java.security.PublicKey;
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
import java.security.cert.CertificateEncodingException;
|
import java.security.cert.CertificateEncodingException;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.TimeZone;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
import java.util.jar.Attributes;
|
import java.util.jar.Attributes;
|
||||||
import java.util.jar.JarEntry;
|
import java.util.jar.JarEntry;
|
||||||
@@ -52,93 +51,28 @@ import java.util.jar.Manifest;
|
|||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Modified from from AOSP(Marshmallow) SignAPK.java
|
* 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 {
|
|
||||||
|
|
||||||
|
public class SignApk {
|
||||||
private static final String CERT_SF_NAME = "META-INF/CERT.SF";
|
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_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.
|
// bitmasks for which hash algorithms we need the manifest to include.
|
||||||
private static final int USE_SHA1 = 1;
|
private static final int USE_SHA1 = 1;
|
||||||
private static final int USE_SHA256 = 2;
|
private static final int USE_SHA256 = 2;
|
||||||
|
|
||||||
static {
|
/**
|
||||||
sBouncyCastleProvider = new BouncyCastleProvider();
|
* Digest algorithm used when signing the APK using APK Signature Scheme v2.
|
||||||
Security.insertProviderAt(sBouncyCastleProvider, 1);
|
*/
|
||||||
}
|
private static final String APK_SIG_SCHEME_V2_DIGEST_ALGORITHM = "SHA-256";
|
||||||
|
// Files matching this pattern are not copied to the output.
|
||||||
public static void sign(JarMap input, OutputStream output) throws Exception {
|
private static final Pattern stripPattern =
|
||||||
sign(SignAPK.class.getResourceAsStream("/keys/testkey.x509.pem"),
|
Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" +
|
||||||
SignAPK.class.getResourceAsStream("/keys/testkey.pk8"), input, output);
|
Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return one of USE_SHA1 or USE_SHA256 according to the signature
|
* 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) {
|
private static int getDigestAlgorithm(X509Certificate cert) {
|
||||||
String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
|
String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
|
||||||
if ("SHA1WITHRSA".equals(sigAlg) ||
|
if ("SHA1WITHRSA".equals(sigAlg) || "MD5WITHRSA".equals(sigAlg)) {
|
||||||
"MD5WITHRSA".equals(sigAlg)) { // see "HISTORICAL NOTE" above.
|
|
||||||
return USE_SHA1;
|
return USE_SHA1;
|
||||||
} else if (sigAlg.startsWith("SHA256WITH")) {
|
} else if (sigAlg.startsWith("SHA256WITH")) {
|
||||||
return USE_SHA256;
|
return USE_SHA256;
|
||||||
@@ -156,9 +89,11 @@ public class SignAPK {
|
|||||||
"\" in cert [" + cert.getSubjectDN());
|
"\" 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) {
|
private static String getSignatureAlgorithm(X509Certificate cert) {
|
||||||
String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
|
|
||||||
String keyType = cert.getPublicKey().getAlgorithm().toUpperCase(Locale.US);
|
String keyType = cert.getPublicKey().getAlgorithm().toUpperCase(Locale.US);
|
||||||
if ("RSA".equalsIgnoreCase(keyType)) {
|
if ("RSA".equalsIgnoreCase(keyType)) {
|
||||||
if (getDigestAlgorithm(cert) == USE_SHA256) {
|
if (getDigestAlgorithm(cert) == USE_SHA256) {
|
||||||
@@ -172,10 +107,6 @@ public class SignAPK {
|
|||||||
throw new IllegalArgumentException("unsupported key type: " + keyType);
|
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
|
* 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("Manifest-Version", "1.0");
|
||||||
main.putValue("Created-By", "1.0 (Android SignApk)");
|
main.putValue("Created-By", "1.0 (Android SignApk)");
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDigest md_sha1 = null;
|
MessageDigest md_sha1 = null;
|
||||||
MessageDigest md_sha256 = null;
|
MessageDigest md_sha256 = null;
|
||||||
if ((hashes & USE_SHA1) != 0) {
|
if ((hashes & USE_SHA1) != 0) {
|
||||||
@@ -200,32 +132,51 @@ public class SignAPK {
|
|||||||
if ((hashes & USE_SHA256) != 0) {
|
if ((hashes & USE_SHA256) != 0) {
|
||||||
md_sha256 = MessageDigest.getInstance("SHA256");
|
md_sha256 = MessageDigest.getInstance("SHA256");
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] buffer = new byte[4096];
|
byte[] buffer = new byte[4096];
|
||||||
int num;
|
int num;
|
||||||
|
|
||||||
// We sort the input entries by name, and add them to the
|
// We sort the input entries by name, and add them to the
|
||||||
// output manifest in sorted order. We expect that the output
|
// output manifest in sorted order. We expect that the output
|
||||||
// map will be deterministic.
|
// 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(); ) {
|
for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
|
||||||
JarEntry entry = e.nextElement();
|
JarEntry entry = e.nextElement();
|
||||||
byName.put(entry.getName(), entry);
|
byName.put(entry.getName(), entry);
|
||||||
}
|
}
|
||||||
for (JarEntry entry: byName.values()) {
|
|
||||||
|
for (JarEntry entry : byName.values()) {
|
||||||
String name = entry.getName();
|
String name = entry.getName();
|
||||||
if (!entry.isDirectory() &&
|
if (!entry.isDirectory() && !stripPattern.matcher(name).matches()) {
|
||||||
(stripPattern == null || !stripPattern.matcher(name).matches())) {
|
|
||||||
InputStream data = jar.getInputStream(entry);
|
InputStream data = jar.getInputStream(entry);
|
||||||
while ((num = data.read(buffer)) > 0) {
|
while ((num = data.read(buffer)) > 0) {
|
||||||
if (md_sha1 != null) md_sha1.update(buffer, 0, num);
|
if (md_sha1 != null) md_sha1.update(buffer, 0, num);
|
||||||
if (md_sha256 != null) md_sha256.update(buffer, 0, num);
|
if (md_sha256 != null) md_sha256.update(buffer, 0, num);
|
||||||
}
|
}
|
||||||
|
|
||||||
Attributes attr = null;
|
Attributes attr = null;
|
||||||
if (input != null) attr = input.getAttributes(name);
|
if (input != null) attr = input.getAttributes(name);
|
||||||
attr = attr != null ? new Attributes(attr) : new Attributes();
|
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) {
|
if (md_sha1 != null) {
|
||||||
attr.putValue("SHA1-Digest",
|
attr.putValue("SHA1-Digest",
|
||||||
new String(Base64.encode(md_sha1.digest()), "ASCII"));
|
new String(Base64.encode(md_sha1.digest()), "ASCII"));
|
||||||
}
|
}
|
||||||
|
// Add SHA-256 digest if requested
|
||||||
if (md_sha256 != null) {
|
if (md_sha256 != null) {
|
||||||
attr.putValue("SHA-256-Digest",
|
attr.putValue("SHA-256-Digest",
|
||||||
new String(Base64.encode(md_sha256.digest()), "ASCII"));
|
new String(Base64.encode(md_sha256.digest()), "ASCII"));
|
||||||
@@ -233,33 +184,13 @@ public class SignAPK {
|
|||||||
output.getEntries().put(name, attr);
|
output.getEntries().put(name, attr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return output;
|
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,
|
private static void writeSignatureFile(Manifest manifest, OutputStream out,
|
||||||
int hash)
|
int hash)
|
||||||
throws IOException, GeneralSecurityException {
|
throws IOException, GeneralSecurityException {
|
||||||
@@ -267,16 +198,25 @@ public class SignAPK {
|
|||||||
Attributes main = sf.getMainAttributes();
|
Attributes main = sf.getMainAttributes();
|
||||||
main.putValue("Signature-Version", "1.0");
|
main.putValue("Signature-Version", "1.0");
|
||||||
main.putValue("Created-By", "1.0 (Android SignApk)");
|
main.putValue("Created-By", "1.0 (Android SignApk)");
|
||||||
MessageDigest md = MessageDigest.getInstance(
|
// Add APK Signature Scheme v2 signature stripping protection.
|
||||||
hash == USE_SHA256 ? "SHA256" : "SHA1");
|
// This attribute indicates that this APK is supposed to have been signed using one or
|
||||||
PrintStream print = new PrintStream(
|
// more APK-specific signature schemes in addition to the standard JAR signature scheme
|
||||||
new DigestOutputStream(new ByteArrayOutputStream(), md),
|
// 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");
|
true, "UTF-8");
|
||||||
|
|
||||||
// Digest of the entire manifest
|
// Digest of the entire manifest
|
||||||
manifest.write(print);
|
manifest.write(print);
|
||||||
print.flush();
|
print.flush();
|
||||||
main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",
|
main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",
|
||||||
new String(Base64.encode(md.digest()), "ASCII"));
|
new String(Base64.encode(md.digest()), "ASCII"));
|
||||||
|
|
||||||
Map<String, Attributes> entries = manifest.getEntries();
|
Map<String, Attributes> entries = manifest.getEntries();
|
||||||
for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
|
for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
|
||||||
// Digest of the manifest stanza for this entry.
|
// Digest of the manifest stanza for this entry.
|
||||||
@@ -286,13 +226,16 @@ public class SignAPK {
|
|||||||
}
|
}
|
||||||
print.print("\r\n");
|
print.print("\r\n");
|
||||||
print.flush();
|
print.flush();
|
||||||
|
|
||||||
Attributes sfAttr = new Attributes();
|
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"));
|
new String(Base64.encode(md.digest()), "ASCII"));
|
||||||
sf.getEntries().put(entry.getKey(), sfAttr);
|
sf.getEntries().put(entry.getKey(), sfAttr);
|
||||||
}
|
}
|
||||||
|
|
||||||
CountOutputStream cout = new CountOutputStream(out);
|
CountOutputStream cout = new CountOutputStream(out);
|
||||||
sf.write(cout);
|
sf.write(cout);
|
||||||
|
|
||||||
// A bug in the java.util.jar implementation of Android platforms
|
// A bug in the java.util.jar implementation of Android platforms
|
||||||
// up to version 1.6 will cause a spurious IOException to be thrown
|
// 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.
|
// if the length of the signature file is a multiple of 1024 bytes.
|
||||||
@@ -302,10 +245,12 @@ public class SignAPK {
|
|||||||
cout.write('\n');
|
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(
|
private static void writeSignatureBlock(
|
||||||
CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
|
CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, OutputStream out)
|
||||||
OutputStream out)
|
|
||||||
throws IOException,
|
throws IOException,
|
||||||
CertificateEncodingException,
|
CertificateEncodingException,
|
||||||
OperatorCreationException,
|
OperatorCreationException,
|
||||||
@@ -313,23 +258,24 @@ public class SignAPK {
|
|||||||
ArrayList<X509Certificate> certList = new ArrayList<>(1);
|
ArrayList<X509Certificate> certList = new ArrayList<>(1);
|
||||||
certList.add(publicKey);
|
certList.add(publicKey);
|
||||||
JcaCertStore certs = new JcaCertStore(certList);
|
JcaCertStore certs = new JcaCertStore(certList);
|
||||||
|
|
||||||
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
|
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
|
||||||
ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(publicKey))
|
ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(publicKey))
|
||||||
.setProvider(sBouncyCastleProvider)
|
|
||||||
.build(privateKey);
|
.build(privateKey);
|
||||||
gen.addSignerInfoGenerator(
|
gen.addSignerInfoGenerator(
|
||||||
new JcaSignerInfoGeneratorBuilder(
|
new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build())
|
||||||
new JcaDigestCalculatorProviderBuilder()
|
|
||||||
.setProvider(sBouncyCastleProvider)
|
|
||||||
.build())
|
|
||||||
.setDirectSignature(true)
|
.setDirectSignature(true)
|
||||||
.build(signer, publicKey));
|
.build(signer, publicKey)
|
||||||
|
);
|
||||||
gen.addCertificates(certs);
|
gen.addCertificates(certs);
|
||||||
CMSSignedData sigData = gen.generate(data, false);
|
CMSSignedData sigData = gen.generate(data, false);
|
||||||
ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded());
|
|
||||||
DEROutputStream dos = new DEROutputStream(out);
|
try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) {
|
||||||
dos.writeObject(asn1.readObject());
|
ASN1OutputStream dos = ASN1OutputStream.create(out, ASN1Encoding.DER);
|
||||||
|
dos.writeObject(asn1.readObject());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copy all the files in a manifest from input to output. We set
|
* 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
|
* the modification times in the output to a fixed time, so as to
|
||||||
@@ -337,27 +283,37 @@ public class SignAPK {
|
|||||||
* more efficient.
|
* more efficient.
|
||||||
*/
|
*/
|
||||||
private static void copyFiles(Manifest manifest, JarMap in, JarOutputStream out,
|
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];
|
byte[] buffer = new byte[4096];
|
||||||
int num;
|
int num;
|
||||||
|
|
||||||
Map<String, Attributes> entries = manifest.getEntries();
|
Map<String, Attributes> entries = manifest.getEntries();
|
||||||
ArrayList<String> names = new ArrayList<>(entries.keySet());
|
ArrayList<String> names = new ArrayList<>(entries.keySet());
|
||||||
Collections.sort(names);
|
Collections.sort(names);
|
||||||
|
|
||||||
boolean firstEntry = true;
|
boolean firstEntry = true;
|
||||||
long offset = 0L;
|
long offset = 0L;
|
||||||
|
|
||||||
// We do the copy in two passes -- first copying all the
|
// We do the copy in two passes -- first copying all the
|
||||||
// entries that are STORED, then copying all the entries that
|
// entries that are STORED, then copying all the entries that
|
||||||
// have any other compression flag (which in practice means
|
// have any other compression flag (which in practice means
|
||||||
// DEFLATED). This groups all the stored entries together at
|
// DEFLATED). This groups all the stored entries together at
|
||||||
// the start of the file and makes it easier to do alignment
|
// the start of the file and makes it easier to do alignment
|
||||||
// on them (since only stored entries are aligned).
|
// on them (since only stored entries are aligned).
|
||||||
|
|
||||||
for (String name : names) {
|
for (String name : names) {
|
||||||
JarEntry inEntry = in.getJarEntry(name);
|
JarEntry inEntry = in.getJarEntry(name);
|
||||||
JarEntry outEntry = null;
|
JarEntry outEntry;
|
||||||
if (inEntry.getMethod() != JarEntry.STORED) continue;
|
if (inEntry.getMethod() != JarEntry.STORED) continue;
|
||||||
// Preserve the STORED method of the input entry.
|
// Preserve the STORED method of the input entry.
|
||||||
outEntry = new JarEntry(inEntry);
|
outEntry = new JarEntry(inEntry);
|
||||||
outEntry.setTime(timestamp);
|
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
|
// 'offset' is the offset into the file at which we expect
|
||||||
// the file data to begin. This is the value we need to
|
// the file data to begin. This is the value we need to
|
||||||
// make a multiple of 'alignement'.
|
// make a multiple of 'alignement'.
|
||||||
@@ -371,15 +327,18 @@ public class SignAPK {
|
|||||||
offset += 4;
|
offset += 4;
|
||||||
firstEntry = false;
|
firstEntry = false;
|
||||||
}
|
}
|
||||||
|
int alignment = getStoredEntryDataAlignment(name, defaultAlignment);
|
||||||
if (alignment > 0 && (offset % alignment != 0)) {
|
if (alignment > 0 && (offset % alignment != 0)) {
|
||||||
// Set the "extra data" of the entry to between 1 and
|
// Set the "extra data" of the entry to between 1 and
|
||||||
// alignment-1 bytes, to make the file data begin at
|
// alignment-1 bytes, to make the file data begin at
|
||||||
// an aligned offset.
|
// an aligned offset.
|
||||||
int needed = alignment - (int)(offset % alignment);
|
int needed = alignment - (int) (offset % alignment);
|
||||||
outEntry.setExtra(new byte[needed]);
|
outEntry.setExtra(new byte[needed]);
|
||||||
offset += needed;
|
offset += needed;
|
||||||
}
|
}
|
||||||
|
|
||||||
out.putNextEntry(outEntry);
|
out.putNextEntry(outEntry);
|
||||||
|
|
||||||
InputStream data = in.getInputStream(inEntry);
|
InputStream data = in.getInputStream(inEntry);
|
||||||
while ((num = data.read(buffer)) > 0) {
|
while ((num = data.read(buffer)) > 0) {
|
||||||
out.write(buffer, 0, num);
|
out.write(buffer, 0, num);
|
||||||
@@ -387,17 +346,20 @@ public class SignAPK {
|
|||||||
}
|
}
|
||||||
out.flush();
|
out.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy all the non-STORED entries. We don't attempt to
|
// Copy all the non-STORED entries. We don't attempt to
|
||||||
// maintain the 'offset' variable past this point; we don't do
|
// maintain the 'offset' variable past this point; we don't do
|
||||||
// alignment on these entries.
|
// alignment on these entries.
|
||||||
|
|
||||||
for (String name : names) {
|
for (String name : names) {
|
||||||
JarEntry inEntry = in.getJarEntry(name);
|
JarEntry inEntry = in.getJarEntry(name);
|
||||||
JarEntry outEntry = null;
|
JarEntry outEntry;
|
||||||
if (inEntry.getMethod() == JarEntry.STORED) continue;
|
if (inEntry.getMethod() == JarEntry.STORED) continue;
|
||||||
// Create a new entry so that the compressed len is recomputed.
|
// Create a new entry so that the compressed len is recomputed.
|
||||||
outEntry = new JarEntry(name);
|
outEntry = new JarEntry(name);
|
||||||
outEntry.setTime(timestamp);
|
outEntry.setTime(timestamp);
|
||||||
out.putNextEntry(outEntry);
|
out.putNextEntry(outEntry);
|
||||||
|
|
||||||
InputStream data = in.getInputStream(inEntry);
|
InputStream data = in.getInputStream(inEntry);
|
||||||
while ((num = data.read(buffer)) > 0) {
|
while ((num = data.read(buffer)) > 0) {
|
||||||
out.write(buffer, 0, num);
|
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
|
* Returns the multiple (in bytes) at which the provided {@code STORED} entry's data must start
|
||||||
private static class CMSProcessableFile implements CMSTypedData {
|
* relative to start of file or {@code 0} if alignment of this entry's data is not important.
|
||||||
|
*/
|
||||||
private ASN1ObjectIdentifier type;
|
private static int getStoredEntryDataAlignment(String entryName, int defaultAlignment) {
|
||||||
private RandomAccessFile file;
|
if (defaultAlignment <= 0) {
|
||||||
|
return 0;
|
||||||
CMSProcessableFile(File file) throws FileNotFoundException {
|
|
||||||
this.file = new RandomAccessFile(file, "r");
|
|
||||||
type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
if (entryName.endsWith(".so")) {
|
||||||
public ASN1ObjectIdentifier getContentType() {
|
// Align .so contents to memory page boundary to enable memory-mapped
|
||||||
return type;
|
// execution.
|
||||||
}
|
return 4096;
|
||||||
|
} else {
|
||||||
@Override
|
return defaultAlignment;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void signWholeFile(File input, X509Certificate publicKey,
|
private static void signFile(Manifest manifest,
|
||||||
PrivateKey privateKey, OutputStream outputStream)
|
X509Certificate[] publicKey, PrivateKey[] privateKey,
|
||||||
throws Exception {
|
long timestamp, JarOutputStream outputJar) 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;
|
|
||||||
// MANIFEST.MF
|
// MANIFEST.MF
|
||||||
JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
|
JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
|
||||||
je.setTime(timestamp);
|
je.setTime(timestamp);
|
||||||
outputJar.putNextEntry(je);
|
outputJar.putNextEntry(je);
|
||||||
manifest.write(outputJar);
|
manifest.write(outputJar);
|
||||||
je = new JarEntry(CERT_SF_NAME);
|
|
||||||
je.setTime(timestamp);
|
int numKeys = publicKey.length;
|
||||||
outputJar.putNextEntry(je);
|
for (int k = 0; k < numKeys; ++k) {
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
// CERT.SF / CERT#.SF
|
||||||
writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey));
|
je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
|
||||||
byte[] signedData = baos.toByteArray();
|
(String.format(Locale.US, CERT_SF_MULTI_NAME, k)));
|
||||||
outputJar.write(signedData);
|
je.setTime(timestamp);
|
||||||
// CERT.{EC,RSA} / CERT#.{EC,RSA}
|
outputJar.putNextEntry(je);
|
||||||
final String keyType = publicKey.getPublicKey().getAlgorithm();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
je = new JarEntry(String.format(CERT_SIG_NAME, keyType));
|
writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey[k]));
|
||||||
je.setTime(timestamp);
|
byte[] signedData = baos.toByteArray();
|
||||||
outputJar.putNextEntry(je);
|
outputJar.write(signedData);
|
||||||
writeSignatureBlock(new CMSProcessableByteArray(signedData),
|
|
||||||
publicKey, privateKey, outputJar);
|
// 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.topjohnwu.utils;
|
package com.topjohnwu.signing;
|
||||||
|
|
||||||
import org.bouncycastle.asn1.ASN1Encodable;
|
import org.bouncycastle.asn1.ASN1Encodable;
|
||||||
import org.bouncycastle.asn1.ASN1EncodableVector;
|
import org.bouncycastle.asn1.ASN1EncodableVector;
|
||||||
@@ -12,9 +12,9 @@ import org.bouncycastle.asn1.DEROctetString;
|
|||||||
import org.bouncycastle.asn1.DERPrintableString;
|
import org.bouncycastle.asn1.DERPrintableString;
|
||||||
import org.bouncycastle.asn1.DERSequence;
|
import org.bouncycastle.asn1.DERSequence;
|
||||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
|
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.FilterInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
@@ -22,7 +22,6 @@ import java.nio.ByteBuffer;
|
|||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.security.Security;
|
|
||||||
import java.security.Signature;
|
import java.security.Signature;
|
||||||
import java.security.cert.CertificateEncodingException;
|
import java.security.cert.CertificateEncodingException;
|
||||||
import java.security.cert.CertificateFactory;
|
import java.security.cert.CertificateFactory;
|
||||||
@@ -31,37 +30,83 @@ import java.util.Arrays;
|
|||||||
|
|
||||||
public class SignBoot {
|
public class SignBoot {
|
||||||
|
|
||||||
static {
|
private static final int BOOT_IMAGE_HEADER_V1_RECOVERY_DTBO_SIZE_OFFSET = 1632;
|
||||||
Security.addProvider(new BouncyCastleProvider());
|
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;
|
||||||
|
private int pos = 0;
|
||||||
|
private byte[] backBuf;
|
||||||
|
|
||||||
|
PushBackRWStream(InputStream in, OutputStream o) {
|
||||||
|
super(in);
|
||||||
|
out = o;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException {
|
||||||
|
int b;
|
||||||
|
if (backBuf != null && backBuf.length > pos) {
|
||||||
|
b = backBuf[pos++];
|
||||||
|
} else {
|
||||||
|
b = super.read();
|
||||||
|
out.write(b);
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] bytes, int off, int len) throws IOException {
|
||||||
|
int read = 0;
|
||||||
|
if (backBuf != null && backBuf.length > pos) {
|
||||||
|
read = Math.min(len, backBuf.length - pos);
|
||||||
|
System.arraycopy(backBuf, pos, bytes, off, read);
|
||||||
|
pos += read;
|
||||||
|
off += read;
|
||||||
|
len -= read;
|
||||||
|
}
|
||||||
|
if (len > 0) {
|
||||||
|
int ar = super.read(bytes, off, len);
|
||||||
|
read += ar;
|
||||||
|
out.write(bytes, off, ar);
|
||||||
|
}
|
||||||
|
return read;
|
||||||
|
}
|
||||||
|
|
||||||
|
void unread(byte[] buf) {
|
||||||
|
backBuf = buf;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean doSignature(String target, InputStream imgIn, OutputStream imgOut,
|
public static boolean doSignature(String target, InputStream imgIn, OutputStream imgOut,
|
||||||
InputStream cert, InputStream key) {
|
InputStream cert, InputStream key) {
|
||||||
try {
|
try {
|
||||||
ByteArrayStream image = new ByteArrayStream();
|
PushBackRWStream in = new PushBackRWStream(imgIn, imgOut);
|
||||||
image.readFrom(imgIn);
|
byte[] hdr = new byte[BOOT_IMAGE_HEADER_SIZE_MAXIMUM];
|
||||||
int signableSize = getSignableImageSize(image.getBuf());
|
// First read the header
|
||||||
if (signableSize < image.size()) {
|
in.read(hdr);
|
||||||
System.err.println("NOTE: truncating input from " +
|
int signableSize = getSignableImageSize(hdr);
|
||||||
image.size() + " to " + signableSize + " bytes");
|
// Unread header
|
||||||
} else if (signableSize > image.size()) {
|
in.unread(hdr);
|
||||||
throw new IllegalArgumentException("Invalid image: too short, expected " +
|
BootSignature bootsig = new BootSignature(target, signableSize);
|
||||||
signableSize + " bytes");
|
|
||||||
}
|
|
||||||
BootSignature bootsig = new BootSignature(target, image.size());
|
|
||||||
if (cert == null) {
|
if (cert == null) {
|
||||||
cert = SignBoot.class.getResourceAsStream("/keys/testkey.x509.pem");
|
cert = SignBoot.class.getResourceAsStream("/keys/verity.x509.pem");
|
||||||
}
|
}
|
||||||
X509Certificate certificate = CryptoUtils.readCertificate(cert);
|
X509Certificate certificate = CryptoUtils.readCertificate(cert);
|
||||||
bootsig.setCertificate(certificate);
|
bootsig.setCertificate(certificate);
|
||||||
if (key == null) {
|
if (key == null) {
|
||||||
key = SignBoot.class.getResourceAsStream("/keys/testkey.pk8");
|
key = SignBoot.class.getResourceAsStream("/keys/verity.pk8");
|
||||||
}
|
}
|
||||||
PrivateKey privateKey = CryptoUtils.readPrivateKey(key);
|
PrivateKey privateKey = CryptoUtils.readPrivateKey(key);
|
||||||
bootsig.setSignature(bootsig.sign(privateKey, image.getBuf(), signableSize),
|
byte[] sig = bootsig.sign(privateKey, in, signableSize);
|
||||||
CryptoUtils.getSignatureAlgorithmIdentifier(privateKey));
|
bootsig.setSignature(sig, CryptoUtils.getSignatureAlgorithmIdentifier(privateKey));
|
||||||
byte[] encoded_bootsig = bootsig.getEncoded();
|
byte[] encoded_bootsig = bootsig.getEncoded();
|
||||||
image.writeTo(imgOut);
|
|
||||||
imgOut.write(encoded_bootsig);
|
imgOut.write(encoded_bootsig);
|
||||||
imgOut.flush();
|
imgOut.flush();
|
||||||
return true;
|
return true;
|
||||||
@@ -73,26 +118,42 @@ public class SignBoot {
|
|||||||
|
|
||||||
public static boolean verifySignature(InputStream imgIn, InputStream certIn) {
|
public static boolean verifySignature(InputStream imgIn, InputStream certIn) {
|
||||||
try {
|
try {
|
||||||
ByteArrayStream image = new ByteArrayStream();
|
// Read the header for size
|
||||||
image.readFrom(imgIn);
|
byte[] hdr = new byte[BOOT_IMAGE_HEADER_SIZE_MAXIMUM];
|
||||||
int signableSize = getSignableImageSize(image.getBuf());
|
if (imgIn.read(hdr) != hdr.length) {
|
||||||
if (signableSize >= image.size()) {
|
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("Unable to read image");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read footer, which contains the signature
|
||||||
|
byte[] signature = new byte[4096];
|
||||||
|
if (imgIn.read(signature) == -1 || Arrays.equals(signature, new byte [signature.length])) {
|
||||||
System.err.println("Invalid image: not signed");
|
System.err.println("Invalid image: not signed");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
byte[] signature = Arrays.copyOfRange(image.getBuf(), signableSize, image.size());
|
|
||||||
BootSignature bootsig = new BootSignature(signature);
|
BootSignature bootsig = new BootSignature(signature);
|
||||||
if (certIn != null) {
|
if (certIn != null) {
|
||||||
bootsig.setCertificate(CryptoUtils.readCertificate(certIn));
|
bootsig.setCertificate(CryptoUtils.readCertificate(certIn));
|
||||||
}
|
}
|
||||||
if (bootsig.verify(image.getBuf(), signableSize)) {
|
if (bootsig.verify(rawImg, signableSize)) {
|
||||||
System.err.println("Signature is VALID");
|
System.err.println("Signature is VALID");
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
System.err.println("Signature is INVALID");
|
System.err.println("Signature is INVALID");
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.err.println("Invalid image: not signed");
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -116,6 +177,27 @@ public class SignBoot {
|
|||||||
+ ((kernelSize + pageSize - 1) / pageSize) * pageSize
|
+ ((kernelSize + pageSize - 1) / pageSize) * pageSize
|
||||||
+ ((ramdskSize + pageSize - 1) / pageSize) * pageSize
|
+ ((ramdskSize + pageSize - 1) / pageSize) * pageSize
|
||||||
+ ((secondSize + 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;
|
length = ((length + pageSize - 1) / pageSize) * pageSize;
|
||||||
if (length <= 0) {
|
if (length <= 0) {
|
||||||
throw new IllegalArgumentException("Invalid image header: invalid length");
|
throw new IllegalArgumentException("Invalid image header: invalid length");
|
||||||
@@ -148,8 +230,7 @@ public class SignBoot {
|
|||||||
* Initializes the object for verifying a signed image file
|
* Initializes the object for verifying a signed image file
|
||||||
* @param signature Signature footer
|
* @param signature Signature footer
|
||||||
*/
|
*/
|
||||||
public BootSignature(byte[] signature)
|
public BootSignature(byte[] signature) throws Exception {
|
||||||
throws Exception {
|
|
||||||
ASN1InputStream stream = new ASN1InputStream(signature);
|
ASN1InputStream stream = new ASN1InputStream(signature);
|
||||||
ASN1Sequence sequence = (ASN1Sequence) stream.readObject();
|
ASN1Sequence sequence = (ASN1Sequence) stream.readObject();
|
||||||
formatVersion = (ASN1Integer) sequence.getObjectAt(0);
|
formatVersion = (ASN1Integer) sequence.getObjectAt(0);
|
||||||
@@ -193,10 +274,15 @@ public class SignBoot {
|
|||||||
publicKey = cert.getPublicKey();
|
publicKey = cert.getPublicKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] sign(PrivateKey key, byte[] image, int length) throws Exception {
|
public byte[] sign(PrivateKey key, InputStream is, int len) throws Exception {
|
||||||
Signature signer = Signature.getInstance(CryptoUtils.getSignatureAlgorithm(key));
|
Signature signer = Signature.getInstance(CryptoUtils.getSignatureAlgorithm(key));
|
||||||
signer.initSign(key);
|
signer.initSign(key);
|
||||||
signer.update(image, 0, length);
|
int read;
|
||||||
|
byte buffer[] = new byte[4096];
|
||||||
|
while ((read = is.read(buffer, 0, Math.min(len, buffer.length))) > 0) {
|
||||||
|
signer.update(buffer, 0, read);
|
||||||
|
len -= read;
|
||||||
|
}
|
||||||
signer.update(getEncodedAuthenticatedAttributes());
|
signer.update(getEncodedAuthenticatedAttributes());
|
||||||
return signer.sign();
|
return signer.sign();
|
||||||
}
|
}
|
||||||
@@ -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-----
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
package="com.topjohnwu.magisk">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
||||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:name="a.q"
|
|
||||||
android:theme="@style/AppTheme"
|
|
||||||
tools:ignore="GoogleAppIndexingWarning">
|
|
||||||
|
|
||||||
<!-- Activities -->
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name="a.b"
|
|
||||||
android:configChanges="orientation|screenSize"
|
|
||||||
android:exported="true" />
|
|
||||||
<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>
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name="a.d"
|
|
||||||
android:theme="@style/AppTheme.StatusBar" />
|
|
||||||
<activity
|
|
||||||
android:name="a.e"
|
|
||||||
android:theme="@style/AppTheme.StatusBar"/>
|
|
||||||
<activity
|
|
||||||
android:name="a.f"
|
|
||||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
|
||||||
android:screenOrientation="nosensor"
|
|
||||||
android:theme="@style/AppTheme.StatusBar" />
|
|
||||||
<activity
|
|
||||||
android:name="a.g"
|
|
||||||
android:theme="@style/AppTheme.Translucent" />
|
|
||||||
|
|
||||||
<!-- Superuser -->
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name="a.p"
|
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:taskAffinity="internal.superuser"
|
|
||||||
android:theme="@style/SuRequest" />
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".superuser.RequestActivity"
|
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:taskAffinity="internal.superuser"
|
|
||||||
android:theme="@style/AppTheme.Translucent" />
|
|
||||||
|
|
||||||
<receiver android:name=".superuser.SuReceiver" />
|
|
||||||
|
|
||||||
<!-- Receiver -->
|
|
||||||
|
|
||||||
<receiver android:name="a.h">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
<receiver android:name="a.i">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.PACKAGE_REPLACED" />
|
|
||||||
<action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" />
|
|
||||||
<data android:scheme="package" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
<receiver android:name="a.j" />
|
|
||||||
<receiver android:name="a.k" />
|
|
||||||
<receiver android:name="a.l">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<!-- Service -->
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name="a.m"
|
|
||||||
android:exported="true"
|
|
||||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
|
||||||
<service
|
|
||||||
android:name="a.n"
|
|
||||||
android:exported="true"
|
|
||||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
|
||||||
|
|
||||||
<!-- Hardcode GMS version -->
|
|
||||||
<meta-data
|
|
||||||
android:name="com.google.android.gms.version"
|
|
||||||
android:value="12451000" />
|
|
||||||
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package a;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.utils.BootSigner;
|
|
||||||
|
|
||||||
import androidx.annotation.Keep;
|
|
||||||
|
|
||||||
@Keep
|
|
||||||
public class a extends BootSigner {
|
|
||||||
/* stub */
|
|
||||||
}
|
|
||||||
@@ -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.AboutActivity;
|
|
||||||
|
|
||||||
public class d extends AboutActivity {
|
|
||||||
/* stub */
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package a;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.DonationActivity;
|
|
||||||
|
|
||||||
public class e extends DonationActivity {
|
|
||||||
/* stub */
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package a;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.FlashActivity;
|
|
||||||
|
|
||||||
public class f extends FlashActivity {
|
|
||||||
/* stub */
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package a;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.receivers.BootReceiver;
|
|
||||||
|
|
||||||
public class h extends BootReceiver {
|
|
||||||
/* stub */
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package a;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.receivers.PackageReceiver;
|
|
||||||
|
|
||||||
public class i extends PackageReceiver {
|
|
||||||
/* stub */
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package a;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.receivers.ManagerUpdate;
|
|
||||||
|
|
||||||
public class j extends ManagerUpdate {
|
|
||||||
/* stub */
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package a;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.receivers.RebootReceiver;
|
|
||||||
|
|
||||||
public class k extends RebootReceiver {
|
|
||||||
/* stub */
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package a;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.receivers.ShortcutReceiver;
|
|
||||||
|
|
||||||
public class l extends ShortcutReceiver {
|
|
||||||
/* stub */
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package a;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.services.OnBootService;
|
|
||||||
|
|
||||||
public class m extends OnBootService {
|
|
||||||
/* stub */
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package a;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.services.UpdateCheckService;
|
|
||||||
|
|
||||||
public class n extends UpdateCheckService {
|
|
||||||
/* stub */
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package a;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.components.AboutCardRow;
|
|
||||||
|
|
||||||
public class o extends AboutCardRow {
|
|
||||||
/* stub */
|
|
||||||
|
|
||||||
public o(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public o(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public o(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package a;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.SuRequestActivity;
|
|
||||||
|
|
||||||
public class p extends SuRequestActivity {
|
|
||||||
/* stub */
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package a;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.MagiskManager;
|
|
||||||
|
|
||||||
public class q extends MagiskManager {
|
|
||||||
/* stub */
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
package com.topjohnwu.magisk;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.asyncs.MarkDownWindow;
|
|
||||||
import com.topjohnwu.magisk.components.AboutCardRow;
|
|
||||||
import com.topjohnwu.magisk.components.BaseActivity;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
|
||||||
|
|
||||||
public class AboutActivity extends BaseActivity {
|
|
||||||
|
|
||||||
Toolbar toolbar;
|
|
||||||
AboutCardRow appVersionInfo;
|
|
||||||
AboutCardRow appChangelog;
|
|
||||||
AboutCardRow appTranslators;
|
|
||||||
AboutCardRow appSourceCode;
|
|
||||||
AboutCardRow supportThread;
|
|
||||||
AboutCardRow twitter;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getDarkTheme() {
|
|
||||||
return R.style.AppTheme_StatusBar_Dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_about);
|
|
||||||
ViewBinder.bind(this);
|
|
||||||
|
|
||||||
setSupportActionBar(toolbar);
|
|
||||||
toolbar.setNavigationOnClickListener(view -> finish());
|
|
||||||
|
|
||||||
ActionBar ab = getSupportActionBar();
|
|
||||||
if (ab != null) {
|
|
||||||
ab.setTitle(R.string.about);
|
|
||||||
ab.setDisplayHomeAsUpEnabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
appVersionInfo.setSummary(String.format(Locale.US, "%s (%d) (%s)",
|
|
||||||
BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, getPackageName()));
|
|
||||||
|
|
||||||
appChangelog.setOnClickListener(v -> {
|
|
||||||
new MarkDownWindow(this, getString(R.string.app_changelog),
|
|
||||||
getResources().openRawResource(R.raw.changelog)).exec();
|
|
||||||
});
|
|
||||||
|
|
||||||
String translators = getString(R.string.translators);
|
|
||||||
if (TextUtils.isEmpty(translators)) {
|
|
||||||
appTranslators.setVisibility(View.GONE);
|
|
||||||
} else {
|
|
||||||
appTranslators.setSummary(translators);
|
|
||||||
}
|
|
||||||
|
|
||||||
appSourceCode.setOnClickListener(v -> Utils.openLink(this, Uri.parse(Const.Url.SOURCE_CODE_URL)));
|
|
||||||
supportThread.setOnClickListener(v -> Utils.openLink(this, Uri.parse(Const.Url.XDA_THREAD)));
|
|
||||||
twitter.setOnClickListener(v -> Utils.openLink(this, Uri.parse(Const.Url.TWITTER_URL)));
|
|
||||||
|
|
||||||
setFloating();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
package com.topjohnwu.magisk;
|
|
||||||
|
|
||||||
import android.os.Process;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class Const {
|
|
||||||
|
|
||||||
public static final String DEBUG_TAG = "MagiskManager";
|
|
||||||
public static final String ORIG_PKG_NAME = BuildConfig.APPLICATION_ID;
|
|
||||||
public static final String MAGISKHIDE_PROP = "persist.magisk.hide";
|
|
||||||
|
|
||||||
// APK content
|
|
||||||
public static final String ANDROID_MANIFEST = "AndroidManifest.xml";
|
|
||||||
|
|
||||||
public static final String SU_KEYSTORE_KEY = "su_key";
|
|
||||||
|
|
||||||
// Paths
|
|
||||||
public static File MAGISK_PATH;
|
|
||||||
public static File MAGISK_DISABLE_FILE;
|
|
||||||
public static File MAGISK_HOST_FILE;
|
|
||||||
|
|
||||||
static {
|
|
||||||
/* Prevent crashing on unrooted devices */
|
|
||||||
MAGISK_PATH = MAGISK_DISABLE_FILE = MAGISK_HOST_FILE = new File("xxx");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final String BUSYBOX_PATH = "/sbin/.core/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 int MIN_MODULE_VER() {
|
|
||||||
return Data.magiskVersionCode >= MAGISK_VER.REMOVE_LEGACY_LINK ? 1500 : 1400;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* A list of apps that should not be shown as hide-able */
|
|
||||||
public static final List<String> HIDE_BLACKLIST = Arrays.asList(
|
|
||||||
"android",
|
|
||||||
Data.MM().getPackageName(),
|
|
||||||
"com.google.android.gms"
|
|
||||||
);
|
|
||||||
|
|
||||||
public static final int USER_ID = Process.myUid() / 100000;
|
|
||||||
|
|
||||||
public static final class MAGISK_VER {
|
|
||||||
public static final int UNIFIED = 1300;
|
|
||||||
public static final int FBE_AWARE = 1410;
|
|
||||||
public static final int RESETPROP_PERSIST = 1436;
|
|
||||||
public static final int MANAGER_HIDE = 1440;
|
|
||||||
public static final int HIDDEN_PATH = 1460;
|
|
||||||
public static final int REMOVE_LEGACY_LINK = 1630;
|
|
||||||
public static final int SEPOL_REFACTOR = 1640;
|
|
||||||
public static final int FIX_ENV = 1650;
|
|
||||||
public static final int DBVER_SIX = 17000;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 String NOTIFICATION_CHANNEL = "magisk_notification";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Url {
|
|
||||||
public static final String STABLE_URL = "https://raw.githubusercontent.com/topjohnwu/magisk_files/master/stable.json";
|
|
||||||
public static final String BETA_URL = "https://raw.githubusercontent.com/topjohnwu/magisk_files/master/beta.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 PAYPAL_URL = "https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=CC7FZ7526MNGG";
|
|
||||||
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 class Key {
|
|
||||||
// su
|
|
||||||
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";
|
|
||||||
|
|
||||||
// intents
|
|
||||||
public static final String OPEN_SECTION = "section";
|
|
||||||
public static final String INTENT_SET_FILENAME = "filename";
|
|
||||||
public static final String INTENT_SET_LINK = "link";
|
|
||||||
public static final String FLASH_ACTION = "action";
|
|
||||||
public static final String FLASH_SET_BOOT = "boot";
|
|
||||||
|
|
||||||
// others
|
|
||||||
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 UPDATE_SERVICE_VER = "update_service_version";
|
|
||||||
public static final String APP_VER = "app_version";
|
|
||||||
public static final String MAGISKHIDE = "magiskhide";
|
|
||||||
public static final String HOSTS = "hosts";
|
|
||||||
public static final String COREONLY = "disable";
|
|
||||||
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 LINK_KEY = "Link";
|
|
||||||
public static final String IF_NONE_MATCH = "If-None-Match";
|
|
||||||
public static final String REPO_ORDER = "repo_order";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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 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 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";
|
|
||||||
public static final int[] timeoutList = {0, -1, 10, 20, 30, 60};
|
|
||||||
public static final int ORDER_NAME = 0;
|
|
||||||
public static final int ORDER_DATE = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
package com.topjohnwu.magisk;
|
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.util.Xml;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.components.AboutCardRow;
|
|
||||||
import com.topjohnwu.magisk.receivers.BootReceiver;
|
|
||||||
import com.topjohnwu.magisk.receivers.ManagerUpdate;
|
|
||||||
import com.topjohnwu.magisk.receivers.PackageReceiver;
|
|
||||||
import com.topjohnwu.magisk.receivers.RebootReceiver;
|
|
||||||
import com.topjohnwu.magisk.receivers.ShortcutReceiver;
|
|
||||||
import com.topjohnwu.magisk.services.OnBootService;
|
|
||||||
import com.topjohnwu.magisk.services.UpdateCheckService;
|
|
||||||
import com.topjohnwu.magisk.utils.FingerprintHelper;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
import com.topjohnwu.superuser.Shell;
|
|
||||||
import com.topjohnwu.superuser.ShellUtils;
|
|
||||||
import com.topjohnwu.superuser.io.SuFile;
|
|
||||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
|
||||||
|
|
||||||
import org.xmlpull.v1.XmlPullParser;
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class Data {
|
|
||||||
// Global app instance
|
|
||||||
public static WeakReference<MagiskManager> weakApp;
|
|
||||||
public static Handler mainHandler = new Handler(Looper.getMainLooper());
|
|
||||||
public static Map<Class, Class> classMap = new HashMap<>();
|
|
||||||
|
|
||||||
// Current status
|
|
||||||
public static String magiskVersionString;
|
|
||||||
public static int magiskVersionCode = -1;
|
|
||||||
public 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;
|
|
||||||
public static int snetVersionCode;
|
|
||||||
public static String snetLink;
|
|
||||||
|
|
||||||
// Install flags
|
|
||||||
public static boolean keepVerity = false;
|
|
||||||
public static boolean keepEnc = false;
|
|
||||||
|
|
||||||
// Configs
|
|
||||||
public static boolean isDarkTheme;
|
|
||||||
public static int suRequestTimeout;
|
|
||||||
public static int suLogTimeout = 14;
|
|
||||||
public static int suAccessState;
|
|
||||||
public static boolean suFingerprint;
|
|
||||||
public static int multiuserMode;
|
|
||||||
public static int suResponseType;
|
|
||||||
public static int suNotificationType;
|
|
||||||
public static int suNamespaceMode;
|
|
||||||
public static int updateChannel;
|
|
||||||
public static int repoOrder;
|
|
||||||
|
|
||||||
static {
|
|
||||||
classMap.put(MagiskManager.class, a.q.class);
|
|
||||||
classMap.put(MainActivity.class, a.b.class);
|
|
||||||
classMap.put(SplashActivity.class, a.c.class);
|
|
||||||
classMap.put(AboutActivity.class, a.d.class);
|
|
||||||
classMap.put(DonationActivity.class, a.e.class);
|
|
||||||
classMap.put(FlashActivity.class, a.f.class);
|
|
||||||
classMap.put(NoUIActivity.class, a.g.class);
|
|
||||||
classMap.put(BootReceiver.class, a.h.class);
|
|
||||||
classMap.put(PackageReceiver.class, a.i.class);
|
|
||||||
classMap.put(ManagerUpdate.class, a.j.class);
|
|
||||||
classMap.put(RebootReceiver.class, a.k.class);
|
|
||||||
classMap.put(ShortcutReceiver.class, a.l.class);
|
|
||||||
classMap.put(OnBootService.class, a.m.class);
|
|
||||||
classMap.put(UpdateCheckService.class, a.n.class);
|
|
||||||
classMap.put(AboutCardRow.class, a.o.class);
|
|
||||||
classMap.put(SuRequestActivity.class, a.p.class);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void loadMagiskInfo() {
|
|
||||||
try {
|
|
||||||
magiskVersionString = ShellUtils.fastCmd("magisk -v").split(":")[0];
|
|
||||||
magiskVersionCode = Integer.parseInt(ShellUtils.fastCmd("magisk -V"));
|
|
||||||
String s = ShellUtils.fastCmd((magiskVersionCode >= Const.MAGISK_VER.RESETPROP_PERSIST ?
|
|
||||||
"resetprop -p " : "getprop ") + Const.MAGISKHIDE_PROP);
|
|
||||||
magiskHide = s.isEmpty() || Integer.parseInt(s) != 0;
|
|
||||||
} catch (NumberFormatException ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MagiskManager MM() {
|
|
||||||
return weakApp.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void exportPrefs() {
|
|
||||||
// Flush prefs to disk
|
|
||||||
MagiskManager mm = MM();
|
|
||||||
mm.prefs.edit().commit();
|
|
||||||
File xml = new File(mm.getFilesDir().getParent() + "/shared_prefs",
|
|
||||||
mm.getPackageName() + "_preferences.xml");
|
|
||||||
Shell.su(Utils.fmt("for usr in /data/user/*; do cat %s > ${usr}/%s; done", xml, Const.MANAGER_CONFIGS)).exec();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void importPrefs() {
|
|
||||||
MagiskManager mm = MM();
|
|
||||||
SuFile config = new SuFile(Utils.fmt("/data/user/%d/%s", Const.USER_ID, Const.MANAGER_CONFIGS));
|
|
||||||
if (config.exists()) {
|
|
||||||
SharedPreferences.Editor editor = mm.prefs.edit();
|
|
||||||
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(Const.Key.ETAG_KEY);
|
|
||||||
editor.apply();
|
|
||||||
loadConfig();
|
|
||||||
config.delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void loadConfig() {
|
|
||||||
MagiskManager mm = MM();
|
|
||||||
// su
|
|
||||||
suRequestTimeout = Utils.getPrefsInt(mm.prefs, Const.Key.SU_REQUEST_TIMEOUT, Const.Value.timeoutList[2]);
|
|
||||||
suResponseType = Utils.getPrefsInt(mm.prefs, Const.Key.SU_AUTO_RESPONSE, Const.Value.SU_PROMPT);
|
|
||||||
suNotificationType = Utils.getPrefsInt(mm.prefs, Const.Key.SU_NOTIFICATION, Const.Value.NOTIFICATION_TOAST);
|
|
||||||
suAccessState = mm.mDB.getSettings(Const.Key.ROOT_ACCESS, Const.Value.ROOT_ACCESS_APPS_AND_ADB);
|
|
||||||
multiuserMode = mm.mDB.getSettings(Const.Key.SU_MULTIUSER_MODE, Const.Value.MULTIUSER_MODE_OWNER_ONLY);
|
|
||||||
suNamespaceMode = mm.mDB.getSettings(Const.Key.SU_MNT_NS, Const.Value.NAMESPACE_MODE_REQUESTER);
|
|
||||||
suFingerprint = mm.mDB.getSettings(Const.Key.SU_FINGERPRINT, 0) != 0;
|
|
||||||
if (suFingerprint && !FingerprintHelper.canUseFingerprint()) {
|
|
||||||
// User revoked the fingerprint
|
|
||||||
mm.mDB.setSettings(Const.Key.SU_FINGERPRINT, 0);
|
|
||||||
suFingerprint = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// config
|
|
||||||
isDarkTheme = mm.prefs.getBoolean(Const.Key.DARK_THEME, false);
|
|
||||||
updateChannel = Utils.getPrefsInt(mm.prefs, Const.Key.UPDATE_CHANNEL, Const.Value.STABLE_CHANNEL);
|
|
||||||
repoOrder = mm.prefs.getInt(Const.Key.REPO_ORDER, Const.Value.ORDER_DATE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void writeConfig() {
|
|
||||||
MM().prefs.edit()
|
|
||||||
.putBoolean(Const.Key.DARK_THEME, isDarkTheme)
|
|
||||||
.putBoolean(Const.Key.MAGISKHIDE, magiskHide)
|
|
||||||
.putBoolean(Const.Key.HOSTS, Const.MAGISK_HOST_FILE.exists())
|
|
||||||
.putBoolean(Const.Key.COREONLY, Const.MAGISK_DISABLE_FILE.exists())
|
|
||||||
.putBoolean(Const.Key.SU_FINGERPRINT, suFingerprint)
|
|
||||||
.putString(Const.Key.SU_REQUEST_TIMEOUT, String.valueOf(suRequestTimeout))
|
|
||||||
.putString(Const.Key.SU_AUTO_RESPONSE, String.valueOf(suResponseType))
|
|
||||||
.putString(Const.Key.SU_NOTIFICATION, String.valueOf(suNotificationType))
|
|
||||||
.putString(Const.Key.ROOT_ACCESS, String.valueOf(suAccessState))
|
|
||||||
.putString(Const.Key.SU_MULTIUSER_MODE, String.valueOf(multiuserMode))
|
|
||||||
.putString(Const.Key.SU_MNT_NS, String.valueOf(suNamespaceMode))
|
|
||||||
.putString(Const.Key.UPDATE_CHANNEL, String.valueOf(updateChannel))
|
|
||||||
.putInt(Const.Key.UPDATE_SERVICE_VER, Const.UPDATE_SERVICE_VER)
|
|
||||||
.putInt(Const.Key.REPO_ORDER, repoOrder)
|
|
||||||
.apply();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package com.topjohnwu.magisk;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.components.AboutCardRow;
|
|
||||||
import com.topjohnwu.magisk.components.BaseActivity;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
|
||||||
|
|
||||||
public class DonationActivity extends BaseActivity {
|
|
||||||
|
|
||||||
Toolbar toolbar;
|
|
||||||
AboutCardRow paypal;
|
|
||||||
AboutCardRow patreon;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getDarkTheme() {
|
|
||||||
return R.style.AppTheme_StatusBar_Dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_donation);
|
|
||||||
ViewBinder.bind(this);
|
|
||||||
|
|
||||||
setSupportActionBar(toolbar);
|
|
||||||
toolbar.setNavigationOnClickListener(view -> finish());
|
|
||||||
|
|
||||||
ActionBar ab = getSupportActionBar();
|
|
||||||
if (ab != null) {
|
|
||||||
ab.setTitle(R.string.donation);
|
|
||||||
ab.setDisplayHomeAsUpEnabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
paypal.setOnClickListener(v -> Utils.openLink(this, Uri.parse(Const.Url.PAYPAL_URL)));
|
|
||||||
patreon.setOnClickListener(v -> Utils.openLink(this, Uri.parse(Const.Url.PATREON_URL)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
package com.topjohnwu.magisk;
|
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.ScrollView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.asyncs.FlashZip;
|
|
||||||
import com.topjohnwu.magisk.asyncs.InstallMagisk;
|
|
||||||
import com.topjohnwu.magisk.components.BaseActivity;
|
|
||||||
import com.topjohnwu.magisk.utils.Download;
|
|
||||||
import com.topjohnwu.magisk.utils.RootUtils;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
import com.topjohnwu.superuser.CallbackList;
|
|
||||||
import com.topjohnwu.superuser.Shell;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileWriter;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
|
||||||
|
|
||||||
public class FlashActivity extends BaseActivity {
|
|
||||||
|
|
||||||
Toolbar toolbar;
|
|
||||||
TextView flashLogs;
|
|
||||||
public LinearLayout buttonPanel;
|
|
||||||
public Button reboot;
|
|
||||||
ScrollView sv;
|
|
||||||
|
|
||||||
private List<String> logs;
|
|
||||||
|
|
||||||
void dismiss() {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
void reboot() {
|
|
||||||
Shell.su("/system/bin/reboot").submit();
|
|
||||||
}
|
|
||||||
|
|
||||||
void saveLogs() {
|
|
||||||
runWithPermission(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, () -> {
|
|
||||||
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(Download.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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getDarkTheme() {
|
|
||||||
return R.style.AppTheme_StatusBar_Dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_flash);
|
|
||||||
ViewBinder.bind(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 = new ArrayList<>();
|
|
||||||
CallbackList<String> console = new CallbackList<String>(new ArrayList<>()) {
|
|
||||||
|
|
||||||
private void updateUI() {
|
|
||||||
flashLogs.setText(TextUtils.join("\n", this));
|
|
||||||
sv.postDelayed(() -> sv.fullScroll(ScrollView.FOCUS_DOWN), 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);
|
|
||||||
Data.mainHandler.post(this::updateUI);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// We must receive a Uri of the target zip
|
|
||||||
Intent intent = getIntent();
|
|
||||||
Uri uri = intent.getData();
|
|
||||||
|
|
||||||
switch (intent.getStringExtra(Const.Key.FLASH_ACTION)) {
|
|
||||||
case Const.Value.FLASH_ZIP:
|
|
||||||
new FlashZip(this, uri, console, logs).exec();
|
|
||||||
break;
|
|
||||||
case Const.Value.UNINSTALL:
|
|
||||||
new UninstallMagisk(this, uri, console, logs).exec();
|
|
||||||
break;
|
|
||||||
case Const.Value.FLASH_MAGISK:
|
|
||||||
new InstallMagisk(this, console, logs, InstallMagisk.DIRECT_MODE).exec();
|
|
||||||
break;
|
|
||||||
case Const.Value.FLASH_INACTIVE_SLOT:
|
|
||||||
new InstallMagisk(this, console, logs, InstallMagisk.SECOND_SLOT_MODE).exec();
|
|
||||||
break;
|
|
||||||
case Const.Value.PATCH_BOOT:
|
|
||||||
runWithPermission(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
|
||||||
() -> new InstallMagisk(this, console, logs,
|
|
||||||
intent.getParcelableExtra(Const.Key.FLASH_SET_BOOT)).exec());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
// Prevent user accidentally press back button
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class UninstallMagisk extends FlashZip {
|
|
||||||
|
|
||||||
private UninstallMagisk(BaseActivity context, Uri uri, List<String> console, List<String> logs) {
|
|
||||||
super(context, uri, console, logs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Integer result) {
|
|
||||||
if (result == 1) {
|
|
||||||
Data.mainHandler.postDelayed(() ->
|
|
||||||
RootUtils.uninstallPkg(getActivity().getPackageName()), 3000);
|
|
||||||
} else {
|
|
||||||
super.onPostExecute(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
package com.topjohnwu.magisk;
|
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.database.MagiskDatabaseHelper;
|
|
||||||
import com.topjohnwu.magisk.database.RepoDatabaseHelper;
|
|
||||||
import com.topjohnwu.magisk.utils.LocaleManager;
|
|
||||||
import com.topjohnwu.magisk.utils.RootUtils;
|
|
||||||
import com.topjohnwu.superuser.ContainerApp;
|
|
||||||
import com.topjohnwu.superuser.Shell;
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
|
|
||||||
public class MagiskManager extends ContainerApp {
|
|
||||||
|
|
||||||
// Info
|
|
||||||
public boolean hasInit = false;
|
|
||||||
|
|
||||||
// Global resources
|
|
||||||
public SharedPreferences prefs;
|
|
||||||
public MagiskDatabaseHelper mDB;
|
|
||||||
public RepoDatabaseHelper repoDB;
|
|
||||||
|
|
||||||
public MagiskManager() {
|
|
||||||
Data.weakApp = new WeakReference<>(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
|
|
||||||
Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER);
|
|
||||||
Shell.Config.verboseLogging(BuildConfig.DEBUG);
|
|
||||||
Shell.Config.setInitializer(RootUtils.class);
|
|
||||||
|
|
||||||
prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
|
||||||
mDB = MagiskDatabaseHelper.getInstance(this);
|
|
||||||
|
|
||||||
String pkg = mDB.getStrings(Const.Key.SU_MANAGER, null);
|
|
||||||
if (pkg != null && getPackageName().equals(Const.ORIG_PKG_NAME)) {
|
|
||||||
mDB.setStrings(Const.Key.SU_MANAGER, null);
|
|
||||||
Shell.su("pm uninstall " + pkg).exec();
|
|
||||||
}
|
|
||||||
if (TextUtils.equals(pkg, getPackageName())) {
|
|
||||||
try {
|
|
||||||
// We are the manager, remove com.topjohnwu.magisk as it could be malware
|
|
||||||
getPackageManager().getApplicationInfo(Const.ORIG_PKG_NAME, 0);
|
|
||||||
RootUtils.uninstallPkg(Const.ORIG_PKG_NAME);
|
|
||||||
} catch (PackageManager.NameNotFoundException ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
LocaleManager.setLocale(this);
|
|
||||||
Data.loadConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConfigurationChanged(Configuration newConfig) {
|
|
||||||
super.onConfigurationChanged(newConfig);
|
|
||||||
LocaleManager.setLocale(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
package com.topjohnwu.magisk;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
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.Download;
|
|
||||||
import com.topjohnwu.magisk.utils.Topic;
|
|
||||||
import com.topjohnwu.superuser.Shell;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
public class MainActivity extends BaseActivity
|
|
||||||
implements NavigationView.OnNavigationItemSelectedListener, Topic.Subscriber {
|
|
||||||
|
|
||||||
private final Handler mDrawerHandler = new Handler();
|
|
||||||
private int mDrawerItem;
|
|
||||||
private static boolean fromShortcut = false;
|
|
||||||
|
|
||||||
public Toolbar toolbar;
|
|
||||||
DrawerLayout drawer;
|
|
||||||
NavigationView navigationView;
|
|
||||||
|
|
||||||
private float toolbarElevation;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getDarkTheme() {
|
|
||||||
return R.style.AppTheme_Dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(final Bundle savedInstanceState) {
|
|
||||||
if (!mm.hasInit) {
|
|
||||||
startActivity(new Intent(this, Data.classMap.get(SplashActivity.class)));
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_main);
|
|
||||||
ViewBinder.bind(this);
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
checkHideSection();
|
|
||||||
}
|
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPublish(int topic, Object[] result) {
|
|
||||||
recreate();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void checkHideSection() {
|
|
||||||
Menu menu = navigationView.getMenu();
|
|
||||||
menu.findItem(R.id.magiskhide).setVisible(Shell.rootAccess() &&
|
|
||||||
Data.magiskVersionCode >= Const.MAGISK_VER.UNIFIED &&
|
|
||||||
mm.prefs.getBoolean(Const.Key.MAGISKHIDE, false));
|
|
||||||
menu.findItem(R.id.modules).setVisible(Shell.rootAccess() && Data.magiskVersionCode >= 0);
|
|
||||||
menu.findItem(R.id.downloads).setVisible(Download.checkNetworkStatus(this)
|
|
||||||
&& Shell.rootAccess() && Data.magiskVersionCode >= 0);
|
|
||||||
menu.findItem(R.id.log).setVisible(Shell.rootAccess());
|
|
||||||
menu.findItem(R.id.superuser).setVisible(Shell.rootAccess() &&
|
|
||||||
!(Const.USER_ID > 0 && Data.multiuserMode == Const.Value.MULTIUSER_MODE_OWNER_MANAGED));
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
case "about":
|
|
||||||
itemId = R.id.app_about;
|
|
||||||
break;
|
|
||||||
case "donation":
|
|
||||||
itemId = R.id.donation;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
navigate(itemId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void navigate(int itemId) {
|
|
||||||
int bak = mDrawerItem;
|
|
||||||
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;
|
|
||||||
case R.id.app_about:
|
|
||||||
startActivity(new Intent(this, Data.classMap.get(AboutActivity.class)));
|
|
||||||
mDrawerItem = bak;
|
|
||||||
break;
|
|
||||||
case R.id.donation:
|
|
||||||
startActivity(new Intent(this, Data.classMap.get(DonationActivity.class)));
|
|
||||||
mDrawerItem = bak;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void displayFragment(@NonNull Fragment navFragment, boolean setElevation) {
|
|
||||||
supportInvalidateOptionsMenu();
|
|
||||||
getSupportFragmentManager()
|
|
||||||
.beginTransaction()
|
|
||||||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
|
|
||||||
.replace(R.id.content_frame, navFragment)
|
|
||||||
.commitNow();
|
|
||||||
toolbar.setElevation(setElevation ? toolbarElevation : 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.topjohnwu.magisk;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.components.BaseActivity;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
public class NoUIActivity extends BaseActivity {
|
|
||||||
@Override
|
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package com.topjohnwu.magisk;
|
|
||||||
|
|
||||||
import android.app.NotificationChannel;
|
|
||||||
import android.app.NotificationManager;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.asyncs.CheckUpdates;
|
|
||||||
import com.topjohnwu.magisk.asyncs.UpdateRepos;
|
|
||||||
import com.topjohnwu.magisk.components.BaseActivity;
|
|
||||||
import com.topjohnwu.magisk.database.RepoDatabaseHelper;
|
|
||||||
import com.topjohnwu.magisk.receivers.ShortcutReceiver;
|
|
||||||
import com.topjohnwu.magisk.utils.Download;
|
|
||||||
import com.topjohnwu.magisk.utils.LocaleManager;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
import com.topjohnwu.superuser.Shell;
|
|
||||||
|
|
||||||
public class SplashActivity extends BaseActivity {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
// Magisk working as expected
|
|
||||||
if (Shell.rootAccess() && Data.magiskVersionCode > 0) {
|
|
||||||
// Update check service
|
|
||||||
Utils.setupUpdateCheck();
|
|
||||||
// Load modules
|
|
||||||
Utils.loadModules();
|
|
||||||
}
|
|
||||||
|
|
||||||
mm.repoDB = new RepoDatabaseHelper(this);
|
|
||||||
Data.importPrefs();
|
|
||||||
|
|
||||||
// Dynamic detect all locales
|
|
||||||
LocaleManager.loadAvailableLocales();
|
|
||||||
|
|
||||||
// Create notification channel on Android O
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
NotificationChannel channel = new NotificationChannel(Const.ID.NOTIFICATION_CHANNEL,
|
|
||||||
getString(R.string.magisk_updates), NotificationManager.IMPORTANCE_DEFAULT);
|
|
||||||
getSystemService(NotificationManager.class).createNotificationChannel(channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup shortcuts
|
|
||||||
sendBroadcast(new Intent(this, Data.classMap.get(ShortcutReceiver.class)));
|
|
||||||
|
|
||||||
if (Download.checkNetworkStatus(this)) {
|
|
||||||
// Fire update check
|
|
||||||
CheckUpdates.check();
|
|
||||||
// Repo update check
|
|
||||||
new UpdateRepos().exec();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write back default values
|
|
||||||
Data.writeConfig();
|
|
||||||
|
|
||||||
mm.hasInit = true;
|
|
||||||
|
|
||||||
Intent intent = new Intent(this, Data.classMap.get(MainActivity.class));
|
|
||||||
intent.putExtra(Const.Key.OPEN_SECTION, getIntent().getStringExtra(Const.Key.OPEN_SECTION));
|
|
||||||
intent.putExtra(BaseActivity.INTENT_PERM, getIntent().getStringExtra(BaseActivity.INTENT_PERM));
|
|
||||||
startActivity(intent);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
package com.topjohnwu.magisk;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.hardware.fingerprint.FingerprintManager;
|
|
||||||
import android.net.LocalSocketAddress;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.CountDownTimer;
|
|
||||||
import android.os.FileObserver;
|
|
||||||
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 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.DataOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
public class SuRequestActivity extends BaseActivity {
|
|
||||||
LinearLayout suPopup;
|
|
||||||
Spinner timeout;
|
|
||||||
ImageView appIcon;
|
|
||||||
TextView appNameView;
|
|
||||||
TextView packageNameView;
|
|
||||||
Button grant_btn;
|
|
||||||
Button deny_btn;
|
|
||||||
ImageView fingerprintImg;
|
|
||||||
TextView warning;
|
|
||||||
|
|
||||||
private SuConnector connector;
|
|
||||||
private Policy policy;
|
|
||||||
private CountDownTimer timer;
|
|
||||||
private FingerprintHelper fingerprintHelper;
|
|
||||||
|
|
||||||
class SuConnectorV1 extends SuConnector {
|
|
||||||
|
|
||||||
SuConnectorV1(String name) throws IOException {
|
|
||||||
socket.connect(new LocalSocketAddress(name, LocalSocketAddress.Namespace.FILESYSTEM));
|
|
||||||
new FileObserver(name) {
|
|
||||||
@Override
|
|
||||||
public void onEvent(int fileEvent, String path) {
|
|
||||||
if (fileEvent == FileObserver.DELETE_SELF) {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.startWatching();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void response() {
|
|
||||||
try (OutputStream out = getOutputStream()) {
|
|
||||||
out.write((policy.policy == Policy.ALLOW ? "socket:ALLOW" : "socket:DENY").getBytes());
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SuConnectorV2 extends SuConnector {
|
|
||||||
|
|
||||||
SuConnectorV2(String name) throws IOException {
|
|
||||||
socket.connect(new LocalSocketAddress(name, LocalSocketAddress.Namespace.ABSTRACT));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void response() {
|
|
||||||
try (DataOutputStream out = getOutputStream()) {
|
|
||||||
out.writeInt(policy.policy);
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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();
|
|
||||||
mm.mDB.clearOutdated();
|
|
||||||
|
|
||||||
// Get policy
|
|
||||||
Intent intent = getIntent();
|
|
||||||
try {
|
|
||||||
connector = intent.getIntExtra("version", 1) == 1 ?
|
|
||||||
new SuConnectorV1(intent.getStringExtra("socket")) :
|
|
||||||
new SuConnectorV2(intent.getStringExtra("socket"));
|
|
||||||
Bundle bundle = connector.readSocketInput();
|
|
||||||
int uid = Integer.parseInt(bundle.getString("uid"));
|
|
||||||
policy = mm.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, Const.ORIG_PKG_NAME)) {
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (Data.suResponseType) {
|
|
||||||
case Const.Value.SU_AUTO_DENY:
|
|
||||||
handleAction(Policy.DENY, 0);
|
|
||||||
return;
|
|
||||||
case Const.Value.SU_AUTO_ALLOW:
|
|
||||||
handleAction(Policy.ALLOW, 0);
|
|
||||||
return;
|
|
||||||
case Const.Value.SU_PROMPT:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not interactive, response directly
|
|
||||||
if (policy.policy != Policy.INTERACTIVE) {
|
|
||||||
handleAction();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_request);
|
|
||||||
ViewBinder.bind(this);
|
|
||||||
|
|
||||||
appIcon.setImageDrawable(policy.info.loadIcon(pm));
|
|
||||||
appNameView.setText(policy.appName);
|
|
||||||
packageNameView.setText(policy.packageName);
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
timer = new CountDownTimer(Data.suRequestTimeout * 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 useFingerprint = Data.suFingerprint && FingerprintHelper.canUseFingerprint();
|
|
||||||
|
|
||||||
if (useFingerprint) {
|
|
||||||
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();
|
|
||||||
useFingerprint = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!useFingerprint) {
|
|
||||||
grant_btn.setOnClickListener(v -> {
|
|
||||||
handleAction(Policy.ALLOW);
|
|
||||||
timer.cancel();
|
|
||||||
});
|
|
||||||
grant_btn.requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
grant_btn.setVisibility(useFingerprint ? View.GONE : View.VISIBLE);
|
|
||||||
fingerprintImg.setVisibility(useFingerprint ? 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) {
|
|
||||||
handleAction(action, Const.Value.timeoutList[timeout.getSelectedItemPosition()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleAction(int action, int time) {
|
|
||||||
policy.policy = action;
|
|
||||||
if (time >= 0) {
|
|
||||||
policy.until = (time == 0) ? 0 : (System.currentTimeMillis() / 1000 + time * 60);
|
|
||||||
mm.mDB.addPolicy(policy);
|
|
||||||
}
|
|
||||||
handleAction();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,293 +0,0 @@
|
|||||||
package com.topjohnwu.magisk;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.adapters.ApplicationAdapter;
|
|
||||||
import com.topjohnwu.magisk.adapters.ModulesAdapter;
|
|
||||||
import com.topjohnwu.magisk.adapters.PolicyAdapter;
|
|
||||||
import com.topjohnwu.magisk.adapters.ReposAdapter;
|
|
||||||
import com.topjohnwu.magisk.adapters.SuLogAdapter;
|
|
||||||
import com.topjohnwu.magisk.components.AboutCardRow;
|
|
||||||
import com.topjohnwu.magisk.components.CustomAlertDialog;
|
|
||||||
import com.topjohnwu.magisk.fragments.LogFragment;
|
|
||||||
import com.topjohnwu.magisk.fragments.MagiskFragment;
|
|
||||||
import com.topjohnwu.magisk.fragments.MagiskHideFragment;
|
|
||||||
import com.topjohnwu.magisk.fragments.MagiskLogFragment;
|
|
||||||
import com.topjohnwu.magisk.fragments.ModulesFragment;
|
|
||||||
import com.topjohnwu.magisk.fragments.ReposFragment;
|
|
||||||
import com.topjohnwu.magisk.fragments.SuLogFragment;
|
|
||||||
import com.topjohnwu.magisk.fragments.SuperuserFragment;
|
|
||||||
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
|
|
||||||
public class ViewBinder {
|
|
||||||
|
|
||||||
public static void bind(MainActivity target) {
|
|
||||||
target.drawer = target.findViewById(R.id.drawer_layout);
|
|
||||||
target.toolbar = target.findViewById(R.id.toolbar);
|
|
||||||
target.navigationView = target.findViewById(R.id.nav_view);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void bind(AboutActivity target) {
|
|
||||||
target.toolbar = target.findViewById(R.id.toolbar);
|
|
||||||
target.appVersionInfo = target.findViewById(R.id.app_version_info);
|
|
||||||
target.appChangelog = target.findViewById(R.id.app_changelog);
|
|
||||||
target.appTranslators = target.findViewById(R.id.app_translators);
|
|
||||||
target.appSourceCode = target.findViewById(R.id.app_source_code);
|
|
||||||
target.supportThread = target.findViewById(R.id.support_thread);
|
|
||||||
target.twitter = target.findViewById(R.id.follow_twitter);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void bind(DonationActivity target) {
|
|
||||||
target.toolbar = target.findViewById(R.id.toolbar);
|
|
||||||
target.paypal = target.findViewById(R.id.paypal);
|
|
||||||
target.patreon = target.findViewById(R.id.patreon);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void bind(FlashActivity target) {
|
|
||||||
target.toolbar = target.findViewById(R.id.toolbar);
|
|
||||||
target.flashLogs = target.findViewById(R.id.txtLog);
|
|
||||||
target.buttonPanel = target.findViewById(R.id.button_panel);
|
|
||||||
target.sv = target.findViewById(R.id.scrollView);
|
|
||||||
target.reboot = target.findViewById(R.id.reboot);
|
|
||||||
target.reboot.setOnClickListener(v -> target.reboot());
|
|
||||||
target.findViewById(R.id.no_thanks).setOnClickListener(v -> target.finish());
|
|
||||||
target.findViewById(R.id.save_logs).setOnClickListener(v -> target.saveLogs());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void bind(SuRequestActivity target) {
|
|
||||||
target.suPopup = target.findViewById(R.id.su_popup);
|
|
||||||
target.timeout = target.findViewById(R.id.timeout);
|
|
||||||
target.appIcon = target.findViewById(R.id.app_icon);
|
|
||||||
target.appNameView = target.findViewById(R.id.app_name);
|
|
||||||
target.packageNameView = target.findViewById(R.id.package_name);
|
|
||||||
target.grant_btn = target.findViewById(R.id.grant_btn);
|
|
||||||
target.deny_btn = target.findViewById(R.id.deny_btn);
|
|
||||||
target.fingerprintImg = target.findViewById(R.id.fingerprint);
|
|
||||||
target.warning = target.findViewById(R.id.warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void bind(LogFragment target, View v) {
|
|
||||||
target.viewPager = v.findViewById(R.id.container);
|
|
||||||
target.tab = v.findViewById(R.id.tab);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void unbind(LogFragment target) {
|
|
||||||
target.viewPager = null;
|
|
||||||
target.tab = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void bind(MagiskFragment target, View v) {
|
|
||||||
target.mSwipeRefreshLayout = v.findViewById(R.id.swipeRefreshLayout);
|
|
||||||
target.coreOnlyNotice = v.findViewById(R.id.core_only_notice);
|
|
||||||
target.magiskUpdate = v.findViewById(R.id.magisk_update);
|
|
||||||
target.magiskUpdateIcon = v.findViewById(R.id.magisk_update_icon);
|
|
||||||
target.magiskUpdateText = v.findViewById(R.id.magisk_update_status);
|
|
||||||
target.magiskUpdateProgress = v.findViewById(R.id.magisk_update_progress);
|
|
||||||
target.magiskStatusIcon = v.findViewById(R.id.magisk_status_icon);
|
|
||||||
target.magiskVersionText = v.findViewById(R.id.magisk_version);
|
|
||||||
target.safetyNetCard = v.findViewById(R.id.safetyNet_card);
|
|
||||||
target.safetyNetRefreshIcon = v.findViewById(R.id.safetyNet_refresh);
|
|
||||||
target.safetyNetStatusText = v.findViewById(R.id.safetyNet_status);
|
|
||||||
target.safetyNetProgress = v.findViewById(R.id.safetyNet_check_progress);
|
|
||||||
target.expandLayout = v.findViewById(R.id.expand_layout);
|
|
||||||
target.ctsStatusIcon = v.findViewById(R.id.cts_status_icon);
|
|
||||||
target.ctsStatusText = v.findViewById(R.id.cts_status);
|
|
||||||
target.basicStatusIcon = v.findViewById(R.id.basic_status_icon);
|
|
||||||
target.basicStatusText = v.findViewById(R.id.basic_status);
|
|
||||||
target.installOptionCard = v.findViewById(R.id.install_option_card);
|
|
||||||
target.keepEncChkbox = v.findViewById(R.id.keep_force_enc);
|
|
||||||
target.keepVerityChkbox = v.findViewById(R.id.keep_verity);
|
|
||||||
target.installButton = v.findViewById(R.id.install_button);
|
|
||||||
target.installText = v.findViewById(R.id.install_text);
|
|
||||||
target.uninstallButton = v.findViewById(R.id.uninstall_button);
|
|
||||||
|
|
||||||
v.findViewById(R.id.safetyNet_title).setOnClickListener(v1 -> target.safetyNet());
|
|
||||||
v.findViewById(R.id.install_button).setOnClickListener(v1 -> target.install());
|
|
||||||
v.findViewById(R.id.uninstall_button).setOnClickListener(v1 -> target.uninstall());
|
|
||||||
|
|
||||||
Context ctx = target.getContext();
|
|
||||||
target.colorBad = ContextCompat.getColor(ctx, R.color.red500);
|
|
||||||
target.colorOK = ContextCompat.getColor(ctx, R.color.green500);
|
|
||||||
target.colorWarn = ContextCompat.getColor(ctx, R.color.yellow500);
|
|
||||||
target.colorNeutral = ContextCompat.getColor(ctx, R.color.grey500);
|
|
||||||
target.colorInfo = ContextCompat.getColor(ctx, R.color.blue500);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void unbind(MagiskFragment target) {
|
|
||||||
target.mSwipeRefreshLayout = null;
|
|
||||||
target.coreOnlyNotice = null;
|
|
||||||
target.magiskUpdate = null;
|
|
||||||
target.magiskUpdateIcon = null;
|
|
||||||
target.magiskUpdateText = null;
|
|
||||||
target.magiskUpdateProgress = null;
|
|
||||||
target.magiskStatusIcon = null;
|
|
||||||
target.magiskVersionText = null;
|
|
||||||
target.safetyNetCard = null;
|
|
||||||
target.safetyNetRefreshIcon = null;
|
|
||||||
target.safetyNetStatusText = null;
|
|
||||||
target.safetyNetProgress = null;
|
|
||||||
target.expandLayout = null;
|
|
||||||
target.ctsStatusIcon = null;
|
|
||||||
target.ctsStatusText = null;
|
|
||||||
target.basicStatusIcon = null;
|
|
||||||
target.basicStatusText = null;
|
|
||||||
target.installOptionCard = null;
|
|
||||||
target.keepEncChkbox = null;
|
|
||||||
target.keepVerityChkbox = null;
|
|
||||||
target.installButton = null;
|
|
||||||
target.installText = null;
|
|
||||||
target.uninstallButton = null;
|
|
||||||
|
|
||||||
View v = target.getView();
|
|
||||||
v.findViewById(R.id.safetyNet_title).setOnClickListener(null);
|
|
||||||
v.findViewById(R.id.install_button).setOnClickListener(null);
|
|
||||||
v.findViewById(R.id.uninstall_button).setOnClickListener(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void bind(MagiskHideFragment target, View v) {
|
|
||||||
target.mSwipeRefreshLayout = v.findViewById(R.id.swipeRefreshLayout);
|
|
||||||
target.recyclerView = v.findViewById(R.id.recyclerView);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void unbind(MagiskHideFragment target) {
|
|
||||||
target.mSwipeRefreshLayout = null;
|
|
||||||
target.recyclerView = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void bind(MagiskLogFragment target, View v) {
|
|
||||||
target.txtLog = v.findViewById(R.id.txtLog);
|
|
||||||
target.svLog = v.findViewById(R.id.svLog);
|
|
||||||
target.hsvLog = v.findViewById(R.id.hsvLog);
|
|
||||||
target.progressBar = v.findViewById(R.id.progressBar);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void unbind(MagiskLogFragment target) {
|
|
||||||
target.txtLog = null;
|
|
||||||
target.svLog = null;
|
|
||||||
target.hsvLog = null;
|
|
||||||
target.progressBar = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void bind(ModulesFragment target, View v) {
|
|
||||||
target.mSwipeRefreshLayout = v.findViewById(R.id.swipeRefreshLayout);
|
|
||||||
target.recyclerView = v.findViewById(R.id.recyclerView);
|
|
||||||
target.emptyRv = v.findViewById(R.id.empty_rv);
|
|
||||||
v.findViewById(R.id.fab).setOnClickListener(v1 -> target.selectFile());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void unbind(ModulesFragment target) {
|
|
||||||
target.mSwipeRefreshLayout = null;
|
|
||||||
target.recyclerView = null;
|
|
||||||
target.emptyRv = null;
|
|
||||||
View v = target.getView();
|
|
||||||
v.findViewById(R.id.fab).setOnClickListener(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void bind(ReposFragment target, View source) {
|
|
||||||
target.recyclerView = source.findViewById(R.id.recyclerView);
|
|
||||||
target.emptyRv = source.findViewById(R.id.empty_rv);
|
|
||||||
target.mSwipeRefreshLayout = source.findViewById(R.id.swipeRefreshLayout);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void unbind(ReposFragment target) {
|
|
||||||
target.recyclerView = null;
|
|
||||||
target.emptyRv = null;
|
|
||||||
target.mSwipeRefreshLayout = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void bind(SuLogFragment target, View source) {
|
|
||||||
target.emptyRv = source.findViewById(R.id.empty_rv);
|
|
||||||
target.recyclerView = source.findViewById(R.id.recyclerView);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void unbind(SuLogFragment target) {
|
|
||||||
target.emptyRv = null;
|
|
||||||
target.recyclerView = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void bind(SuperuserFragment target, View source) {
|
|
||||||
target.recyclerView = source.findViewById(R.id.recyclerView);
|
|
||||||
target.emptyRv = source.findViewById(R.id.empty_rv);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void unbind(SuperuserFragment target) {
|
|
||||||
target.emptyRv = null;
|
|
||||||
target.recyclerView = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void bind(CustomAlertDialog.ViewHolder target, View source) {
|
|
||||||
target.dialogLayout = source.findViewById(R.id.dialog_layout);
|
|
||||||
target.buttons = source.findViewById(R.id.button_panel);
|
|
||||||
target.messageView = source.findViewById(R.id.message);
|
|
||||||
target.negative = source.findViewById(R.id.negative);
|
|
||||||
target.positive = source.findViewById(R.id.positive);
|
|
||||||
target.neutral = source.findViewById(R.id.neutral);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void bind(AboutCardRow target, View source) {
|
|
||||||
target.mTitle = source.findViewById(android.R.id.title);
|
|
||||||
target.mSummary = source.findViewById(android.R.id.summary);
|
|
||||||
target.mIcon = source.findViewById(android.R.id.icon);
|
|
||||||
target.mView = source.findViewById(R.id.container);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void bind(ApplicationAdapter.ViewHolder target, View source) {
|
|
||||||
target.appIcon = source.findViewById(R.id.app_icon);
|
|
||||||
target.appName = source.findViewById(R.id.app_name);
|
|
||||||
target.appPackage = source.findViewById(R.id.package_name);
|
|
||||||
target.checkBox = source.findViewById(R.id.checkbox);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void bind(ModulesAdapter.ViewHolder target, View source) {
|
|
||||||
target.title = source.findViewById(R.id.title);
|
|
||||||
target.versionName = source.findViewById(R.id.version_name);
|
|
||||||
target.description = source.findViewById(R.id.description);
|
|
||||||
target.notice = source.findViewById(R.id.notice);
|
|
||||||
target.checkBox = source.findViewById(R.id.checkbox);
|
|
||||||
target.author = source.findViewById(R.id.author);
|
|
||||||
target.delete = source.findViewById(R.id.delete);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void bind(PolicyAdapter.ViewHolder target, View source) {
|
|
||||||
target.appName = source.findViewById(R.id.app_name);
|
|
||||||
target.packageName = source.findViewById(R.id.package_name);
|
|
||||||
target.appIcon = source.findViewById(R.id.app_icon);
|
|
||||||
target.masterSwitch = source.findViewById(R.id.master_switch);
|
|
||||||
target.notificationSwitch = source.findViewById(R.id.notification_switch);
|
|
||||||
target.loggingSwitch = source.findViewById(R.id.logging_switch);
|
|
||||||
target.expandLayout = source.findViewById(R.id.expand_layout);
|
|
||||||
target.delete = source.findViewById(R.id.delete);
|
|
||||||
target.moreInfo = source.findViewById(R.id.more_info);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void bind(ReposAdapter.SectionHolder target, View source) {
|
|
||||||
target.sectionText = source.findViewById(R.id.section_text);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void bind(ReposAdapter.RepoHolder target, View source) {
|
|
||||||
target.title = source.findViewById(R.id.title);
|
|
||||||
target.versionName = source.findViewById(R.id.version_name);
|
|
||||||
target.description = source.findViewById(R.id.description);
|
|
||||||
target.author = source.findViewById(R.id.author);
|
|
||||||
target.infoLayout = source.findViewById(R.id.info_layout);
|
|
||||||
target.downloadImage = source.findViewById(R.id.download);
|
|
||||||
target.updateTime = source.findViewById(R.id.update_time);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void bind(SuLogAdapter.SectionHolder target, View source) {
|
|
||||||
target.date = source.findViewById(R.id.date);
|
|
||||||
target.arrow = source.findViewById(R.id.arrow);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void bind(SuLogAdapter.LogViewHolder target, View source) {
|
|
||||||
target.appName = source.findViewById(R.id.app_name);
|
|
||||||
target.action = source.findViewById(R.id.action);
|
|
||||||
target.time = source.findViewById(R.id.time);
|
|
||||||
target.fromPid = source.findViewById(R.id.fromPid);
|
|
||||||
target.toUid = source.findViewById(R.id.toUid);
|
|
||||||
target.command = source.findViewById(R.id.command);
|
|
||||||
target.expandLayout = source.findViewById(R.id.expand_layout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.adapters;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.pm.ApplicationInfo;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.CheckBox;
|
|
||||||
import android.widget.Filter;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.ViewBinder;
|
|
||||||
import com.topjohnwu.magisk.utils.LocaleManager;
|
|
||||||
import com.topjohnwu.magisk.utils.Topic;
|
|
||||||
import com.topjohnwu.superuser.Shell;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
public class ApplicationAdapter extends RecyclerView.Adapter<ApplicationAdapter.ViewHolder> {
|
|
||||||
|
|
||||||
private List<ApplicationInfo> fullList, showList;
|
|
||||||
private List<String> hideList;
|
|
||||||
private PackageManager pm;
|
|
||||||
private ApplicationFilter filter;
|
|
||||||
|
|
||||||
public ApplicationAdapter(Context context) {
|
|
||||||
fullList = showList = Collections.emptyList();
|
|
||||||
hideList = Collections.emptyList();
|
|
||||||
filter = new ApplicationFilter();
|
|
||||||
pm = context.getPackageManager();
|
|
||||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(this::loadApps);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_app, parent, false);
|
|
||||||
return new ViewHolder(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getLabel(ApplicationInfo info) {
|
|
||||||
if (info.labelRes > 0) {
|
|
||||||
try {
|
|
||||||
Resources res = pm.getResourcesForApplication(info);
|
|
||||||
Configuration config = new Configuration();
|
|
||||||
config.setLocale(LocaleManager.locale);
|
|
||||||
res.updateConfiguration(config, res.getDisplayMetrics());
|
|
||||||
return res.getString(info.labelRes);
|
|
||||||
} catch (PackageManager.NameNotFoundException ignored) { /* Impossible */ }
|
|
||||||
}
|
|
||||||
return info.loadLabel(pm).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadApps() {
|
|
||||||
fullList = pm.getInstalledApplications(0);
|
|
||||||
hideList = Shell.su("magiskhide --ls").exec().getOut();
|
|
||||||
for (Iterator<ApplicationInfo> i = fullList.iterator(); i.hasNext(); ) {
|
|
||||||
ApplicationInfo info = i.next();
|
|
||||||
if (Const.HIDE_BLACKLIST.contains(info.packageName) || !info.enabled) {
|
|
||||||
i.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Collections.sort(fullList, (a, b) -> {
|
|
||||||
boolean ah = hideList.contains(a.packageName);
|
|
||||||
boolean bh = hideList.contains(b.packageName);
|
|
||||||
if (ah == bh) {
|
|
||||||
return getLabel(a).toLowerCase().compareTo(getLabel(b).toLowerCase());
|
|
||||||
} else if (ah) {
|
|
||||||
return -1;
|
|
||||||
} else {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Topic.publish(false, Topic.MAGISK_HIDE_DONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
|
||||||
ApplicationInfo info = showList.get(position);
|
|
||||||
|
|
||||||
holder.appIcon.setImageDrawable(info.loadIcon(pm));
|
|
||||||
holder.appName.setText(getLabel(info));
|
|
||||||
holder.appPackage.setText(info.packageName);
|
|
||||||
|
|
||||||
holder.checkBox.setOnCheckedChangeListener(null);
|
|
||||||
holder.checkBox.setChecked(hideList.contains(info.packageName));
|
|
||||||
holder.checkBox.setOnCheckedChangeListener((v, isChecked) -> {
|
|
||||||
if (isChecked) {
|
|
||||||
Shell.su("magiskhide --add " + info.packageName).submit();
|
|
||||||
hideList.add(info.packageName);
|
|
||||||
} else {
|
|
||||||
Shell.su("magiskhide --rm " + info.packageName).submit();
|
|
||||||
hideList.remove(info.packageName);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return showList.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void filter(String constraint) {
|
|
||||||
filter.filter(constraint);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void refresh() {
|
|
||||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(this::loadApps);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
public ImageView appIcon;
|
|
||||||
public TextView appName;
|
|
||||||
public TextView appPackage;
|
|
||||||
public CheckBox checkBox;
|
|
||||||
|
|
||||||
ViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
ViewBinder.bind(this, itemView);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ApplicationFilter extends Filter {
|
|
||||||
|
|
||||||
private boolean lowercaseContains(String s, CharSequence filter) {
|
|
||||||
return !TextUtils.isEmpty(s) && s.toLowerCase().contains(filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected FilterResults performFiltering(CharSequence constraint) {
|
|
||||||
if (constraint == null || constraint.length() == 0) {
|
|
||||||
showList = fullList;
|
|
||||||
} else {
|
|
||||||
showList = new ArrayList<>();
|
|
||||||
String filter = constraint.toString().toLowerCase();
|
|
||||||
for (ApplicationInfo info : fullList) {
|
|
||||||
if (lowercaseContains(getLabel(info), filter)
|
|
||||||
|| lowercaseContains(info.packageName, filter)) {
|
|
||||||
showList.add(info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void publishResults(CharSequence constraint, FilterResults results) {
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,124 +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 com.google.android.material.snackbar.Snackbar;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.ViewBinder;
|
|
||||||
import com.topjohnwu.magisk.components.SnackbarMaker;
|
|
||||||
import com.topjohnwu.magisk.container.Module;
|
|
||||||
import com.topjohnwu.superuser.Shell;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
public class ModulesAdapter extends RecyclerView.Adapter<ModulesAdapter.ViewHolder> {
|
|
||||||
|
|
||||||
private final List<Module> mList;
|
|
||||||
|
|
||||||
public ModulesAdapter(List<Module> list) {
|
|
||||||
mList = list;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
public TextView title;
|
|
||||||
public TextView versionName;
|
|
||||||
public TextView description;
|
|
||||||
public TextView notice;
|
|
||||||
public CheckBox checkBox;
|
|
||||||
public TextView author;
|
|
||||||
public ImageView delete;
|
|
||||||
|
|
||||||
ViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
ViewBinder.bind(this, itemView);
|
|
||||||
|
|
||||||
if (!Shell.rootAccess()) {
|
|
||||||
checkBox.setEnabled(false);
|
|
||||||
delete.setEnabled(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.adapters;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.Switch;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.ViewBinder;
|
|
||||||
import com.topjohnwu.magisk.components.CustomAlertDialog;
|
|
||||||
import com.topjohnwu.magisk.components.ExpandableView;
|
|
||||||
import com.topjohnwu.magisk.components.SnackbarMaker;
|
|
||||||
import com.topjohnwu.magisk.container.Policy;
|
|
||||||
import com.topjohnwu.magisk.database.MagiskDatabaseHelper;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
public class PolicyAdapter extends RecyclerView.Adapter<PolicyAdapter.ViewHolder> {
|
|
||||||
|
|
||||||
private List<Policy> policyList;
|
|
||||||
private MagiskDatabaseHelper dbHelper;
|
|
||||||
private PackageManager pm;
|
|
||||||
private Set<Policy> expandList = new HashSet<>();
|
|
||||||
|
|
||||||
public PolicyAdapter(List<Policy> list, MagiskDatabaseHelper db, PackageManager pm) {
|
|
||||||
policyList = list;
|
|
||||||
dbHelper = db;
|
|
||||||
this.pm = pm;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ViewHolder onCreateViewHolder(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(ViewHolder holder, int position) {
|
|
||||||
Policy policy = policyList.get(position);
|
|
||||||
|
|
||||||
holder.setExpanded(expandList.contains(policy));
|
|
||||||
|
|
||||||
holder.itemView.setOnClickListener(view -> {
|
|
||||||
if (holder.isExpanded()) {
|
|
||||||
holder.collapse();
|
|
||||||
expandList.remove(policy);
|
|
||||||
} else {
|
|
||||||
holder.expand();
|
|
||||||
expandList.add(policy);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
holder.appName.setText(policy.appName);
|
|
||||||
holder.packageName.setText(policy.packageName);
|
|
||||||
holder.appIcon.setImageDrawable(policy.info.loadIcon(pm));
|
|
||||||
holder.masterSwitch.setOnCheckedChangeListener((v, isChecked) -> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
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 -> 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, (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);
|
|
||||||
})
|
|
||||||
.setNegativeButton(R.string.no_thanks, null)
|
|
||||||
.setCancelable(true)
|
|
||||||
.show());
|
|
||||||
holder.masterSwitch.setChecked(policy.policy == Policy.ALLOW);
|
|
||||||
holder.notificationSwitch.setChecked(policy.notification);
|
|
||||||
holder.loggingSwitch.setChecked(policy.logging);
|
|
||||||
|
|
||||||
// Hide for now
|
|
||||||
holder.moreInfo.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return policyList.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ViewHolder extends RecyclerView.ViewHolder implements ExpandableView {
|
|
||||||
|
|
||||||
public TextView appName;
|
|
||||||
public TextView packageName;
|
|
||||||
public ImageView appIcon;
|
|
||||||
public Switch masterSwitch;
|
|
||||||
public Switch notificationSwitch;
|
|
||||||
public Switch loggingSwitch;
|
|
||||||
public ViewGroup expandLayout;
|
|
||||||
|
|
||||||
public ImageView delete;
|
|
||||||
public ImageView moreInfo;
|
|
||||||
|
|
||||||
private Container container = new Container();
|
|
||||||
|
|
||||||
public ViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
ViewBinder.bind(this, itemView);
|
|
||||||
container.expandLayout = expandLayout;
|
|
||||||
setupExpandable();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Container getContainer() {
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.adapters;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
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.LinearLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.ViewBinder;
|
|
||||||
import com.topjohnwu.magisk.asyncs.MarkDownWindow;
|
|
||||||
import com.topjohnwu.magisk.asyncs.ProcessRepoZip;
|
|
||||||
import com.topjohnwu.magisk.components.BaseActivity;
|
|
||||||
import com.topjohnwu.magisk.components.CustomAlertDialog;
|
|
||||||
import com.topjohnwu.magisk.container.Module;
|
|
||||||
import com.topjohnwu.magisk.container.Repo;
|
|
||||||
import com.topjohnwu.magisk.database.RepoDatabaseHelper;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
public class ReposAdapter extends SectionedAdapter<ReposAdapter.SectionHolder, ReposAdapter.RepoHolder> {
|
|
||||||
|
|
||||||
private static final int UPDATES = 0;
|
|
||||||
private static final int INSTALLED = 1;
|
|
||||||
private static final int OTHERS = 2;
|
|
||||||
|
|
||||||
private Cursor repoCursor = null;
|
|
||||||
private Map<String, Module> moduleMap;
|
|
||||||
private RepoDatabaseHelper repoDB;
|
|
||||||
private List<Pair<Integer, List<Repo>>> repoPairs;
|
|
||||||
|
|
||||||
public ReposAdapter(RepoDatabaseHelper db, Map<String, Module> map) {
|
|
||||||
repoDB = db;
|
|
||||||
moduleMap = map;
|
|
||||||
repoPairs = new ArrayList<>();
|
|
||||||
notifyDBChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@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 ->
|
|
||||||
new MarkDownWindow((BaseActivity) context, null, repo.getDetailUrl()).exec());
|
|
||||||
|
|
||||||
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) ->
|
|
||||||
new ProcessRepoZip((BaseActivity) context, repo, true).exec()
|
|
||||||
)
|
|
||||||
.setNeutralButton(R.string.download, (d, i) ->
|
|
||||||
new ProcessRepoZip((BaseActivity) context, repo, false).exec())
|
|
||||||
.setNegativeButton(R.string.no_thanks, null)
|
|
||||||
.show();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void notifyDBChanged() {
|
|
||||||
if (repoCursor != null)
|
|
||||||
repoCursor.close();
|
|
||||||
repoCursor = repoDB.getRepoCursor();
|
|
||||||
filter("");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void filter(String s) {
|
|
||||||
List<Repo> updates = new ArrayList<>();
|
|
||||||
List<Repo> installed = new ArrayList<>();
|
|
||||||
List<Repo> others = new ArrayList<>();
|
|
||||||
|
|
||||||
repoPairs.clear();
|
|
||||||
while (repoCursor.moveToNext()) {
|
|
||||||
Repo repo = new Repo(repoCursor);
|
|
||||||
if (repo.getName().toLowerCase().contains(s.toLowerCase())
|
|
||||||
|| repo.getAuthor().toLowerCase().contains(s.toLowerCase())
|
|
||||||
|| repo.getDescription().toLowerCase().contains(s.toLowerCase())
|
|
||||||
) {
|
|
||||||
// Passed the repoFilter
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
repoCursor.moveToFirst();
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class SectionHolder extends RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
public TextView sectionText;
|
|
||||||
|
|
||||||
SectionHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
ViewBinder.bind(this, itemView);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class RepoHolder extends RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
public TextView title;
|
|
||||||
public TextView versionName;
|
|
||||||
public TextView description;
|
|
||||||
public TextView author;
|
|
||||||
public LinearLayout infoLayout;
|
|
||||||
public ImageView downloadImage;
|
|
||||||
public TextView updateTime;
|
|
||||||
|
|
||||||
RepoHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
ViewBinder.bind(this, itemView);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.adapters;
|
|
||||||
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
final public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
|
||||||
if (viewType == SECTION_TYPE)
|
|
||||||
return onCreateSectionViewHolder(parent);
|
|
||||||
return onCreateItemViewHolder(parent, viewType);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
final public void onBindViewHolder(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,154 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.adapters;
|
|
||||||
|
|
||||||
import android.database.Cursor;
|
|
||||||
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 com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.ViewBinder;
|
|
||||||
import com.topjohnwu.magisk.components.ExpandableView;
|
|
||||||
import com.topjohnwu.magisk.container.SuLogEntry;
|
|
||||||
import com.topjohnwu.magisk.database.MagiskDatabaseHelper;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
public class SuLogAdapter extends SectionedAdapter<SuLogAdapter.SectionHolder, SuLogAdapter.LogViewHolder> {
|
|
||||||
|
|
||||||
private List<List<Integer>> logEntryList;
|
|
||||||
private Set<Integer> itemExpanded, sectionExpanded;
|
|
||||||
private MagiskDatabaseHelper suDB;
|
|
||||||
private Cursor suLogCursor = null;
|
|
||||||
|
|
||||||
public SuLogAdapter(MagiskDatabaseHelper db) {
|
|
||||||
suDB = db;
|
|
||||||
logEntryList = Collections.emptyList();
|
|
||||||
sectionExpanded = new HashSet<>();
|
|
||||||
itemExpanded = new HashSet<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getSectionCount() {
|
|
||||||
return logEntryList.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount(int section) {
|
|
||||||
return sectionExpanded.contains(section) ? logEntryList.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) {
|
|
||||||
suLogCursor.moveToPosition(logEntryList.get(section).get(0));
|
|
||||||
SuLogEntry entry = new SuLogEntry(suLogCursor);
|
|
||||||
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), logEntryList.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), logEntryList.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) {
|
|
||||||
int sqlPosition = logEntryList.get(section).get(position);
|
|
||||||
suLogCursor.moveToPosition(sqlPosition);
|
|
||||||
SuLogEntry entry = new SuLogEntry(suLogCursor);
|
|
||||||
holder.setExpanded(itemExpanded.contains(sqlPosition));
|
|
||||||
holder.itemView.setOnClickListener(view -> {
|
|
||||||
if (holder.isExpanded()) {
|
|
||||||
holder.collapse();
|
|
||||||
itemExpanded.remove(sqlPosition);
|
|
||||||
} else {
|
|
||||||
holder.expand();
|
|
||||||
itemExpanded.add(sqlPosition);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
holder.appName.setText(entry.appName);
|
|
||||||
holder.action.setText(entry.action ? R.string.grant : R.string.deny);
|
|
||||||
holder.command.setText(entry.command);
|
|
||||||
holder.fromPid.setText(String.valueOf(entry.fromPid));
|
|
||||||
holder.toUid.setText(String.valueOf(entry.toUid));
|
|
||||||
holder.time.setText(entry.getTimeString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void notifyDBChanged() {
|
|
||||||
if (suLogCursor != null)
|
|
||||||
suLogCursor.close();
|
|
||||||
suLogCursor = suDB.getLogCursor();
|
|
||||||
logEntryList = suDB.getLogStructure();
|
|
||||||
itemExpanded.clear();
|
|
||||||
sectionExpanded.clear();
|
|
||||||
sectionExpanded.add(0);
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class SectionHolder extends RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
public TextView date;
|
|
||||||
public ImageView arrow;
|
|
||||||
|
|
||||||
SectionHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
ViewBinder.bind(this, itemView);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class LogViewHolder extends RecyclerView.ViewHolder implements ExpandableView {
|
|
||||||
|
|
||||||
public TextView appName;
|
|
||||||
public TextView action;
|
|
||||||
public TextView time;
|
|
||||||
public TextView fromPid;
|
|
||||||
public TextView toUid;
|
|
||||||
public TextView command;
|
|
||||||
public ViewGroup expandLayout;
|
|
||||||
|
|
||||||
private Container container = new Container();
|
|
||||||
|
|
||||||
LogViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
ViewBinder.bind(this, itemView);
|
|
||||||
container.expandLayout = expandLayout;
|
|
||||||
setupExpandable();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Container getContainer() {
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.adapters;
|
|
||||||
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
|
||||||
import androidx.fragment.app.FragmentPagerAdapter;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.asyncs;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.Data;
|
|
||||||
import com.topjohnwu.magisk.utils.ISafetyNetHelper;
|
|
||||||
import com.topjohnwu.magisk.utils.Topic;
|
|
||||||
import com.topjohnwu.magisk.utils.WebService;
|
|
||||||
import com.topjohnwu.superuser.Shell;
|
|
||||||
import com.topjohnwu.superuser.ShellUtils;
|
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
|
|
||||||
import dalvik.system.DexClassLoader;
|
|
||||||
|
|
||||||
public class CheckSafetyNet extends ParallelTask<Void, Void, Void> {
|
|
||||||
|
|
||||||
public static final File dexPath =
|
|
||||||
new File(Data.MM().getFilesDir().getParent() + "/snet", "snet.apk");
|
|
||||||
private ISafetyNetHelper helper;
|
|
||||||
|
|
||||||
public CheckSafetyNet(Activity activity) {
|
|
||||||
super(activity);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void dlSnet() throws Exception {
|
|
||||||
Shell.sh("rm -rf " + dexPath.getParent()).exec();
|
|
||||||
dexPath.getParentFile().mkdir();
|
|
||||||
HttpURLConnection conn = WebService.mustRequest(Data.snetLink, null);
|
|
||||||
try (
|
|
||||||
OutputStream out = new BufferedOutputStream(new FileOutputStream(dexPath));
|
|
||||||
InputStream in = new BufferedInputStream(conn.getInputStream())) {
|
|
||||||
ShellUtils.pump(in, out);
|
|
||||||
} finally {
|
|
||||||
conn.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void dyload() throws Exception {
|
|
||||||
DexClassLoader loader = new DexClassLoader(dexPath.getPath(), dexPath.getParent(),
|
|
||||||
null, ISafetyNetHelper.class.getClassLoader());
|
|
||||||
Class<?> clazz = loader.loadClass("com.topjohnwu.snet.Snet");
|
|
||||||
helper = (ISafetyNetHelper) clazz.getMethod("newHelper",
|
|
||||||
Class.class, String.class, Activity.class, Object.class)
|
|
||||||
.invoke(null, ISafetyNetHelper.class, dexPath.getPath(), getActivity(),
|
|
||||||
(ISafetyNetHelper.Callback) code ->
|
|
||||||
Topic.publish(false, Topic.SNET_CHECK_DONE, code));
|
|
||||||
if (helper.getVersion() < Data.snetVersionCode) {
|
|
||||||
throw new Exception();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Void... voids) {
|
|
||||||
try {
|
|
||||||
try {
|
|
||||||
dyload();
|
|
||||||
} catch (Exception e) {
|
|
||||||
// If dynamic load failed, try re-downloading and reload
|
|
||||||
dlSnet();
|
|
||||||
dyload();
|
|
||||||
}
|
|
||||||
// Run attestation
|
|
||||||
helper.attest();
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
Topic.publish(false, Topic.SNET_CHECK_DONE, -1);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.asyncs;
|
|
||||||
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.BuildConfig;
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.Data;
|
|
||||||
import com.topjohnwu.magisk.utils.NotificationMgr;
|
|
||||||
import com.topjohnwu.magisk.utils.Topic;
|
|
||||||
import com.topjohnwu.magisk.utils.WebService;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
public class CheckUpdates {
|
|
||||||
|
|
||||||
private static int getInt(JSONObject json, String name, int defValue) {
|
|
||||||
if (json == null)
|
|
||||||
return defValue;
|
|
||||||
try {
|
|
||||||
return json.getInt(name);
|
|
||||||
} catch (JSONException e) {
|
|
||||||
return defValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getString(JSONObject json, String name, String defValue) {
|
|
||||||
if (json == null)
|
|
||||||
return defValue;
|
|
||||||
try {
|
|
||||||
return json.getString(name);
|
|
||||||
} catch (JSONException e) {
|
|
||||||
return defValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static JSONObject getJson(JSONObject json, String name) {
|
|
||||||
try {
|
|
||||||
return json.getJSONObject(name);
|
|
||||||
} catch (JSONException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void fetchUpdates() {
|
|
||||||
String jsonStr = "";
|
|
||||||
switch (Data.updateChannel) {
|
|
||||||
case Const.Value.STABLE_CHANNEL:
|
|
||||||
jsonStr = WebService.getString(Const.Url.STABLE_URL);
|
|
||||||
break;
|
|
||||||
case Const.Value.BETA_CHANNEL:
|
|
||||||
jsonStr = WebService.getString(Const.Url.BETA_URL);
|
|
||||||
break;
|
|
||||||
case Const.Value.CUSTOM_CHANNEL:
|
|
||||||
jsonStr = WebService.getString(Data.MM().prefs.getString(Const.Key.CUSTOM_CHANNEL, ""));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
JSONObject json;
|
|
||||||
try {
|
|
||||||
json = new JSONObject(jsonStr);
|
|
||||||
} catch (JSONException e) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
JSONObject magisk = getJson(json, "magisk");
|
|
||||||
Data.remoteMagiskVersionString = getString(magisk, "version", null);
|
|
||||||
Data.remoteMagiskVersionCode = getInt(magisk, "versionCode", -1);
|
|
||||||
Data.magiskLink = getString(magisk, "link", null);
|
|
||||||
Data.magiskNoteLink = getString(magisk, "note", null);
|
|
||||||
Data.magiskMD5 = getString(magisk, "md5", null);
|
|
||||||
|
|
||||||
JSONObject manager = getJson(json, "app");
|
|
||||||
Data.remoteManagerVersionString = getString(manager, "version", null);
|
|
||||||
Data.remoteManagerVersionCode = getInt(manager, "versionCode", -1);
|
|
||||||
Data.managerLink = getString(manager, "link", null);
|
|
||||||
Data.managerNoteLink = getString(manager, "note", null);
|
|
||||||
|
|
||||||
JSONObject uninstaller = getJson(json, "uninstaller");
|
|
||||||
Data.uninstallerLink = getString(uninstaller, "link", null);
|
|
||||||
|
|
||||||
JSONObject snet = getJson(json, "snet");
|
|
||||||
Data.snetVersionCode = getInt(snet, "versionCode", -1);
|
|
||||||
Data.snetLink = getString(snet, "link", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void check(Runnable cb) {
|
|
||||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
|
||||||
fetchUpdates();
|
|
||||||
if (cb != null) {
|
|
||||||
if (BuildConfig.VERSION_CODE < Data.remoteManagerVersionCode) {
|
|
||||||
NotificationMgr.managerUpdate();
|
|
||||||
} else if (Data.magiskVersionCode < Data.remoteMagiskVersionCode) {
|
|
||||||
NotificationMgr.magiskUpdate();
|
|
||||||
}
|
|
||||||
cb.run();
|
|
||||||
}
|
|
||||||
Topic.publish(Topic.UPDATE_CHECK_DONE);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void check() {
|
|
||||||
check(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.asyncs;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.Data;
|
|
||||||
import com.topjohnwu.magisk.FlashActivity;
|
|
||||||
import com.topjohnwu.magisk.MagiskManager;
|
|
||||||
import com.topjohnwu.magisk.components.SnackbarMaker;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
import com.topjohnwu.magisk.utils.ZipUtils;
|
|
||||||
import com.topjohnwu.superuser.Shell;
|
|
||||||
import com.topjohnwu.superuser.ShellUtils;
|
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class FlashZip extends ParallelTask<Void, Void, Integer> {
|
|
||||||
|
|
||||||
private Uri mUri;
|
|
||||||
private File mCachedFile;
|
|
||||||
private List<String> console, logs;
|
|
||||||
|
|
||||||
public FlashZip(Activity context, Uri uri, List<String> console, List<String> logs) {
|
|
||||||
super(context);
|
|
||||||
mUri = uri;
|
|
||||||
this.console = console;
|
|
||||||
this.logs = logs;
|
|
||||||
mCachedFile = new File(context.getCacheDir(), "install.zip");
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean unzipAndCheck() throws Exception {
|
|
||||||
ZipUtils.unzip(mCachedFile, mCachedFile.getParentFile(), "META-INF/com/google/android", true);
|
|
||||||
return ShellUtils.fastCmdResult("grep -q '#MAGISK' " + new File(mCachedFile.getParentFile(), "updater-script"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Integer doInBackground(Void... voids) {
|
|
||||||
MagiskManager mm = Data.MM();
|
|
||||||
try {
|
|
||||||
console.add("- Copying zip to temp directory");
|
|
||||||
|
|
||||||
mCachedFile.delete();
|
|
||||||
try (
|
|
||||||
InputStream in = mm.getContentResolver().openInputStream(mUri);
|
|
||||||
OutputStream out = new BufferedOutputStream(new FileOutputStream(mCachedFile))
|
|
||||||
) {
|
|
||||||
if (in == null) throw new FileNotFoundException();
|
|
||||||
InputStream buf= new BufferedInputStream(in);
|
|
||||||
ShellUtils.pump(buf, out);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
console.add("! Invalid Uri");
|
|
||||||
throw e;
|
|
||||||
} catch (IOException e) {
|
|
||||||
console.add("! Cannot copy to cache");
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
if (!unzipAndCheck()) return 0;
|
|
||||||
console.add("- Installing " + Utils.getNameFromUri(mm, mUri));
|
|
||||||
if (!Shell.su("cd " + mCachedFile.getParent(),
|
|
||||||
"BOOTMODE=true sh update-binary dummy 1 " + mCachedFile)
|
|
||||||
.to(console, logs)
|
|
||||||
.exec().isSuccess())
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
console.add("- All done!");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -1 = error, manual install; 0 = invalid zip; 1 = success
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Integer result) {
|
|
||||||
FlashActivity activity = (FlashActivity) getActivity();
|
|
||||||
Shell.su("rm -rf " + mCachedFile.getParent(), "rm -rf " + Const.TMP_FOLDER_PATH).submit();
|
|
||||||
switch (result) {
|
|
||||||
case -1:
|
|
||||||
console.add("! Installation failed");
|
|
||||||
SnackbarMaker.showUri(getActivity(), mUri);
|
|
||||||
break;
|
|
||||||
case 0:
|
|
||||||
console.add("! This zip is not a Magisk Module!");
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
// Reload modules
|
|
||||||
Utils.loadModules();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
activity.reboot.setVisibility(result > 0 ? View.VISIBLE : View.GONE);
|
|
||||||
activity.buttonPanel.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,398 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.asyncs;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.ProgressDialog;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.Data;
|
|
||||||
import com.topjohnwu.magisk.FlashActivity;
|
|
||||||
import com.topjohnwu.magisk.MagiskManager;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.container.TarEntry;
|
|
||||||
import com.topjohnwu.magisk.utils.Download;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
import com.topjohnwu.magisk.utils.WebService;
|
|
||||||
import com.topjohnwu.magisk.utils.ZipUtils;
|
|
||||||
import com.topjohnwu.superuser.Shell;
|
|
||||||
import com.topjohnwu.superuser.ShellUtils;
|
|
||||||
import com.topjohnwu.superuser.internal.NOPList;
|
|
||||||
import com.topjohnwu.superuser.io.SuFile;
|
|
||||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
|
||||||
import com.topjohnwu.superuser.io.SuFileOutputStream;
|
|
||||||
import com.topjohnwu.utils.SignBoot;
|
|
||||||
|
|
||||||
import org.kamranzafar.jtar.TarInputStream;
|
|
||||||
import org.kamranzafar.jtar.TarOutputStream;
|
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.FilterInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
public class InstallMagisk extends ParallelTask<Void, Void, Boolean> {
|
|
||||||
|
|
||||||
private static final int PATCH_MODE = 0;
|
|
||||||
public static final int DIRECT_MODE = 1;
|
|
||||||
private static final int FIX_ENV_MODE = 2;
|
|
||||||
public static final int SECOND_SLOT_MODE = 3;
|
|
||||||
|
|
||||||
private Uri bootUri;
|
|
||||||
private List<String> console, logs;
|
|
||||||
private String mBoot;
|
|
||||||
private int mode;
|
|
||||||
private File installDir;
|
|
||||||
private ProgressDialog dialog;
|
|
||||||
private MagiskManager mm;
|
|
||||||
|
|
||||||
public InstallMagisk(Activity context) {
|
|
||||||
super(context);
|
|
||||||
mm = Data.MM();
|
|
||||||
mode = FIX_ENV_MODE;
|
|
||||||
}
|
|
||||||
|
|
||||||
public InstallMagisk(Activity context, List<String> console, List<String> logs, int mode) {
|
|
||||||
this(context);
|
|
||||||
this.console = console;
|
|
||||||
this.logs = logs;
|
|
||||||
this.mode = mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public InstallMagisk(FlashActivity context, List<String> console, List<String> logs, Uri boot) {
|
|
||||||
this(context, console, logs, PATCH_MODE);
|
|
||||||
bootUri = boot;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPreExecute() {
|
|
||||||
if (mode == FIX_ENV_MODE) {
|
|
||||||
Activity a = getActivity();
|
|
||||||
dialog = ProgressDialog.show(a, a.getString(R.string.setup_title), a.getString(R.string.setup_msg));
|
|
||||||
console = NOPList.getInstance();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ProgressStream extends FilterInputStream {
|
|
||||||
|
|
||||||
private int prev = -1;
|
|
||||||
private int progress = 0;
|
|
||||||
private int total;
|
|
||||||
|
|
||||||
private ProgressStream(HttpURLConnection conn) throws IOException {
|
|
||||||
super(conn.getInputStream());
|
|
||||||
total = conn.getContentLength();
|
|
||||||
console.add("... 0%");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void update(int step) {
|
|
||||||
progress += step;
|
|
||||||
int curr = (int) (100 * (double) progress / total);
|
|
||||||
if (prev != curr) {
|
|
||||||
prev = curr;
|
|
||||||
console.set(console.size() - 1, "... " + prev + "%");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read() throws IOException {
|
|
||||||
int b = super.read();
|
|
||||||
if (b > 0)
|
|
||||||
update(1);
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read(@NonNull byte[] b) throws IOException {
|
|
||||||
return read(b, 0, b.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read(@NonNull byte[] b, int off, int len) throws IOException {
|
|
||||||
int step = super.read(b, off, len);
|
|
||||||
if (step > 0)
|
|
||||||
update(step);
|
|
||||||
return step;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void extractFiles(String arch) throws IOException {
|
|
||||||
File zip = new File(mm.getFilesDir(), "magisk.zip");
|
|
||||||
BufferedInputStream buf;
|
|
||||||
|
|
||||||
if (!ShellUtils.checkSum("MD5", zip, Data.magiskMD5)) {
|
|
||||||
console.add("- Downloading zip");
|
|
||||||
HttpURLConnection conn = WebService.mustRequest(Data.magiskLink, null);
|
|
||||||
buf = new BufferedInputStream(new ProgressStream(conn), conn.getContentLength());
|
|
||||||
buf.mark(conn.getContentLength() + 1);
|
|
||||||
try (OutputStream out = new FileOutputStream(zip)) {
|
|
||||||
ShellUtils.pump(buf, out);
|
|
||||||
}
|
|
||||||
buf.reset();
|
|
||||||
conn.disconnect();
|
|
||||||
} else {
|
|
||||||
console.add("- Existing zip found");
|
|
||||||
buf = new BufferedInputStream(new FileInputStream(zip), (int) zip.length());
|
|
||||||
buf.mark((int) zip.length() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.add("- Extracting files");
|
|
||||||
try (InputStream in = buf) {
|
|
||||||
ZipUtils.unzip(in, installDir, arch + "/", true);
|
|
||||||
in.reset();
|
|
||||||
ZipUtils.unzip(in, installDir, "common/", true);
|
|
||||||
in.reset();
|
|
||||||
ZipUtils.unzip(in, installDir, "chromeos/", false);
|
|
||||||
in.reset();
|
|
||||||
ZipUtils.unzip(in, installDir, "META-INF/com/google/android/update-binary", true);
|
|
||||||
} catch (IOException e) {
|
|
||||||
console.add("! Cannot unzip zip");
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
Shell.sh(Utils.fmt("chmod -R 755 %s/*; %s/magiskinit -x magisk %s/magisk",
|
|
||||||
installDir, installDir, installDir)).exec();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean dumpBoot() {
|
|
||||||
console.add("- Copying image to cache");
|
|
||||||
// Copy boot image to local
|
|
||||||
try (InputStream in = mm.getContentResolver().openInputStream(bootUri);
|
|
||||||
OutputStream out = new FileOutputStream(mBoot)
|
|
||||||
) {
|
|
||||||
if (in == null)
|
|
||||||
throw new FileNotFoundException();
|
|
||||||
|
|
||||||
InputStream src;
|
|
||||||
if (Utils.getNameFromUri(mm, bootUri).endsWith(".tar")) {
|
|
||||||
// Extract boot.img from tar
|
|
||||||
TarInputStream tar = new TarInputStream(new BufferedInputStream(in));
|
|
||||||
org.kamranzafar.jtar.TarEntry entry;
|
|
||||||
while ((entry = tar.getNextEntry()) != null) {
|
|
||||||
if (entry.getName().equals("boot.img"))
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
src = tar;
|
|
||||||
} else {
|
|
||||||
// Direct copy raw image
|
|
||||||
src = new BufferedInputStream(in);
|
|
||||||
}
|
|
||||||
ShellUtils.pump(src, out);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
console.add("! Invalid Uri");
|
|
||||||
return false;
|
|
||||||
} catch (IOException e) {
|
|
||||||
console.add("! Copy failed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private File patchBoot() throws IOException {
|
|
||||||
boolean isSigned;
|
|
||||||
try (InputStream in = new SuFileInputStream(mBoot)) {
|
|
||||||
isSigned = SignBoot.verifySignature(in, null);
|
|
||||||
if (isSigned) {
|
|
||||||
console.add("- Boot image is signed with AVB 1.0");
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
console.add("! Unable to check signature");
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patch boot image
|
|
||||||
if (!Shell.sh("cd " + installDir, Utils.fmt(
|
|
||||||
"KEEPFORCEENCRYPT=%b KEEPVERITY=%b sh update-binary indep boot_patch.sh %s",
|
|
||||||
Data.keepEnc, Data.keepVerity, mBoot))
|
|
||||||
.to(console, logs).exec().isSuccess())
|
|
||||||
return null;
|
|
||||||
|
|
||||||
Shell.Job job = Shell.sh("mv bin/busybox busybox",
|
|
||||||
"rm -rf magisk.apk bin boot.img update-binary",
|
|
||||||
"cd /");
|
|
||||||
|
|
||||||
File patched = new File(installDir, "new-boot.img");
|
|
||||||
if (isSigned) {
|
|
||||||
console.add("- Signing boot image with test keys");
|
|
||||||
File signed = new File(installDir, "signed.img");
|
|
||||||
try (InputStream in = new SuFileInputStream(patched);
|
|
||||||
OutputStream out = new BufferedOutputStream(new FileOutputStream(signed))
|
|
||||||
) {
|
|
||||||
SignBoot.doSignature("/boot", in, out, null, null);
|
|
||||||
}
|
|
||||||
job.add("mv -f " + signed + " " + patched);
|
|
||||||
}
|
|
||||||
job.exec();
|
|
||||||
return patched;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean outputBoot(File patched) throws IOException {
|
|
||||||
switch (mode) {
|
|
||||||
case PATCH_MODE:
|
|
||||||
String fmt = mm.prefs.getString(Const.Key.BOOT_FORMAT, ".img");
|
|
||||||
File dest = new File(Download.EXTERNAL_PATH, "patched_boot" + fmt);
|
|
||||||
dest.getParentFile().mkdirs();
|
|
||||||
OutputStream out;
|
|
||||||
switch (fmt) {
|
|
||||||
case ".img.tar":
|
|
||||||
out = new TarOutputStream(new BufferedOutputStream(new FileOutputStream(dest)));
|
|
||||||
((TarOutputStream) out).putNextEntry(new TarEntry(patched, "boot.img"));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
case ".img":
|
|
||||||
out = new BufferedOutputStream(new FileOutputStream(dest));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
try (InputStream in = new SuFileInputStream(patched)) {
|
|
||||||
ShellUtils.pump(in, out);
|
|
||||||
out.close();
|
|
||||||
}
|
|
||||||
Shell.sh("rm -f " + patched).exec();
|
|
||||||
console.add("");
|
|
||||||
console.add("****************************");
|
|
||||||
console.add(" Patched image is placed in ");
|
|
||||||
console.add(" " + dest + " ");
|
|
||||||
console.add("****************************");
|
|
||||||
break;
|
|
||||||
case SECOND_SLOT_MODE:
|
|
||||||
case DIRECT_MODE:
|
|
||||||
if (!Shell.su(Utils.fmt("direct_install %s %s", installDir, mBoot))
|
|
||||||
.to(console, logs).exec().isSuccess())
|
|
||||||
return false;
|
|
||||||
if (!Data.keepVerity)
|
|
||||||
Shell.su("find_dtbo_image", "patch_dtbo_image").to(console, logs).exec();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void postOTA() {
|
|
||||||
SuFile bootctl = new SuFile(Const.MAGISK_PATH + "/.core/bootctl");
|
|
||||||
try (InputStream in = mm.getResources().openRawResource(R.raw.bootctl);
|
|
||||||
OutputStream out = new SuFileOutputStream(bootctl)) {
|
|
||||||
ShellUtils.pump(in, out);
|
|
||||||
Shell.su("post_ota " + bootctl.getParent()).exec();
|
|
||||||
console.add("***************************************");
|
|
||||||
console.add(" Next reboot will boot to second slot!");
|
|
||||||
console.add("***************************************");
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Boolean doInBackground(Void... voids) {
|
|
||||||
if (mode == FIX_ENV_MODE) {
|
|
||||||
installDir = new File("/data/adb/magisk");
|
|
||||||
Shell.su("rm -rf /data/adb/magisk/*").exec();
|
|
||||||
} else {
|
|
||||||
installDir = new File(
|
|
||||||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ?
|
|
||||||
mm.createDeviceProtectedStorageContext() : mm)
|
|
||||||
.getFilesDir().getParent()
|
|
||||||
, "install");
|
|
||||||
Shell.sh("rm -rf " + installDir).exec();
|
|
||||||
installDir.mkdirs();
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (mode) {
|
|
||||||
case PATCH_MODE:
|
|
||||||
mBoot = new File(installDir, "boot.img").getAbsolutePath();
|
|
||||||
if (!dumpBoot())
|
|
||||||
return false;
|
|
||||||
break;
|
|
||||||
case DIRECT_MODE:
|
|
||||||
console.add("- Detecting target image");
|
|
||||||
mBoot = ShellUtils.fastCmd("find_boot_image", "echo \"$BOOTIMAGE\"");
|
|
||||||
break;
|
|
||||||
case SECOND_SLOT_MODE:
|
|
||||||
String slot = ShellUtils.fastCmd("echo $SLOT");
|
|
||||||
String target = (TextUtils.equals(slot, "_a") ? "_b" : "_a");
|
|
||||||
console.add("- Target slot: " + target);
|
|
||||||
console.add("- Detecting target image");
|
|
||||||
mBoot = ShellUtils.fastCmd(
|
|
||||||
"SLOT=" + target,
|
|
||||||
"find_boot_image",
|
|
||||||
"SLOT=" + slot,
|
|
||||||
"echo \"$BOOTIMAGE\""
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case FIX_ENV_MODE:
|
|
||||||
mBoot = "";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (mBoot == null) {
|
|
||||||
console.add("! Unable to detect target image");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode == DIRECT_MODE || mode == SECOND_SLOT_MODE)
|
|
||||||
console.add("- Target image: " + mBoot);
|
|
||||||
|
|
||||||
List<String> abis = Arrays.asList(Build.SUPPORTED_ABIS);
|
|
||||||
String arch;
|
|
||||||
|
|
||||||
if (Data.remoteMagiskVersionCode >= Const.MAGISK_VER.SEPOL_REFACTOR) {
|
|
||||||
// 32-bit only
|
|
||||||
if (abis.contains("x86")) arch = "x86";
|
|
||||||
else arch = "arm";
|
|
||||||
} else {
|
|
||||||
if (abis.contains("x86_64")) arch = "x64";
|
|
||||||
else if (abis.contains("arm64-v8a")) arch = "arm64";
|
|
||||||
else if (abis.contains("x86")) arch = "x86";
|
|
||||||
else arch = "arm";
|
|
||||||
}
|
|
||||||
|
|
||||||
console.add("- Device platform: " + Build.SUPPORTED_ABIS[0]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
extractFiles(arch);
|
|
||||||
if (mode == FIX_ENV_MODE) {
|
|
||||||
Shell.su("fix_env").exec();
|
|
||||||
} else {
|
|
||||||
File patched = patchBoot();
|
|
||||||
if (patched == null)
|
|
||||||
return false;
|
|
||||||
if (!outputBoot(patched))
|
|
||||||
return false;
|
|
||||||
if (mode == SECOND_SLOT_MODE)
|
|
||||||
postOTA();
|
|
||||||
console.add("- All done!");
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Boolean result) {
|
|
||||||
if (mode == FIX_ENV_MODE) {
|
|
||||||
dialog.dismiss();
|
|
||||||
Utils.toast(result ? R.string.setup_done : R.string.setup_fail, Toast.LENGTH_LONG);
|
|
||||||
} else {
|
|
||||||
// Running in FlashActivity
|
|
||||||
FlashActivity activity = (FlashActivity) getActivity();
|
|
||||||
if (!result) {
|
|
||||||
Shell.sh("rm -rf " + installDir).submit();
|
|
||||||
console.add("! Installation failed");
|
|
||||||
activity.reboot.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
activity.buttonPanel.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.asyncs;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.webkit.WebView;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.Data;
|
|
||||||
import com.topjohnwu.magisk.MagiskManager;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.utils.WebService;
|
|
||||||
import com.topjohnwu.superuser.ShellUtils;
|
|
||||||
|
|
||||||
import org.commonmark.node.Node;
|
|
||||||
import org.commonmark.parser.Parser;
|
|
||||||
import org.commonmark.renderer.html.HtmlRenderer;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
|
|
||||||
public class MarkDownWindow extends ParallelTask<Void, Void, String> {
|
|
||||||
|
|
||||||
private String mTitle;
|
|
||||||
private String mUrl;
|
|
||||||
private InputStream is;
|
|
||||||
|
|
||||||
|
|
||||||
public MarkDownWindow(Activity context, String title, String url) {
|
|
||||||
super(context);
|
|
||||||
mTitle = title;
|
|
||||||
mUrl = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MarkDownWindow(Activity context, String title, InputStream in) {
|
|
||||||
super(context);
|
|
||||||
mTitle = title;
|
|
||||||
is = in;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String doInBackground(Void... voids) {
|
|
||||||
MagiskManager mm = Data.MM();
|
|
||||||
String md;
|
|
||||||
if (mUrl != null) {
|
|
||||||
md = WebService.getString(mUrl);
|
|
||||||
} else {
|
|
||||||
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
|
||||||
ShellUtils.pump(is, out);
|
|
||||||
md = out.toString();
|
|
||||||
is.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String css;
|
|
||||||
try (
|
|
||||||
InputStream in = mm.getResources().openRawResource(
|
|
||||||
Data.isDarkTheme ? R.raw.dark : R.raw.light);
|
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream()
|
|
||||||
) {
|
|
||||||
ShellUtils.pump(in, out);
|
|
||||||
css = out.toString();
|
|
||||||
in.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
Parser parser = Parser.builder().build();
|
|
||||||
HtmlRenderer renderer = HtmlRenderer.builder().build();
|
|
||||||
Node doc = parser.parse(md);
|
|
||||||
return String.format("<style>%s</style>%s", css, renderer.render(doc));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(String html) {
|
|
||||||
AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
|
|
||||||
alert.setTitle(mTitle);
|
|
||||||
|
|
||||||
WebView wv = new WebView(getActivity());
|
|
||||||
wv.loadDataWithBaseURL("fake://", html, "text/html", "UTF-8", null);
|
|
||||||
|
|
||||||
alert.setView(wv);
|
|
||||||
alert.setNegativeButton(R.string.close, (dialog, id) -> dialog.dismiss());
|
|
||||||
alert.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.asyncs;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
|
|
||||||
public abstract class ParallelTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
|
|
||||||
|
|
||||||
private WeakReference<Activity> weakActivity;
|
|
||||||
|
|
||||||
public ParallelTask() {}
|
|
||||||
|
|
||||||
public ParallelTask(Activity context) {
|
|
||||||
weakActivity = new WeakReference<>(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Activity getActivity() {
|
|
||||||
return weakActivity.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public void exec(Params... params) {
|
|
||||||
executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.asyncs;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.ProgressDialog;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.Data;
|
|
||||||
import com.topjohnwu.magisk.MagiskManager;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.utils.RootUtils;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
import com.topjohnwu.superuser.ShellUtils;
|
|
||||||
import com.topjohnwu.superuser.io.SuFile;
|
|
||||||
import com.topjohnwu.superuser.io.SuFileOutputStream;
|
|
||||||
import com.topjohnwu.utils.JarMap;
|
|
||||||
import com.topjohnwu.utils.SignAPK;
|
|
||||||
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.util.jar.JarEntry;
|
|
||||||
|
|
||||||
public class PatchAPK {
|
|
||||||
|
|
||||||
private static String genPackageName(String prefix, int length) {
|
|
||||||
StringBuilder builder = new StringBuilder(length);
|
|
||||||
builder.append(prefix);
|
|
||||||
length -= prefix.length();
|
|
||||||
SecureRandom random = new SecureRandom();
|
|
||||||
String base = "abcdefghijklmnopqrstuvwxyz";
|
|
||||||
String alpha = base + base.toUpperCase();
|
|
||||||
String full = alpha + "0123456789..........";
|
|
||||||
char next, prev = '\0';
|
|
||||||
for (int i = 0; i < length; ++i) {
|
|
||||||
if (prev == '.' || i == length - 1 || i == 0) {
|
|
||||||
next = alpha.charAt(random.nextInt(alpha.length()));
|
|
||||||
} else {
|
|
||||||
next = full.charAt(random.nextInt(full.length()));
|
|
||||||
}
|
|
||||||
builder.append(next);
|
|
||||||
prev = next;
|
|
||||||
}
|
|
||||||
return builder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int findOffset(byte buf[], byte pattern[]) {
|
|
||||||
int offset = -1;
|
|
||||||
for (int i = 0; i < buf.length - pattern.length; ++i) {
|
|
||||||
boolean match = true;
|
|
||||||
for (int j = 0; j < pattern.length; ++j) {
|
|
||||||
if (buf[i + j] != pattern[j]) {
|
|
||||||
match = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (match) {
|
|
||||||
offset = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* It seems that AAPT sometimes generate another type of string format */
|
|
||||||
private static boolean fallbackPatch(byte xml[], String from, String to) {
|
|
||||||
|
|
||||||
byte[] target = new byte[from.length() * 2 + 2];
|
|
||||||
for (int i = 0; i < from.length(); ++i) {
|
|
||||||
target[i * 2] = (byte) from.charAt(i);
|
|
||||||
}
|
|
||||||
int offset = findOffset(xml, target);
|
|
||||||
if (offset < 0)
|
|
||||||
return false;
|
|
||||||
byte[] dest = new byte[target.length - 2];
|
|
||||||
for (int i = 0; i < to.length(); ++i) {
|
|
||||||
dest[i * 2] = (byte) to.charAt(i);
|
|
||||||
}
|
|
||||||
System.arraycopy(dest, 0, xml, offset, dest.length);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean findAndPatch(byte xml[], String from, String to) {
|
|
||||||
byte target[] = (from + '\0').getBytes();
|
|
||||||
int offset = findOffset(xml, target);
|
|
||||||
if (offset < 0)
|
|
||||||
return fallbackPatch(xml, from, to);
|
|
||||||
System.arraycopy(to.getBytes(), 0, xml, offset, to.length());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean patchAndHide() {
|
|
||||||
MagiskManager mm = Data.MM();
|
|
||||||
|
|
||||||
// Generate a new app with random package name
|
|
||||||
SuFile repack = new SuFile("/data/local/tmp/repack.apk");
|
|
||||||
String pkg = genPackageName("com.", Const.ORIG_PKG_NAME.length());
|
|
||||||
|
|
||||||
try {
|
|
||||||
JarMap apk = new JarMap(mm.getPackageCodePath());
|
|
||||||
if (!patchPackageID(apk, Const.ORIG_PKG_NAME, pkg))
|
|
||||||
return false;
|
|
||||||
SignAPK.sign(apk, new SuFileOutputStream(repack));
|
|
||||||
} catch (Exception e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install the application
|
|
||||||
if (!ShellUtils.fastCmdResult("pm install " + repack))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
repack.delete();
|
|
||||||
|
|
||||||
mm.mDB.setStrings(Const.Key.SU_MANAGER, pkg);
|
|
||||||
Data.exportPrefs();
|
|
||||||
RootUtils.uninstallPkg(Const.ORIG_PKG_NAME);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean patchPackageID(JarMap apk, String from, String to) {
|
|
||||||
try {
|
|
||||||
JarEntry je = apk.getJarEntry(Const.ANDROID_MANIFEST);
|
|
||||||
byte xml[] = apk.getRawData(je);
|
|
||||||
|
|
||||||
if (!findAndPatch(xml, from, to))
|
|
||||||
return false;
|
|
||||||
if (!findAndPatch(xml, from + ".provider", to + ".provider"))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Write in changes
|
|
||||||
apk.getOutputStream(je).write(xml);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void hideManager(Activity activity) {
|
|
||||||
ProgressDialog dialog = ProgressDialog.show(activity,
|
|
||||||
activity.getString(R.string.hide_manager_toast),
|
|
||||||
activity.getString(R.string.hide_manager_toast2));
|
|
||||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
|
||||||
boolean b = patchAndHide();
|
|
||||||
Data.mainHandler.post(() -> {
|
|
||||||
dialog.cancel();
|
|
||||||
if (!b) {
|
|
||||||
Utils.toast(R.string.hide_manager_fail_toast, Toast.LENGTH_LONG);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.asyncs;
|
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.app.ProgressDialog;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.Data;
|
|
||||||
import com.topjohnwu.magisk.FlashActivity;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.components.BaseActivity;
|
|
||||||
import com.topjohnwu.magisk.components.SnackbarMaker;
|
|
||||||
import com.topjohnwu.magisk.container.Repo;
|
|
||||||
import com.topjohnwu.magisk.utils.Download;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
import com.topjohnwu.magisk.utils.WebService;
|
|
||||||
import com.topjohnwu.magisk.utils.ZipUtils;
|
|
||||||
import com.topjohnwu.superuser.Shell;
|
|
||||||
import com.topjohnwu.superuser.ShellUtils;
|
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.FilterInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.util.jar.JarEntry;
|
|
||||||
import java.util.jar.JarInputStream;
|
|
||||||
import java.util.jar.JarOutputStream;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
public class ProcessRepoZip extends ParallelTask<Void, Object, Boolean> {
|
|
||||||
|
|
||||||
private ProgressDialog progressDialog;
|
|
||||||
private boolean mInstall;
|
|
||||||
private File mFile;
|
|
||||||
private Repo mRepo;
|
|
||||||
private int progress = 0, total = -1;
|
|
||||||
|
|
||||||
public ProcessRepoZip(BaseActivity context, Repo repo, boolean install) {
|
|
||||||
super(context);
|
|
||||||
mRepo = repo;
|
|
||||||
mInstall = install && Shell.rootAccess();
|
|
||||||
mFile = new File(Download.EXTERNAL_PATH, repo.getDownloadFilename());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeTopFolder(File input, File output) throws IOException {
|
|
||||||
JarEntry entry;
|
|
||||||
try (
|
|
||||||
JarInputStream in = new JarInputStream(new BufferedInputStream(new FileInputStream(input)));
|
|
||||||
JarOutputStream out = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(output)))
|
|
||||||
) {
|
|
||||||
String path;
|
|
||||||
while ((entry = in.getNextJarEntry()) != null) {
|
|
||||||
// Remove the top directory from the path
|
|
||||||
path = entry.getName().substring(entry.getName().indexOf("/") + 1);
|
|
||||||
// If it's the top folder, ignore it
|
|
||||||
if (path.isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Don't include placeholder
|
|
||||||
if (path.equals("system/placeholder")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
out.putNextEntry(new JarEntry(path));
|
|
||||||
ShellUtils.pump(in, out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected BaseActivity getActivity() {
|
|
||||||
return (BaseActivity) super.getActivity();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPreExecute() {
|
|
||||||
BaseActivity activity = getActivity();
|
|
||||||
mFile.getParentFile().mkdirs();
|
|
||||||
progressDialog = ProgressDialog.show(activity, activity.getString(R.string.zip_download_title), activity.getString(R.string.zip_download_msg, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Boolean doInBackground(Void... params) {
|
|
||||||
BaseActivity activity = getActivity();
|
|
||||||
if (activity == null) return null;
|
|
||||||
try {
|
|
||||||
// Request zip from Internet
|
|
||||||
HttpURLConnection conn = WebService.mustRequest(mRepo.getZipUrl(), null);
|
|
||||||
total = conn.getContentLength();
|
|
||||||
|
|
||||||
// Temp files
|
|
||||||
File temp1 = new File(activity.getCacheDir(), "1.zip");
|
|
||||||
File temp2 = new File(temp1.getParentFile(), "2.zip");
|
|
||||||
temp1.getParentFile().mkdir();
|
|
||||||
|
|
||||||
// First download the zip, Web -> temp1
|
|
||||||
try (
|
|
||||||
InputStream in = new BufferedInputStream(new ProgressInputStream(conn.getInputStream()));
|
|
||||||
OutputStream out = new BufferedOutputStream(new FileOutputStream(temp1))
|
|
||||||
) {
|
|
||||||
ShellUtils.pump(in, out);
|
|
||||||
in.close();
|
|
||||||
}
|
|
||||||
conn.disconnect();
|
|
||||||
|
|
||||||
Data.mainHandler.post(() -> {
|
|
||||||
progressDialog.setTitle(R.string.zip_process_title);
|
|
||||||
progressDialog.setMessage(getActivity().getString(R.string.zip_process_msg));
|
|
||||||
});
|
|
||||||
|
|
||||||
// First remove top folder in Github source zip, temp1 -> temp2
|
|
||||||
removeTopFolder(temp1, temp2);
|
|
||||||
|
|
||||||
// Then sign the zip
|
|
||||||
ZipUtils.signZip(temp2, mFile);
|
|
||||||
|
|
||||||
// Delete temp files
|
|
||||||
temp1.delete();
|
|
||||||
temp2.delete();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Boolean result) {
|
|
||||||
BaseActivity activity = getActivity();
|
|
||||||
if (activity == null) return;
|
|
||||||
progressDialog.dismiss();
|
|
||||||
if (result) {
|
|
||||||
Uri uri = Uri.fromFile(mFile);
|
|
||||||
if (mInstall) {
|
|
||||||
Intent intent = new Intent(activity, Data.classMap.get(FlashActivity.class));
|
|
||||||
intent.setData(uri).putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP);
|
|
||||||
activity.startActivity(intent);
|
|
||||||
} else {
|
|
||||||
SnackbarMaker.showUri(activity, uri);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Utils.toast(R.string.process_error, Toast.LENGTH_LONG);
|
|
||||||
}
|
|
||||||
super.onPostExecute(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void exec(Void... voids) {
|
|
||||||
getActivity().runWithPermission(
|
|
||||||
new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, super::exec);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ProgressInputStream extends FilterInputStream {
|
|
||||||
|
|
||||||
ProgressInputStream(InputStream in) {
|
|
||||||
super(in);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateDlProgress(int step) {
|
|
||||||
progress += step;
|
|
||||||
progressDialog.setMessage(getActivity().getString(R.string.zip_download_msg,
|
|
||||||
(int) (100 * (double) progress / total + 0.5)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized int read() throws IOException {
|
|
||||||
int b = super.read();
|
|
||||||
if (b > 0) {
|
|
||||||
Data.mainHandler.post(() -> updateDlProgress(1));
|
|
||||||
}
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read(@NonNull byte[] b) throws IOException {
|
|
||||||
return read(b, 0, b.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized int read(@NonNull byte[] b, int off, int len) throws IOException {
|
|
||||||
int read = super.read(b, off, len);
|
|
||||||
if (read > 0) {
|
|
||||||
Data.mainHandler.post(() -> updateDlProgress(read));
|
|
||||||
}
|
|
||||||
return read;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.asyncs;
|
|
||||||
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.Data;
|
|
||||||
import com.topjohnwu.magisk.MagiskManager;
|
|
||||||
import com.topjohnwu.magisk.container.Repo;
|
|
||||||
import com.topjohnwu.magisk.utils.Logger;
|
|
||||||
import com.topjohnwu.magisk.utils.Topic;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
import com.topjohnwu.magisk.utils.WebService;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.text.DateFormat;
|
|
||||||
import java.text.ParseException;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.TimeZone;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
public class UpdateRepos {
|
|
||||||
|
|
||||||
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
|
|
||||||
private static final int CORE_POOL_SIZE = Math.max(2, CPU_COUNT - 1);
|
|
||||||
private static final DateFormat dateFormat;
|
|
||||||
|
|
||||||
static {
|
|
||||||
dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
|
|
||||||
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private MagiskManager mm;
|
|
||||||
private Set<String> cached;
|
|
||||||
private ExecutorService threadPool;
|
|
||||||
|
|
||||||
public UpdateRepos() {
|
|
||||||
mm = Data.MM();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void waitTasks() {
|
|
||||||
threadPool.shutdown();
|
|
||||||
try {
|
|
||||||
threadPool.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
|
|
||||||
} catch (InterruptedException ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean loadJSON(String jsonString) throws JSONException, ParseException {
|
|
||||||
JSONArray jsonArray = new JSONArray(jsonString);
|
|
||||||
|
|
||||||
// Empty page, halt
|
|
||||||
if (jsonArray.length() == 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
for (int i = 0; i < jsonArray.length(); i++) {
|
|
||||||
JSONObject rawRepo = jsonArray.getJSONObject(i);
|
|
||||||
String id = rawRepo.getString("description");
|
|
||||||
String name = rawRepo.getString("name");
|
|
||||||
Date date = dateFormat.parse(rawRepo.getString("pushed_at"));
|
|
||||||
Set<String> set = Collections.synchronizedSet(cached);
|
|
||||||
threadPool.execute(() -> {
|
|
||||||
Repo repo = mm.repoDB.getRepo(id);
|
|
||||||
try {
|
|
||||||
if (repo == null)
|
|
||||||
repo = new Repo(name);
|
|
||||||
else
|
|
||||||
set.remove(id);
|
|
||||||
repo.update(date);
|
|
||||||
mm.repoDB.addRepo(repo);
|
|
||||||
} catch (Repo.IllegalRepoException e) {
|
|
||||||
Logger.debug(e.getMessage());
|
|
||||||
mm.repoDB.removeRepo(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* We sort repos by last push, which means that we only need to check whether the
|
|
||||||
* first page is updated to determine whether the online repo database is changed
|
|
||||||
*/
|
|
||||||
private boolean loadPage(int page) {
|
|
||||||
Map<String, String> header = new HashMap<>();
|
|
||||||
if (page == 0)
|
|
||||||
header.put(Const.Key.IF_NONE_MATCH, mm.prefs.getString(Const.Key.ETAG_KEY, ""));
|
|
||||||
String url = Utils.fmt(Const.Url.REPO_URL, page + 1);
|
|
||||||
|
|
||||||
try {
|
|
||||||
HttpURLConnection conn = WebService.request(url, header);
|
|
||||||
// No updates
|
|
||||||
if (conn.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED)
|
|
||||||
return false;
|
|
||||||
// Current page is the last page
|
|
||||||
if (!loadJSON(WebService.getString(conn)))
|
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Should not happen, but if exception occurs, page load fails
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update ETAG
|
|
||||||
if (page == 0) {
|
|
||||||
String etag = header.get(Const.Key.ETAG_KEY);
|
|
||||||
etag = etag.substring(etag.indexOf('\"'), etag.lastIndexOf('\"') + 1);
|
|
||||||
mm.prefs.edit().putString(Const.Key.ETAG_KEY, etag).apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
String links = header.get(Const.Key.LINK_KEY);
|
|
||||||
return links == null || !links.contains("next") || loadPage(page + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void fullReload() {
|
|
||||||
Cursor c = mm.repoDB.getRawCursor();
|
|
||||||
while (c.moveToNext()) {
|
|
||||||
Repo repo = new Repo(c);
|
|
||||||
threadPool.execute(() -> {
|
|
||||||
try {
|
|
||||||
repo.update();
|
|
||||||
mm.repoDB.addRepo(repo);
|
|
||||||
} catch (Repo.IllegalRepoException e) {
|
|
||||||
Logger.debug(e.getMessage());
|
|
||||||
mm.repoDB.removeRepo(repo);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
waitTasks();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void exec(boolean force) {
|
|
||||||
Topic.reset(Topic.REPO_LOAD_DONE);
|
|
||||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
|
||||||
cached = mm.repoDB.getRepoIDSet();
|
|
||||||
threadPool = Executors.newFixedThreadPool(CORE_POOL_SIZE);
|
|
||||||
|
|
||||||
if (loadPage(0)) {
|
|
||||||
waitTasks();
|
|
||||||
// The leftover cached means they are removed from online repo
|
|
||||||
mm.repoDB.removeRepo(cached);
|
|
||||||
} else if (force) {
|
|
||||||
fullReload();
|
|
||||||
}
|
|
||||||
Topic.publish(Topic.REPO_LOAD_DONE);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void exec() {
|
|
||||||
exec(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 dvdandroid
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.topjohnwu.magisk.components;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.TypedArray;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.ViewBinder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author dvdandroid
|
|
||||||
*/
|
|
||||||
public class AboutCardRow extends LinearLayout {
|
|
||||||
|
|
||||||
public TextView mTitle;
|
|
||||||
public TextView mSummary;
|
|
||||||
public ImageView mIcon;
|
|
||||||
public View mView;
|
|
||||||
|
|
||||||
public AboutCardRow(Context context) {
|
|
||||||
this(context, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AboutCardRow(Context context, AttributeSet attrs) {
|
|
||||||
this(context, attrs, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AboutCardRow(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
|
||||||
LayoutInflater.from(context).inflate(R.layout.info_item_row, this);
|
|
||||||
ViewBinder.bind(this, this);
|
|
||||||
|
|
||||||
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.AboutCardRow, 0, 0);
|
|
||||||
String title;
|
|
||||||
Drawable icon;
|
|
||||||
try {
|
|
||||||
title = a.getString(R.styleable.AboutCardRow_text);
|
|
||||||
icon = a.getDrawable(R.styleable.AboutCardRow_icon);
|
|
||||||
} finally {
|
|
||||||
a.recycle();
|
|
||||||
}
|
|
||||||
mTitle.setText(title);
|
|
||||||
mIcon.setImageDrawable(icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setOnClickListener(OnClickListener l) {
|
|
||||||
mView.setOnClickListener(l);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSummary(String s) {
|
|
||||||
mSummary.setVisibility(VISIBLE);
|
|
||||||
mSummary.setText(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.components;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.Data;
|
|
||||||
import com.topjohnwu.magisk.MagiskManager;
|
|
||||||
import com.topjohnwu.magisk.utils.Topic;
|
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
|
|
||||||
public class BaseFragment extends Fragment implements Topic.AutoSubscriber {
|
|
||||||
|
|
||||||
public MagiskManager mm;
|
|
||||||
|
|
||||||
public BaseFragment() {
|
|
||||||
mm = Data.MM();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
Topic.subscribe(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPause() {
|
|
||||||
Topic.unsubscribe(this);
|
|
||||||
super.onPause();
|
|
||||||
}
|
|
||||||
|
|
||||||
@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[] getSubscribedTopics() {
|
|
||||||
return FlavorActivity.EMPTY_INT_ARRAY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.components;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.ViewBinder;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.StringRes;
|
|
||||||
import androidx.annotation.StyleRes;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
|
|
||||||
public class CustomAlertDialog extends AlertDialog.Builder {
|
|
||||||
|
|
||||||
private DialogInterface.OnClickListener positiveListener;
|
|
||||||
private DialogInterface.OnClickListener negativeListener;
|
|
||||||
private DialogInterface.OnClickListener neutralListener;
|
|
||||||
private AlertDialog dialog;
|
|
||||||
|
|
||||||
private ViewHolder vh;
|
|
||||||
|
|
||||||
public class ViewHolder {
|
|
||||||
public LinearLayout dialogLayout;
|
|
||||||
public LinearLayout buttons;
|
|
||||||
|
|
||||||
public TextView messageView;
|
|
||||||
public Button negative;
|
|
||||||
public Button positive;
|
|
||||||
public Button neutral;
|
|
||||||
|
|
||||||
ViewHolder(View v) {
|
|
||||||
ViewBinder.bind(this, v);
|
|
||||||
messageView.setVisibility(View.GONE);
|
|
||||||
negative.setVisibility(View.GONE);
|
|
||||||
positive.setVisibility(View.GONE);
|
|
||||||
neutral.setVisibility(View.GONE);
|
|
||||||
buttons.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
View v = LayoutInflater.from(getContext()).inflate(R.layout.alert_dialog, null);
|
|
||||||
vh = new ViewHolder(v);
|
|
||||||
super.setView(v);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public CustomAlertDialog(@NonNull Activity context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CustomAlertDialog(@NonNull Activity context, @StyleRes int themeResId) {
|
|
||||||
super(context, themeResId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ViewHolder getViewHolder() {
|
|
||||||
return vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CustomAlertDialog setView(int layoutResId) { return this; }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CustomAlertDialog setView(View view) { return this; }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CustomAlertDialog setMessage(@Nullable CharSequence message) {
|
|
||||||
vh.messageView.setVisibility(View.VISIBLE);
|
|
||||||
vh.messageView.setText(message);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CustomAlertDialog setMessage(@StringRes int messageId) {
|
|
||||||
return setMessage(getContext().getString(messageId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CustomAlertDialog setPositiveButton(CharSequence text, DialogInterface.OnClickListener listener) {
|
|
||||||
vh.buttons.setVisibility(View.VISIBLE);
|
|
||||||
vh.positive.setVisibility(View.VISIBLE);
|
|
||||||
vh.positive.setText(text);
|
|
||||||
positiveListener = listener;
|
|
||||||
vh.positive.setOnClickListener((v) -> {
|
|
||||||
if (positiveListener != null) {
|
|
||||||
positiveListener.onClick(dialog, DialogInterface.BUTTON_POSITIVE);
|
|
||||||
}
|
|
||||||
dialog.dismiss();
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CustomAlertDialog setPositiveButton(@StringRes int textId, DialogInterface.OnClickListener listener) {
|
|
||||||
return setPositiveButton(getContext().getString(textId), listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CustomAlertDialog setNegativeButton(CharSequence text, DialogInterface.OnClickListener listener) {
|
|
||||||
vh.buttons.setVisibility(View.VISIBLE);
|
|
||||||
vh.negative.setVisibility(View.VISIBLE);
|
|
||||||
vh.negative.setText(text);
|
|
||||||
negativeListener = listener;
|
|
||||||
vh.negative.setOnClickListener((v) -> {
|
|
||||||
if (negativeListener != null) {
|
|
||||||
negativeListener.onClick(dialog, DialogInterface.BUTTON_NEGATIVE);
|
|
||||||
}
|
|
||||||
dialog.dismiss();
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CustomAlertDialog setNegativeButton(@StringRes int textId, DialogInterface.OnClickListener listener) {
|
|
||||||
return setNegativeButton(getContext().getString(textId), listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CustomAlertDialog setNeutralButton(CharSequence text, DialogInterface.OnClickListener listener) {
|
|
||||||
vh.buttons.setVisibility(View.VISIBLE);
|
|
||||||
vh.neutral.setVisibility(View.VISIBLE);
|
|
||||||
vh.neutral.setText(text);
|
|
||||||
neutralListener = listener;
|
|
||||||
vh.neutral.setOnClickListener((v) -> {
|
|
||||||
if (neutralListener != null) {
|
|
||||||
neutralListener.onClick(dialog, DialogInterface.BUTTON_NEUTRAL);
|
|
||||||
}
|
|
||||||
dialog.dismiss();
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CustomAlertDialog setNeutralButton(@StringRes int textId, DialogInterface.OnClickListener listener) {
|
|
||||||
return setNeutralButton(getContext().getString(textId), listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public AlertDialog create() {
|
|
||||||
dialog = super.create();
|
|
||||||
return dialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public AlertDialog show() {
|
|
||||||
create();
|
|
||||||
dialog.show();
|
|
||||||
return dialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void dismiss() {
|
|
||||||
dialog.dismiss();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.components;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.asyncs.InstallMagisk;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
public class EnvFixDialog extends CustomAlertDialog {
|
|
||||||
|
|
||||||
public EnvFixDialog(@NonNull Activity activity) {
|
|
||||||
super(activity);
|
|
||||||
setTitle(R.string.env_fix_title);
|
|
||||||
setMessage(R.string.env_fix_msg);
|
|
||||||
setCancelable(true);
|
|
||||||
setPositiveButton(R.string.yes, (d, i) -> new InstallMagisk(activity).exec());
|
|
||||||
setNegativeButton(R.string.no_thanks, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.components;
|
|
||||||
|
|
||||||
import android.animation.ValueAnimator;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.ViewTreeObserver;
|
|
||||||
|
|
||||||
public interface ExpandableView {
|
|
||||||
|
|
||||||
class Container {
|
|
||||||
public ViewGroup expandLayout;
|
|
||||||
ValueAnimator expandAnimator, collapseAnimator;
|
|
||||||
boolean mExpanded = false;
|
|
||||||
int expandHeight = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provide state info
|
|
||||||
Container getContainer();
|
|
||||||
|
|
||||||
default void setupExpandable() {
|
|
||||||
Container container = getContainer();
|
|
||||||
container.expandLayout.getViewTreeObserver().addOnPreDrawListener(
|
|
||||||
new ViewTreeObserver.OnPreDrawListener() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onPreDraw() {
|
|
||||||
if (container.expandHeight == 0) {
|
|
||||||
final int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
|
|
||||||
final int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
|
|
||||||
container.expandLayout.measure(widthSpec, heightSpec);
|
|
||||||
container.expandHeight = container.expandLayout.getMeasuredHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
container.expandLayout.getViewTreeObserver().removeOnPreDrawListener(this);
|
|
||||||
container.expandLayout.setVisibility(View.GONE);
|
|
||||||
container.expandAnimator = slideAnimator(0, container.expandHeight);
|
|
||||||
container.collapseAnimator = slideAnimator(container.expandHeight, 0);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
default boolean isExpanded() {
|
|
||||||
return getContainer().mExpanded;
|
|
||||||
}
|
|
||||||
|
|
||||||
default void setExpanded(boolean expanded) {
|
|
||||||
Container container = getContainer();
|
|
||||||
container.mExpanded = expanded;
|
|
||||||
ViewGroup.LayoutParams layoutParams = container.expandLayout.getLayoutParams();
|
|
||||||
layoutParams.height = expanded ? container.expandHeight : 0;
|
|
||||||
container.expandLayout.setLayoutParams(layoutParams);
|
|
||||||
container.expandLayout.setVisibility(expanded ? View.VISIBLE : View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
default void expand() {
|
|
||||||
Container container = getContainer();
|
|
||||||
if (container.mExpanded) return;
|
|
||||||
container.expandLayout.setVisibility(View.VISIBLE);
|
|
||||||
container.expandAnimator.start();
|
|
||||||
container.mExpanded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
default void collapse() {
|
|
||||||
Container container = getContainer();
|
|
||||||
if (!container.mExpanded) return;
|
|
||||||
container.collapseAnimator.start();
|
|
||||||
container.mExpanded = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
default ValueAnimator slideAnimator(int start, int end) {
|
|
||||||
Container container = getContainer();
|
|
||||||
ValueAnimator animator = ValueAnimator.ofInt(start, end);
|
|
||||||
|
|
||||||
animator.addUpdateListener(valueAnimator -> {
|
|
||||||
int value = (Integer) valueAnimator.getAnimatedValue();
|
|
||||||
ViewGroup.LayoutParams layoutParams = container.expandLayout.getLayoutParams();
|
|
||||||
layoutParams.height = value;
|
|
||||||
container.expandLayout.setLayoutParams(layoutParams);
|
|
||||||
});
|
|
||||||
return animator;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.components;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.Data;
|
|
||||||
import com.topjohnwu.magisk.MagiskManager;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.utils.LocaleManager;
|
|
||||||
import com.topjohnwu.magisk.utils.Topic;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.StyleRes;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
|
|
||||||
public abstract class FlavorActivity extends AppCompatActivity implements Topic.AutoSubscriber {
|
|
||||||
|
|
||||||
private ActivityResultListener activityResultListener;
|
|
||||||
static int[] EMPTY_INT_ARRAY = new int[0];
|
|
||||||
public MagiskManager mm;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void attachBaseContext(Context base) {
|
|
||||||
super.attachBaseContext(base);
|
|
||||||
Configuration config = base.getResources().getConfiguration();
|
|
||||||
config.setLocale(LocaleManager.locale);
|
|
||||||
applyOverrideConfiguration(config);
|
|
||||||
mm = Data.MM();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int[] getSubscribedTopics() {
|
|
||||||
return EMPTY_INT_ARRAY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@StyleRes
|
|
||||||
public int getDarkTheme() {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
Topic.subscribe(this);
|
|
||||||
if (Data.isDarkTheme && getDarkTheme() != -1) {
|
|
||||||
setTheme(getDarkTheme());
|
|
||||||
}
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
Topic.unsubscribe(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface ActivityResultListener {
|
|
||||||
void onActivityResult(int requestCode, int resultCode, Intent data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.components;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.Data;
|
|
||||||
import com.topjohnwu.magisk.FlashActivity;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.receivers.DownloadReceiver;
|
|
||||||
import com.topjohnwu.magisk.utils.Download;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
|
|
||||||
class InstallMethodDialog extends AlertDialog.Builder {
|
|
||||||
|
|
||||||
InstallMethodDialog(BaseActivity activity, List<String> options) {
|
|
||||||
super(activity);
|
|
||||||
setTitle(R.string.select_method);
|
|
||||||
setItems(options.toArray(new String [0]), (dialog, idx) -> {
|
|
||||||
Intent intent;
|
|
||||||
switch (idx) {
|
|
||||||
case 1:
|
|
||||||
if (Data.remoteMagiskVersionCode < 1400) {
|
|
||||||
SnackbarMaker.make(activity, R.string.no_boot_file_patch_support,
|
|
||||||
Snackbar.LENGTH_LONG).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Utils.toast(R.string.boot_file_patch_msg, Toast.LENGTH_LONG);
|
|
||||||
intent = new Intent(Intent.ACTION_GET_CONTENT).setType("*/*");
|
|
||||||
activity.startActivityForResult(intent, Const.ID.SELECT_BOOT,
|
|
||||||
(requestCode, resultCode, data) -> {
|
|
||||||
if (requestCode == Const.ID.SELECT_BOOT &&
|
|
||||||
resultCode == BaseActivity.RESULT_OK && data != null) {
|
|
||||||
Intent i = new Intent(activity, Data.classMap.get(FlashActivity.class))
|
|
||||||
.putExtra(Const.Key.FLASH_SET_BOOT, data.getData())
|
|
||||||
.putExtra(Const.Key.FLASH_ACTION, Const.Value.PATCH_BOOT);
|
|
||||||
activity.startActivity(i);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 0:
|
|
||||||
String filename = Utils.fmt("Magisk-v%s(%d).zip",
|
|
||||||
Data.remoteMagiskVersionString, Data.remoteMagiskVersionCode);
|
|
||||||
Download.receive(activity, new DownloadReceiver() {
|
|
||||||
@Override
|
|
||||||
public void onDownloadDone(Context context, Uri uri) {
|
|
||||||
SnackbarMaker.showUri(activity, uri);
|
|
||||||
}
|
|
||||||
}, Data.magiskLink, filename);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
intent = new Intent(activity, Data.classMap.get(FlashActivity.class))
|
|
||||||
.putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_MAGISK);
|
|
||||||
activity.startActivity(intent);
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
new CustomAlertDialog(activity)
|
|
||||||
.setTitle(R.string.warning)
|
|
||||||
.setMessage(R.string.install_inactive_slot_msg)
|
|
||||||
.setCancelable(true)
|
|
||||||
.setPositiveButton(R.string.yes, (d, i) -> {
|
|
||||||
Intent it = new Intent(activity, Data.classMap.get(FlashActivity.class))
|
|
||||||
.putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_INACTIVE_SLOT);
|
|
||||||
activity.startActivity(it);
|
|
||||||
})
|
|
||||||
.setNegativeButton(R.string.no_thanks, null)
|
|
||||||
.show();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.components;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.Data;
|
|
||||||
import com.topjohnwu.magisk.MagiskManager;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.asyncs.MarkDownWindow;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
import com.topjohnwu.superuser.Shell;
|
|
||||||
import com.topjohnwu.superuser.ShellUtils;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class MagiskInstallDialog extends CustomAlertDialog {
|
|
||||||
public MagiskInstallDialog(BaseActivity activity) {
|
|
||||||
super(activity);
|
|
||||||
MagiskManager mm = Data.MM();
|
|
||||||
String filename = Utils.fmt("Magisk-v%s(%d).zip",
|
|
||||||
Data.remoteMagiskVersionString, Data.remoteMagiskVersionCode);
|
|
||||||
setTitle(mm.getString(R.string.repo_install_title, mm.getString(R.string.magisk)));
|
|
||||||
setMessage(mm.getString(R.string.repo_install_msg, filename));
|
|
||||||
setCancelable(true);
|
|
||||||
setPositiveButton(R.string.install, (d, i) -> {
|
|
||||||
List<String> options = new ArrayList<>();
|
|
||||||
options.add(mm.getString(R.string.download_zip_only));
|
|
||||||
options.add(mm.getString(R.string.patch_boot_file));
|
|
||||||
if (Shell.rootAccess()) {
|
|
||||||
options.add(mm.getString(R.string.direct_install));
|
|
||||||
String s = ShellUtils.fastCmd("grep_prop ro.build.ab_update");
|
|
||||||
if (!s.isEmpty() && Boolean.parseBoolean(s)) {
|
|
||||||
options.add(mm.getString(R.string.install_inactive_slot));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
new InstallMethodDialog(activity, options).show();
|
|
||||||
});
|
|
||||||
setNegativeButton(R.string.no_thanks, null);
|
|
||||||
if (!TextUtils.isEmpty(Data.magiskNoteLink)) {
|
|
||||||
setNeutralButton(R.string.release_notes, (d, i) -> {
|
|
||||||
if (Data.magiskNoteLink.contains("forum.xda-developers")) {
|
|
||||||
// Open forum links in browser
|
|
||||||
Utils.openLink(activity, Uri.parse(Data.magiskNoteLink));
|
|
||||||
} else {
|
|
||||||
new MarkDownWindow(activity, null, Data.magiskNoteLink).exec();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.components;
|
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.Data;
|
|
||||||
import com.topjohnwu.magisk.MagiskManager;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.asyncs.MarkDownWindow;
|
|
||||||
import com.topjohnwu.magisk.receivers.ManagerUpdate;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
public class ManagerInstallDialog extends CustomAlertDialog {
|
|
||||||
|
|
||||||
public ManagerInstallDialog(@NonNull BaseActivity activity) {
|
|
||||||
super(activity);
|
|
||||||
MagiskManager mm = Data.MM();
|
|
||||||
String filename = Utils.fmt("MagiskManager-v%s(%d).apk",
|
|
||||||
Data.remoteManagerVersionString, Data.remoteManagerVersionCode);
|
|
||||||
setTitle(mm.getString(R.string.repo_install_title, mm.getString(R.string.app_name)));
|
|
||||||
setMessage(mm.getString(R.string.repo_install_msg, filename));
|
|
||||||
setCancelable(true);
|
|
||||||
setPositiveButton(R.string.install, (d, i) -> activity.runWithPermission(
|
|
||||||
new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, () -> {
|
|
||||||
Intent intent = new Intent(mm, Data.classMap.get(ManagerUpdate.class));
|
|
||||||
intent.putExtra(Const.Key.INTENT_SET_LINK, Data.managerLink);
|
|
||||||
intent.putExtra(Const.Key.INTENT_SET_FILENAME, filename);
|
|
||||||
mm.sendBroadcast(intent);
|
|
||||||
}))
|
|
||||||
.setNegativeButton(R.string.no_thanks, null);
|
|
||||||
if (!TextUtils.isEmpty(Data.managerNoteLink)) {
|
|
||||||
setNeutralButton(R.string.app_changelog, (d, i) ->
|
|
||||||
new MarkDownWindow(activity, null, Data.managerNoteLink).exec());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.components;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
|
|
||||||
import androidx.annotation.StringRes;
|
|
||||||
|
|
||||||
public class SnackbarMaker {
|
|
||||||
|
|
||||||
public static Snackbar make(Activity activity, CharSequence text, int duration) {
|
|
||||||
View view = activity.findViewById(android.R.id.content);
|
|
||||||
return make(view, text, duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Snackbar make(Activity activity, @StringRes int resId, int duration) {
|
|
||||||
return make(activity, activity.getString(resId), duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Snackbar make(View view, CharSequence text, int duration) {
|
|
||||||
Snackbar snack = Snackbar.make(view, text, duration);
|
|
||||||
setup(snack);
|
|
||||||
return snack;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Snackbar make(View view, @StringRes int resId, int duration) {
|
|
||||||
Snackbar snack = Snackbar.make(view, resId, duration);
|
|
||||||
setup(snack);
|
|
||||||
return snack;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void setup(Snackbar snack) {
|
|
||||||
TextView text = snack.getView().findViewById(com.google.android.material.R.id.snackbar_text);
|
|
||||||
text.setMaxLines(Integer.MAX_VALUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void showUri(Activity activity, Uri uri) {
|
|
||||||
make(activity, activity.getString(R.string.internal_storage,
|
|
||||||
"/Download/" + Utils.getNameFromUri(activity, uri)),
|
|
||||||
Snackbar.LENGTH_LONG)
|
|
||||||
.setAction(R.string.ok, (v)->{}).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.components;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.ProgressDialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.Data;
|
|
||||||
import com.topjohnwu.magisk.FlashActivity;
|
|
||||||
import com.topjohnwu.magisk.MagiskManager;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.receivers.DownloadReceiver;
|
|
||||||
import com.topjohnwu.magisk.utils.Download;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
import com.topjohnwu.superuser.Shell;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
public class UninstallDialog extends CustomAlertDialog {
|
|
||||||
|
|
||||||
public UninstallDialog(@NonNull Activity activity) {
|
|
||||||
super(activity);
|
|
||||||
MagiskManager mm = Data.MM();
|
|
||||||
setTitle(R.string.uninstall_magisk_title);
|
|
||||||
setMessage(R.string.uninstall_magisk_msg);
|
|
||||||
setNeutralButton(R.string.restore_img, (d, i) -> {
|
|
||||||
ProgressDialog dialog = ProgressDialog.show(activity,
|
|
||||||
activity.getString(R.string.restore_img),
|
|
||||||
activity.getString(R.string.restore_img_msg));
|
|
||||||
Shell.su("restore_imgs").submit(result -> {
|
|
||||||
dialog.cancel();
|
|
||||||
if (result.isSuccess()) {
|
|
||||||
Utils.toast(R.string.restore_done, Toast.LENGTH_SHORT);
|
|
||||||
} else {
|
|
||||||
Utils.toast(R.string.restore_fail, Toast.LENGTH_LONG);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if (!TextUtils.isEmpty(Data.uninstallerLink)) {
|
|
||||||
setPositiveButton(R.string.complete_uninstall, (d, i) ->
|
|
||||||
Download.receive(activity, new DownloadReceiver() {
|
|
||||||
@Override
|
|
||||||
public void onDownloadDone(Context context, Uri uri) {
|
|
||||||
Intent intent = new Intent(context, Data.classMap.get(FlashActivity.class))
|
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
.setData(uri)
|
|
||||||
.putExtra(Const.Key.FLASH_ACTION, Const.Value.UNINSTALL);
|
|
||||||
context.startActivity(intent);
|
|
||||||
}
|
|
||||||
}, Data.uninstallerLink, "magisk-uninstaller.zip"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.container;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
public abstract class BaseModule implements Comparable<BaseModule> {
|
|
||||||
|
|
||||||
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"));
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compareTo(@NonNull BaseModule module) {
|
|
||||||
return this.getName().toLowerCase().compareTo(module.getName().toLowerCase());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.container;
|
|
||||||
|
|
||||||
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.Sync.su("dos2unix < " + path + "/module.prop"));
|
|
||||||
} catch (NumberFormatException ignored) {}
|
|
||||||
|
|
||||||
mRemoveFile = new SuFile(path, "remove");
|
|
||||||
mDisableFile = new SuFile(path, "disable");
|
|
||||||
mUpdateFile = new SuFile(path, "update");
|
|
||||||
|
|
||||||
if (getId() == null) {
|
|
||||||
int sep = path.lastIndexOf('/');
|
|
||||||
setId(path.substring(sep + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getName() == null) {
|
|
||||||
setName(getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
mEnable = !mDisableFile.exists();
|
|
||||||
mRemove = mRemoveFile.exists();
|
|
||||||
mUpdated = mUpdateFile.exists();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void createDisableFile() {
|
|
||||||
mEnable = false;
|
|
||||||
mDisableFile.createNewFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeDisableFile() {
|
|
||||||
mEnable = true;
|
|
||||||
mDisableFile.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isEnabled() {
|
|
||||||
return mEnable;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void createRemoveFile() {
|
|
||||||
mRemove = true;
|
|
||||||
mRemoveFile.createNewFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deleteRemoveFile() {
|
|
||||||
mRemove = false;
|
|
||||||
mRemoveFile.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean willBeRemoved() {
|
|
||||||
return mRemove;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isUpdated() {
|
|
||||||
return mUpdated;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.container;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.content.pm.ApplicationInfo;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.database.Cursor;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
|
|
||||||
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 = info.loadLabel(pm).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Policy(Cursor c, PackageManager pm) throws PackageManager.NameNotFoundException {
|
|
||||||
uid = c.getInt(c.getColumnIndex("uid"));
|
|
||||||
packageName = c.getString(c.getColumnIndex("package_name"));
|
|
||||||
policy = c.getInt(c.getColumnIndex("policy"));
|
|
||||||
until = c.getLong(c.getColumnIndex("until"));
|
|
||||||
logging = c.getInt(c.getColumnIndex("logging")) != 0;
|
|
||||||
notification = c.getInt(c.getColumnIndex("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,96 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.container;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.utils.Download;
|
|
||||||
import com.topjohnwu.magisk.utils.Logger;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
import com.topjohnwu.magisk.utils.WebService;
|
|
||||||
|
|
||||||
import java.text.DateFormat;
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
public class Repo extends BaseModule {
|
|
||||||
|
|
||||||
private String repoName;
|
|
||||||
private Date mLastUpdate;
|
|
||||||
|
|
||||||
public Repo(String name) {
|
|
||||||
repoName = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Repo(Cursor c) {
|
|
||||||
super(c);
|
|
||||||
repoName = c.getString(c.getColumnIndex("repo_name"));
|
|
||||||
mLastUpdate = new Date(c.getLong(c.getColumnIndex("last_update")));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void update() throws IllegalRepoException {
|
|
||||||
String props[] = Utils.dos2unix(WebService.getString(getManifestUrl())).split("\\n");
|
|
||||||
try {
|
|
||||||
parseProps(props);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
throw new IllegalRepoException("Repo [" + repoName + "] parse error: " + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(getId())) {
|
|
||||||
throw new IllegalRepoException("Repo [" + repoName + "] does not contain id");
|
|
||||||
}
|
|
||||||
if (getVersionCode() < 0) {
|
|
||||||
throw new IllegalRepoException("Repo [" + repoName + "] does not contain versionCode");
|
|
||||||
}
|
|
||||||
if (getMinMagiskVersion() < Const.MIN_MODULE_VER()) {
|
|
||||||
Logger.debug("Repo [" + repoName + "] is outdated");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void update(Date lastUpdate) throws IllegalRepoException {
|
|
||||||
mLastUpdate = lastUpdate;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ContentValues getContentValues() {
|
|
||||||
ContentValues values = super.getContentValues();
|
|
||||||
values.put("repo_name", repoName);
|
|
||||||
values.put("last_update", mLastUpdate.getTime());
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getRepoName() {
|
|
||||||
return repoName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getZipUrl() {
|
|
||||||
return String.format(Const.Url.ZIP_URL, repoName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getManifestUrl() {
|
|
||||||
return String.format(Const.Url.FILE_URL, repoName, "module.prop");
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDetailUrl() {
|
|
||||||
return String.format(Const.Url.FILE_URL, repoName, "README.md");
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getLastUpdateString() {
|
|
||||||
return DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(mLastUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Date getLastUpdate() {
|
|
||||||
return mLastUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDownloadFilename() {
|
|
||||||
return Download.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 android.database.Cursor;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SuLogEntry(Cursor c) {
|
|
||||||
fromUid = c.getInt(c.getColumnIndex("from_uid"));
|
|
||||||
fromPid = c.getInt(c.getColumnIndex("from_pid"));
|
|
||||||
toUid = c.getInt(c.getColumnIndex("to_uid"));
|
|
||||||
packageName = c.getString(c.getColumnIndex("package_name"));
|
|
||||||
appName = c.getString(c.getColumnIndex("app_name"));
|
|
||||||
command = c.getString(c.getColumnIndex("command"));
|
|
||||||
action = c.getInt(c.getColumnIndex("action")) != 0;
|
|
||||||
date = new Date(c.getLong(c.getColumnIndex("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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user