mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-12-07 13:20:50 -08:00
Compare commits
2203 Commits
v18.0
...
manager-v8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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++
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -2,8 +2,8 @@ out
|
|||||||
*.zip
|
*.zip
|
||||||
*.jks
|
*.jks
|
||||||
*.apk
|
*.apk
|
||||||
config.prop
|
/config.prop
|
||||||
update.sh
|
/update.sh
|
||||||
|
|
||||||
# Built binaries
|
# Built binaries
|
||||||
native/out
|
native/out
|
||||||
@@ -15,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
|
||||||
|
|||||||
86
README.MD
86
README.MD
@@ -1,31 +1,80 @@
|
|||||||
# Magisk
|

|
||||||
[Downloads](https://github.com/topjohnwu/Magisk/releases) | [Documentation](https://topjohnwu.github.io/Magisk/) | [XDA Thread](https://forum.xda-developers.com/apps/magisk/official-magisk-v7-universal-systemless-t3473445)
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
Magisk is a suite of open source tools for customizing Android, supporting devices higher than Android 5.0 (API 21). It covers the fundamental parts for Android customization: root, boot scripts, SELinux patches, AVB2.0 / dm-verity / forceencrypt removals etc.
|
|
||||||
|
|
||||||
Furthermore, Magisk provides a **Systemless Interface** to alter the system (or vendor) arbitrarily while the actual partitions stay completely intact. With its systemless nature along with several other hacks, Magisk can hide modifications from nearly any system integrity verifications used in banking apps, corporation monitoring apps, game cheat detections, and most importantly [Google's SafetyNet API](https://developer.android.com/training/safetynet/index.html).
|
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.
|
||||||
|
|
||||||
|
Here are some feature highlights:
|
||||||
|
|
||||||
|
- **MagiskSU**: Provide root access to your device
|
||||||
|
- **Magisk Modules**: Modify read-only partitions by installing modules
|
||||||
|
- **MagiskHide**: Hide Magisk from root detections / system integrity checks
|
||||||
|
|
||||||
|
## Downloads
|
||||||
|
|
||||||
|
[](https://github.com/topjohnwu/Magisk/releases/download/manager-v7.5.1/MagiskManager-v7.5.1.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/v20.4)
|
||||||
|
|
||||||
|
## Useful Links
|
||||||
|
|
||||||
|
- [Installation Instruction](https://topjohnwu.github.io/Magisk/install.html)
|
||||||
|
- [Frequently Asked Questions](https://topjohnwu.github.io/Magisk/faq.html)
|
||||||
|
- [Full Official Docs](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!** **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.
|
|
||||||
|
|
||||||
## Building Environment Requirements
|
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.
|
||||||
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
|
**Only bug reports from Canary builds will be accepted.**
|
||||||
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.
|
For installation issues, upload both boot image and install logs.<br>
|
||||||
3. Set configurations in `config.prop`. A sample file `config.prop.sample` is provided as an example.
|
For Magisk issues, upload boot logcat or dmesg.<br>
|
||||||
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`
|
For Magisk Manager crashes, record and upload the logcat when the crash occurs.
|
||||||
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).
|
|
||||||
|
## 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_HOME` 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,
|
||||||
@@ -38,4 +87,3 @@ 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,92 +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')
|
|
||||||
implementation 'androidx.core:core:1.0.1'
|
|
||||||
fullImplementation project(':utils')
|
|
||||||
fullImplementation 'com.amitshekhar.android:android-networking:1.0.2'
|
|
||||||
fullImplementation 'androidx.appcompat:appcompat:1.0.2'
|
|
||||||
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.1.2'
|
|
||||||
fullImplementation 'com.atlassian.commonmark:commonmark:0.11.0'
|
|
||||||
fullImplementation 'org.kamranzafar:jtar:2.3'
|
|
||||||
|
|
||||||
def butterKnifeVersion = '9.0.0-rc2'
|
|
||||||
if (properties.containsKey('android.injected.invoked.from.ide')) {
|
|
||||||
fullImplementation "com.jakewharton:butterknife-reflect:${butterKnifeVersion}"
|
|
||||||
} else {
|
|
||||||
fullImplementation "com.jakewharton:butterknife-runtime:${butterKnifeVersion}"
|
|
||||||
fullAnnotationProcessor "com.jakewharton:butterknife-compiler:${butterKnifeVersion}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
147
app/build.gradle.kts
Normal file
147
app/build.gradle.kts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
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("/androidsupportmultidexversion.txt")
|
||||||
|
exclude("/org/bouncycastle/**")
|
||||||
|
exclude("/kotlin/**")
|
||||||
|
exclude("/kotlinx/**")
|
||||||
|
exclude("/okhttp3/**")
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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.10.0"
|
||||||
|
implementation("com.squareup.moshi:moshi:${vMoshi}")
|
||||||
|
kapt("com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}")
|
||||||
|
|
||||||
|
val vRoom = "2.2.5"
|
||||||
|
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.1")
|
||||||
|
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.1")
|
||||||
|
implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.0.0")
|
||||||
|
implementation("com.google.android.material:material:1.2.1")
|
||||||
|
}
|
||||||
46
app/proguard-rules.pro
vendored
46
app/proguard-rules.pro
vendored
@@ -16,24 +16,42 @@
|
|||||||
# public *;
|
# public *;
|
||||||
#}
|
#}
|
||||||
|
|
||||||
# BouncyCastle
|
# Kotlin
|
||||||
-keep,allowoptimization class org.bouncycastle.jcajce.provider.asymmetric.rsa.**SHA1** { *; }
|
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
||||||
-keep,allowoptimization class org.bouncycastle.jcajce.provider.asymmetric.RSA** { *; }
|
public static void checkExpressionValueIsNotNull(...);
|
||||||
-keep,allowoptimization 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.* { *; }
|
||||||
|
|
||||||
# Fast Android Networking Library
|
# Snet
|
||||||
-dontwarn okhttp3.**
|
-keepclassmembers class com.topjohnwu.magisk.ui.safetynet.SafetyNetHelper { *; }
|
||||||
|
-keep,allowobfuscation interface com.topjohnwu.magisk.ui.safetynet.SafetyNetHelper$Callback
|
||||||
|
-keepclassmembers class * implements com.topjohnwu.magisk.ui.safetynet.SafetyNetHelper$Callback {
|
||||||
|
void onResponse(org.json.JSONObject);
|
||||||
|
}
|
||||||
|
|
||||||
# Strip logging
|
# Fragments
|
||||||
-assumenosideeffects class com.topjohnwu.magisk.utils.Logger {
|
# TODO: Remove when AGP 4.1 release
|
||||||
public *** debug(...);
|
# https://issuetracker.google.com/issues/142601969
|
||||||
|
-keep,allowobfuscation class * extends androidx.fragment.app.Fragment
|
||||||
|
-keepnames class androidx.navigation.fragment.NavHostFragment
|
||||||
|
|
||||||
|
# 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
|
||||||
-optimizationpasses 6
|
|
||||||
|
# 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;
|
||||||
|
}
|
||||||
23
app/shared/src/main/AndroidManifest.xml
Normal file
23
app/shared/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<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">
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="a.p"
|
||||||
|
android:authorities="${applicationId}.provider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
</provider>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
7
app/shared/src/main/java/a/p.java
Normal file
7
app/shared/src/main/java/a/p.java
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package a;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.FileProvider;
|
||||||
|
|
||||||
|
public class p extends FileProvider {
|
||||||
|
/* Stub */
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
351
app/shared/src/main/java/com/topjohnwu/magisk/FileProvider.java
Normal file
351
app/shared/src/main/java/com/topjohnwu/magisk/FileProvider.java
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
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.Bundle;
|
||||||
|
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;
|
||||||
|
|
||||||
|
public static ProviderCallHandler callHandler;
|
||||||
|
|
||||||
|
@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 Bundle call(String method, String arg, Bundle extras) {
|
||||||
|
if (callHandler != null)
|
||||||
|
return callHandler.call(getContext(), method, arg, extras);
|
||||||
|
return Bundle.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ParcelFileDescriptor openFile(Uri uri, String mode)
|
||||||
|
throws FileNotFoundException {
|
||||||
|
|
||||||
|
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,8 @@
|
|||||||
|
package com.topjohnwu.magisk;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
public interface ProviderCallHandler {
|
||||||
|
Bundle call(Context context, String method, String arg, Bundle extras);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,16 @@ import android.content.Intent;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
import java.io.File;
|
import com.topjohnwu.magisk.FileProvider;
|
||||||
|
|
||||||
import androidx.core.content.FileProvider;
|
import java.io.File;
|
||||||
|
|
||||||
public class APKInstall {
|
public class APKInstall {
|
||||||
public static void install(Context c, File apk) {
|
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);
|
Intent install = new Intent(Intent.ACTION_INSTALL_PACKAGE);
|
||||||
install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
@@ -20,6 +24,6 @@ public class APKInstall {
|
|||||||
apk.setReadable(true, false);
|
apk.setReadable(true, false);
|
||||||
install.setData(Uri.fromFile(apk));
|
install.setData(Uri.fromFile(apk));
|
||||||
}
|
}
|
||||||
c.startActivity(install);
|
return install;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.topjohnwu.magisk.utils;
|
||||||
|
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
|
public class CompoundEnumeration<E> implements Enumeration<E> {
|
||||||
|
private Enumeration<E>[] enums;
|
||||||
|
private int index = 0;
|
||||||
|
|
||||||
|
@SafeVarargs
|
||||||
|
public CompoundEnumeration(Enumeration<E> ...enums) {
|
||||||
|
this.enums = enums;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean next() {
|
||||||
|
while (index < enums.length) {
|
||||||
|
if (enums[index] != null && enums[index].hasMoreElements()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasMoreElements() {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
public E nextElement() {
|
||||||
|
if (!next()) {
|
||||||
|
throw new NoSuchElementException();
|
||||||
|
}
|
||||||
|
return enums[index].nextElement();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.topjohnwu.magisk.utils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
|
||||||
|
import dalvik.system.DexClassLoader;
|
||||||
|
|
||||||
|
public class DynamicClassLoader extends DexClassLoader {
|
||||||
|
|
||||||
|
private ClassLoader base = Object.class.getClassLoader();
|
||||||
|
|
||||||
|
public DynamicClassLoader(File apk, ClassLoader parent) {
|
||||||
|
super(apk.getPath(), apk.getParent(), null, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||||
|
// First check if already loaded
|
||||||
|
Class cls = findLoadedClass(name);
|
||||||
|
if (cls != null)
|
||||||
|
return cls;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Then check boot classpath
|
||||||
|
return base.loadClass(name);
|
||||||
|
} catch (ClassNotFoundException ignored) {
|
||||||
|
try {
|
||||||
|
// Next try current dex
|
||||||
|
return findClass(name);
|
||||||
|
} catch (ClassNotFoundException fromSuper) {
|
||||||
|
try {
|
||||||
|
// Finally try parent
|
||||||
|
return getParent().loadClass(name);
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
throw fromSuper;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URL getResource(String name) {
|
||||||
|
URL resource = base.getResource(name);
|
||||||
|
if (resource != null)
|
||||||
|
return resource;
|
||||||
|
resource = findResource(name);
|
||||||
|
if (resource != null)
|
||||||
|
return resource;
|
||||||
|
resource = getParent().getResource(name);
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Enumeration<URL> getResources(String name) throws IOException {
|
||||||
|
return new CompoundEnumeration<>(base.getResources(name),
|
||||||
|
findResources(name), getParent().getResources(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/signing/build.gradle.kts
Normal file
35
app/signing/build.gradle.kts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("java-library")
|
||||||
|
id("java")
|
||||||
|
id("com.github.johnrengelman.shadow") version "6.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
val jar by tasks.getting(Jar::class) {
|
||||||
|
manifest {
|
||||||
|
attributes["Main-Class"] = "com.topjohnwu.signing.ZipSigner"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val shadowJar by tasks.getting(ShadowJar::class) {
|
||||||
|
archiveBaseName.set("zipsigner")
|
||||||
|
archiveClassifier.set(null as String?)
|
||||||
|
archiveVersion.set("4.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
jcenter()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
|
||||||
|
|
||||||
|
api("org.bouncycastle:bcprov-jdk15on:1.66")
|
||||||
|
api("org.bouncycastle:bcpkix-jdk15on:1.66")
|
||||||
|
}
|
||||||
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())) {
|
||||||
|
ASN1OutputStream dos = ASN1OutputStream.create(out, ASN1Encoding.DER);
|
||||||
dos.writeObject(asn1.readObject());
|
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,6 +327,7 @@ 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
|
||||||
@@ -379,7 +336,9 @@ public class SignAPK {
|
|||||||
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
|
private static void signFile(Manifest manifest,
|
||||||
public Object getContent() {
|
X509Certificate[] publicKey, PrivateKey[] privateKey,
|
||||||
return file;
|
long timestamp, JarOutputStream outputJar) throws Exception {
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
PrivateKey privateKey, OutputStream outputStream)
|
|
||||||
throws Exception {
|
|
||||||
ByteArrayOutputStream temp = new ByteArrayOutputStream();
|
|
||||||
// put a readable message and a null char at the start of the
|
|
||||||
// archive comment, so that tools that display the comment
|
|
||||||
// (hopefully) show something sensible.
|
|
||||||
// TODO: anything more useful we can put in this message?
|
|
||||||
byte[] message = "signed by SignApk".getBytes("UTF-8");
|
|
||||||
temp.write(message);
|
|
||||||
temp.write(0);
|
|
||||||
|
|
||||||
CMSProcessableFile cmsFile = new CMSProcessableFile(input);
|
|
||||||
writeSignatureBlock(cmsFile, publicKey, privateKey, temp);
|
|
||||||
|
|
||||||
// For a zip with no archive comment, the
|
|
||||||
// end-of-central-directory record will be 22 bytes long, so
|
|
||||||
// we expect to find the EOCD marker 22 bytes from the end.
|
|
||||||
byte[] zipData = cmsFile.getTail();
|
|
||||||
if (zipData[zipData.length-22] != 0x50 ||
|
|
||||||
zipData[zipData.length-21] != 0x4b ||
|
|
||||||
zipData[zipData.length-20] != 0x05 ||
|
|
||||||
zipData[zipData.length-19] != 0x06) {
|
|
||||||
throw new IllegalArgumentException("zip data already has an archive comment");
|
|
||||||
}
|
|
||||||
int total_size = temp.size() + 6;
|
|
||||||
if (total_size > 0xffff) {
|
|
||||||
throw new IllegalArgumentException("signature is too big for ZIP file comment");
|
|
||||||
}
|
|
||||||
// signature starts this many bytes from the end of the file
|
|
||||||
int signature_start = total_size - message.length - 1;
|
|
||||||
temp.write(signature_start & 0xff);
|
|
||||||
temp.write((signature_start >> 8) & 0xff);
|
|
||||||
// Why the 0xff bytes? In a zip file with no archive comment,
|
|
||||||
// bytes [-6:-2] of the file are the little-endian offset from
|
|
||||||
// the start of the file to the central directory. So for the
|
|
||||||
// two high bytes to be 0xff 0xff, the archive would have to
|
|
||||||
// be nearly 4GB in size. So it's unlikely that a real
|
|
||||||
// commentless archive would have 0xffs here, and lets us tell
|
|
||||||
// an old signed archive from a new one.
|
|
||||||
temp.write(0xff);
|
|
||||||
temp.write(0xff);
|
|
||||||
temp.write(total_size & 0xff);
|
|
||||||
temp.write((total_size >> 8) & 0xff);
|
|
||||||
temp.flush();
|
|
||||||
// Signature verification checks that the EOCD header is the
|
|
||||||
// last such sequence in the file (to avoid minzip finding a
|
|
||||||
// fake EOCD appended after the signature in its scan). The
|
|
||||||
// odds of producing this sequence by chance are very low, but
|
|
||||||
// let's catch it here if it does.
|
|
||||||
byte[] b = temp.toByteArray();
|
|
||||||
for (int i = 0; i < b.length-3; ++i) {
|
|
||||||
if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {
|
|
||||||
throw new IllegalArgumentException("found spurious EOCD header at " + i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cmsFile.write(outputStream);
|
|
||||||
outputStream.write(total_size & 0xff);
|
|
||||||
outputStream.write((total_size >> 8) & 0xff);
|
|
||||||
temp.writeTo(outputStream);
|
|
||||||
outputStream.close();
|
|
||||||
}
|
|
||||||
private static void signFile(Manifest manifest, JarMap inputJar,
|
|
||||||
X509Certificate publicKey, PrivateKey privateKey,
|
|
||||||
JarOutputStream outputJar)
|
|
||||||
throws Exception {
|
|
||||||
// Assume the certificate is valid for at least an hour.
|
|
||||||
long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
|
|
||||||
// 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);
|
|
||||||
|
int numKeys = publicKey.length;
|
||||||
|
for (int k = 0; k < numKeys; ++k) {
|
||||||
|
// CERT.SF / CERT#.SF
|
||||||
|
je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
|
||||||
|
(String.format(Locale.US, CERT_SF_MULTI_NAME, k)));
|
||||||
je.setTime(timestamp);
|
je.setTime(timestamp);
|
||||||
outputJar.putNextEntry(je);
|
outputJar.putNextEntry(je);
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey));
|
writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey[k]));
|
||||||
byte[] signedData = baos.toByteArray();
|
byte[] signedData = baos.toByteArray();
|
||||||
outputJar.write(signedData);
|
outputJar.write(signedData);
|
||||||
|
|
||||||
// CERT.{EC,RSA} / CERT#.{EC,RSA}
|
// CERT.{EC,RSA} / CERT#.{EC,RSA}
|
||||||
final String keyType = publicKey.getPublicKey().getAlgorithm();
|
final String keyType = publicKey[k].getPublicKey().getAlgorithm();
|
||||||
je = new JarEntry(String.format(CERT_SIG_NAME, keyType));
|
je = new JarEntry(numKeys == 1 ? (String.format(CERT_SIG_NAME, keyType)) :
|
||||||
|
(String.format(Locale.US, CERT_SIG_MULTI_NAME, k, keyType)));
|
||||||
je.setTime(timestamp);
|
je.setTime(timestamp);
|
||||||
outputJar.putNextEntry(je);
|
outputJar.putNextEntry(je);
|
||||||
writeSignatureBlock(new CMSProcessableByteArray(signedData),
|
writeSignatureBlock(new CMSProcessableByteArray(signedData),
|
||||||
publicKey, privateKey, outputJar);
|
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,101 +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.m"
|
|
||||||
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>
|
|
||||||
<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.i">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<!-- Service -->
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name="a.j"
|
|
||||||
android:exported="true"
|
|
||||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
|
||||||
<service
|
|
||||||
android:name="a.k"
|
|
||||||
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.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.NoUIActivity;
|
|
||||||
|
|
||||||
public class g extends NoUIActivity {
|
|
||||||
/* stub */
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package a;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.receivers.GeneralReceiver;
|
|
||||||
|
|
||||||
public class h extends GeneralReceiver {
|
|
||||||
/* stub */
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package a;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.receivers.ShortcutReceiver;
|
|
||||||
|
|
||||||
public class i extends ShortcutReceiver {
|
|
||||||
/* stub */
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package a;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.services.OnBootService;
|
|
||||||
|
|
||||||
public class j extends OnBootService {
|
|
||||||
/* stub */
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package a;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.services.UpdateCheckService;
|
|
||||||
|
|
||||||
public class k 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 l extends AboutCardRow {
|
|
||||||
/* stub */
|
|
||||||
|
|
||||||
public l(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public l(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public l(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package a;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.SuRequestActivity;
|
|
||||||
|
|
||||||
public class m extends SuRequestActivity {
|
|
||||||
/* stub */
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package a;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.MagiskManager;
|
|
||||||
|
|
||||||
public class q extends MagiskManager {
|
|
||||||
/* stub */
|
|
||||||
}
|
|
||||||
@@ -1,72 +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;
|
|
||||||
import butterknife.BindView;
|
|
||||||
|
|
||||||
public class AboutActivity extends BaseActivity {
|
|
||||||
|
|
||||||
@BindView(R.id.toolbar) Toolbar toolbar;
|
|
||||||
@BindView(R.id.app_version_info) AboutCardRow appVersionInfo;
|
|
||||||
@BindView(R.id.app_changelog) AboutCardRow appChangelog;
|
|
||||||
@BindView(R.id.app_translators) AboutCardRow appTranslators;
|
|
||||||
@BindView(R.id.app_source_code) AboutCardRow appSourceCode;
|
|
||||||
@BindView(R.id.support_thread) AboutCardRow supportThread;
|
|
||||||
@BindView(R.id.follow_twitter) 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);
|
|
||||||
new AboutActivity_ViewBinding(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,155 +0,0 @@
|
|||||||
package com.topjohnwu.magisk;
|
|
||||||
|
|
||||||
import android.os.Environment;
|
|
||||||
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 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 final String MAGISK_PATH = "/sbin/.magisk/img";
|
|
||||||
public static final File EXTERNAL_PATH;
|
|
||||||
public static File MAGISK_DISABLE_FILE;
|
|
||||||
|
|
||||||
static {
|
|
||||||
MAGISK_DISABLE_FILE = new File("xxx");
|
|
||||||
EXTERNAL_PATH = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
|
||||||
EXTERNAL_PATH.mkdirs();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final String BUSYBOX_PATH = "/sbin/.magisk/busybox";
|
|
||||||
public static final String TMP_FOLDER_PATH = "/dev/tmp";
|
|
||||||
public static final String MAGISK_LOG = "/cache/magisk.log";
|
|
||||||
public static final String MANAGER_CONFIGS = ".tmp.magisk.config";
|
|
||||||
|
|
||||||
// Versions
|
|
||||||
public static final int UPDATE_SERVICE_VER = 1;
|
|
||||||
public static final int MIN_MODULE_VER = 1500;
|
|
||||||
public static final int SNET_EXT_VER = 12;
|
|
||||||
|
|
||||||
/* A list of apps that should not be shown as hide-able */
|
|
||||||
public static final List<String> HIDE_BLACKLIST = Arrays.asList(
|
|
||||||
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 SEPOL_REFACTOR = 1640;
|
|
||||||
public static final int FIX_ENV = 1650;
|
|
||||||
public static final int DBVER_SIX = 17000;
|
|
||||||
public static final int CMDLINE_DB = 17305;
|
|
||||||
public static final int HIDE_STATUS = 17315;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ID {
|
|
||||||
public static final int UPDATE_SERVICE_ID = 1;
|
|
||||||
public static final int FETCH_ZIP = 2;
|
|
||||||
public static final int SELECT_BOOT = 3;
|
|
||||||
public static final int ONBOOT_SERVICE_ID = 6;
|
|
||||||
|
|
||||||
// notifications
|
|
||||||
public static final int MAGISK_UPDATE_NOTIFICATION_ID = 4;
|
|
||||||
public static final int APK_UPDATE_NOTIFICATION_ID = 5;
|
|
||||||
public static final int DTBO_NOTIFICATION_ID = 7;
|
|
||||||
public static final int HIDE_MANAGER_NOTIFICATION_ID = 8;
|
|
||||||
public static final String UPDATE_NOTIFICATION_CHANNEL = "update";
|
|
||||||
public static final String PROGRESS_NOTIFICATION_CHANNEL = "progress";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static 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";
|
|
||||||
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.me/topjohnwu";
|
|
||||||
public static final String PATREON_URL = "https://www.patreon.com/topjohnwu";
|
|
||||||
public static final String TWITTER_URL = "https://twitter.com/topjohnwu";
|
|
||||||
public static final String XDA_THREAD = "http://forum.xda-developers.com/showthread.php?t=3432382";
|
|
||||||
public static final String SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk";
|
|
||||||
public static final String SNET_URL = "https://raw.githubusercontent.com/topjohnwu/magisk_files/b66b1a914978e5f4c4bbfd74a59f4ad371bac107/snet.apk";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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_NAME = "filename";
|
|
||||||
public static final String INTENT_SET_LINK = "link";
|
|
||||||
public static final String FLASH_ACTION = "action";
|
|
||||||
public static final String FLASH_SET_BOOT = "boot";
|
|
||||||
public static final String BROADCAST_MANAGER_UPDATE = "manager_update";
|
|
||||||
public static final String BROADCAST_REBOOT = "reboot";
|
|
||||||
|
|
||||||
// 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,195 +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.GeneralReceiver;
|
|
||||||
import com.topjohnwu.magisk.receivers.ShortcutReceiver;
|
|
||||||
import com.topjohnwu.magisk.services.OnBootService;
|
|
||||||
import com.topjohnwu.magisk.services.UpdateCheckService;
|
|
||||||
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;
|
|
||||||
|
|
||||||
// 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 multiuserState = -1;
|
|
||||||
public static int suResponseType;
|
|
||||||
public static int suNotificationType;
|
|
||||||
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(GeneralReceiver.class, a.h.class);
|
|
||||||
classMap.put(ShortcutReceiver.class, a.i.class);
|
|
||||||
classMap.put(OnBootService.class, a.j.class);
|
|
||||||
classMap.put(UpdateCheckService.class, a.k.class);
|
|
||||||
classMap.put(AboutCardRow.class, a.l.class);
|
|
||||||
classMap.put(SuRequestActivity.class, a.m.class);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void loadMagiskInfo() {
|
|
||||||
try {
|
|
||||||
magiskVersionString = ShellUtils.fastCmd("magisk -v").split(":")[0];
|
|
||||||
magiskVersionCode = Integer.parseInt(ShellUtils.fastCmd("magisk -V"));
|
|
||||||
if (magiskVersionCode >= Const.MAGISK_VER.HIDE_STATUS) {
|
|
||||||
magiskHide = Shell.su("magiskhide --status").exec().isSuccess();
|
|
||||||
} else {
|
|
||||||
String s = ShellUtils.fastCmd(("resetprop -p ") + 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("cat %s > /data/user/0/%s", xml, Const.MANAGER_CONFIGS)).exec();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void importPrefs() {
|
|
||||||
MagiskManager mm = MM();
|
|
||||||
SuFile config = new SuFile("/data/user/0/" + 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);
|
|
||||||
|
|
||||||
// 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.COREONLY, Const.MAGISK_DISABLE_FILE.exists())
|
|
||||||
.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.UPDATE_CHANNEL, String.valueOf(updateChannel))
|
|
||||||
.putInt(Const.Key.UPDATE_SERVICE_VER, Const.UPDATE_SERVICE_VER)
|
|
||||||
.putInt(Const.Key.REPO_ORDER, repoOrder)
|
|
||||||
.apply();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +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;
|
|
||||||
import butterknife.BindView;
|
|
||||||
|
|
||||||
public class DonationActivity extends BaseActivity {
|
|
||||||
|
|
||||||
@BindView(R.id.toolbar) Toolbar toolbar;
|
|
||||||
@BindView(R.id.paypal) AboutCardRow paypal;
|
|
||||||
@BindView(R.id.patreon) 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);
|
|
||||||
new DonationActivity_ViewBinding(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,169 +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.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;
|
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.OnClick;
|
|
||||||
|
|
||||||
public class FlashActivity extends BaseActivity {
|
|
||||||
|
|
||||||
@BindView(R.id.toolbar) Toolbar toolbar;
|
|
||||||
@BindView(R.id.txtLog) TextView flashLogs;
|
|
||||||
@BindView(R.id.button_panel) public LinearLayout buttonPanel;
|
|
||||||
@BindView(R.id.reboot) public Button reboot;
|
|
||||||
@BindView(R.id.scrollView) ScrollView sv;
|
|
||||||
|
|
||||||
private List<String> logs;
|
|
||||||
|
|
||||||
@OnClick(R.id.reboot)
|
|
||||||
void reboot() {
|
|
||||||
Shell.su("/system/bin/reboot").submit();
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.save_logs)
|
|
||||||
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(Const.EXTERNAL_PATH, filename);
|
|
||||||
try (FileWriter writer = new FileWriter(logFile)) {
|
|
||||||
for (String s : logs) {
|
|
||||||
writer.write(s);
|
|
||||||
writer.write('\n');
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Utils.toast(logFile.getPath(), Toast.LENGTH_LONG);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getDarkTheme() {
|
|
||||||
return R.style.AppTheme_StatusBar_Dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_flash);
|
|
||||||
new FlashActivity_ViewBinding(this);
|
|
||||||
|
|
||||||
setSupportActionBar(toolbar);
|
|
||||||
ActionBar ab = getSupportActionBar();
|
|
||||||
if (ab != null) {
|
|
||||||
ab.setTitle(R.string.flashing);
|
|
||||||
}
|
|
||||||
setFloating();
|
|
||||||
setFinishOnTouchOutside(false);
|
|
||||||
if (!Shell.rootAccess())
|
|
||||||
reboot.setVisibility(View.GONE);
|
|
||||||
|
|
||||||
logs = 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:
|
|
||||||
new InstallMagisk(this, console, logs,
|
|
||||||
intent.getParcelableExtra(Const.Key.FLASH_SET_BOOT)).exec();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.close)
|
|
||||||
@Override
|
|
||||||
public void finish() {
|
|
||||||
super.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
@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,52 +0,0 @@
|
|||||||
package com.topjohnwu.magisk;
|
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.database.MagiskDB;
|
|
||||||
import com.topjohnwu.magisk.database.RepoDatabaseHelper;
|
|
||||||
import com.topjohnwu.magisk.utils.LocaleManager;
|
|
||||||
import com.topjohnwu.magisk.utils.RootUtils;
|
|
||||||
import com.topjohnwu.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 MagiskDB 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);
|
|
||||||
Shell.Config.setTimeout(2);
|
|
||||||
|
|
||||||
prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
|
||||||
mDB = MagiskDB.getInstance();
|
|
||||||
repoDB = new RepoDatabaseHelper(this);
|
|
||||||
|
|
||||||
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.magisk.utils.Utils;
|
|
||||||
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;
|
|
||||||
import butterknife.BindView;
|
|
||||||
|
|
||||||
public class MainActivity extends BaseActivity
|
|
||||||
implements NavigationView.OnNavigationItemSelectedListener, Topic.Subscriber {
|
|
||||||
|
|
||||||
private final Handler mDrawerHandler = new Handler();
|
|
||||||
private int mDrawerItem;
|
|
||||||
private static boolean fromShortcut = false;
|
|
||||||
|
|
||||||
@BindView(R.id.toolbar) public Toolbar toolbar;
|
|
||||||
@BindView(R.id.drawer_layout) DrawerLayout drawer;
|
|
||||||
@BindView(R.id.nav_view) NavigationView navigationView;
|
|
||||||
|
|
||||||
private float toolbarElevation;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getDarkTheme() {
|
|
||||||
return R.style.AppTheme_Dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(final Bundle savedInstanceState) {
|
|
||||||
if (!mm.hasInit) {
|
|
||||||
startActivity(new Intent(this, Data.classMap.get(SplashActivity.class)));
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_main);
|
|
||||||
new MainActivity_ViewBinding(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() &&
|
|
||||||
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(Utils.showSuperUser());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void navigate(String item) {
|
|
||||||
int itemId = R.id.magisk;
|
|
||||||
if (item != null) {
|
|
||||||
switch (item) {
|
|
||||||
case "superuser":
|
|
||||||
itemId = R.id.superuser;
|
|
||||||
break;
|
|
||||||
case "modules":
|
|
||||||
itemId = R.id.modules;
|
|
||||||
break;
|
|
||||||
case "downloads":
|
|
||||||
itemId = R.id.downloads;
|
|
||||||
break;
|
|
||||||
case "magiskhide":
|
|
||||||
itemId = R.id.magiskhide;
|
|
||||||
break;
|
|
||||||
case "log":
|
|
||||||
itemId = R.id.log;
|
|
||||||
break;
|
|
||||||
case "settings":
|
|
||||||
itemId = R.id.settings;
|
|
||||||
break;
|
|
||||||
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,75 +0,0 @@
|
|||||||
package com.topjohnwu.magisk;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.asyncs.CheckUpdates;
|
|
||||||
import com.topjohnwu.magisk.asyncs.UpdateRepos;
|
|
||||||
import com.topjohnwu.magisk.components.BaseActivity;
|
|
||||||
import com.topjohnwu.magisk.components.Notifications;
|
|
||||||
import com.topjohnwu.magisk.receivers.ShortcutReceiver;
|
|
||||||
import com.topjohnwu.magisk.utils.Download;
|
|
||||||
import com.topjohnwu.magisk.utils.LocaleManager;
|
|
||||||
import com.topjohnwu.magisk.utils.RootUtils;
|
|
||||||
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);
|
|
||||||
|
|
||||||
String pkg = mm.mDB.getStrings(Const.Key.SU_MANAGER, null);
|
|
||||||
if (pkg != null && getPackageName().equals(BuildConfig.APPLICATION_ID)) {
|
|
||||||
mm.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(BuildConfig.APPLICATION_ID, 0);
|
|
||||||
RootUtils.uninstallPkg(BuildConfig.APPLICATION_ID);
|
|
||||||
} catch (PackageManager.NameNotFoundException ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Magisk working as expected
|
|
||||||
if (Shell.rootAccess() && Data.magiskVersionCode > 0) {
|
|
||||||
// Update check service
|
|
||||||
Utils.setupUpdateCheck();
|
|
||||||
// Load modules
|
|
||||||
Utils.loadModules();
|
|
||||||
}
|
|
||||||
|
|
||||||
Data.importPrefs();
|
|
||||||
|
|
||||||
// Dynamic detect all locales
|
|
||||||
LocaleManager.loadAvailableLocales();
|
|
||||||
|
|
||||||
// Create notification channel on Android O
|
|
||||||
Notifications.setup(this);
|
|
||||||
|
|
||||||
// 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,259 +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.IOException;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import butterknife.BindView;
|
|
||||||
|
|
||||||
public class SuRequestActivity extends BaseActivity {
|
|
||||||
@BindView(R.id.su_popup) LinearLayout suPopup;
|
|
||||||
@BindView(R.id.timeout) Spinner timeout;
|
|
||||||
@BindView(R.id.app_icon) ImageView appIcon;
|
|
||||||
@BindView(R.id.app_name) TextView appNameView;
|
|
||||||
@BindView(R.id.package_name) TextView packageNameView;
|
|
||||||
@BindView(R.id.grant_btn) Button grant_btn;
|
|
||||||
@BindView(R.id.deny_btn) Button deny_btn;
|
|
||||||
@BindView(R.id.fingerprint) ImageView fingerprintImg;
|
|
||||||
@BindView(R.id.warning) TextView warning;
|
|
||||||
|
|
||||||
private SuConnector connector;
|
|
||||||
private Policy policy;
|
|
||||||
private CountDownTimer timer;
|
|
||||||
private FingerprintHelper fingerprintHelper;
|
|
||||||
|
|
||||||
class SuConnectorV1 extends SuConnector {
|
|
||||||
|
|
||||||
SuConnectorV1(String name) throws IOException {
|
|
||||||
super(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void connect(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 onResponse() throws IOException {
|
|
||||||
out.write((policy.policy == Policy.ALLOW ? "socket:ALLOW" : "socket:DENY").getBytes());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SuConnectorV2 extends SuConnector {
|
|
||||||
|
|
||||||
SuConnectorV2(String name) throws IOException {
|
|
||||||
super(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void connect(String name) throws IOException {
|
|
||||||
socket.connect(new LocalSocketAddress(name, LocalSocketAddress.Namespace.ABSTRACT));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResponse() throws IOException {
|
|
||||||
out.writeInt(policy.policy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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 {
|
|
||||||
String socketName = intent.getStringExtra("socket");
|
|
||||||
connector = intent.getIntExtra("version", 1) == 1 ?
|
|
||||||
new SuConnectorV1(socketName) : new SuConnectorV2(socketName);
|
|
||||||
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, BuildConfig.APPLICATION_ID)) {
|
|
||||||
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);
|
|
||||||
new SuRequestActivity_ViewBinding(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 useFP = FingerprintHelper.useFingerPrint();
|
|
||||||
|
|
||||||
if (useFP) {
|
|
||||||
try {
|
|
||||||
fingerprintHelper = new FingerprintHelper() {
|
|
||||||
@Override
|
|
||||||
public void onAuthenticationError(int errorCode, CharSequence errString) {
|
|
||||||
warning.setText(errString);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
|
|
||||||
warning.setText(helpString);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
|
|
||||||
handleAction(Policy.ALLOW);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAuthenticationFailed() {
|
|
||||||
warning.setText(R.string.auth_fail);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fingerprintHelper.authenticate();
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
useFP = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!useFP) {
|
|
||||||
grant_btn.setOnClickListener(v -> {
|
|
||||||
handleAction(Policy.ALLOW);
|
|
||||||
timer.cancel();
|
|
||||||
});
|
|
||||||
grant_btn.requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
grant_btn.setVisibility(useFP ? View.GONE : View.VISIBLE);
|
|
||||||
fingerprintImg.setVisibility(useFP ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
deny_btn.setOnClickListener(v -> {
|
|
||||||
handleAction(Policy.DENY);
|
|
||||||
timer.cancel();
|
|
||||||
});
|
|
||||||
suPopup.setOnClickListener(v -> cancelTimeout());
|
|
||||||
timeout.setOnTouchListener((v, event) -> cancelTimeout());
|
|
||||||
timer.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean cancelTimeout() {
|
|
||||||
timer.cancel();
|
|
||||||
deny_btn.setText(getString(R.string.deny));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleAction() {
|
|
||||||
connector.response();
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleAction(int action) {
|
|
||||||
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.updatePolicy(policy);
|
|
||||||
}
|
|
||||||
handleAction();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.adapters;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.pm.ApplicationInfo;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
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.utils.Topic;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
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;
|
|
||||||
import butterknife.BindView;
|
|
||||||
|
|
||||||
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 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 || info.uid == 1000) {
|
|
||||||
i.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Collections.sort(fullList, (a, b) -> {
|
|
||||||
boolean ah = hideList.contains(a.packageName);
|
|
||||||
boolean bh = hideList.contains(b.packageName);
|
|
||||||
if (ah == bh) {
|
|
||||||
return Utils.getAppLabel(a, pm).toLowerCase()
|
|
||||||
.compareTo(Utils.getAppLabel(b, pm).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(Utils.getAppLabel(info, pm));
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
@BindView(R.id.app_icon) ImageView appIcon;
|
|
||||||
@BindView(R.id.app_name) TextView appName;
|
|
||||||
@BindView(R.id.package_name) TextView appPackage;
|
|
||||||
@BindView(R.id.checkbox) CheckBox checkBox;
|
|
||||||
|
|
||||||
ViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
new ApplicationAdapter$ViewHolder_ViewBinding(this, itemView);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(Utils.getAppLabel(info, pm), filter)
|
|
||||||
|| lowercaseContains(info.packageName, filter)) {
|
|
||||||
showList.add(info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void publishResults(CharSequence constraint, FilterResults results) {
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,126 +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.components.SnackbarMaker;
|
|
||||||
import com.topjohnwu.magisk.container.Module;
|
|
||||||
import com.topjohnwu.superuser.Shell;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import butterknife.BindView;
|
|
||||||
|
|
||||||
public class ModulesAdapter extends RecyclerView.Adapter<ModulesAdapter.ViewHolder> {
|
|
||||||
|
|
||||||
private final List<Module> mList;
|
|
||||||
|
|
||||||
public ModulesAdapter(List<Module> list) {
|
|
||||||
mList = list;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
|
||||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_module, parent, false);
|
|
||||||
return new ViewHolder(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(final ViewHolder holder, int position) {
|
|
||||||
Context context = holder.itemView.getContext();
|
|
||||||
final Module module = mList.get(position);
|
|
||||||
|
|
||||||
String version = module.getVersion();
|
|
||||||
String author = module.getAuthor();
|
|
||||||
String description = module.getDescription();
|
|
||||||
String noInfo = context.getString(R.string.no_info_provided);
|
|
||||||
|
|
||||||
holder.title.setText(module.getName());
|
|
||||||
holder.versionName.setText(TextUtils.isEmpty(version) ? noInfo : version);
|
|
||||||
holder.author.setText(TextUtils.isEmpty(author) ? noInfo : context.getString(R.string.author, author));
|
|
||||||
holder.description.setText(TextUtils.isEmpty(description) ? noInfo : description);
|
|
||||||
|
|
||||||
holder.checkBox.setOnCheckedChangeListener(null);
|
|
||||||
holder.checkBox.setChecked(module.isEnabled());
|
|
||||||
holder.checkBox.setOnCheckedChangeListener((v, isChecked) -> {
|
|
||||||
int snack;
|
|
||||||
if (isChecked) {
|
|
||||||
module.removeDisableFile();
|
|
||||||
snack = R.string.disable_file_removed;
|
|
||||||
} else {
|
|
||||||
module.createDisableFile();
|
|
||||||
snack = R.string.disable_file_created;
|
|
||||||
}
|
|
||||||
SnackbarMaker.make(holder.itemView, snack, Snackbar.LENGTH_SHORT).show();
|
|
||||||
});
|
|
||||||
|
|
||||||
holder.delete.setOnClickListener(v -> {
|
|
||||||
boolean removed = module.willBeRemoved();
|
|
||||||
int snack;
|
|
||||||
if (removed) {
|
|
||||||
module.deleteRemoveFile();
|
|
||||||
snack = R.string.remove_file_deleted;
|
|
||||||
} else {
|
|
||||||
module.createRemoveFile();
|
|
||||||
snack = R.string.remove_file_created;
|
|
||||||
}
|
|
||||||
SnackbarMaker.make(holder.itemView, snack, Snackbar.LENGTH_SHORT).show();
|
|
||||||
updateDeleteButton(holder, module);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (module.isUpdated()) {
|
|
||||||
holder.notice.setVisibility(View.VISIBLE);
|
|
||||||
holder.notice.setText(R.string.update_file_created);
|
|
||||||
holder.delete.setEnabled(false);
|
|
||||||
} else {
|
|
||||||
updateDeleteButton(holder, module);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateDeleteButton(ViewHolder holder, Module module) {
|
|
||||||
holder.notice.setVisibility(module.willBeRemoved() ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
if (module.willBeRemoved()) {
|
|
||||||
holder.delete.setImageResource(R.drawable.ic_undelete);
|
|
||||||
} else {
|
|
||||||
holder.delete.setImageResource(R.drawable.ic_delete);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return mList.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
@BindView(R.id.title) TextView title;
|
|
||||||
@BindView(R.id.version_name) TextView versionName;
|
|
||||||
@BindView(R.id.description) TextView description;
|
|
||||||
@BindView(R.id.notice) TextView notice;
|
|
||||||
@BindView(R.id.checkbox) CheckBox checkBox;
|
|
||||||
@BindView(R.id.author) TextView author;
|
|
||||||
@BindView(R.id.delete) ImageView delete;
|
|
||||||
|
|
||||||
ViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
new ModulesAdapter$ViewHolder_ViewBinding(this, itemView);
|
|
||||||
|
|
||||||
if (!Shell.rootAccess()) {
|
|
||||||
checkBox.setEnabled(false);
|
|
||||||
delete.setEnabled(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,169 +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.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.MagiskDB;
|
|
||||||
import com.topjohnwu.magisk.utils.FingerprintHelper;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import butterknife.BindView;
|
|
||||||
|
|
||||||
public class PolicyAdapter extends RecyclerView.Adapter<PolicyAdapter.ViewHolder> {
|
|
||||||
|
|
||||||
private List<Policy> policyList;
|
|
||||||
private MagiskDB dbHelper;
|
|
||||||
private PackageManager pm;
|
|
||||||
private Set<Policy> expandList = new HashSet<>();
|
|
||||||
|
|
||||||
public PolicyAdapter(List<Policy> list, MagiskDB db, PackageManager pm) {
|
|
||||||
policyList = list;
|
|
||||||
dbHelper = db;
|
|
||||||
this.pm = pm;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@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.notificationSwitch.setOnCheckedChangeListener(null);
|
|
||||||
holder.loggingSwitch.setOnCheckedChangeListener(null);
|
|
||||||
|
|
||||||
holder.masterSwitch.setChecked(policy.policy == Policy.ALLOW);
|
|
||||||
holder.notificationSwitch.setChecked(policy.notification);
|
|
||||||
holder.loggingSwitch.setChecked(policy.logging);
|
|
||||||
|
|
||||||
holder.masterSwitch.setOnClickListener(v -> {
|
|
||||||
boolean isChecked = holder.masterSwitch.isChecked();
|
|
||||||
Runnable r = () -> {
|
|
||||||
if ((isChecked && policy.policy == Policy.DENY) ||
|
|
||||||
(!isChecked && policy.policy == Policy.ALLOW)) {
|
|
||||||
policy.policy = isChecked ? Policy.ALLOW : Policy.DENY;
|
|
||||||
String message = v.getContext().getString(
|
|
||||||
isChecked ? R.string.su_snack_grant : R.string.su_snack_deny, policy.appName);
|
|
||||||
SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show();
|
|
||||||
dbHelper.updatePolicy(policy);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (FingerprintHelper.useFingerPrint()) {
|
|
||||||
holder.masterSwitch.setChecked(!isChecked);
|
|
||||||
FingerprintHelper.showAuthDialog((Activity) v.getContext(), () -> {
|
|
||||||
holder.masterSwitch.setChecked(isChecked);
|
|
||||||
r.run();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
r.run();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
holder.notificationSwitch.setOnCheckedChangeListener((v, isChecked) -> {
|
|
||||||
if ((isChecked && !policy.notification) ||
|
|
||||||
(!isChecked && policy.notification)) {
|
|
||||||
policy.notification = isChecked;
|
|
||||||
String message = v.getContext().getString(
|
|
||||||
isChecked ? R.string.su_snack_notif_on : R.string.su_snack_notif_off, policy.appName);
|
|
||||||
SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show();
|
|
||||||
dbHelper.updatePolicy(policy);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
holder.loggingSwitch.setOnCheckedChangeListener((v, isChecked) -> {
|
|
||||||
if ((isChecked && !policy.logging) ||
|
|
||||||
(!isChecked && policy.logging)) {
|
|
||||||
policy.logging = isChecked;
|
|
||||||
String message = v.getContext().getString(
|
|
||||||
isChecked ? R.string.su_snack_log_on : R.string.su_snack_log_off, policy.appName);
|
|
||||||
SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show();
|
|
||||||
dbHelper.updatePolicy(policy);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
holder.delete.setOnClickListener(v -> 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());
|
|
||||||
|
|
||||||
// Hide for now
|
|
||||||
holder.moreInfo.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return policyList.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
static class ViewHolder extends RecyclerView.ViewHolder implements ExpandableView {
|
|
||||||
|
|
||||||
@BindView(R.id.app_name) TextView appName;
|
|
||||||
@BindView(R.id.package_name) TextView packageName;
|
|
||||||
@BindView(R.id.app_icon) ImageView appIcon;
|
|
||||||
@BindView(R.id.master_switch) Switch masterSwitch;
|
|
||||||
@BindView(R.id.notification_switch) Switch notificationSwitch;
|
|
||||||
@BindView(R.id.logging_switch) Switch loggingSwitch;
|
|
||||||
@BindView(R.id.expand_layout) ViewGroup expandLayout;
|
|
||||||
|
|
||||||
@BindView(R.id.delete) ImageView delete;
|
|
||||||
@BindView(R.id.more_info) ImageView moreInfo;
|
|
||||||
|
|
||||||
private Container container = new Container();
|
|
||||||
|
|
||||||
public ViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
new PolicyAdapter$ViewHolder_ViewBinding(this, itemView);
|
|
||||||
container.expandLayout = expandLayout;
|
|
||||||
setupExpandable();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Container getContainer() {
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,191 +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.asyncs.DownloadModule;
|
|
||||||
import com.topjohnwu.magisk.asyncs.MarkDownWindow;
|
|
||||||
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;
|
|
||||||
import butterknife.BindView;
|
|
||||||
|
|
||||||
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) ->
|
|
||||||
DownloadModule.exec((BaseActivity) context, repo, true))
|
|
||||||
.setNeutralButton(R.string.download, (d, i) ->
|
|
||||||
DownloadModule.exec((BaseActivity) context, repo, false))
|
|
||||||
.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();
|
|
||||||
}
|
|
||||||
|
|
||||||
static class SectionHolder extends RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
@BindView(R.id.section_text) TextView sectionText;
|
|
||||||
|
|
||||||
SectionHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
new ReposAdapter$SectionHolder_ViewBinding(this, itemView);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class RepoHolder extends RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
@BindView(R.id.title) TextView title;
|
|
||||||
@BindView(R.id.version_name) TextView versionName;
|
|
||||||
@BindView(R.id.description) TextView description;
|
|
||||||
@BindView(R.id.author) TextView author;
|
|
||||||
@BindView(R.id.info_layout) LinearLayout infoLayout;
|
|
||||||
@BindView(R.id.download) ImageView downloadImage;
|
|
||||||
@BindView(R.id.update_time) TextView updateTime;
|
|
||||||
|
|
||||||
RepoHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
new ReposAdapter$RepoHolder_ViewBinding(this, itemView);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.adapters;
|
|
||||||
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
public abstract class SectionedAdapter<S extends RecyclerView.ViewHolder, C extends RecyclerView.ViewHolder>
|
|
||||||
extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
|
||||||
|
|
||||||
private static final int SECTION_TYPE = Integer.MIN_VALUE;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
final public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
if (viewType == SECTION_TYPE)
|
|
||||||
return onCreateSectionViewHolder(parent);
|
|
||||||
return onCreateItemViewHolder(parent, viewType);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
final public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
|
||||||
PositionInfo info = getPositionInfo(position);
|
|
||||||
if (info.position == -1)
|
|
||||||
onBindSectionViewHolder((S) holder, info.section);
|
|
||||||
else
|
|
||||||
onBindItemViewHolder((C) holder, info.section, info.position);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
final public int getItemCount() {
|
|
||||||
int size, sec;
|
|
||||||
size = sec = getSectionCount();
|
|
||||||
for (int i = 0; i < sec; ++i){
|
|
||||||
size += getItemCount(i);
|
|
||||||
}
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
final public int getItemViewType(int position) {
|
|
||||||
PositionInfo info = getPositionInfo(position);
|
|
||||||
if (info.position == -1)
|
|
||||||
return SECTION_TYPE;
|
|
||||||
else
|
|
||||||
return getItemViewType(info.section, info.position);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getItemViewType(int section, int position) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected int getSectionPosition(int section) {
|
|
||||||
return getItemPosition(section, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected int getItemPosition(int section, int position) {
|
|
||||||
int realPosition = 0;
|
|
||||||
// Previous sections
|
|
||||||
for (int i = 0; i < section; ++i) {
|
|
||||||
realPosition += getItemCount(i) + 1;
|
|
||||||
}
|
|
||||||
// Current section
|
|
||||||
realPosition += position + 1;
|
|
||||||
return realPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
private PositionInfo getPositionInfo(int position) {
|
|
||||||
int section = 0;
|
|
||||||
while (true) {
|
|
||||||
if (position == 0)
|
|
||||||
return new PositionInfo(section, -1);
|
|
||||||
position -= 1;
|
|
||||||
if (position < getItemCount(section))
|
|
||||||
return new PositionInfo(section, position);
|
|
||||||
position -= getItemCount(section++);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class PositionInfo {
|
|
||||||
int section;
|
|
||||||
int position;
|
|
||||||
PositionInfo(int section, int position) {
|
|
||||||
this.section = section;
|
|
||||||
this.position = position;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract int getSectionCount();
|
|
||||||
public abstract int getItemCount(int section);
|
|
||||||
public abstract S onCreateSectionViewHolder(ViewGroup parent);
|
|
||||||
public abstract C onCreateItemViewHolder(ViewGroup parent, int viewType);
|
|
||||||
public abstract void onBindSectionViewHolder(S holder, int section);
|
|
||||||
public abstract void onBindItemViewHolder(C holder, int section, int position);
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.adapters;
|
|
||||||
|
|
||||||
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.components.ExpandableView;
|
|
||||||
import com.topjohnwu.magisk.container.SuLogEntry;
|
|
||||||
import com.topjohnwu.magisk.database.MagiskDB;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import butterknife.BindView;
|
|
||||||
|
|
||||||
public class SuLogAdapter extends SectionedAdapter<SuLogAdapter.SectionHolder, SuLogAdapter.LogViewHolder> {
|
|
||||||
|
|
||||||
private List<List<SuLogEntry>> logEntries;
|
|
||||||
private Set<Integer> itemExpanded, sectionExpanded;
|
|
||||||
private MagiskDB suDB;
|
|
||||||
|
|
||||||
public SuLogAdapter(MagiskDB db) {
|
|
||||||
suDB = db;
|
|
||||||
logEntries = Collections.emptyList();
|
|
||||||
sectionExpanded = new HashSet<>();
|
|
||||||
itemExpanded = new HashSet<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getSectionCount() {
|
|
||||||
return logEntries.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount(int section) {
|
|
||||||
return sectionExpanded.contains(section) ? logEntries.get(section).size() : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SectionHolder onCreateSectionViewHolder(ViewGroup parent) {
|
|
||||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_sulog_group, parent, false);
|
|
||||||
return new SectionHolder(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LogViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
|
|
||||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_sulog, parent, false);
|
|
||||||
return new LogViewHolder(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindSectionViewHolder(SectionHolder holder, int section) {
|
|
||||||
SuLogEntry entry = logEntries.get(section).get(0);
|
|
||||||
holder.arrow.setRotation(sectionExpanded.contains(section) ? 180 : 0);
|
|
||||||
holder.itemView.setOnClickListener(v -> {
|
|
||||||
RotateAnimation rotate;
|
|
||||||
if (sectionExpanded.contains(section)) {
|
|
||||||
holder.arrow.setRotation(0);
|
|
||||||
rotate = new RotateAnimation(180, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
|
|
||||||
sectionExpanded.remove(section);
|
|
||||||
notifyItemRangeRemoved(getItemPosition(section, 0), logEntries.get(section).size());
|
|
||||||
} else {
|
|
||||||
rotate = new RotateAnimation(0, 180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
|
|
||||||
sectionExpanded.add(section);
|
|
||||||
notifyItemRangeInserted(getItemPosition(section, 0), logEntries.get(section).size());
|
|
||||||
}
|
|
||||||
rotate.setDuration(300);
|
|
||||||
rotate.setFillAfter(true);
|
|
||||||
holder.arrow.setAnimation(rotate);
|
|
||||||
});
|
|
||||||
holder.date.setText(entry.getDateString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindItemViewHolder(LogViewHolder holder, int section, int position) {
|
|
||||||
SuLogEntry entry = logEntries.get(section).get(position);
|
|
||||||
int realIdx = getItemPosition(section, position);
|
|
||||||
holder.setExpanded(itemExpanded.contains(realIdx));
|
|
||||||
holder.itemView.setOnClickListener(view -> {
|
|
||||||
if (holder.isExpanded()) {
|
|
||||||
holder.collapse();
|
|
||||||
itemExpanded.remove(realIdx);
|
|
||||||
} else {
|
|
||||||
holder.expand();
|
|
||||||
itemExpanded.add(realIdx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
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() {
|
|
||||||
logEntries = suDB.getLogs();
|
|
||||||
itemExpanded.clear();
|
|
||||||
sectionExpanded.clear();
|
|
||||||
sectionExpanded.add(0);
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
static class SectionHolder extends RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
@BindView(R.id.date) TextView date;
|
|
||||||
@BindView(R.id.arrow) ImageView arrow;
|
|
||||||
|
|
||||||
SectionHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
new SuLogAdapter$SectionHolder_ViewBinding(this, itemView);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class LogViewHolder extends RecyclerView.ViewHolder implements ExpandableView {
|
|
||||||
|
|
||||||
@BindView(R.id.app_name) TextView appName;
|
|
||||||
@BindView(R.id.action) TextView action;
|
|
||||||
@BindView(R.id.time) TextView time;
|
|
||||||
@BindView(R.id.fromPid) TextView fromPid;
|
|
||||||
@BindView(R.id.toUid) TextView toUid;
|
|
||||||
@BindView(R.id.command) TextView command;
|
|
||||||
@BindView(R.id.expand_layout) ViewGroup expandLayout;
|
|
||||||
|
|
||||||
private Container container = new Container();
|
|
||||||
|
|
||||||
LogViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
new SuLogAdapter$LogViewHolder_ViewBinding(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,78 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.asyncs;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
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(Const.Url.SNET_URL);
|
|
||||||
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() < Const.SNET_EXT_VER) {
|
|
||||||
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,107 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.asyncs;
|
|
||||||
|
|
||||||
import com.androidnetworking.AndroidNetworking;
|
|
||||||
import com.androidnetworking.error.ANError;
|
|
||||||
import com.androidnetworking.interfaces.JSONObjectRequestListener;
|
|
||||||
import com.topjohnwu.magisk.BuildConfig;
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.Data;
|
|
||||||
import com.topjohnwu.magisk.components.Notifications;
|
|
||||||
import com.topjohnwu.magisk.utils.Topic;
|
|
||||||
|
|
||||||
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 check(Runnable cb) {
|
|
||||||
String url;
|
|
||||||
switch (Data.updateChannel) {
|
|
||||||
case Const.Value.STABLE_CHANNEL:
|
|
||||||
url = Const.Url.STABLE_URL;
|
|
||||||
break;
|
|
||||||
case Const.Value.BETA_CHANNEL:
|
|
||||||
url = Const.Url.BETA_URL;
|
|
||||||
break;
|
|
||||||
case Const.Value.CUSTOM_CHANNEL:
|
|
||||||
url = Data.MM().prefs.getString(Const.Key.CUSTOM_CHANNEL, "");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
AndroidNetworking.get(url).build().getAsJSONObject(new UpdateListener(cb));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void check() {
|
|
||||||
check(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class UpdateListener implements JSONObjectRequestListener {
|
|
||||||
|
|
||||||
private Runnable cb;
|
|
||||||
|
|
||||||
UpdateListener(Runnable callback) {
|
|
||||||
cb = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResponse(JSONObject json) {
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (cb != null) {
|
|
||||||
if (BuildConfig.VERSION_CODE < Data.remoteManagerVersionCode) {
|
|
||||||
Notifications.managerUpdate();
|
|
||||||
} else if (Data.magiskVersionCode < Data.remoteMagiskVersionCode) {
|
|
||||||
Notifications.magiskUpdate();
|
|
||||||
}
|
|
||||||
cb.run();
|
|
||||||
}
|
|
||||||
Topic.publish(Topic.UPDATE_CHECK_DONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(ANError anError) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.asyncs;
|
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
|
|
||||||
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.BaseActivity;
|
|
||||||
import com.topjohnwu.magisk.components.ProgressNotification;
|
|
||||||
import com.topjohnwu.magisk.container.Repo;
|
|
||||||
import com.topjohnwu.magisk.utils.WebService;
|
|
||||||
import com.topjohnwu.superuser.ShellUtils;
|
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
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.zip.ZipEntry;
|
|
||||||
import java.util.zip.ZipInputStream;
|
|
||||||
import java.util.zip.ZipOutputStream;
|
|
||||||
|
|
||||||
public class DownloadModule {
|
|
||||||
|
|
||||||
public static void exec(BaseActivity activity, Repo repo, boolean install) {
|
|
||||||
activity.runWithPermission(new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE },
|
|
||||||
() -> AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> dlProcessInstall(repo, install)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void dlProcessInstall(Repo repo, boolean install) {
|
|
||||||
File output = new File(Const.EXTERNAL_PATH, repo.getDownloadFilename());
|
|
||||||
ProgressNotification progress = new ProgressNotification(output.getName());
|
|
||||||
try {
|
|
||||||
MagiskManager mm = Data.MM();
|
|
||||||
HttpURLConnection conn = WebService.mustRequest(repo.getZipUrl());
|
|
||||||
ProgressInputStream pis = new ProgressInputStream(conn.getInputStream(),
|
|
||||||
conn.getContentLength(), progress);
|
|
||||||
removeTopFolder(new BufferedInputStream(pis),
|
|
||||||
new BufferedOutputStream(new FileOutputStream(output)));
|
|
||||||
conn.disconnect();
|
|
||||||
if (install) {
|
|
||||||
progress.dismiss();
|
|
||||||
Intent intent = new Intent(mm, Data.classMap.get(FlashActivity.class));
|
|
||||||
intent.setData(Uri.fromFile(output))
|
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
.putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP);
|
|
||||||
mm.startActivity(intent);
|
|
||||||
} else {
|
|
||||||
progress.getNotification().setContentTitle(output.getName());
|
|
||||||
progress.dlDone();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
progress.dlFail();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void removeTopFolder(InputStream in, OutputStream out) throws IOException {
|
|
||||||
try (ZipInputStream zin = new ZipInputStream(in);
|
|
||||||
ZipOutputStream zout = new ZipOutputStream(out)) {
|
|
||||||
ZipEntry entry;
|
|
||||||
int off = -1;
|
|
||||||
while ((entry = zin.getNextEntry()) != null) {
|
|
||||||
if (off < 0)
|
|
||||||
off = entry.getName().indexOf('/') + 1;
|
|
||||||
String path = entry.getName().substring(off);
|
|
||||||
if (path.isEmpty())
|
|
||||||
continue;
|
|
||||||
zout.putNextEntry(new ZipEntry(path));
|
|
||||||
if (!entry.isDirectory())
|
|
||||||
ShellUtils.pump(zin, zout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class ProgressInputStream extends FilterInputStream {
|
|
||||||
|
|
||||||
private long totalBytes;
|
|
||||||
private long bytesDownloaded;
|
|
||||||
private ProgressNotification progress;
|
|
||||||
|
|
||||||
protected ProgressInputStream(InputStream in, long size, ProgressNotification p) {
|
|
||||||
super(in);
|
|
||||||
totalBytes = size;
|
|
||||||
progress = p;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateProgress() {
|
|
||||||
progress.onProgress(bytesDownloaded, totalBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read() throws IOException {
|
|
||||||
int b = super.read();
|
|
||||||
if (b >= 0) {
|
|
||||||
bytesDownloaded++;
|
|
||||||
updateProgress();
|
|
||||||
}
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read(byte[] b) throws IOException {
|
|
||||||
return read(b, 0, b.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read(byte[] b, int off, int len) throws IOException {
|
|
||||||
int sz = super.read(b, off, len);
|
|
||||||
if (sz > 0) {
|
|
||||||
bytesDownloaded += sz;
|
|
||||||
updateProgress();
|
|
||||||
}
|
|
||||||
return sz;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,397 +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.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);
|
|
||||||
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(Const.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.Utils;
|
|
||||||
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 = Utils.dlString(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,156 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.asyncs;
|
|
||||||
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.BuildConfig;
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.Data;
|
|
||||||
import com.topjohnwu.magisk.MagiskManager;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.components.Notifications;
|
|
||||||
import com.topjohnwu.magisk.utils.RootUtils;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
import com.topjohnwu.superuser.ShellUtils;
|
|
||||||
import com.topjohnwu.utils.JarMap;
|
|
||||||
import com.topjohnwu.utils.SignAPK;
|
|
||||||
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.ByteOrder;
|
|
||||||
import java.nio.CharBuffer;
|
|
||||||
import java.nio.IntBuffer;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.util.jar.JarEntry;
|
|
||||||
|
|
||||||
import androidx.core.app.NotificationCompat;
|
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
|
||||||
|
|
||||||
public class PatchAPK {
|
|
||||||
|
|
||||||
public static final String LOWERALPHA = "abcdefghijklmnopqrstuvwxyz";
|
|
||||||
public static final String UPPERALPHA = LOWERALPHA.toUpperCase();
|
|
||||||
public static final String ALPHA = LOWERALPHA + UPPERALPHA;
|
|
||||||
public static final String DIGITS = "0123456789";
|
|
||||||
public static final String ALPHANUM = ALPHA + DIGITS;
|
|
||||||
public static final String ALPHANUMDOTS = ALPHANUM + "............";
|
|
||||||
|
|
||||||
private static String genPackageName(String prefix, int length) {
|
|
||||||
StringBuilder builder = new StringBuilder(length);
|
|
||||||
builder.append(prefix);
|
|
||||||
length -= prefix.length();
|
|
||||||
SecureRandom random = new SecureRandom();
|
|
||||||
char next, prev = '.';
|
|
||||||
for (int i = 0; i < length; ++i) {
|
|
||||||
if (prev == '.' || i == length - 1) {
|
|
||||||
next = ALPHA.charAt(random.nextInt(ALPHA.length()));
|
|
||||||
} else {
|
|
||||||
next = ALPHANUMDOTS.charAt(random.nextInt(ALPHANUMDOTS.length()));
|
|
||||||
}
|
|
||||||
builder.append(next);
|
|
||||||
prev = next;
|
|
||||||
}
|
|
||||||
return builder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean findAndPatch(byte xml[], String a, String b) {
|
|
||||||
if (a.length() != b.length())
|
|
||||||
return false;
|
|
||||||
char[] from = a.toCharArray(), to = b.toCharArray();
|
|
||||||
CharBuffer buf = ByteBuffer.wrap(xml).order(ByteOrder.LITTLE_ENDIAN).asCharBuffer();
|
|
||||||
int offset = -1;
|
|
||||||
for (int i = 0; i < buf.length() - from.length; ++i) {
|
|
||||||
boolean match = true;
|
|
||||||
for (int j = 0; j < from.length; ++j) {
|
|
||||||
if (buf.get(i + j) != from[j]) {
|
|
||||||
match = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Make sure it is null terminated
|
|
||||||
if (match && buf.get(i + from.length) == '\0') {
|
|
||||||
offset = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (offset < 0)
|
|
||||||
return false;
|
|
||||||
buf.position(offset);
|
|
||||||
buf.put(to);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean findAndPatch(byte xml[], int a, int b) {
|
|
||||||
IntBuffer buf = ByteBuffer.wrap(xml).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer();
|
|
||||||
int len = xml.length / 4;
|
|
||||||
for (int i = 0; i < len; ++i) {
|
|
||||||
if (buf.get(i) == a) {
|
|
||||||
buf.put(i, b);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean patchAndHide() {
|
|
||||||
MagiskManager mm = Data.MM();
|
|
||||||
|
|
||||||
// Generate a new app with random package name
|
|
||||||
File repack = new File(mm.getFilesDir(), "patched.apk");
|
|
||||||
String pkg = genPackageName("com.", BuildConfig.APPLICATION_ID.length());
|
|
||||||
|
|
||||||
try {
|
|
||||||
JarMap apk = new JarMap(mm.getPackageCodePath());
|
|
||||||
if (!patch(apk, pkg))
|
|
||||||
return false;
|
|
||||||
SignAPK.sign(apk, new BufferedOutputStream(new FileOutputStream(repack)));
|
|
||||||
} catch (Exception e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install the application
|
|
||||||
repack.setReadable(true, false);
|
|
||||||
if (!ShellUtils.fastCmdResult("pm install " + repack))
|
|
||||||
return false;;
|
|
||||||
|
|
||||||
mm.mDB.setStrings(Const.Key.SU_MANAGER, pkg);
|
|
||||||
Data.exportPrefs();
|
|
||||||
RootUtils.rmAndLaunch(BuildConfig.APPLICATION_ID, pkg);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean patch(JarMap apk, String pkg) {
|
|
||||||
try {
|
|
||||||
JarEntry je = apk.getJarEntry(Const.ANDROID_MANIFEST);
|
|
||||||
byte xml[] = apk.getRawData(je);
|
|
||||||
|
|
||||||
if (!findAndPatch(xml, BuildConfig.APPLICATION_ID, pkg) ||
|
|
||||||
!findAndPatch(xml, BuildConfig.APPLICATION_ID + ".provider", pkg + ".provider") ||
|
|
||||||
!findAndPatch(xml, R.string.app_name, R.string.re_app_name))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Write in changes
|
|
||||||
apk.getOutputStream(je).write(xml);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void hideManager() {
|
|
||||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
|
||||||
MagiskManager mm = Data.MM();
|
|
||||||
NotificationCompat.Builder progress =
|
|
||||||
Notifications.progress(mm.getString(R.string.hide_manager_title));
|
|
||||||
NotificationManagerCompat mgr = NotificationManagerCompat.from(mm);
|
|
||||||
mgr.notify(Const.ID.HIDE_MANAGER_NOTIFICATION_ID, progress.build());
|
|
||||||
boolean b = patchAndHide();
|
|
||||||
mgr.cancel(Const.ID.HIDE_MANAGER_NOTIFICATION_ID);
|
|
||||||
if (!b) Utils.toast(R.string.hide_manager_fail_toast, Toast.LENGTH_LONG);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.asyncs;
|
|
||||||
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
|
|
||||||
import com.androidnetworking.AndroidNetworking;
|
|
||||||
import com.androidnetworking.common.ANRequest;
|
|
||||||
import com.androidnetworking.common.ANResponse;
|
|
||||||
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 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.Locale;
|
|
||||||
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;
|
|
||||||
|
|
||||||
private MagiskManager mm;
|
|
||||||
private Set<String> cached;
|
|
||||||
private ExecutorService threadPool;
|
|
||||||
|
|
||||||
static {
|
|
||||||
dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
|
|
||||||
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public UpdateRepos() {
|
|
||||||
mm = Data.MM();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void waitTasks() {
|
|
||||||
threadPool.shutdown();
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
if (threadPool.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS))
|
|
||||||
break;
|
|
||||||
} catch (InterruptedException ignored) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadJSON(JSONArray array) throws JSONException, ParseException {
|
|
||||||
for (int i = 0; i < array.length(); i++) {
|
|
||||||
JSONObject rawRepo = array.getJSONObject(i);
|
|
||||||
String id = rawRepo.getString("name");
|
|
||||||
Date date = dateFormat.parse(rawRepo.getString("pushed_at"));
|
|
||||||
threadPool.execute(() -> {
|
|
||||||
Repo repo = mm.repoDB.getRepo(id);
|
|
||||||
try {
|
|
||||||
if (repo == null)
|
|
||||||
repo = new Repo(id);
|
|
||||||
else
|
|
||||||
cached.remove(id);
|
|
||||||
repo.update(date);
|
|
||||||
mm.repoDB.addRepo(repo);
|
|
||||||
} catch (Repo.IllegalRepoException e) {
|
|
||||||
Logger.debug(e.getMessage());
|
|
||||||
mm.repoDB.removeRepo(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 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) {
|
|
||||||
ANRequest.GetRequestBuilder req = AndroidNetworking.get(Const.Url.REPO_URL)
|
|
||||||
.addQueryParameter("page", String.valueOf(page + 1));
|
|
||||||
if (page == 0) {
|
|
||||||
String etag = mm.prefs.getString(Const.Key.ETAG_KEY, null);
|
|
||||||
if (etag != null)
|
|
||||||
req.addHeaders(Const.Key.IF_NONE_MATCH, etag);
|
|
||||||
}
|
|
||||||
ANResponse<JSONArray> res = req.build().executeForJSONArray();
|
|
||||||
if (res.getOkHttpResponse().code() == HttpURLConnection.HTTP_NOT_MODIFIED)
|
|
||||||
return false;
|
|
||||||
// Current page is the last page
|
|
||||||
if (res.getResult() == null || res.getResult().length() == 0)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
loadJSON(res.getResult());
|
|
||||||
} catch (JSONException | ParseException e) {
|
|
||||||
// Should not happen, but if exception occurs, page load fails
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update ETAG
|
|
||||||
if (page == 0) {
|
|
||||||
String etag = res.getOkHttpResponse().header(Const.Key.ETAG_KEY);
|
|
||||||
if (etag != null) {
|
|
||||||
etag = etag.substring(etag.indexOf('\"'), etag.lastIndexOf('\"') + 1);
|
|
||||||
mm.prefs.edit().putString(Const.Key.ETAG_KEY, etag).apply();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String links = res.getOkHttpResponse().header(Const.Key.LINK_KEY);
|
|
||||||
return links == null || !links.contains("next") || loadPage(page + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean loadPages() {
|
|
||||||
return loadPage(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = Collections.synchronizedSet(mm.repoDB.getRepoIDSet());
|
|
||||||
threadPool = Executors.newFixedThreadPool(CORE_POOL_SIZE);
|
|
||||||
|
|
||||||
if (loadPages()) {
|
|
||||||
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,78 +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 butterknife.BindView;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author dvdandroid
|
|
||||||
*/
|
|
||||||
public class AboutCardRow extends LinearLayout {
|
|
||||||
|
|
||||||
@BindView(android.R.id.title) TextView mTitle;
|
|
||||||
@BindView(android.R.id.summary) TextView mSummary;
|
|
||||||
@BindView(android.R.id.icon) ImageView mIcon;
|
|
||||||
@BindView(R.id.container) 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);
|
|
||||||
new AboutCardRow_ViewBinding(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,147 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.components;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.Data;
|
|
||||||
import com.topjohnwu.magisk.MagiskManager;
|
|
||||||
import com.topjohnwu.magisk.NoUIActivity;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.utils.LocaleManager;
|
|
||||||
import com.topjohnwu.magisk.utils.Topic;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.StyleRes;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.core.app.ActivityCompat;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
|
|
||||||
public abstract class BaseActivity extends AppCompatActivity implements Topic.AutoSubscriber {
|
|
||||||
|
|
||||||
public static final String INTENT_PERM = "perm_dialog";
|
|
||||||
|
|
||||||
protected static Runnable permissionGrantCallback;
|
|
||||||
static int[] EMPTY_INT_ARRAY = new int[0];
|
|
||||||
|
|
||||||
private ActivityResultListener activityResultListener;
|
|
||||||
public MagiskManager mm;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int[] getSubscribedTopics() {
|
|
||||||
return EMPTY_INT_ARRAY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@StyleRes
|
|
||||||
public int getDarkTheme() {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void attachBaseContext(Context base) {
|
|
||||||
super.attachBaseContext(base);
|
|
||||||
Configuration config = base.getResources().getConfiguration();
|
|
||||||
config.setLocale(LocaleManager.locale);
|
|
||||||
applyOverrideConfiguration(config);
|
|
||||||
mm = Data.MM();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
Topic.subscribe(this);
|
|
||||||
if (Data.isDarkTheme && getDarkTheme() != -1) {
|
|
||||||
setTheme(getDarkTheme());
|
|
||||||
}
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
String[] perms = getIntent().getStringArrayExtra(INTENT_PERM);
|
|
||||||
if (perms != null)
|
|
||||||
ActivityCompat.requestPermissions(this, perms, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void runWithPermission(Context context, String[] permissions, Runnable callback) {
|
|
||||||
boolean granted = true;
|
|
||||||
for (String perm : permissions) {
|
|
||||||
if (ContextCompat.checkSelfPermission(context, perm) != PackageManager.PERMISSION_GRANTED)
|
|
||||||
granted = false;
|
|
||||||
}
|
|
||||||
if (granted) {
|
|
||||||
Const.EXTERNAL_PATH.mkdirs();
|
|
||||||
callback.run();
|
|
||||||
} else {
|
|
||||||
// Passed in context should be an activity if not granted, need to show dialog!
|
|
||||||
permissionGrantCallback = callback;
|
|
||||||
if (!(context instanceof BaseActivity)) {
|
|
||||||
// Start NoUIActivity to show dialog
|
|
||||||
Intent intent = new Intent(context, Data.classMap.get(NoUIActivity.class));
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
intent.putExtra(INTENT_PERM, permissions);
|
|
||||||
context.startActivity(intent);
|
|
||||||
} else {
|
|
||||||
ActivityCompat.requestPermissions((BaseActivity) context, permissions, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void runWithPermission(String[] permissions, Runnable callback) {
|
|
||||||
runWithPermission(this, permissions, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
||||||
if (activityResultListener != null)
|
|
||||||
activityResultListener.onActivityResult(requestCode, resultCode, data);
|
|
||||||
activityResultListener = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void startActivityForResult(Intent intent, int requestCode, ActivityResultListener listener) {
|
|
||||||
activityResultListener = listener;
|
|
||||||
super.startActivityForResult(intent, requestCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
|
||||||
boolean grant = true;
|
|
||||||
for (int result : grantResults) {
|
|
||||||
if (result != PackageManager.PERMISSION_GRANTED)
|
|
||||||
grant = false;
|
|
||||||
}
|
|
||||||
if (grant) {
|
|
||||||
if (permissionGrantCallback != null) {
|
|
||||||
permissionGrantCallback.run();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Toast.makeText(this, R.string.no_rw_storage, Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
permissionGrantCallback = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface ActivityResultListener {
|
|
||||||
void onActivityResult(int requestCode, int resultCode, Intent data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +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;
|
|
||||||
import butterknife.Unbinder;
|
|
||||||
|
|
||||||
public class BaseFragment extends Fragment implements Topic.AutoSubscriber {
|
|
||||||
|
|
||||||
public MagiskManager mm;
|
|
||||||
protected Unbinder unbinder = null;
|
|
||||||
|
|
||||||
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 onDestroyView() {
|
|
||||||
super.onDestroyView();
|
|
||||||
if (unbinder != null)
|
|
||||||
unbinder.unbind();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void startActivityForResult(Intent intent, int requestCode) {
|
|
||||||
startActivityForResult(intent, requestCode, this::onActivityResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void startActivityForResult(Intent intent, int requestCode, BaseActivity.ActivityResultListener listener) {
|
|
||||||
((BaseActivity) requireActivity()).startActivityForResult(intent, requestCode, listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void runWithPermission(String[] permissions, Runnable callback) {
|
|
||||||
((BaseActivity) requireActivity()).runWithPermission(permissions,callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int[] getSubscribedTopics() {
|
|
||||||
return BaseActivity.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 androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.StringRes;
|
|
||||||
import androidx.annotation.StyleRes;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import butterknife.BindView;
|
|
||||||
|
|
||||||
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 {
|
|
||||||
@BindView(R.id.dialog_layout) public LinearLayout dialogLayout;
|
|
||||||
@BindView(R.id.button_panel) public LinearLayout buttons;
|
|
||||||
|
|
||||||
@BindView(R.id.message) public TextView messageView;
|
|
||||||
@BindView(R.id.negative) public Button negative;
|
|
||||||
@BindView(R.id.positive) public Button positive;
|
|
||||||
@BindView(R.id.neutral) public Button neutral;
|
|
||||||
|
|
||||||
ViewHolder(View v) {
|
|
||||||
new CustomAlertDialog$ViewHolder_ViewBinding(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,105 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.components;
|
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.androidnetworking.AndroidNetworking;
|
|
||||||
import com.androidnetworking.error.ANError;
|
|
||||||
import com.androidnetworking.interfaces.DownloadListener;
|
|
||||||
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.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:
|
|
||||||
patchBoot(activity);
|
|
||||||
break;
|
|
||||||
case 0:
|
|
||||||
downloadOnly(activity);
|
|
||||||
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:
|
|
||||||
installInactiveSlot(activity);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void patchBoot(BaseActivity a) {
|
|
||||||
Utils.toast(R.string.boot_file_patch_msg, Toast.LENGTH_LONG);
|
|
||||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT).setType("*/*");
|
|
||||||
a.runWithPermission(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, () ->
|
|
||||||
a.startActivityForResult(intent, Const.ID.SELECT_BOOT,
|
|
||||||
(requestCode, resultCode, data) -> {
|
|
||||||
if (requestCode == Const.ID.SELECT_BOOT &&
|
|
||||||
resultCode == Activity.RESULT_OK && data != null) {
|
|
||||||
Intent i = new Intent(a, Data.classMap.get(FlashActivity.class))
|
|
||||||
.putExtra(Const.Key.FLASH_SET_BOOT, data.getData())
|
|
||||||
.putExtra(Const.Key.FLASH_ACTION, Const.Value.PATCH_BOOT);
|
|
||||||
a.startActivity(i);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void downloadOnly(BaseActivity a) {
|
|
||||||
a.runWithPermission(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, () -> {
|
|
||||||
String filename = Utils.fmt("Magisk-v%s(%d).zip",
|
|
||||||
Data.remoteMagiskVersionString, Data.remoteMagiskVersionCode);
|
|
||||||
ProgressNotification progress = new ProgressNotification(filename);
|
|
||||||
AndroidNetworking
|
|
||||||
.download(Data.magiskLink, Const.EXTERNAL_PATH.getPath(), filename)
|
|
||||||
.build()
|
|
||||||
.setDownloadProgressListener(progress)
|
|
||||||
.startDownload(new DownloadListener() {
|
|
||||||
@Override
|
|
||||||
public void onDownloadComplete() {
|
|
||||||
progress.dlDone();
|
|
||||||
SnackbarMaker.make(a,
|
|
||||||
a.getString(R.string.internal_storage, "/Download/" + filename),
|
|
||||||
Snackbar.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(ANError anError) {
|
|
||||||
progress.dlFail();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void installInactiveSlot(BaseActivity activity) {
|
|
||||||
new CustomAlertDialog(activity)
|
|
||||||
.setTitle(R.string.warning)
|
|
||||||
.setMessage(R.string.install_inactive_slot_msg)
|
|
||||||
.setCancelable(true)
|
|
||||||
.setPositiveButton(R.string.yes, (d, i) -> {
|
|
||||||
Intent intent = new Intent(activity, Data.classMap.get(FlashActivity.class))
|
|
||||||
.putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_INACTIVE_SLOT);
|
|
||||||
activity.startActivity(intent);
|
|
||||||
})
|
|
||||||
.setNegativeButton(R.string.no_thanks, null)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,31 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.components;
|
|
||||||
|
|
||||||
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.DlInstallManager;
|
|
||||||
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 name = Utils.fmt("MagiskManager v%s(%d)",
|
|
||||||
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, name));
|
|
||||||
setCancelable(true);
|
|
||||||
setPositiveButton(R.string.install, (d, i) -> DlInstallManager.upgrade(name));
|
|
||||||
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,114 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.components;
|
|
||||||
|
|
||||||
import android.app.NotificationChannel;
|
|
||||||
import android.app.NotificationManager;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.Data;
|
|
||||||
import com.topjohnwu.magisk.MagiskManager;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.SplashActivity;
|
|
||||||
import com.topjohnwu.magisk.receivers.GeneralReceiver;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
|
|
||||||
import androidx.core.app.NotificationCompat;
|
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
|
||||||
import androidx.core.app.TaskStackBuilder;
|
|
||||||
|
|
||||||
public class Notifications {
|
|
||||||
|
|
||||||
public static void setup(Context c) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
NotificationManager mgr = c.getSystemService(NotificationManager.class);
|
|
||||||
mgr.deleteNotificationChannel("magisk_notification");
|
|
||||||
NotificationChannel channel =
|
|
||||||
new NotificationChannel(Const.ID.UPDATE_NOTIFICATION_CHANNEL,
|
|
||||||
c.getString(R.string.update_channel), NotificationManager.IMPORTANCE_DEFAULT);
|
|
||||||
mgr.createNotificationChannel(channel);
|
|
||||||
channel = new NotificationChannel(Const.ID.PROGRESS_NOTIFICATION_CHANNEL,
|
|
||||||
c.getString(R.string.progress_channel), NotificationManager.IMPORTANCE_LOW);
|
|
||||||
mgr.createNotificationChannel(channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void magiskUpdate() {
|
|
||||||
MagiskManager mm = Data.MM();
|
|
||||||
|
|
||||||
Intent intent = new Intent(mm, Data.classMap.get(SplashActivity.class));
|
|
||||||
intent.putExtra(Const.Key.OPEN_SECTION, "magisk");
|
|
||||||
TaskStackBuilder stackBuilder = TaskStackBuilder.create(mm);
|
|
||||||
stackBuilder.addParentStack(Data.classMap.get(SplashActivity.class));
|
|
||||||
stackBuilder.addNextIntent(intent);
|
|
||||||
PendingIntent pendingIntent = stackBuilder.getPendingIntent(Const.ID.MAGISK_UPDATE_NOTIFICATION_ID,
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
|
||||||
|
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(mm, Const.ID.UPDATE_NOTIFICATION_CHANNEL);
|
|
||||||
builder.setSmallIcon(R.drawable.ic_magisk_outline)
|
|
||||||
.setContentTitle(mm.getString(R.string.magisk_update_title))
|
|
||||||
.setContentText(mm.getString(R.string.magisk_update_available, Data.remoteMagiskVersionString))
|
|
||||||
.setVibrate(new long[]{0, 100, 100, 100})
|
|
||||||
.setAutoCancel(true)
|
|
||||||
.setContentIntent(pendingIntent);
|
|
||||||
|
|
||||||
NotificationManagerCompat mgr = NotificationManagerCompat.from(mm);
|
|
||||||
mgr.notify(Const.ID.MAGISK_UPDATE_NOTIFICATION_ID, builder.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void managerUpdate() {
|
|
||||||
MagiskManager mm = Data.MM();
|
|
||||||
String name = Utils.fmt("MagiskManager v%s(%d)",
|
|
||||||
Data.remoteManagerVersionString, Data.remoteManagerVersionCode);
|
|
||||||
|
|
||||||
Intent intent = new Intent(mm, Data.classMap.get(GeneralReceiver.class));
|
|
||||||
intent.setAction(Const.Key.BROADCAST_MANAGER_UPDATE);
|
|
||||||
intent.putExtra(Const.Key.INTENT_SET_LINK, Data.managerLink);
|
|
||||||
intent.putExtra(Const.Key.INTENT_SET_NAME, name);
|
|
||||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(mm,
|
|
||||||
Const.ID.APK_UPDATE_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
||||||
|
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(mm, Const.ID.UPDATE_NOTIFICATION_CHANNEL);
|
|
||||||
builder.setSmallIcon(R.drawable.ic_magisk_outline)
|
|
||||||
.setContentTitle(mm.getString(R.string.manager_update_title))
|
|
||||||
.setContentText(mm.getString(R.string.manager_download_install))
|
|
||||||
.setVibrate(new long[]{0, 100, 100, 100})
|
|
||||||
.setAutoCancel(true)
|
|
||||||
.setContentIntent(pendingIntent);
|
|
||||||
|
|
||||||
NotificationManagerCompat mgr = NotificationManagerCompat.from(mm);
|
|
||||||
mgr.notify(Const.ID.APK_UPDATE_NOTIFICATION_ID, builder.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void dtboPatched() {
|
|
||||||
MagiskManager mm = Data.MM();
|
|
||||||
|
|
||||||
Intent intent = new Intent(mm, Data.classMap.get(GeneralReceiver.class))
|
|
||||||
.setAction(Const.Key.BROADCAST_REBOOT);
|
|
||||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(mm,
|
|
||||||
Const.ID.DTBO_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
||||||
|
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(mm, Const.ID.UPDATE_NOTIFICATION_CHANNEL);
|
|
||||||
builder.setSmallIcon(R.drawable.ic_magisk_outline)
|
|
||||||
.setContentTitle(mm.getString(R.string.dtbo_patched_title))
|
|
||||||
.setContentText(mm.getString(R.string.dtbo_patched_reboot))
|
|
||||||
.setVibrate(new long[]{0, 100, 100, 100})
|
|
||||||
.addAction(R.drawable.ic_refresh, mm.getString(R.string.reboot), pendingIntent);
|
|
||||||
|
|
||||||
NotificationManagerCompat mgr = NotificationManagerCompat.from(mm);
|
|
||||||
mgr.notify(Const.ID.DTBO_NOTIFICATION_ID, builder.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static NotificationCompat.Builder progress(String title) {
|
|
||||||
MagiskManager mm = Data.MM();
|
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(mm, Const.ID.PROGRESS_NOTIFICATION_CHANNEL);
|
|
||||||
builder.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
||||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
|
||||||
.setContentTitle(title)
|
|
||||||
.setProgress(0, 0, true)
|
|
||||||
.setOngoing(true);
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.components;
|
|
||||||
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.androidnetworking.interfaces.DownloadProgressListener;
|
|
||||||
import com.topjohnwu.magisk.Data;
|
|
||||||
import com.topjohnwu.magisk.MagiskManager;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
|
|
||||||
import androidx.core.app.NotificationCompat;
|
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
|
||||||
|
|
||||||
public class ProgressNotification implements DownloadProgressListener {
|
|
||||||
|
|
||||||
private NotificationManagerCompat mgr;
|
|
||||||
private NotificationCompat.Builder builder;
|
|
||||||
private long prevTime;
|
|
||||||
|
|
||||||
public ProgressNotification(String title) {
|
|
||||||
MagiskManager mm = Data.MM();
|
|
||||||
mgr = NotificationManagerCompat.from(mm);
|
|
||||||
builder = Notifications.progress(title);
|
|
||||||
prevTime = System.currentTimeMillis();
|
|
||||||
update();
|
|
||||||
Utils.toast(mm.getString(R.string.downloading_toast, title), Toast.LENGTH_SHORT);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onProgress(long bytesDownloaded, long totalBytes) {
|
|
||||||
long cur = System.currentTimeMillis();
|
|
||||||
if (cur - prevTime >= 1000) {
|
|
||||||
prevTime = cur;
|
|
||||||
int progress = (int) (bytesDownloaded * 100 / totalBytes);
|
|
||||||
builder.setProgress(100, progress, false);
|
|
||||||
builder.setContentText(progress + "%");
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public NotificationCompat.Builder getNotification() {
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void update() {
|
|
||||||
mgr.notify(hashCode(), builder.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void dlDone() {
|
|
||||||
builder.setProgress(0, 0, false)
|
|
||||||
.setContentText(Data.MM().getString(R.string.download_complete))
|
|
||||||
.setSmallIcon(R.drawable.ic_check_circle)
|
|
||||||
.setOngoing(false);
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void dlFail() {
|
|
||||||
builder.setProgress(0, 0, false)
|
|
||||||
.setContentText(Data.MM().getString(R.string.download_file_error))
|
|
||||||
.setSmallIcon(R.drawable.ic_cancel)
|
|
||||||
.setOngoing(false);
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void dismiss() {
|
|
||||||
mgr.cancel(hashCode());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,69 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.components;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.ProgressDialog;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.androidnetworking.AndroidNetworking;
|
|
||||||
import com.androidnetworking.error.ANError;
|
|
||||||
import com.androidnetworking.interfaces.DownloadListener;
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.Data;
|
|
||||||
import com.topjohnwu.magisk.FlashActivity;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
import com.topjohnwu.superuser.Shell;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
public class UninstallDialog extends CustomAlertDialog {
|
|
||||||
|
|
||||||
public UninstallDialog(@NonNull Activity activity) {
|
|
||||||
super(activity);
|
|
||||||
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) -> {
|
|
||||||
File zip = new File(activity.getFilesDir(), "uninstaller.zip");
|
|
||||||
ProgressNotification progress = new ProgressNotification(zip.getName());
|
|
||||||
AndroidNetworking.download(Data.uninstallerLink, zip.getParent(), zip.getName())
|
|
||||||
.build()
|
|
||||||
.setDownloadProgressListener(progress)
|
|
||||||
.startDownload(new DownloadListener() {
|
|
||||||
@Override
|
|
||||||
public void onDownloadComplete() {
|
|
||||||
progress.dismiss();
|
|
||||||
Intent intent = new Intent(activity, Data.classMap.get(FlashActivity.class))
|
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
.setData(Uri.fromFile(zip))
|
|
||||||
.putExtra(Const.Key.FLASH_ACTION, Const.Value.UNINSTALL);
|
|
||||||
activity.startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(ANError anError) {
|
|
||||||
progress.dlFail();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,63 +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.su("dos2unix < " + path + "/module.prop").exec().getOut());
|
|
||||||
} catch (NumberFormatException ignored) {}
|
|
||||||
|
|
||||||
mRemoveFile = new SuFile(path, "remove");
|
|
||||||
mDisableFile = new SuFile(path, "disable");
|
|
||||||
mUpdateFile = new SuFile(path, "update");
|
|
||||||
|
|
||||||
if (getId().isEmpty()) {
|
|
||||||
int sep = path.lastIndexOf('/');
|
|
||||||
setId(path.substring(sep + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getName().isEmpty()) {
|
|
||||||
setName(getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
mEnable = !mDisableFile.exists();
|
|
||||||
mRemove = mRemoveFile.exists();
|
|
||||||
mUpdated = mUpdateFile.exists();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void createDisableFile() {
|
|
||||||
mEnable = !mDisableFile.createNewFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeDisableFile() {
|
|
||||||
mEnable = mDisableFile.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isEnabled() {
|
|
||||||
return mEnable;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void createRemoveFile() {
|
|
||||||
mRemove = mRemoveFile.createNewFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deleteRemoveFile() {
|
|
||||||
mRemove = !mRemoveFile.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean willBeRemoved() {
|
|
||||||
return mRemove;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isUpdated() {
|
|
||||||
return mUpdated;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.container;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.content.pm.ApplicationInfo;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
|
|
||||||
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 = Utils.getAppLabel(info, pm);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Policy(ContentValues values, PackageManager pm) throws PackageManager.NameNotFoundException {
|
|
||||||
uid = values.getAsInteger("uid");
|
|
||||||
packageName = values.getAsString("package_name");
|
|
||||||
policy = values.getAsInteger("policy");
|
|
||||||
until = values.getAsInteger("until");
|
|
||||||
logging = values.getAsInteger("logging") != 0;
|
|
||||||
notification = values.getAsInteger("notification") != 0;
|
|
||||||
info = pm.getApplicationInfo(packageName, 0);
|
|
||||||
if (info.uid != uid)
|
|
||||||
throw new PackageManager.NameNotFoundException();
|
|
||||||
appName = info.loadLabel(pm).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ContentValues getContentValues() {
|
|
||||||
ContentValues values = new ContentValues();
|
|
||||||
values.put("uid", uid);
|
|
||||||
values.put("package_name", packageName);
|
|
||||||
values.put("policy", policy);
|
|
||||||
values.put("until", until);
|
|
||||||
values.put("logging", logging ? 1 : 0);
|
|
||||||
values.put("notification", notification ? 1 : 0);
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compareTo(@NonNull Policy policy) {
|
|
||||||
return appName.toLowerCase().compareTo(policy.appName.toLowerCase());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.container;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.utils.Download;
|
|
||||||
import com.topjohnwu.magisk.utils.Logger;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
|
|
||||||
import java.text.DateFormat;
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
public class Repo extends BaseModule {
|
|
||||||
|
|
||||||
private Date mLastUpdate;
|
|
||||||
|
|
||||||
public Repo(String id) {
|
|
||||||
setId(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Repo(Cursor c) {
|
|
||||||
super(c);
|
|
||||||
mLastUpdate = new Date(c.getLong(c.getColumnIndex("last_update")));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void update() throws IllegalRepoException {
|
|
||||||
String props[] = Utils.dlString(getPropUrl()).split("\\n");
|
|
||||||
try {
|
|
||||||
parseProps(props);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
throw new IllegalRepoException("Repo [" + getId() + "] parse error: " + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getVersionCode() < 0) {
|
|
||||||
throw new IllegalRepoException("Repo [" + getId() + "] does not contain versionCode");
|
|
||||||
}
|
|
||||||
if (getMinMagiskVersion() < Const.MIN_MODULE_VER) {
|
|
||||||
Logger.debug("Repo [" + getId() + "] is outdated");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void update(Date lastUpdate) throws IllegalRepoException {
|
|
||||||
mLastUpdate = lastUpdate;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ContentValues getContentValues() {
|
|
||||||
ContentValues values = super.getContentValues();
|
|
||||||
values.put("last_update", mLastUpdate.getTime());
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getZipUrl() {
|
|
||||||
return String.format(Const.Url.ZIP_URL, getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPropUrl() {
|
|
||||||
return String.format(Const.Url.FILE_URL, getId(), "module.prop");
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDetailUrl() {
|
|
||||||
return String.format(Const.Url.FILE_URL, getId(), "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 com.topjohnwu.magisk.utils.LocaleManager;
|
|
||||||
|
|
||||||
import java.text.DateFormat;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
public class SuLogEntry {
|
|
||||||
|
|
||||||
public int fromUid, toUid, fromPid;
|
|
||||||
public String packageName, appName, command;
|
|
||||||
public boolean action;
|
|
||||||
public Date date;
|
|
||||||
|
|
||||||
public SuLogEntry(Policy policy) {
|
|
||||||
fromUid = policy.uid;
|
|
||||||
packageName = policy.packageName;
|
|
||||||
appName = policy.appName;
|
|
||||||
action = policy.policy == Policy.ALLOW;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SuLogEntry(ContentValues values) {
|
|
||||||
fromUid = values.getAsInteger("from_uid");
|
|
||||||
packageName = values.getAsString("package_name");
|
|
||||||
appName = values.getAsString("app_name");
|
|
||||||
fromPid = values.getAsInteger("from_pid");
|
|
||||||
command = values.getAsString("command");
|
|
||||||
toUid = values.getAsInteger("to_uid");
|
|
||||||
action = values.getAsInteger("action") != 0;
|
|
||||||
date = new Date(values.getAsLong("time"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public ContentValues getContentValues() {
|
|
||||||
ContentValues values = new ContentValues();
|
|
||||||
values.put("from_uid", fromUid);
|
|
||||||
values.put("package_name", packageName);
|
|
||||||
values.put("app_name", appName);
|
|
||||||
values.put("from_pid", fromPid);
|
|
||||||
values.put("command", command);
|
|
||||||
values.put("to_uid", toUid);
|
|
||||||
values.put("action", action ? 1 : 0);
|
|
||||||
values.put("time", date.getTime());
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDateString() {
|
|
||||||
return DateFormat.getDateInstance(DateFormat.MEDIUM, LocaleManager.locale).format(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTimeString() {
|
|
||||||
return new SimpleDateFormat("h:mm a", LocaleManager.locale).format(date);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user