mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-08-14 10:07:27 +00:00
Compare commits
2886 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
59fd38bbf8 | ||
![]() |
06dc6df270 | ||
![]() |
ff8460b361 | ||
![]() |
674d272eaa | ||
![]() |
c3e00c279d | ||
![]() |
175d920c94 | ||
![]() |
04920883ea | ||
![]() |
5e44b0b9d5 | ||
![]() |
23c1a1dab8 | ||
![]() |
f5d054b93c | ||
![]() |
d25ae5e0a9 | ||
![]() |
c42a51dcbb | ||
![]() |
da3fd92b31 | ||
![]() |
4a45ba3c14 | ||
![]() |
dbc8bed234 | ||
![]() |
f8b4190a11 | ||
![]() |
479972e3ae | ||
![]() |
3ea28b0afb | ||
![]() |
2b3cc28966 | ||
![]() |
751642b39a | ||
![]() |
d6c2c821a4 | ||
![]() |
dfc65b95f7 | ||
![]() |
b45d922463 | ||
![]() |
f87ee3fcf9 | ||
![]() |
e0927cd763 | ||
![]() |
21099eabfa | ||
![]() |
abbd2e6b72 | ||
![]() |
5b7ddbbb01 | ||
![]() |
6352fbb3b2 | ||
![]() |
d3f49334e2 | ||
![]() |
c4356171b3 | ||
![]() |
5c5625911d | ||
![]() |
6a10cc9c55 | ||
![]() |
6b317f918e | ||
![]() |
08b528dc4f | ||
![]() |
fc886a5a47 | ||
![]() |
0cb90e2e55 | ||
![]() |
64113a69b4 | ||
![]() |
544bb7459c | ||
![]() |
578a50b464 | ||
![]() |
3d4081d0af | ||
![]() |
b763b81f56 | ||
![]() |
947dae4900 | ||
![]() |
debd1d7d54 | ||
![]() |
cba0d04000 | ||
![]() |
695e7e6da0 | ||
![]() |
4cd4bfa1d7 | ||
![]() |
16b400964b | ||
![]() |
cf2d02c0dd | ||
![]() |
0fcd0de0d1 | ||
![]() |
748a35774f | ||
![]() |
a52a3e38ed | ||
![]() |
ee0cef06a6 | ||
![]() |
0e5a113a0c | ||
![]() |
a1ccd44013 | ||
![]() |
4d91e50d6d | ||
![]() |
120668c7bc | ||
![]() |
d81ccde569 | ||
![]() |
e8581b4adb | ||
![]() |
19906575a3 | ||
![]() |
9329094a4e | ||
![]() |
b44f5122fd | ||
![]() |
17981730a4 | ||
![]() |
53de6da26c | ||
![]() |
3e30ccdeee | ||
![]() |
baaaf7d5de | ||
![]() |
45d8d139a9 | ||
![]() |
fe644e10d0 | ||
![]() |
f383d11d10 | ||
![]() |
ef1b928532 | ||
![]() |
6e46d394b1 | ||
![]() |
f109038d12 | ||
![]() |
e31e687602 | ||
![]() |
86bfb22d4c | ||
![]() |
3f057367e3 | ||
![]() |
3d7ed5820e | ||
![]() |
0118f2efa7 | ||
![]() |
15312e4709 | ||
![]() |
bf1568a73a | ||
![]() |
13a2520ea5 | ||
![]() |
f53238f206 | ||
![]() |
9375748d9b | ||
![]() |
201df54e79 | ||
![]() |
0b54fe477b | ||
![]() |
4119e6669e | ||
![]() |
d33e5226b3 | ||
![]() |
d73f39c706 | ||
![]() |
087b451e17 | ||
![]() |
86481c74ff | ||
![]() |
5b937fb1fa | ||
![]() |
ff828116bc | ||
![]() |
ee39616a8b | ||
![]() |
cdb53ca049 | ||
![]() |
8cf475f708 | ||
![]() |
0cb449e1d6 | ||
![]() |
e6adb7abca | ||
![]() |
cfad7dd317 | ||
![]() |
dd35224f92 | ||
![]() |
1283590eeb | ||
![]() |
dca3fe396f | ||
![]() |
8d87eae11b | ||
![]() |
fd7eaacae0 | ||
![]() |
fba33cbbe9 | ||
![]() |
950ffcd790 | ||
![]() |
c178299013 | ||
![]() |
5d17c1f588 | ||
![]() |
a75c00d94e | ||
![]() |
cd19517414 | ||
![]() |
155f39aab5 | ||
![]() |
4514d0b467 | ||
![]() |
6f4a938a31 | ||
![]() |
1303ea95dd | ||
![]() |
727fe1bd15 | ||
![]() |
64ebc977e9 | ||
![]() |
e89c50d934 | ||
![]() |
c859ddfb8f | ||
![]() |
a6126c5eda | ||
![]() |
85d9bd9106 | ||
![]() |
39e9622205 | ||
![]() |
021994c9f3 | ||
![]() |
2e7ce2a769 | ||
![]() |
84f0ff2fad | ||
![]() |
e6561e5f84 | ||
![]() |
5fa452aa74 | ||
![]() |
2225ccb146 | ||
![]() |
5aafc78847 | ||
![]() |
0d03833cff | ||
![]() |
a797d5d396 | ||
![]() |
f2494374f8 | ||
![]() |
48395ba860 | ||
![]() |
5ba5f5f94e | ||
![]() |
42ce6fd334 | ||
![]() |
f5c3ee3ae1 | ||
![]() |
3c7ece1605 | ||
![]() |
870efc49ea | ||
![]() |
085ede6d93 | ||
![]() |
4ef19d17da | ||
![]() |
223913c30a | ||
![]() |
010e4de4e1 | ||
![]() |
41134466ed | ||
![]() |
8f07747452 | ||
![]() |
eb5ce5be1e | ||
![]() |
71d855e836 | ||
![]() |
33b7ab593c | ||
![]() |
8706d834b4 | ||
![]() |
7cfab33ebb | ||
![]() |
1ababc8c7f | ||
![]() |
1f75e63c37 | ||
![]() |
cb3f9b9740 | ||
![]() |
9784353223 | ||
![]() |
7d93ca5c73 | ||
![]() |
ac20063e86 | ||
![]() |
debaec32af | ||
![]() |
0e9b71e7a9 | ||
![]() |
85f5ff3c14 | ||
![]() |
3d81f167ea | ||
![]() |
fb70a2e52d | ||
![]() |
460e85a1b5 | ||
![]() |
539b64bd57 | ||
![]() |
90e38a06a2 | ||
![]() |
09ab910630 | ||
![]() |
c15f80b33f | ||
![]() |
b2e6ba3c4a | ||
![]() |
b16f696b0e | ||
![]() |
9adfb382e8 | ||
![]() |
44368383f4 | ||
![]() |
d1ff7e0ffe | ||
![]() |
42e7db8d13 | ||
![]() |
0c17ea5755 | ||
![]() |
cdaff5b39c | ||
![]() |
2b1b970e78 | ||
![]() |
0aebc0a8e3 | ||
![]() |
c3a89f589e | ||
![]() |
971cd73fb3 | ||
![]() |
1947860d61 | ||
![]() |
55aaa421e8 | ||
![]() |
a8932706d8 | ||
![]() |
a97972aac0 | ||
![]() |
094c3d559a | ||
![]() |
6fb032b3c2 | ||
![]() |
8ca188f4d4 | ||
![]() |
746a1d8d59 | ||
![]() |
63c5e00d86 | ||
![]() |
9d2e5d6665 | ||
![]() |
f6045bf8b5 | ||
![]() |
e83f40d5c5 | ||
![]() |
e5118418b2 | ||
![]() |
7cd814d917 | ||
![]() |
78282c1a49 | ||
![]() |
fd4214ccf3 | ||
![]() |
0785945635 | ||
![]() |
967bdeae7b | ||
![]() |
452db51669 | ||
![]() |
5875ced367 | ||
![]() |
fbac6bcfd0 | ||
![]() |
0dcd3ece9d | ||
![]() |
224fff89e3 | ||
![]() |
22e73644f9 | ||
![]() |
6a0f6ab319 | ||
![]() |
88a394836f | ||
![]() |
f822c1c2e4 | ||
![]() |
1d16d980b3 | ||
![]() |
501b18f986 | ||
![]() |
21ed759e53 | ||
![]() |
8d50dfd93c | ||
![]() |
51e40dd98c | ||
![]() |
b2048379af | ||
![]() |
011539f6f1 | ||
![]() |
5457c3803f | ||
![]() |
b3d777bb6c | ||
![]() |
12e00c3054 | ||
![]() |
40b683111c | ||
![]() |
9542ca773f | ||
![]() |
8af832a496 | ||
![]() |
6836130fda | ||
![]() |
724893879f | ||
![]() |
736729f5ef | ||
![]() |
aa47966347 | ||
![]() |
d64d12afe8 | ||
![]() |
1f8df419c4 | ||
![]() |
7ba8202af5 | ||
![]() |
d7b691cf59 | ||
![]() |
7058d5e4cd | ||
![]() |
52fd508fea | ||
![]() |
41045b62dc | ||
![]() |
188ea2644a | ||
![]() |
4c8f357978 | ||
![]() |
4bb2fd6ba6 | ||
![]() |
33c9f74508 | ||
![]() |
f53fe67372 | ||
![]() |
51ff724691 | ||
![]() |
291bf93f9d | ||
![]() |
5fcd629f16 | ||
![]() |
ab90901793 | ||
![]() |
4f206fd918 | ||
![]() |
7233285437 | ||
![]() |
8e348a11c2 | ||
![]() |
085ea6d0a1 | ||
![]() |
aaf88b1895 | ||
![]() |
4f4a9412a3 | ||
![]() |
a92e039363 | ||
![]() |
33aa4ca4b7 | ||
![]() |
05658cafc7 | ||
![]() |
ff3710de66 | ||
![]() |
db8dd9f186 | ||
![]() |
e8b73ba6d1 | ||
![]() |
f1112fdf37 | ||
![]() |
a48c4f9e05 | ||
![]() |
19a521d2e9 | ||
![]() |
dd6e55ac31 | ||
![]() |
b1e63f0f14 | ||
![]() |
b0e49a4cc8 | ||
![]() |
1e94517a72 | ||
![]() |
98f60216ac | ||
![]() |
e29b712108 | ||
![]() |
a462435f2f | ||
![]() |
911b8273fe | ||
![]() |
09935e591a | ||
![]() |
4a212dba35 | ||
![]() |
aac9e85e04 | ||
![]() |
bb67a837d3 | ||
![]() |
6cde695194 | ||
![]() |
a1a1ac0bbb | ||
![]() |
9ec8bc2166 | ||
![]() |
28cd6a75e7 | ||
![]() |
4cc7aced15 | ||
![]() |
1058aeb04f | ||
![]() |
cfec0db947 | ||
![]() |
120bd6cd68 | ||
![]() |
9aef06d1b8 | ||
![]() |
e6e9dd751c | ||
![]() |
5dd677756f | ||
![]() |
b77c590910 | ||
![]() |
7e5f2822ae | ||
![]() |
12bbc7fd6b | ||
![]() |
bf9ac8252b | ||
![]() |
4a3f5dc619 | ||
![]() |
ca156befbd | ||
![]() |
4db41e2ac4 | ||
![]() |
982a43fce1 | ||
![]() |
dd76a74e1c | ||
![]() |
70cb52b2c7 | ||
![]() |
5c7f69acaa | ||
![]() |
f1d9015e5f | ||
![]() |
e8d900c58e | ||
![]() |
a6241ae912 | ||
![]() |
4a697ca2ec | ||
![]() |
58bec7f2c9 | ||
![]() |
213f84985c | ||
![]() |
074b1f8c61 | ||
![]() |
326eee8c83 | ||
![]() |
00bff4912e | ||
![]() |
0ce1720516 | ||
![]() |
ee407472cf | ||
![]() |
f341f3b2dd | ||
![]() |
8513946e09 | ||
![]() |
8ebd9c8927 | ||
![]() |
1d54c5144e | ||
![]() |
e40d4318fa | ||
![]() |
7756e10779 | ||
![]() |
3e58d502d0 | ||
![]() |
1c8846dc57 | ||
![]() |
2f320c7239 | ||
![]() |
e799918ab6 | ||
![]() |
86c4928e0f | ||
![]() |
0293eb5c51 | ||
![]() |
1ee75b6aa6 | ||
![]() |
4b30b224b5 | ||
![]() |
16b232d2a3 | ||
![]() |
3f3b1f5b1d | ||
![]() |
cec017b7bf | ||
![]() |
3123cc1059 | ||
![]() |
caa9df86bc | ||
![]() |
f417389a7a | ||
![]() |
662a5c8ea6 | ||
![]() |
7edfbfb764 | ||
![]() |
c1602d2554 | ||
![]() |
9f8d4e1022 | ||
![]() |
d1dfda405f | ||
![]() |
28efded624 | ||
![]() |
06c86ee267 | ||
![]() |
5892780871 | ||
![]() |
4fcdcd9a8a | ||
![]() |
80d834fb55 | ||
![]() |
4122ebe18f | ||
![]() |
7d87777bf8 | ||
![]() |
4a73d634e0 | ||
![]() |
373dc10a40 | ||
![]() |
ed43ec8ea2 | ||
![]() |
7918fc3528 | ||
![]() |
bf58205b0a | ||
![]() |
c0d1ce96d1 | ||
![]() |
b31d3802eb | ||
![]() |
be1228c3b4 | ||
![]() |
15c94c6b34 | ||
![]() |
202d23426a | ||
![]() |
fc26de48b2 | ||
![]() |
76c88913f9 | ||
![]() |
a3a1aed723 | ||
![]() |
81aa56f60f | ||
![]() |
73bb850209 | ||
![]() |
8dfec12330 | ||
![]() |
ae24397793 | ||
![]() |
3b0f888407 | ||
![]() |
845d1e02b0 | ||
![]() |
5d357bc41f | ||
![]() |
6a54672b13 | ||
![]() |
3d9a15df44 | ||
![]() |
449c7fda2f | ||
![]() |
8b7b05da68 | ||
![]() |
92400ebcab | ||
![]() |
23d3e56967 | ||
![]() |
6785dc4967 | ||
![]() |
dad20f6a2d | ||
![]() |
bb15671046 | ||
![]() |
21984fac8b | ||
![]() |
f392afe87f | ||
![]() |
6a243ec7bc | ||
![]() |
8cd3b603df | ||
![]() |
6e1aefe6d8 | ||
![]() |
1c90b6eca3 | ||
![]() |
c33cf9f878 | ||
![]() |
27cb40eec9 | ||
![]() |
c06081b75d | ||
![]() |
a7eec2f0a0 | ||
![]() |
4fd0fe3194 | ||
![]() |
cc74593ddd | ||
![]() |
fdb7c5dba1 | ||
![]() |
77470c7cfa | ||
![]() |
f0a734fdab | ||
![]() |
75405b2b25 | ||
![]() |
90ed4b3c49 | ||
![]() |
290a17a764 | ||
![]() |
aaabd836e4 | ||
![]() |
076e5cea3b | ||
![]() |
8515971ccf | ||
![]() |
d86fb033ea | ||
![]() |
99d7d8ddbc | ||
![]() |
df78fd2d41 | ||
![]() |
dabe6267b9 | ||
![]() |
0119ebddbe | ||
![]() |
3216ef9f47 | ||
![]() |
b79d1bcded | ||
![]() |
17e234f9d5 | ||
![]() |
ea1f75f80e | ||
![]() |
8c40db5730 | ||
![]() |
6fe03d2795 | ||
![]() |
c595a87ccf | ||
![]() |
fac07c3913 | ||
![]() |
c63fdbbc6b | ||
![]() |
2ff5d9606b | ||
![]() |
ed43452c1a | ||
![]() |
8f28d4028f | ||
![]() |
b54543b18c | ||
![]() |
966d6593ca | ||
![]() |
ad95b1c9d1 | ||
![]() |
3bfa38c60a | ||
![]() |
0bdbcad8be | ||
![]() |
80855e89ec | ||
![]() |
0850401dc4 | ||
![]() |
337fda2023 | ||
![]() |
64f238191e | ||
![]() |
eb169cb133 | ||
![]() |
80cd85b061 | ||
![]() |
89275270f3 | ||
![]() |
e7339ba619 | ||
![]() |
d9ad7d522c | ||
![]() |
92789c3113 | ||
![]() |
c1c677e161 | ||
![]() |
2fe917ff82 | ||
![]() |
0e6c205732 | ||
![]() |
125ae0a173 | ||
![]() |
0245e13591 | ||
![]() |
d546733287 | ||
![]() |
c275326d59 | ||
![]() |
d4561507b8 | ||
![]() |
ef0e22cc41 | ||
![]() |
62db65bf18 | ||
![]() |
d5371f752c | ||
![]() |
a5f5e94115 | ||
![]() |
2624706c69 | ||
![]() |
d39d885ec2 | ||
![]() |
d83c744725 | ||
![]() |
843995cdb9 | ||
![]() |
9491ba77e0 | ||
![]() |
58a449d437 | ||
![]() |
7f55e0f05b | ||
![]() |
67c3f40adb | ||
![]() |
ff7a0ba599 | ||
![]() |
b152c63102 | ||
![]() |
415ff23be5 | ||
![]() |
b0d6de783e | ||
![]() |
ac28e6e5ca | ||
![]() |
4f9e8d2e8a | ||
![]() |
21be2f46f3 | ||
![]() |
a6e7680212 | ||
![]() |
e79e744e08 | ||
![]() |
7abdac72a4 | ||
![]() |
90d85eaf7d | ||
![]() |
e65f9740fb | ||
![]() |
7538f89b56 | ||
![]() |
7c755a3991 | ||
![]() |
10e903c9fc | ||
![]() |
b018124226 | ||
![]() |
a9350f50c9 | ||
![]() |
ed7babcbf1 | ||
![]() |
61ebc335c4 | ||
![]() |
0167bd76f1 | ||
![]() |
79d704008b | ||
![]() |
0a703585b0 | ||
![]() |
5d632d0d90 | ||
![]() |
4eecaea601 | ||
![]() |
63055818ec | ||
![]() |
0beb08b687 | ||
![]() |
b27801a27c | ||
![]() |
a0cfce7cbc | ||
![]() |
8b7144c986 | ||
![]() |
d3f5f5ee59 | ||
![]() |
a2a3c7f438 | ||
![]() |
4496f82d5b | ||
![]() |
09d531557d | ||
![]() |
7fee82f731 | ||
![]() |
475054c48a | ||
![]() |
a743d05751 | ||
![]() |
d1ed502e03 | ||
![]() |
37744c7ab6 | ||
![]() |
bbc9e60a12 | ||
![]() |
6c975ecc4c | ||
![]() |
23e8a4ce4b | ||
![]() |
50134a2f9b | ||
![]() |
628b37c4fa | ||
![]() |
1b4ae70a43 | ||
![]() |
b25c49725f | ||
![]() |
b245782c7e | ||
![]() |
a9f32baae0 | ||
![]() |
e7ef71865d | ||
![]() |
88c4f72b37 | ||
![]() |
abbcdf91a5 | ||
![]() |
b876df6e21 | ||
![]() |
4bb81f35d7 | ||
![]() |
ff20267b3f | ||
![]() |
2c9586d811 | ||
![]() |
2813d2031a | ||
![]() |
4040a0242f | ||
![]() |
781ec810d9 | ||
![]() |
9e90a71c04 | ||
![]() |
5571714b26 | ||
![]() |
e0d1f02ef5 | ||
![]() |
1b729e5ff2 | ||
![]() |
51e587d4e8 | ||
![]() |
ac9c55dbc1 | ||
![]() |
065051a360 | ||
![]() |
0893ac3141 | ||
![]() |
fb40e96917 | ||
![]() |
4ca25f74c6 | ||
![]() |
7fda917b86 | ||
![]() |
e6bd5f2c40 | ||
![]() |
8a904ee384 | ||
![]() |
00a9f18a1e | ||
![]() |
8d68ebb074 | ||
![]() |
5f53cfb4a9 | ||
![]() |
a2fa8d8be1 | ||
![]() |
70a3c78ebb | ||
![]() |
db218407b0 | ||
![]() |
d52210dd90 | ||
![]() |
f3cd9a096a | ||
![]() |
e426090a18 | ||
![]() |
cbe64fd559 | ||
![]() |
63ea7a70bd | ||
![]() |
fb0998f7a2 | ||
![]() |
a9b00dd537 | ||
![]() |
52eb059515 | ||
![]() |
7640246255 | ||
![]() |
52c83b2916 | ||
![]() |
d9cded0fc9 | ||
![]() |
750c42caf1 | ||
![]() |
bbf650c6cf | ||
![]() |
a25dace7e0 | ||
![]() |
14ff22fbcd | ||
![]() |
07eb7dda2d | ||
![]() |
54d1207f92 | ||
![]() |
003e44fb84 | ||
![]() |
515f346dcc | ||
![]() |
d4058175b4 | ||
![]() |
2de984ae24 | ||
![]() |
761a8bf2a9 | ||
![]() |
6df7006b36 | ||
![]() |
aceb3ee863 | ||
![]() |
11d716a3c8 | ||
![]() |
7cc8c014eb | ||
![]() |
f21241d944 | ||
![]() |
a181fa0652 | ||
![]() |
3f748b4d2a | ||
![]() |
683450f9c6 | ||
![]() |
6050c4e8ba | ||
![]() |
158af8819a | ||
![]() |
7787bb31fa | ||
![]() |
a1fe3e7ccd | ||
![]() |
4316028b23 | ||
![]() |
f2b52755d6 | ||
![]() |
adbd47a36c | ||
![]() |
ce693aa5e9 | ||
![]() |
ad80804461 | ||
![]() |
2d55632430 | ||
![]() |
e81f00ef1a | ||
![]() |
93fb0e3d74 | ||
![]() |
71ce0de606 | ||
![]() |
0407062c1d | ||
![]() |
f315c4416b | ||
![]() |
cda14af208 | ||
![]() |
258f170cd7 | ||
![]() |
f76015d714 | ||
![]() |
7e5e14163c | ||
![]() |
bcd1064e94 | ||
![]() |
8a8441c875 | ||
![]() |
15aa813416 | ||
![]() |
605faccffd | ||
![]() |
79f2d08c81 | ||
![]() |
0568ae5391 | ||
![]() |
5330dda9f8 | ||
![]() |
ebab126579 | ||
![]() |
0e5417a13e | ||
![]() |
9a968e0584 | ||
![]() |
ffec64d209 | ||
![]() |
f332746188 | ||
![]() |
b2fa5b551e | ||
![]() |
36e83edddc | ||
![]() |
6b045eadef | ||
![]() |
147264822c | ||
![]() |
36e4ccd800 | ||
![]() |
796c16237d | ||
![]() |
861ad9881c | ||
![]() |
3101c538e9 | ||
![]() |
42adc7382f | ||
![]() |
9bb4dfad13 | ||
![]() |
4e7dafb0e4 | ||
![]() |
bd00ae8ede | ||
![]() |
f309522268 | ||
![]() |
a6395d35db | ||
![]() |
a028cd5cec | ||
![]() |
540000d26e | ||
![]() |
888c656aa8 | ||
![]() |
0efaddff23 | ||
![]() |
94ba7cb0c5 | ||
![]() |
2d58c725e0 | ||
![]() |
e035523eb8 | ||
![]() |
bea5308ab7 | ||
![]() |
f006a85fec | ||
![]() |
ea93013ebc | ||
![]() |
8d4c407201 | ||
![]() |
fdeede23f7 | ||
![]() |
53c5ca59b6 | ||
![]() |
679db97209 | ||
![]() |
fbdd72273e | ||
![]() |
0165602515 | ||
![]() |
96127f8bd1 | ||
![]() |
0dbdf336d6 | ||
![]() |
48879df2da | ||
![]() |
b067a5bb13 | ||
![]() |
4b54cf1288 | ||
![]() |
6128c24f96 | ||
![]() |
d9c58f307f | ||
![]() |
b521fbeeda | ||
![]() |
d00a3b89f2 | ||
![]() |
3d15518191 | ||
![]() |
9b6535fdf5 | ||
![]() |
e0424fdba3 | ||
![]() |
7481c53451 | ||
![]() |
7219947237 | ||
![]() |
b72004e9cc | ||
![]() |
f187213568 | ||
![]() |
fc0df84edd | ||
![]() |
f24df4f43d | ||
![]() |
dab32e1599 | ||
![]() |
bc286fd4d3 | ||
![]() |
befe1a83b5 | ||
![]() |
82ea9db9fd | ||
![]() |
c5758b3f2d | ||
![]() |
ace3708c9c | ||
![]() |
fc5026d268 | ||
![]() |
77fd0e54be | ||
![]() |
24490e0ff5 | ||
![]() |
da3937ff4e | ||
![]() |
ebe1ab982e | ||
![]() |
98590cb00d | ||
![]() |
ff95f634f0 | ||
![]() |
ced9b4a8ee | ||
![]() |
7af7910e78 | ||
![]() |
a4f5d47e72 | ||
![]() |
6953cc2411 | ||
![]() |
6a0b2ddee9 | ||
![]() |
24f5bc98d8 | ||
![]() |
5203886f0b | ||
![]() |
c10b376575 | ||
![]() |
ceb21ced2b | ||
![]() |
86789a8694 | ||
![]() |
ca2235aee7 | ||
![]() |
a385e5cd92 | ||
![]() |
0c7a95bdf6 | ||
![]() |
036b5acf42 | ||
![]() |
056dafc59f | ||
![]() |
a9c90718d6 | ||
![]() |
cc77a24502 | ||
![]() |
71a91ac7a7 | ||
![]() |
08a70f033a | ||
![]() |
1b0c36dbd5 | ||
![]() |
91da1cf817 | ||
![]() |
c577a9525d | ||
![]() |
0149b1368d | ||
![]() |
cd6bcb97ef | ||
![]() |
df4161ffcc | ||
![]() |
7a133eaf03 | ||
![]() |
1cd45b53b1 | ||
![]() |
5b30c77403 | ||
![]() |
8248480d56 | ||
![]() |
345d992d39 | ||
![]() |
a7f6afa4bc | ||
![]() |
d22c7de79a | ||
![]() |
3eae9494ce | ||
![]() |
be7e737253 | ||
![]() |
b6eb912dba | ||
![]() |
8049b08918 | ||
![]() |
d1fa5be210 | ||
![]() |
fdbb1af02c | ||
![]() |
c85a5cae88 | ||
![]() |
649ef53409 | ||
![]() |
e784212283 | ||
![]() |
66eb1078fe | ||
![]() |
1c09b3642f | ||
![]() |
d08b1a6639 | ||
![]() |
10f50e2401 | ||
![]() |
8e9a7b25a1 | ||
![]() |
4859ee2da9 | ||
![]() |
b45db44ad9 | ||
![]() |
e25ce63872 | ||
![]() |
162eeaa0a6 | ||
![]() |
f36ce905aa | ||
![]() |
8ac3aaf36c | ||
![]() |
a199b0ace1 | ||
![]() |
2f2108e4e8 | ||
![]() |
f5f7fd9132 | ||
![]() |
f9ae4ab475 | ||
![]() |
8de03eef3f | ||
![]() |
8df942f96e | ||
![]() |
9bb2243b56 | ||
![]() |
db06038548 | ||
![]() |
ecb33d3176 | ||
![]() |
eae1c17738 | ||
![]() |
ea55532e33 | ||
![]() |
2a40cb60a9 | ||
![]() |
d371d017b7 | ||
![]() |
1d9359d563 | ||
![]() |
945f88105f | ||
![]() |
957feca626 | ||
![]() |
c0447009db | ||
![]() |
8893cbd64a | ||
![]() |
f0240b1f06 | ||
![]() |
e476c18c99 | ||
![]() |
a1b5185ecb | ||
![]() |
981e90cc32 | ||
![]() |
da0a72e8b0 | ||
![]() |
b7e2e972c7 | ||
![]() |
650b2ce6b1 | ||
![]() |
ecf3d30349 | ||
![]() |
15ddd0e284 | ||
![]() |
18ac6b270f | ||
![]() |
3e35de9b39 | ||
![]() |
1e24c72c11 | ||
![]() |
217564963d | ||
![]() |
f2f4649ab0 | ||
![]() |
4395ffec5f | ||
![]() |
9a7a26407a | ||
![]() |
5072a67807 | ||
![]() |
dce0b6c05a | ||
![]() |
a4a661bf34 | ||
![]() |
771e500468 | ||
![]() |
7e3ff03109 | ||
![]() |
a1827fd680 | ||
![]() |
9ce334feac | ||
![]() |
ed11e0bff6 | ||
![]() |
5111086637 | ||
![]() |
20f204810e | ||
![]() |
4581354e7a | ||
![]() |
faf4d76388 | ||
![]() |
a46e255709 | ||
![]() |
63e2bbb4d1 | ||
![]() |
c3dabae237 | ||
![]() |
f1abcbb7fb | ||
![]() |
70efddb90f | ||
![]() |
f24a5dfd45 | ||
![]() |
081074ad9d | ||
![]() |
ab0cc78d2c | ||
![]() |
de5c902fdb | ||
![]() |
cf65169c99 | ||
![]() |
745865ee53 | ||
![]() |
c134fb1939 | ||
![]() |
0204d05316 | ||
![]() |
c345633d80 | ||
![]() |
a57a94040e | ||
![]() |
1bde78d121 | ||
![]() |
bbd014ad1b | ||
![]() |
1287372f5a | ||
![]() |
d2cb638fcd | ||
![]() |
bbe4b69c8d | ||
![]() |
7f08c06943 | ||
![]() |
8f4a6415cd | ||
![]() |
0442d6d509 | ||
![]() |
a3fc6d2a27 | ||
![]() |
7db05ac927 | ||
![]() |
8bed93b3c5 | ||
![]() |
915b49014f | ||
![]() |
c699f30831 | ||
![]() |
3e73e3a906 | ||
![]() |
32c65d8a88 | ||
![]() |
a49328edd3 | ||
![]() |
9a15365a57 | ||
![]() |
82c864d57e | ||
![]() |
6226f875ff | ||
![]() |
370015a853 | ||
![]() |
6597b7adc0 | ||
![]() |
4e53ebfe44 | ||
![]() |
04ef1e6405 | ||
![]() |
b278d07b05 | ||
![]() |
6c3896079d | ||
![]() |
e73fa57d54 | ||
![]() |
eaa9c7e2a0 | ||
![]() |
14ae29d907 | ||
![]() |
e8f35b02ca | ||
![]() |
dee3c3e7ba | ||
![]() |
d8cd2031c7 | ||
![]() |
7203e7df5c | ||
![]() |
b51feffe80 | ||
![]() |
b1afd554fc | ||
![]() |
885e3c574b | ||
![]() |
05dd5f3396 | ||
![]() |
ec3c43faf1 | ||
![]() |
e72c6685ed | ||
![]() |
99d6bd8efc | ||
![]() |
4c8587a9f2 | ||
![]() |
54a8a05dae | ||
![]() |
164a99681b | ||
![]() |
0eef4eacd6 | ||
![]() |
5764f0c839 | ||
![]() |
f28e425542 | ||
![]() |
d1a4f046e9 | ||
![]() |
2ce1dc4afe | ||
![]() |
37ac249fd7 | ||
![]() |
f152bea8d8 | ||
![]() |
7b089b888a | ||
![]() |
68f0e1fe39 | ||
![]() |
8032bd0bac | ||
![]() |
0c227f2917 | ||
![]() |
c9fa8118d1 | ||
![]() |
63b18246d8 | ||
![]() |
16ec37a226 | ||
![]() |
bd4e5bfc1a | ||
![]() |
621fd0ee29 | ||
![]() |
6ca8db2f0c | ||
![]() |
ea129fb206 | ||
![]() |
3356d7b6ff | ||
![]() |
c84023bdc2 | ||
![]() |
86f778c0aa | ||
![]() |
defbbdfe21 | ||
![]() |
0f46493477 | ||
![]() |
340bac7e42 | ||
![]() |
1d3ce9fef1 | ||
![]() |
4a398642b8 | ||
![]() |
9c89e56c56 | ||
![]() |
267c59b1f1 | ||
![]() |
2ab17204c6 | ||
![]() |
75939047d1 | ||
![]() |
2d7f130d2c | ||
![]() |
f7ae72a36c | ||
![]() |
391783e268 | ||
![]() |
6f12c08204 | ||
![]() |
cb8fe70734 | ||
![]() |
69d10b747a | ||
![]() |
da3394f34e | ||
![]() |
b4c2a9f49f | ||
![]() |
7cee77f57a | ||
![]() |
f28bd1972f | ||
![]() |
0f92d1de1b | ||
![]() |
e59c5c8780 | ||
![]() |
86d8026301 | ||
![]() |
d67b827338 | ||
![]() |
660e0dc09a | ||
![]() |
3ebc886f8a | ||
![]() |
5b54ef840a | ||
![]() |
c08b0d4974 | ||
![]() |
7d652afd87 | ||
![]() |
0f61c627b1 | ||
![]() |
7126648404 | ||
![]() |
4a5e2dc9c7 | ||
![]() |
10613686ed | ||
![]() |
17ab55115a | ||
![]() |
2708c74ebe | ||
![]() |
50ff11405f | ||
![]() |
31a27838f5 | ||
![]() |
2f1b0fe57f | ||
![]() |
692f893e1f | ||
![]() |
14aa6041ec | ||
![]() |
fb55fe184c | ||
![]() |
6412bfc7b5 | ||
![]() |
3c56f38229 | ||
![]() |
f4f2274c60 | ||
![]() |
19ee189468 | ||
![]() |
a19c7215d2 | ||
![]() |
8b84039f1f | ||
![]() |
9430dbb96c | ||
![]() |
4872df6a46 | ||
![]() |
014105f0a0 | ||
![]() |
b106d1c501 | ||
![]() |
99db0672b4 | ||
![]() |
d584360de2 | ||
![]() |
4eed6794c7 | ||
![]() |
c66cabd80f | ||
![]() |
24da3485bd | ||
![]() |
7384d2d330 | ||
![]() |
e5940168fe | ||
![]() |
6855baf0f8 | ||
![]() |
dfd16e8fef | ||
![]() |
98a36819bc | ||
![]() |
de8bc9ca9d | ||
![]() |
c137f2de4f | ||
![]() |
0f55fcafe8 | ||
![]() |
ed027ec3ee | ||
![]() |
b3fd79cbb9 | ||
![]() |
ed4df87b57 | ||
![]() |
1321f097b8 | ||
![]() |
cfa28f0c4a | ||
![]() |
ab47b717b1 | ||
![]() |
65ebb0d2f8 | ||
![]() |
49640ce03a | ||
![]() |
e05cdc83f3 | ||
![]() |
992a9ea2f9 | ||
![]() |
228351fc13 | ||
![]() |
8a5b6f2b86 | ||
![]() |
71ecbb3af3 | ||
![]() |
5746614ccf | ||
![]() |
3a422c3f15 | ||
![]() |
b3242322fd | ||
![]() |
9826640ae6 | ||
![]() |
1f5267204b | ||
![]() |
ed25e1bbd6 | ||
![]() |
c8491d008f | ||
![]() |
08e3405394 | ||
![]() |
4ebfa07186 | ||
![]() |
6698c189fc | ||
![]() |
f0639390aa | ||
![]() |
bbdfed2d5a | ||
![]() |
7f4daa2c50 | ||
![]() |
baf9b67b35 | ||
![]() |
caf73b0b36 | ||
![]() |
acf87c2794 | ||
![]() |
7f5f6b54fb | ||
![]() |
a08eb8a446 | ||
![]() |
b31402766e | ||
![]() |
9ab3143bf0 | ||
![]() |
81a0cddb9e | ||
![]() |
f620ac769f | ||
![]() |
dc91041edd | ||
![]() |
6ee08b6717 | ||
![]() |
5a2cd2ac84 | ||
![]() |
2bd8448aaa | ||
![]() |
2360adb592 | ||
![]() |
c7301a5161 | ||
![]() |
72270825c1 | ||
![]() |
1e94f0a094 | ||
![]() |
e39d2567ea | ||
![]() |
949136c92a | ||
![]() |
9f456a9b19 | ||
![]() |
4cf6ba25ca | ||
![]() |
093f971896 | ||
![]() |
df38a9da71 | ||
![]() |
813814c54a | ||
![]() |
68cb32f375 | ||
![]() |
93c9590b0f | ||
![]() |
619d979c39 | ||
![]() |
c30faad838 | ||
![]() |
bea0de4980 | ||
![]() |
ef5a490415 | ||
![]() |
fa404285be | ||
![]() |
0e526258ff | ||
![]() |
56d2fb9a3b | ||
![]() |
7c82690852 | ||
![]() |
62acc17e42 | ||
![]() |
9fbe5895b7 | ||
![]() |
6bbe0f07d4 | ||
![]() |
bd3e0b9336 | ||
![]() |
699debdaca | ||
![]() |
70eba568af | ||
![]() |
bb7560e441 | ||
![]() |
43c0cac52f | ||
![]() |
4b4aa148a9 | ||
![]() |
c9c90c4e7f | ||
![]() |
99093e9a4c | ||
![]() |
2cf33d635d | ||
![]() |
d6abaf846e | ||
![]() |
4b88131977 | ||
![]() |
4520f46a57 | ||
![]() |
348d47076a | ||
![]() |
6e7b90a184 | ||
![]() |
28d7a7a6d2 | ||
![]() |
da13b5dbf2 | ||
![]() |
a60710e3bb | ||
![]() |
7d2a2b9983 | ||
![]() |
749df5dacd | ||
![]() |
af88b7c807 | ||
![]() |
4091687733 | ||
![]() |
cfb0a3ba2a | ||
![]() |
6c4d082f35 | ||
![]() |
262185046a | ||
![]() |
da9d00be7d | ||
![]() |
454abc388b | ||
![]() |
3e9174deed | ||
![]() |
4df1047b07 | ||
![]() |
60f69feaff | ||
![]() |
5df426380d | ||
![]() |
976c299657 | ||
![]() |
18ab6b51fd | ||
![]() |
4be8bd4d18 | ||
![]() |
075bc4a6d5 | ||
![]() |
1c61feb368 | ||
![]() |
d32b788988 | ||
![]() |
7565ea2787 | ||
![]() |
9275975b2c | ||
![]() |
b3e0d5ba58 | ||
![]() |
841dee94c6 | ||
![]() |
71638191ee | ||
![]() |
9d6851cbbd | ||
![]() |
d633d05803 | ||
![]() |
45d7879d7b | ||
![]() |
4a8375355c | ||
![]() |
d3ebd763a2 | ||
![]() |
b7f69238a1 | ||
![]() |
118a9f224e | ||
![]() |
a44dc8df37 | ||
![]() |
abf19aad74 | ||
![]() |
d73127b175 | ||
![]() |
00f4242fa4 | ||
![]() |
f6a4510659 | ||
![]() |
33215424d8 | ||
![]() |
6094bc9210 | ||
![]() |
a8cd9b3aa9 | ||
![]() |
a189dec1c8 | ||
![]() |
858216796a | ||
![]() |
f24342f117 | ||
![]() |
50b55a77de | ||
![]() |
fdf167db11 | ||
![]() |
a4f8bd4ee0 | ||
![]() |
3e4c12cf56 | ||
![]() |
03c39e692a | ||
![]() |
ab63b0e970 | ||
![]() |
6ea42a35a9 | ||
![]() |
d366dfc72b | ||
![]() |
85042fbe25 | ||
![]() |
23e5188422 | ||
![]() |
93ee0c8798 | ||
![]() |
aa88486f59 | ||
![]() |
1d9c441038 | ||
![]() |
928c56bda2 | ||
![]() |
bc6f37eecc | ||
![]() |
ffebff8cab | ||
![]() |
6d6bd89d6b | ||
![]() |
0a64a7e5d4 | ||
![]() |
586488af48 | ||
![]() |
7b9a45f1a8 | ||
![]() |
4ff70aefac | ||
![]() |
d63b5d7014 | ||
![]() |
ab5f6bf901 | ||
![]() |
04088b34a2 | ||
![]() |
3edcd2004e | ||
![]() |
7bd52d0245 | ||
![]() |
1df65940b9 | ||
![]() |
d9ace35c3e | ||
![]() |
1fe92cee6f | ||
![]() |
267868c3b0 | ||
![]() |
6d27eb7f64 | ||
![]() |
2e10fa494f | ||
![]() |
039be65a89 | ||
![]() |
570ecd9987 | ||
![]() |
a575180475 | ||
![]() |
07d1a20f3d | ||
![]() |
76491cbb31 | ||
![]() |
bf7d6ddcb2 | ||
![]() |
44b969e0b6 | ||
![]() |
176e470497 | ||
![]() |
646a10d9bf | ||
![]() |
52137fd64f | ||
![]() |
3ccac8c3b8 | ||
![]() |
0be158afa1 | ||
![]() |
e6942e0122 | ||
![]() |
496b22026f | ||
![]() |
bb2df02dff | ||
![]() |
4c850ecc31 | ||
![]() |
da9c6f6e23 | ||
![]() |
58ba0b0b4e | ||
![]() |
1d0b87246a | ||
![]() |
920b60da19 | ||
![]() |
523e66294b | ||
![]() |
23f8f35098 | ||
![]() |
8d210b5e37 | ||
![]() |
3c6c0e6700 | ||
![]() |
01344c451f | ||
![]() |
2c42c79482 | ||
![]() |
75c2cfe7bf | ||
![]() |
6c6eeb3f28 | ||
![]() |
31053e0cd0 | ||
![]() |
aad9aced18 | ||
![]() |
dd2c9eeafe | ||
![]() |
740d76bc42 | ||
![]() |
45f4f5afd9 | ||
![]() |
e875de3e98 | ||
![]() |
fd7786633d | ||
![]() |
bce9cfa39a | ||
![]() |
ff3d66a661 | ||
![]() |
006d28abd5 | ||
![]() |
59b1e63bdf | ||
![]() |
eab74ef06b | ||
![]() |
89837de9b0 | ||
![]() |
b245931c79 | ||
![]() |
fd5e42698c | ||
![]() |
c75512ba6e | ||
![]() |
a22e7aa0b1 | ||
![]() |
020dd97f99 | ||
![]() |
e9882d9702 | ||
![]() |
fd4a27dbf2 | ||
![]() |
9c63e31da6 | ||
![]() |
c91f809eba | ||
![]() |
a54eaf5371 | ||
![]() |
8032bd4bb9 | ||
![]() |
ea1beec2f7 | ||
![]() |
05f2f6820e | ||
![]() |
0f5f15a5ce | ||
![]() |
14ac37e8a5 | ||
![]() |
1fae89cbb6 | ||
![]() |
8b4008798f | ||
![]() |
fd4faf59b8 | ||
![]() |
109891d668 | ||
![]() |
bdea796121 | ||
![]() |
c8813c05c9 | ||
![]() |
1cff08ce5d | ||
![]() |
a868118f6f | ||
![]() |
e5c62f5750 | ||
![]() |
4084e8790b | ||
![]() |
8d931dd773 | ||
![]() |
25d6366297 | ||
![]() |
08cd5b81d1 | ||
![]() |
5d3a8a5b1a | ||
![]() |
79b84da4b8 | ||
![]() |
68b07c5913 | ||
![]() |
553db9124d | ||
![]() |
b495f37299 | ||
![]() |
1915547594 | ||
![]() |
03de29164a | ||
![]() |
86d8b50547 | ||
![]() |
7b04386162 | ||
![]() |
07bfdf3e4d | ||
![]() |
d510224e2a | ||
![]() |
e658f9297d | ||
![]() |
2b502e9a0f | ||
![]() |
59141f9bbe | ||
![]() |
3af66b72f2 | ||
![]() |
422c24bd68 | ||
![]() |
f0f87c8eb9 | ||
![]() |
80dad54119 | ||
![]() |
56a76df28e | ||
![]() |
ee2c801fe0 | ||
![]() |
fc314cc248 | ||
![]() |
fe231a4c80 | ||
![]() |
2e2bbe0a7f | ||
![]() |
857e6e8345 | ||
![]() |
3402981ada | ||
![]() |
f401e577e5 | ||
![]() |
0241a50c6f | ||
![]() |
2a2e1236fc | ||
![]() |
9b170f2b4f | ||
![]() |
51e9ff59de | ||
![]() |
2977dbcded | ||
![]() |
ac60b51035 | ||
![]() |
4c2f33a089 | ||
![]() |
3b071116ac | ||
![]() |
a9f265a591 | ||
![]() |
5b62fc8103 | ||
![]() |
0598f5f89a | ||
![]() |
f723427b8b | ||
![]() |
f69a004c1c | ||
![]() |
1134b18a8b | ||
![]() |
2e4aa507f7 | ||
![]() |
5fb96cdcf4 | ||
![]() |
e8cba3524e | ||
![]() |
7e6b5363f1 | ||
![]() |
29457a1d28 | ||
![]() |
731455f164 | ||
![]() |
b01a8cace6 | ||
![]() |
72db5b4fac | ||
![]() |
ddfd42994e | ||
![]() |
2a9ff9c5ef | ||
![]() |
6d49f05356 | ||
![]() |
85a5e62e36 | ||
![]() |
e67965a381 | ||
![]() |
ec4723096f | ||
![]() |
762b678d24 | ||
![]() |
38fcc57bbf | ||
![]() |
c8c57c74cc | ||
![]() |
0784448c69 | ||
![]() |
de0064af47 | ||
![]() |
baae1fc84f | ||
![]() |
2ab999f4ca | ||
![]() |
c9f390d6e0 | ||
![]() |
af05922ecc | ||
![]() |
299edbf3ab | ||
![]() |
c8abed9d48 | ||
![]() |
3622c49ce1 | ||
![]() |
0462e9a7d9 | ||
![]() |
c3a6091908 | ||
![]() |
ab5fedda0b | ||
![]() |
ba70269398 | ||
![]() |
77fd5fa7de | ||
![]() |
ab74290fe3 | ||
![]() |
3aad9d8166 | ||
![]() |
572e078d87 | ||
![]() |
ee4548230b | ||
![]() |
96b93bd876 | ||
![]() |
927f69fe30 | ||
![]() |
7e9ad5927a | ||
![]() |
6d6b07865e | ||
![]() |
376e7977f0 | ||
![]() |
83ae66daea | ||
![]() |
89e0be0099 | ||
![]() |
ef40c1212e | ||
![]() |
3a2a2a4ffa | ||
![]() |
9592a69986 | ||
![]() |
89be07e1f2 | ||
![]() |
c61c3ae0e9 | ||
![]() |
817350c8c5 | ||
![]() |
3603b7c82b | ||
![]() |
5743c72cca | ||
![]() |
4cdd66ceff | ||
![]() |
d3947d2cfa | ||
![]() |
07718b994a | ||
![]() |
ef9d463bd7 | ||
![]() |
8745c7884e | ||
![]() |
b6965105b7 | ||
![]() |
3d269fe8be | ||
![]() |
be5f00aa1a | ||
![]() |
59ba350f34 | ||
![]() |
803c5377a6 | ||
![]() |
7c12bf7fa1 | ||
![]() |
ca35a9681f | ||
![]() |
9fe5f37337 | ||
![]() |
0742901cd2 | ||
![]() |
5e4d2dedbe | ||
![]() |
411ea56a3e | ||
![]() |
cda57dd4b4 | ||
![]() |
4351de503f | ||
![]() |
6339ba6bfb | ||
![]() |
ef6677f43d | ||
![]() |
a7824af5a8 | ||
![]() |
1eb7d7b7a8 | ||
![]() |
11c33d4447 | ||
![]() |
b8a3cc8b60 | ||
![]() |
27c688252d | ||
![]() |
3e2afd4b1d | ||
![]() |
f45b0686d2 | ||
![]() |
1f3f881f81 | ||
![]() |
ceb51bb14f | ||
![]() |
3e22573d8d | ||
![]() |
79418a3767 | ||
![]() |
40d4683de1 | ||
![]() |
79e5b54ec7 | ||
![]() |
bd81923f2f | ||
![]() |
69560b8ad7 | ||
![]() |
dc413e7b73 | ||
![]() |
7fc00c446b | ||
![]() |
2efc423cf8 | ||
![]() |
8ec3086cdd | ||
![]() |
5fc7079023 | ||
![]() |
bfbd254be7 | ||
![]() |
f8ea43466c | ||
![]() |
75ab1fa570 | ||
![]() |
bf4a46d57c | ||
![]() |
1046dd5eda | ||
![]() |
f9e32a119a | ||
![]() |
dbb8b8a439 | ||
![]() |
2a65c3dc8f | ||
![]() |
f17ec9e9d7 | ||
![]() |
675d6d8328 | ||
![]() |
6dc9ccad75 | ||
![]() |
6add02702b | ||
![]() |
958d6377e3 | ||
![]() |
9954154ca2 | ||
![]() |
4ecbf8c12c | ||
![]() |
fc8a3c5fb4 | ||
![]() |
01e7dff1a0 | ||
![]() |
018c0064cd | ||
![]() |
c2b016370b | ||
![]() |
fc791b4371 | ||
![]() |
f76bb009f4 | ||
![]() |
8a1292b295 | ||
![]() |
d7d80d3fc1 | ||
![]() |
41b01003fd | ||
![]() |
6557070ae1 | ||
![]() |
e7e580e177 | ||
![]() |
dd9ddd2019 | ||
![]() |
74aae523ba | ||
![]() |
48c40f9516 | ||
![]() |
e0e7674715 | ||
![]() |
e1a65276b9 | ||
![]() |
469adc85ad | ||
![]() |
e1b181ca4e | ||
![]() |
a4f0fbf8b7 | ||
![]() |
190cdaddf8 | ||
![]() |
5c4ba13839 | ||
![]() |
e62630cf3e | ||
![]() |
36fe7846c0 | ||
![]() |
8d150dd67a | ||
![]() |
506df00d81 | ||
![]() |
a9121fa28f | ||
![]() |
d5a56d9e85 | ||
![]() |
acf7c0c665 | ||
![]() |
619d48c97a | ||
![]() |
2cb198c38c | ||
![]() |
e8e39e0f3c | ||
![]() |
37860181d4 | ||
![]() |
d119dd9a0c | ||
![]() |
09ef19f7ec | ||
![]() |
6a06c92fa6 | ||
![]() |
58ae596b0f | ||
![]() |
f1ca21678d | ||
![]() |
d7eeef2c8a | ||
![]() |
4f626897f2 | ||
![]() |
b127e01845 | ||
![]() |
2118beeb23 | ||
![]() |
5020cd1bbf | ||
![]() |
cce636224c | ||
![]() |
60b3b8ddce | ||
![]() |
41446ec9ba | ||
![]() |
df8b047bca | ||
![]() |
12ced52012 | ||
![]() |
1d53335ae5 | ||
![]() |
971a50d290 | ||
![]() |
36dd9106a8 | ||
![]() |
0a4ee3ffc7 | ||
![]() |
cfe32f1a70 | ||
![]() |
d877f5d5c6 | ||
![]() |
0ab6ffefb4 | ||
![]() |
a292a1d23a | ||
![]() |
3f87f6aee3 | ||
![]() |
04bcd145d3 | ||
![]() |
244e811291 | ||
![]() |
ac7467fb59 | ||
![]() |
2c0436216f | ||
![]() |
017fbf267b | ||
![]() |
e6afbf2ec0 | ||
![]() |
906b4aad9e | ||
![]() |
4cf8d41f6a | ||
![]() |
47c860142e | ||
![]() |
2fba3f213b | ||
![]() |
af7c6f9fce | ||
![]() |
78534deab6 | ||
![]() |
6710314832 | ||
![]() |
0cd4fa6fa0 | ||
![]() |
065949496e | ||
![]() |
39c82576ae | ||
![]() |
37221a508d | ||
![]() |
6b43a32a10 | ||
![]() |
d7cd1ff142 | ||
![]() |
659d947863 | ||
![]() |
39be7a6288 | ||
![]() |
8ac976c579 | ||
![]() |
70fd432c57 | ||
![]() |
00135f2f49 | ||
![]() |
9b944bc29c | ||
![]() |
d520b3d2a0 | ||
![]() |
6f41d9855b | ||
![]() |
2d7c1da741 | ||
![]() |
c0f45b6b1e | ||
![]() |
7a0025673c | ||
![]() |
ad7ec79903 | ||
![]() |
0543239cca | ||
![]() |
ff3dad2457 | ||
![]() |
298d5e197b | ||
![]() |
d73c0a998d | ||
![]() |
1b79a3ddbf | ||
![]() |
a8478ace18 | ||
![]() |
72cf5f3f9f | ||
![]() |
6f9d493a18 | ||
![]() |
08f7d5ebff | ||
![]() |
1fe3675403 | ||
![]() |
a0f956d2c1 | ||
![]() |
1560f91b4a | ||
![]() |
c20f362594 | ||
![]() |
7ae8c26e50 | ||
![]() |
adfffe6121 | ||
![]() |
64601baa76 | ||
![]() |
aa374b51f1 | ||
![]() |
5c483745ff | ||
![]() |
0c247110a0 | ||
![]() |
1643638a78 | ||
![]() |
4ace228fc2 | ||
![]() |
25aa86a0dc | ||
![]() |
70d3b24338 | ||
![]() |
8664e9d19b | ||
![]() |
50d9877446 | ||
![]() |
fe06352089 | ||
![]() |
7b599419b5 | ||
![]() |
491adf072e | ||
![]() |
f6aae2b048 | ||
![]() |
d2d5c94633 | ||
![]() |
10581f9ef2 | ||
![]() |
c7e0e1c038 | ||
![]() |
a914d701eb | ||
![]() |
0f9dee6e9c | ||
![]() |
aa383e2190 | ||
![]() |
9bbfcf326c | ||
![]() |
3948e67c8f | ||
![]() |
d56e1b2cc5 | ||
![]() |
bfac1f1bc2 | ||
![]() |
d4a956c355 | ||
![]() |
6c71fefa58 | ||
![]() |
ad3003c00a | ||
![]() |
0ad5dcb258 | ||
![]() |
d790309b02 | ||
![]() |
1072faf309 | ||
![]() |
d2c196896d | ||
![]() |
e42b608444 | ||
![]() |
89a501a3af | ||
![]() |
c19b78180c | ||
![]() |
c0b750a09a | ||
![]() |
c967e618a1 | ||
![]() |
59f78d7dfc | ||
![]() |
d8405f0d05 | ||
![]() |
0f34f0033c | ||
![]() |
190646d50c | ||
![]() |
a46c6252c6 | ||
![]() |
5c1886c8f5 | ||
![]() |
afcb3d8f34 | ||
![]() |
9fbffafdbf | ||
![]() |
075f0458f7 | ||
![]() |
d4568aa0a7 | ||
![]() |
97588408a2 | ||
![]() |
1def9b301b | ||
![]() |
5bac442b18 | ||
![]() |
6add682705 | ||
![]() |
8b50d84a05 | ||
![]() |
d3858b81e2 | ||
![]() |
bdff9769be | ||
![]() |
c61df75e5e | ||
![]() |
a74bf2cc27 | ||
![]() |
ada0f93686 | ||
![]() |
ff36f2ba17 | ||
![]() |
5164cfd399 | ||
![]() |
5fa021503e | ||
![]() |
7b5d79d313 | ||
![]() |
3e3f38500d | ||
![]() |
5109b9abfd | ||
![]() |
7fb4777c1c | ||
![]() |
c38533e0f8 | ||
![]() |
51ba99d09e | ||
![]() |
9159f86a9e | ||
![]() |
e139f4fc13 | ||
![]() |
2fbfeacb87 | ||
![]() |
ebb7a9fcda | ||
![]() |
9e72317302 | ||
![]() |
d764c20c08 | ||
![]() |
9c17b8a098 | ||
![]() |
3084873154 | ||
![]() |
32809e56d0 | ||
![]() |
9f05b182a2 | ||
![]() |
525484e834 | ||
![]() |
65a4e69cae | ||
![]() |
e973f8bab9 | ||
![]() |
92466671ff | ||
![]() |
6d61106070 | ||
![]() |
ac13749fb8 | ||
![]() |
7ec1a9a316 | ||
![]() |
cf17e21ad3 | ||
![]() |
0e0240c4ab | ||
![]() |
d1b290b91a | ||
![]() |
a63696836c | ||
![]() |
46aad00f16 | ||
![]() |
252afe8932 | ||
![]() |
9dd467a613 | ||
![]() |
4c14df67cc | ||
![]() |
20e0fe3ba1 | ||
![]() |
6a005135f2 | ||
![]() |
82e8375957 | ||
![]() |
bb25edc09e | ||
![]() |
169c0fe4af | ||
![]() |
cd6918e6eb | ||
![]() |
5be035fd44 | ||
![]() |
f1edc8443c | ||
![]() |
d9564bd04c | ||
![]() |
35f1c396f2 | ||
![]() |
6acb950990 | ||
![]() |
27e0d1641a | ||
![]() |
9ac71ff8af | ||
![]() |
075737a4ec | ||
![]() |
6d0e4a6a5e | ||
![]() |
a2544768a0 | ||
![]() |
8574a14ed2 | ||
![]() |
e90c555c18 | ||
![]() |
863b9a410f | ||
![]() |
23c7bbc7d5 | ||
![]() |
f900189f90 | ||
![]() |
7c74be2790 | ||
![]() |
70dd2d4829 | ||
![]() |
914b7ee056 | ||
![]() |
e39f83edbf | ||
![]() |
52fe0c6abb | ||
![]() |
5cb3e5937f | ||
![]() |
e0cd224831 | ||
![]() |
de225ac64a | ||
![]() |
5807808a10 | ||
![]() |
362877d18f | ||
![]() |
88b8dd0149 | ||
![]() |
1552f32e09 | ||
![]() |
50b73a6720 | ||
![]() |
53e51f1735 | ||
![]() |
40b63bfebe | ||
![]() |
89861eceef | ||
![]() |
b8eaff66fa | ||
![]() |
a747fdd27d | ||
![]() |
27851bdefa | ||
![]() |
3fdeb40ddf | ||
![]() |
546c7cebd3 | ||
![]() |
473902f5f4 | ||
![]() |
41c0721159 | ||
![]() |
413d4badfd | ||
![]() |
c5d67ebf72 | ||
![]() |
91818cfa1a | ||
![]() |
6263d684d9 | ||
![]() |
07140d33a7 | ||
![]() |
4ffc388491 | ||
![]() |
0ef026c610 | ||
![]() |
153c7fdf20 | ||
![]() |
90379eeb35 | ||
![]() |
3ae959af95 | ||
![]() |
c8cc652b71 | ||
![]() |
4b6285e5c2 | ||
![]() |
013de7b3ef | ||
![]() |
e11e88a9c5 | ||
![]() |
7cec8baa55 | ||
![]() |
e987db9fb5 | ||
![]() |
c603b9084f | ||
![]() |
492d6dfcf0 | ||
![]() |
a3e0f2dcc3 | ||
![]() |
cf211e26f4 | ||
![]() |
c5aaaa7c55 | ||
![]() |
f86d077e27 | ||
![]() |
f8076825cb | ||
![]() |
201d8a97d4 | ||
![]() |
d08f326990 | ||
![]() |
8dc9d3bc78 | ||
![]() |
adf95ce3a0 | ||
![]() |
3c1aca114f | ||
![]() |
18d0fd9d2a | ||
![]() |
c2e673f978 | ||
![]() |
2bde8a1975 | ||
![]() |
bf9927c7dd | ||
![]() |
f339a087a2 | ||
![]() |
6ccc5f3788 | ||
![]() |
1affb91f17 | ||
![]() |
7779c3e372 | ||
![]() |
49ba7ad22e | ||
![]() |
6ad33d60f7 | ||
![]() |
0117274061 | ||
![]() |
e50192a407 | ||
![]() |
c6fc0e587e | ||
![]() |
68c448bc34 | ||
![]() |
ef62272df7 | ||
![]() |
375cd0e42b | ||
![]() |
b885ccbd63 | ||
![]() |
da6f1d0f12 | ||
![]() |
3934821436 | ||
![]() |
c3b473e4bc | ||
![]() |
4c0d435b6b | ||
![]() |
7ed2c077de | ||
![]() |
52a6a7bce8 | ||
![]() |
1283167595 | ||
![]() |
23c2e22910 | ||
![]() |
f44b2dbd45 | ||
![]() |
46ee2c3f4e | ||
![]() |
5d5ec08566 | ||
![]() |
0e717a2de4 | ||
![]() |
cada862214 | ||
![]() |
c3a6179a21 | ||
![]() |
682c6d4e7b | ||
![]() |
d0a253c97e | ||
![]() |
c0e2b3027b | ||
![]() |
e7dc14b07d | ||
![]() |
0da9146e90 | ||
![]() |
ad05a33e02 | ||
![]() |
ef175e3cbe | ||
![]() |
4de51d93ef | ||
![]() |
8224e038a3 | ||
![]() |
03c04c2141 | ||
![]() |
2e091b04e5 | ||
![]() |
60296493fe | ||
![]() |
20c20f8f9b | ||
![]() |
f1d642a4e5 | ||
![]() |
e0e5ea17a4 | ||
![]() |
91a0ba72dc | ||
![]() |
c54c5a974a | ||
![]() |
532b8c54ab | ||
![]() |
5ac87891b5 | ||
![]() |
2d905ce3fb | ||
![]() |
831112abd2 | ||
![]() |
153d0f5505 | ||
![]() |
c78896a335 | ||
![]() |
316ec98e0f | ||
![]() |
cf58545a45 | ||
![]() |
e7a2144def | ||
![]() |
52a2c6958b | ||
![]() |
70243d7a47 | ||
![]() |
b5b8c4b725 | ||
![]() |
6c4d81b1e9 | ||
![]() |
513d732934 | ||
![]() |
c88dc8795b | ||
![]() |
a8030c39b1 | ||
![]() |
7243b9e72f | ||
![]() |
d149af9628 | ||
![]() |
c0ac2d540b | ||
![]() |
528634d755 | ||
![]() |
3283439fd4 | ||
![]() |
e86015badc | ||
![]() |
c8f65fc9a1 | ||
![]() |
c8216f9bc5 | ||
![]() |
e579f314a6 | ||
![]() |
2c4001387e | ||
![]() |
caa39474cb | ||
![]() |
7684602ea8 | ||
![]() |
d1a7372bd2 | ||
![]() |
4601989d4a | ||
![]() |
23f697d62b | ||
![]() |
e837bdc8ad | ||
![]() |
7265450e2e | ||
![]() |
058dbc9f9e | ||
![]() |
daf9b019c6 | ||
![]() |
14eebd582f | ||
![]() |
9a8eeacee8 | ||
![]() |
45b0bf5bc5 | ||
![]() |
88db822c43 | ||
![]() |
fbf3588fdf | ||
![]() |
a82ef6bd35 | ||
![]() |
312466aaf8 | ||
![]() |
c0ca99f4b4 | ||
![]() |
196f15d240 | ||
![]() |
bfddef2671 | ||
![]() |
44395e8ff0 | ||
![]() |
4ff39f8817 | ||
![]() |
1df41003ec | ||
![]() |
1f39ee41ad | ||
![]() |
42d8b1ecb9 | ||
![]() |
a4da7b33e6 | ||
![]() |
e4ee9e9095 | ||
![]() |
835ece5469 | ||
![]() |
77430a282f | ||
![]() |
d93fc67a75 | ||
![]() |
838f3cc01e | ||
![]() |
4d5841332a | ||
![]() |
9b41976252 | ||
![]() |
d08fd0561a | ||
![]() |
30e459252c | ||
![]() |
a6958ac139 | ||
![]() |
d7d76f54cc | ||
![]() |
e6c1dd532d | ||
![]() |
970a2e87b3 | ||
![]() |
cabaae8403 | ||
![]() |
d1f301e059 | ||
![]() |
79eb5b2ed2 | ||
![]() |
f0533fca70 | ||
![]() |
08e98eeb15 | ||
![]() |
f2064a84ed | ||
![]() |
b2f719989d | ||
![]() |
1e812c40ce | ||
![]() |
a949641342 | ||
![]() |
6db27c7758 | ||
![]() |
c231e88a5d | ||
![]() |
3f83919e09 | ||
![]() |
72a5b83544 | ||
![]() |
d2e8ecc646 | ||
![]() |
30eb4074cb | ||
![]() |
79c71509f6 | ||
![]() |
5dab580cfc | ||
![]() |
9929e7d8e8 | ||
![]() |
f6ee252572 | ||
![]() |
90d218ebc8 | ||
![]() |
499a157946 | ||
![]() |
c5a7ab2415 | ||
![]() |
3dd5a6f378 | ||
![]() |
7be26a0677 | ||
![]() |
c183fdd3ca | ||
![]() |
baa439457e | ||
![]() |
4dbcd54b72 | ||
![]() |
11062f2d4f | ||
![]() |
b0a5dbb4c2 | ||
![]() |
0abdfda5a2 | ||
![]() |
a0466085fe | ||
![]() |
a7ceb04cb7 | ||
![]() |
274efb49e7 | ||
![]() |
b3cd83bbca | ||
![]() |
b8bd83ba05 | ||
![]() |
34dcf49fbc | ||
![]() |
f2f7d77847 | ||
![]() |
b2105f2d88 | ||
![]() |
4126f3bdcb | ||
![]() |
74ccfe6088 | ||
![]() |
48085b5573 | ||
![]() |
ef2f8d485b | ||
![]() |
9fb9212b0a | ||
![]() |
7b9ddc9b3b | ||
![]() |
15726a759c | ||
![]() |
2c7474ea87 | ||
![]() |
c726aee643 | ||
![]() |
f31a24b16d | ||
![]() |
b436bce565 | ||
![]() |
886286a819 | ||
![]() |
c3e94e1480 | ||
![]() |
5f1343e5b4 | ||
![]() |
ffb1303d61 | ||
![]() |
a0b0d938f0 | ||
![]() |
158f5ba7d9 | ||
![]() |
b8cf40161c | ||
![]() |
fb96e6a56f | ||
![]() |
6668ba2511 | ||
![]() |
6d93831488 | ||
![]() |
4668ef3020 | ||
![]() |
bcdadc6581 | ||
![]() |
36448191b7 | ||
![]() |
be5be108c3 | ||
![]() |
c9ca42aaa9 | ||
![]() |
630f2b7d19 | ||
![]() |
dde0a4a7c8 | ||
![]() |
c0e2f44092 | ||
![]() |
1412fcbb22 | ||
![]() |
c69dc0f036 | ||
![]() |
9b445d89a1 | ||
![]() |
c3c78428c4 | ||
![]() |
c6d2bf577f | ||
![]() |
b06f69573d | ||
![]() |
8fd03f7434 | ||
![]() |
25703c1750 | ||
![]() |
90e4ac2d23 | ||
![]() |
956bceae75 | ||
![]() |
c663be86de | ||
![]() |
aca78baecf | ||
![]() |
fbcf6b7954 | ||
![]() |
84123222aa | ||
![]() |
e9dbcf693d | ||
![]() |
1cd0a9d48f | ||
![]() |
1b48e44914 | ||
![]() |
0a398f03fd | ||
![]() |
3a9a3ed184 | ||
![]() |
88fae36b8a | ||
![]() |
15ed3e52f2 | ||
![]() |
8990919dab | ||
![]() |
e5638e4b15 | ||
![]() |
404c6fac9a | ||
![]() |
fc9d4034a9 | ||
![]() |
cecc0b932d | ||
![]() |
0faed7159c | ||
![]() |
fb491cfdcf | ||
![]() |
fc706dcb40 | ||
![]() |
a2c1b024f3 | ||
![]() |
267395bfa2 | ||
![]() |
920fc5ae99 | ||
![]() |
92ed0ae51b | ||
![]() |
3d865394d7 | ||
![]() |
76ef1d0d86 | ||
![]() |
c694776162 | ||
![]() |
9484ec0c17 | ||
![]() |
7e2ba41c64 | ||
![]() |
614c552e55 | ||
![]() |
7db3d84ba2 | ||
![]() |
bb2c744ec0 | ||
![]() |
87f6018468 | ||
![]() |
9194c50590 | ||
![]() |
873f14bbe0 | ||
![]() |
31110b1927 | ||
![]() |
6764a98409 | ||
![]() |
7ff45974c6 | ||
![]() |
fd7b5f393a | ||
![]() |
2533a4fc4a | ||
![]() |
2ca528f93f | ||
![]() |
42284c5efb | ||
![]() |
ce2e6b7d35 | ||
![]() |
684c5d225a | ||
![]() |
b75018b03b | ||
![]() |
41499d4b3c | ||
![]() |
383c97c303 | ||
![]() |
74b54ef371 | ||
![]() |
bbf7b4db79 | ||
![]() |
c61f0acab5 | ||
![]() |
398af123b2 | ||
![]() |
315fa9d7d3 | ||
![]() |
fb5e8ef40c | ||
![]() |
7d7686da33 | ||
![]() |
e79d764148 | ||
![]() |
ebbee0dc43 | ||
![]() |
65e455ef0b | ||
![]() |
ed0c16e201 | ||
![]() |
209fdf349a | ||
![]() |
f49f2afacd | ||
![]() |
8c6330a3c4 | ||
![]() |
337b777125 | ||
![]() |
1b756e8d96 | ||
![]() |
ac05e2f2e2 | ||
![]() |
787f7b3035 | ||
![]() |
31bd642b80 | ||
![]() |
f0bac6b154 | ||
![]() |
cc7e74ca11 | ||
![]() |
52d478df1a | ||
![]() |
e8a44646b8 | ||
![]() |
0c782edf21 | ||
![]() |
e3948d295e | ||
![]() |
5f2c742a5c | ||
![]() |
ae97d011ae | ||
![]() |
1b7657a374 | ||
![]() |
5665e04014 | ||
![]() |
b30c77aab9 | ||
![]() |
a5916b9c49 | ||
![]() |
453180e30b | ||
![]() |
8bd432d391 | ||
![]() |
c9d3e20aef | ||
![]() |
bb70385a42 | ||
![]() |
9855877b03 | ||
![]() |
d5408d1f09 | ||
![]() |
f334532aba | ||
![]() |
be77c09f3d | ||
![]() |
7de6a92753 | ||
![]() |
36f76f5a14 | ||
![]() |
b84523d557 | ||
![]() |
21a557a184 | ||
![]() |
2c78c415e9 | ||
![]() |
79ccb30dd2 | ||
![]() |
3c566becf6 | ||
![]() |
76c9188fae | ||
![]() |
e4e5269836 | ||
![]() |
9e737df534 | ||
![]() |
151ca593af | ||
![]() |
4132eacba0 | ||
![]() |
06e6151816 | ||
![]() |
70277d4edd | ||
![]() |
d21d2f1a9c | ||
![]() |
74a7be996f | ||
![]() |
0b3192c4d5 | ||
![]() |
968e6237bd | ||
![]() |
d780b5a0e4 | ||
![]() |
3e48427eaf | ||
![]() |
31360c34ed | ||
![]() |
e9624e2304 | ||
![]() |
3f38579529 | ||
![]() |
4d5a9f6e15 | ||
![]() |
41f47acd76 | ||
![]() |
821dcaa7c7 | ||
![]() |
7135d26419 | ||
![]() |
f7fd354dce | ||
![]() |
0c69a65bc4 | ||
![]() |
2f2ca5eab4 | ||
![]() |
9c6e64f47d | ||
![]() |
0afa601551 | ||
![]() |
df9c40c035 | ||
![]() |
25b67017e4 | ||
![]() |
bc9c3346f3 | ||
![]() |
1db7e19fe8 | ||
![]() |
102c03ce2b | ||
![]() |
ec19eb4455 | ||
![]() |
6d9924d50e | ||
![]() |
16c4d74274 | ||
![]() |
e4af5fd36a | ||
![]() |
702775493a | ||
![]() |
b2ae826066 | ||
![]() |
cc3e9990fa | ||
![]() |
271cbddd5e | ||
![]() |
26dfbb3028 | ||
![]() |
f16cd987e4 | ||
![]() |
c1423ca9ad | ||
![]() |
74379150a1 | ||
![]() |
a94fa81195 | ||
![]() |
6119c24720 | ||
![]() |
c840a30c30 | ||
![]() |
ae5277a898 | ||
![]() |
bffa837825 | ||
![]() |
b9e7d0faea | ||
![]() |
860b08d9ed | ||
![]() |
691dc1d49e | ||
![]() |
7da205f4c8 | ||
![]() |
9d6886d367 | ||
![]() |
9589b68f5a | ||
![]() |
28d88af1af | ||
![]() |
8b5acd1849 | ||
![]() |
33dc63a7fd | ||
![]() |
754fafcfe9 | ||
![]() |
bd7766b17e | ||
![]() |
70b7d73453 | ||
![]() |
5ad4702a5b | ||
![]() |
40b6fe03c2 | ||
![]() |
49ecba2476 | ||
![]() |
ebd509d92d | ||
![]() |
7193374a7e | ||
![]() |
6728445542 | ||
![]() |
10ed299c78 | ||
![]() |
d0a86385b7 | ||
![]() |
32b124913e | ||
![]() |
599ae95251 | ||
![]() |
d1be34c34a | ||
![]() |
bc2cac90fe | ||
![]() |
50a49e2c8c | ||
![]() |
c60adb113e | ||
![]() |
aee015e8f6 | ||
![]() |
bf6af29205 | ||
![]() |
329905d472 | ||
![]() |
00d450d262 | ||
![]() |
2365d1bd20 | ||
![]() |
5b385c18e5 | ||
![]() |
98c0434ec0 | ||
![]() |
f318d0a3bc | ||
![]() |
27f5b410c0 | ||
![]() |
3f55be9676 | ||
![]() |
28350e3ad9 | ||
![]() |
f48e6c93b8 | ||
![]() |
7cfc24d68f | ||
![]() |
a58d3ea04d | ||
![]() |
dfee9954e0 | ||
![]() |
eed86c760f | ||
![]() |
c471bb6f67 | ||
![]() |
518c2b0f95 | ||
![]() |
328fc44194 | ||
![]() |
b6f735a8f6 | ||
![]() |
b05d2d3a2d | ||
![]() |
deae08fc4b | ||
![]() |
19af5f9e0b | ||
![]() |
c61135ee7b | ||
![]() |
f37f330670 | ||
![]() |
40082d4571 | ||
![]() |
97cf15007f | ||
![]() |
00d655f346 | ||
![]() |
821726e7c0 | ||
![]() |
759e905c3c | ||
![]() |
8bf7e42913 | ||
![]() |
0dcd073554 | ||
![]() |
2fe35d578d | ||
![]() |
8d139e156e | ||
![]() |
7c2849356a | ||
![]() |
0025ffd1c0 | ||
![]() |
2ef7146642 | ||
![]() |
1b27e69e40 | ||
![]() |
8e7b757efd | ||
![]() |
1ab543cea1 | ||
![]() |
a3f86903e4 | ||
![]() |
c239c305ab | ||
![]() |
2e02af994e | ||
![]() |
2f4062a923 | ||
![]() |
836d9afe17 | ||
![]() |
007a352742 | ||
![]() |
e526e5659e | ||
![]() |
4a5227c7bf | ||
![]() |
c2c151ec4c | ||
![]() |
452096e7e4 | ||
![]() |
50c2a9859e | ||
![]() |
677b667307 | ||
![]() |
5c338cd0a7 | ||
![]() |
1adf331268 | ||
![]() |
349b3e961b | ||
![]() |
96650c06f0 | ||
![]() |
26038a0a07 | ||
![]() |
6a148b5dd9 | ||
![]() |
0e109ef979 | ||
![]() |
de2285d5e9 | ||
![]() |
b2483ba437 | ||
![]() |
a82a5e5a49 | ||
![]() |
d161a02e71 | ||
![]() |
d2b6a700b1 | ||
![]() |
af203cef24 | ||
![]() |
673e917e76 | ||
![]() |
a3bd41db54 | ||
![]() |
0d9527921a | ||
![]() |
f0e4aec0af | ||
![]() |
b0d65b5edd | ||
![]() |
75532ef591 | ||
![]() |
9a6d1bd700 | ||
![]() |
a7ed6c15d3 | ||
![]() |
5ee49ba065 | ||
![]() |
190d857949 | ||
![]() |
d34bd47bea | ||
![]() |
f17792380b | ||
![]() |
c11920110e | ||
![]() |
ec5a993fea | ||
![]() |
d250c2cc89 | ||
![]() |
767e73f40c | ||
![]() |
3f699c9d2f | ||
![]() |
50dbd9befd | ||
![]() |
760e01bf92 | ||
![]() |
543f435b1e | ||
![]() |
91337218b3 | ||
![]() |
afff3c0a49 | ||
![]() |
2b6c271d37 | ||
![]() |
b0c1a6f73a | ||
![]() |
a1871e4bc3 | ||
![]() |
3aa0294cd4 | ||
![]() |
310b266251 | ||
![]() |
21b1b5098e | ||
![]() |
a3a4a5d8a5 | ||
![]() |
270536f33c | ||
![]() |
66bb433cc6 | ||
![]() |
bd4ef1a03a | ||
![]() |
aa2d9a3bf1 | ||
![]() |
257308d5db | ||
![]() |
d4620e1654 | ||
![]() |
fd6cbb138c | ||
![]() |
aa75c8e5e4 | ||
![]() |
c461fc6daa | ||
![]() |
96eaa833f5 | ||
![]() |
863b13a694 | ||
![]() |
e6fea4e6dd | ||
![]() |
83bfc13056 | ||
![]() |
bc4f09209b | ||
![]() |
967ca17238 | ||
![]() |
595c72147c | ||
![]() |
f3c3b5a649 | ||
![]() |
1cd2c5e653 | ||
![]() |
b2873dd44b | ||
![]() |
bb80ab4026 | ||
![]() |
80cabb338b | ||
![]() |
2c69e2c151 | ||
![]() |
c1dd23f5e0 | ||
![]() |
f93624a41c | ||
![]() |
9f4559a059 | ||
![]() |
fd05cad303 | ||
![]() |
d58b06e493 | ||
![]() |
d7a6127273 | ||
![]() |
8ee9984e4e | ||
![]() |
2f0b549027 | ||
![]() |
87dbd7e541 | ||
![]() |
96e5da36be | ||
![]() |
43745edac0 | ||
![]() |
18bee21cfc | ||
![]() |
e5b6121d17 | ||
![]() |
f5ceee547c | ||
![]() |
b612bce779 | ||
![]() |
2e88e5e9c7 | ||
![]() |
9a7aa25c90 | ||
![]() |
c4420fe932 | ||
![]() |
a5260f3a95 | ||
![]() |
47ccf4b1f5 | ||
![]() |
a356b21895 | ||
![]() |
614a36c888 | ||
![]() |
b7e717ee8c | ||
![]() |
f520fe36bd | ||
![]() |
7273a1c34d | ||
![]() |
dc45cbce37 | ||
![]() |
708d8f75c0 | ||
![]() |
bd37d90228 | ||
![]() |
b1ad691464 | ||
![]() |
f4e7baf31e | ||
![]() |
c0e60c41f2 | ||
![]() |
c8dad43e00 | ||
![]() |
a8f124704d | ||
![]() |
eed2816491 | ||
![]() |
a6334b3e35 | ||
![]() |
334beebfeb | ||
![]() |
13dad848bd | ||
![]() |
e518f4cef8 | ||
![]() |
c8fd5da2da | ||
![]() |
3a74729ecc | ||
![]() |
49c672ac4d | ||
![]() |
b570cb5b77 | ||
![]() |
97bf388471 | ||
![]() |
1a32aaea6f | ||
![]() |
4635883dec | ||
![]() |
3ba6db4a50 | ||
![]() |
2f1de25747 | ||
![]() |
f60fd42ac0 | ||
![]() |
ecc8f9c792 | ||
![]() |
e295dfdcf7 | ||
![]() |
fc42c25390 | ||
![]() |
27d5858e06 | ||
![]() |
e1ef732b60 | ||
![]() |
9840b95c21 | ||
![]() |
a6f8446d81 | ||
![]() |
c1c844c830 | ||
![]() |
389299afd1 | ||
![]() |
826543a291 | ||
![]() |
4ac83cfded | ||
![]() |
64c363ce53 | ||
![]() |
cca4347bf9 | ||
![]() |
3ae3d4926a | ||
![]() |
36025d6d9f | ||
![]() |
e171362e3e | ||
![]() |
3e0bf2ae15 | ||
![]() |
07aa9f4b8b | ||
![]() |
b2d9f3fc64 | ||
![]() |
5fb3e9167e | ||
![]() |
99c74b31be | ||
![]() |
ce5b13824e | ||
![]() |
c39170c42e | ||
![]() |
9e96824161 | ||
![]() |
fd19fbf300 | ||
![]() |
166469827f | ||
![]() |
a34ed538b6 | ||
![]() |
5f22d3e055 | ||
![]() |
fdd700f3e5 | ||
![]() |
adf930f126 | ||
![]() |
05f41928cd | ||
![]() |
2ee0829871 | ||
![]() |
743560825d | ||
![]() |
e3d84ac349 | ||
![]() |
266c832b30 | ||
![]() |
f5374a024e | ||
![]() |
4956d826fb | ||
![]() |
f5cc2af5d0 | ||
![]() |
84ca8e1f3e | ||
![]() |
5880d4a6ec | ||
![]() |
ae05dce958 | ||
![]() |
9ebe372a9a | ||
![]() |
e6e04cc5b3 | ||
![]() |
12352510fd | ||
![]() |
2b3d927937 | ||
![]() |
a8890740f5 | ||
![]() |
f60d7ee54b | ||
![]() |
896ca2ef6b | ||
![]() |
c036f6d529 | ||
![]() |
6f457c0c59 | ||
![]() |
13bf1b27b4 | ||
![]() |
f742bb1c47 | ||
![]() |
aa0b9e2db2 | ||
![]() |
c10076f7ed | ||
![]() |
bcd92499f2 | ||
![]() |
b2bb0d4f72 | ||
![]() |
e140481f14 | ||
![]() |
6b7b71b1f8 | ||
![]() |
186bd11463 | ||
![]() |
a0490d6687 | ||
![]() |
beef740ade | ||
![]() |
2ac7786a90 | ||
![]() |
a3fb5e910f | ||
![]() |
319afe86b5 | ||
![]() |
762ab66b86 | ||
![]() |
0c239a42de | ||
![]() |
e9322fba26 | ||
![]() |
39b6df27b3 | ||
![]() |
b1ee284e7f | ||
![]() |
e986332bf2 | ||
![]() |
48f9b27381 | ||
![]() |
42a6e0dd10 | ||
![]() |
d4798b02ac | ||
![]() |
963edfe8ab | ||
![]() |
53237f3ae0 | ||
![]() |
64da9281a4 | ||
![]() |
ab7fd9799d | ||
![]() |
f6bcc84251 | ||
![]() |
35dc3d9df9 | ||
![]() |
566714a75d | ||
![]() |
c92f30b122 | ||
![]() |
294ad094c4 | ||
![]() |
c1a0f520f9 | ||
![]() |
773c24b7fc | ||
![]() |
8f926c7ca9 | ||
![]() |
c562cbc2bb | ||
![]() |
3fbbb0865a | ||
![]() |
7d5f612a48 | ||
![]() |
4a5a36440b | ||
![]() |
43dd5cfea1 | ||
![]() |
7b5fec1842 | ||
![]() |
5762ded601 | ||
![]() |
a3abb86daa | ||
![]() |
4f5c656b05 | ||
![]() |
a31cddbe7b | ||
![]() |
b4ecd93f1c | ||
![]() |
1a702b08b9 | ||
![]() |
8c52dfb804 | ||
![]() |
0acc23e058 | ||
![]() |
cdd5f9b628 | ||
![]() |
4c9f5f4655 | ||
![]() |
b80ba13cb4 | ||
![]() |
8260bdc09c | ||
![]() |
24f856e02b | ||
![]() |
3aa619b928 | ||
![]() |
4cb5e98d94 | ||
![]() |
272910575e | ||
![]() |
a15a62f4bc | ||
![]() |
53cf11db8c | ||
![]() |
01052fbe47 | ||
![]() |
a5e1e075c7 | ||
![]() |
6be32ac688 | ||
![]() |
b362c0ef38 | ||
![]() |
bba9969e31 | ||
![]() |
007ba24809 | ||
![]() |
df21539311 | ||
![]() |
2592cb6019 | ||
![]() |
f7df17a7ed | ||
![]() |
62f42b72f8 | ||
![]() |
a1ba4fda6f | ||
![]() |
1c06b04c45 | ||
![]() |
2ee22fd374 | ||
![]() |
4c230d9e61 | ||
![]() |
727294fbbe | ||
![]() |
478c43969b | ||
![]() |
79b5303350 | ||
![]() |
ce4b742b25 | ||
![]() |
a9dc15bda5 | ||
![]() |
ba6387ff5c | ||
![]() |
8fa98508b7 | ||
![]() |
decdbaecf9 | ||
![]() |
6d87cf9be0 | ||
![]() |
94f434c4a6 | ||
![]() |
7ba867c30b | ||
![]() |
3424395e10 | ||
![]() |
926c7359a2 | ||
![]() |
ec0af99a2e | ||
![]() |
b4d948886c | ||
![]() |
4d8d79372a | ||
![]() |
04a589722c | ||
![]() |
d4a10e2873 | ||
![]() |
4998ad6c7e | ||
![]() |
a07ca5ff50 | ||
![]() |
f07e7571ab | ||
![]() |
834c16485c | ||
![]() |
04a4265ef3 | ||
![]() |
0ec473195d | ||
![]() |
0bf09256b0 | ||
![]() |
db8fd2c913 | ||
![]() |
dbe6e5b3d7 | ||
![]() |
cc81cd446b | ||
![]() |
439c7118f1 | ||
![]() |
d8154a5815 | ||
![]() |
4e3787bc0d | ||
![]() |
02e0955924 | ||
![]() |
3c6a170138 | ||
![]() |
a78950e822 | ||
![]() |
1ce1a94a35 | ||
![]() |
977b6d9f67 | ||
![]() |
b5e6dbd797 | ||
![]() |
833e6688f1 | ||
![]() |
bc22c9f84f | ||
![]() |
2149a7d116 | ||
![]() |
29175d2c17 | ||
![]() |
803454d5c8 | ||
![]() |
36cf32dc42 | ||
![]() |
657f4ab303 | ||
![]() |
c0c38022ea | ||
![]() |
93b66d26ff | ||
![]() |
ea6552615d | ||
![]() |
4bf3287fce | ||
![]() |
832c2034c2 | ||
![]() |
b0aa26e1f1 | ||
![]() |
e52baeb967 | ||
![]() |
8268eb9a83 | ||
![]() |
3cc458abd9 | ||
![]() |
337b4c4268 | ||
![]() |
001f8657f6 | ||
![]() |
ea884e7fa1 | ||
![]() |
9be2844c82 | ||
![]() |
1b1394cf5d | ||
![]() |
1eef930dbb | ||
![]() |
875c687e3f | ||
![]() |
1e175e74ed | ||
![]() |
75a46c365e | ||
![]() |
8e7b8825f5 | ||
![]() |
2ecbca303b | ||
![]() |
8195a4d616 | ||
![]() |
7ba40f925f | ||
![]() |
345cd1795f | ||
![]() |
959aaee045 | ||
![]() |
53477f0f59 | ||
![]() |
5716218f41 | ||
![]() |
9df6b9d5c0 | ||
![]() |
a0be47ab8b | ||
![]() |
ec46031d36 | ||
![]() |
55b84d166a | ||
![]() |
34ae8bacec | ||
![]() |
cb4e5ca0f7 | ||
![]() |
0ba45468c4 | ||
![]() |
710502784e | ||
![]() |
0275a8558d | ||
![]() |
58acc75cf6 | ||
![]() |
874ababb9f | ||
![]() |
3771e6b0cd | ||
![]() |
33eaefa966 | ||
![]() |
cd7e236d57 | ||
![]() |
54c0b7c7d5 | ||
![]() |
a2177daec2 | ||
![]() |
628386b453 | ||
![]() |
b222bfb3e0 | ||
![]() |
ab199d883d | ||
![]() |
356065d1ee | ||
![]() |
76e7c5623d | ||
![]() |
085fba050a | ||
![]() |
295334d3ac | ||
![]() |
36124ddca4 | ||
![]() |
bd6585765e | ||
![]() |
c325deb4ed | ||
![]() |
73bb0b10ee | ||
![]() |
72820b162c | ||
![]() |
89e5b8d057 | ||
![]() |
da4f53ebbb | ||
![]() |
8458553b74 | ||
![]() |
55ecc41d06 | ||
![]() |
28fcdf2cbb | ||
![]() |
24087679a8 | ||
![]() |
5ac6a8cb4a | ||
![]() |
668d85d14e | ||
![]() |
c11a3dc95c | ||
![]() |
56f57c20a2 | ||
![]() |
240d14779a | ||
![]() |
3550d1e61c | ||
![]() |
6513ad249c | ||
![]() |
50297b1880 | ||
![]() |
f189b78b9e | ||
![]() |
5c0250f495 | ||
![]() |
2093f726e9 | ||
![]() |
10efe3859d | ||
![]() |
6933bcf7bb | ||
![]() |
2ea046cd80 | ||
![]() |
f4097a372b | ||
![]() |
40b6de599c | ||
![]() |
87ea2a2bef | ||
![]() |
cc14a1c361 | ||
![]() |
bcdface60d | ||
![]() |
4dc9419d2e | ||
![]() |
d2bcac813e | ||
![]() |
080c37a7f6 | ||
![]() |
c1c6f55f8f | ||
![]() |
f9a3838db6 | ||
![]() |
1e61db104b | ||
![]() |
30a9c7718d | ||
![]() |
34b052b5d3 | ||
![]() |
aaa12853ad | ||
![]() |
b0ab55b0bf | ||
![]() |
d2f8496f4e | ||
![]() |
4c7e081e15 | ||
![]() |
1a69b16d36 | ||
![]() |
b5e8673e62 | ||
![]() |
264c6a50b6 | ||
![]() |
493642eb38 | ||
![]() |
28d42b9164 | ||
![]() |
42f29062ca | ||
![]() |
09392be069 | ||
![]() |
5529dab84e | ||
![]() |
60ca704a9e | ||
![]() |
c4377ed6c2 | ||
![]() |
7c4d5cee95 | ||
![]() |
7d283ed65f | ||
![]() |
bf1f941e50 | ||
![]() |
789fef34ba | ||
![]() |
1daf5a611c | ||
![]() |
6aed1db67e | ||
![]() |
cf68854770 | ||
![]() |
711392c73b | ||
![]() |
9573c32481 | ||
![]() |
a15f80f79d | ||
![]() |
23e7475f06 | ||
![]() |
1eb571b787 | ||
![]() |
dd3b716d85 | ||
![]() |
28649c07e3 | ||
![]() |
961e02be0d | ||
![]() |
a161491bfd | ||
![]() |
e0b4d1c1e4 | ||
![]() |
fd4aaab137 | ||
![]() |
42d14d5ca2 | ||
![]() |
d3ff482c9b | ||
![]() |
c9286624d4 | ||
![]() |
f682368eeb | ||
![]() |
4a5d033efb | ||
![]() |
343161b195 | ||
![]() |
bc576a9659 | ||
![]() |
19e407fcc4 | ||
![]() |
bc7327d004 | ||
![]() |
666fa1c797 | ||
![]() |
0eda4a7821 | ||
![]() |
862058fd2b | ||
![]() |
193d160bed | ||
![]() |
69e5bcd57d | ||
![]() |
efeddda328 | ||
![]() |
1ddd746862 | ||
![]() |
ff6938280e | ||
![]() |
1e4425b30f | ||
![]() |
b5d1d8cdad | ||
![]() |
029be5ccca | ||
![]() |
29c2d785b5 | ||
![]() |
abda8cfa32 | ||
![]() |
44e7d79d4c | ||
![]() |
9a1dc8ee0e | ||
![]() |
27879c3f01 | ||
![]() |
29096eb5d7 | ||
![]() |
a573baea03 | ||
![]() |
48ace3de57 | ||
![]() |
5af07c4531 | ||
![]() |
44e36feb09 | ||
![]() |
3395c84560 | ||
![]() |
2a7d996881 | ||
![]() |
94c2fc80d2 | ||
![]() |
738f943a68 | ||
![]() |
47e62a5681 | ||
![]() |
1ecbfd7590 | ||
![]() |
67c139a04b | ||
![]() |
31cc008249 | ||
![]() |
9cb026439d | ||
![]() |
e6f10176c6 | ||
![]() |
0917c79470 | ||
![]() |
597baa986d | ||
![]() |
75cc4b4843 | ||
![]() |
aac088d496 | ||
![]() |
a822e5bbc5 | ||
![]() |
c527249c21 | ||
![]() |
9ef798f534 | ||
![]() |
e69b99f089 | ||
![]() |
55b8079e86 | ||
![]() |
e272dbe9af | ||
![]() |
962f8354ac | ||
![]() |
20e4a960f7 | ||
![]() |
371db886b4 | ||
![]() |
3904ca38c0 | ||
![]() |
16527ceaf6 | ||
![]() |
feec3e8255 | ||
![]() |
82249cb50a | ||
![]() |
fad417e553 | ||
![]() |
5ba692f50c | ||
![]() |
f799db67eb | ||
![]() |
3e106a9dc5 | ||
![]() |
907e01e524 | ||
![]() |
b8ed23efa7 | ||
![]() |
2b3bbf7e67 | ||
![]() |
464fe627a3 | ||
![]() |
6a9e39c470 | ||
![]() |
7fec9a3cc6 | ||
![]() |
008f6ef462 | ||
![]() |
2440c108ca | ||
![]() |
430baad8a4 | ||
![]() |
51132e74b4 | ||
![]() |
a4f33e106a | ||
![]() |
baba3190e0 | ||
![]() |
47b13aa5ea | ||
![]() |
a0de3fc643 | ||
![]() |
9de3c582c0 | ||
![]() |
45cff2b51b | ||
![]() |
670397a73e | ||
![]() |
272eb37e9a | ||
![]() |
ca79e58ab9 | ||
![]() |
977c049875 | ||
![]() |
aefbc1c9bf | ||
![]() |
c37a2e61ed | ||
![]() |
7f6cd5e469 | ||
![]() |
f6d1f1985c | ||
![]() |
222c31b306 | ||
![]() |
e99185f011 | ||
![]() |
5c662f1230 | ||
![]() |
a65c7ee2fc | ||
![]() |
743c4f554d | ||
![]() |
838b2757eb | ||
![]() |
a92c9fc226 | ||
![]() |
ed052e0b0b | ||
![]() |
ae88d3054d | ||
![]() |
7bb8b9039c | ||
![]() |
3800b4b45c | ||
![]() |
cd498711bc | ||
![]() |
411b600e14 | ||
![]() |
0a0ad9a184 | ||
![]() |
234bead59e | ||
![]() |
76de310986 | ||
![]() |
817f050bcd | ||
![]() |
60ae685d1e | ||
![]() |
dc9670c439 | ||
![]() |
03c8079858 | ||
![]() |
0cfc527328 | ||
![]() |
f66a820e14 | ||
![]() |
4c7bdbb284 | ||
![]() |
435251ca41 | ||
![]() |
324a0dd38f | ||
![]() |
cc77d93918 | ||
![]() |
0ea7d8bd8c | ||
![]() |
2e6bea23ac | ||
![]() |
ca75dd0728 | ||
![]() |
849b217143 | ||
![]() |
9af6efba59 | ||
![]() |
079d6f06ef | ||
![]() |
9cf0757689 | ||
![]() |
b54c438948 | ||
![]() |
c3ff4bfdad | ||
![]() |
e103676b65 | ||
![]() |
17e395c2a8 | ||
![]() |
d50c1f39ab | ||
![]() |
ef6b25b3bb | ||
![]() |
9f35fa0fa3 | ||
![]() |
ff48996bbe | ||
![]() |
2fe4d97061 | ||
![]() |
eb38393cad | ||
![]() |
5d62e066e2 | ||
![]() |
e94219c5a3 | ||
![]() |
8ed9634adf | ||
![]() |
0aefa9599f | ||
![]() |
e279cf0575 | ||
![]() |
a3f0ef8e77 | ||
![]() |
8eba05ed4a | ||
![]() |
2f78155723 | ||
![]() |
6785221479 | ||
![]() |
9bc410dd3d | ||
![]() |
2491ab6bf9 | ||
![]() |
f615ed40cd | ||
![]() |
430f2cafc1 | ||
![]() |
0ad049da88 | ||
![]() |
2c7691567b | ||
![]() |
1d70d0fe94 | ||
![]() |
ac44f05811 | ||
![]() |
d99252f394 | ||
![]() |
b58c7ba7c5 | ||
![]() |
8c5acd1a0a | ||
![]() |
b9b1ebf18c | ||
![]() |
8ca132cef0 | ||
![]() |
a03bb90754 | ||
![]() |
d1c939f48a | ||
![]() |
21b11f1b48 | ||
![]() |
23c84a7803 | ||
![]() |
f9ab060403 | ||
![]() |
df7a5bf149 | ||
![]() |
e205969b11 | ||
![]() |
6bf19ecc34 | ||
![]() |
c4afa069df | ||
![]() |
1bfafdb44f | ||
![]() |
1ef5bd7076 | ||
![]() |
29176fa4f4 | ||
![]() |
958c95732b | ||
![]() |
44b0d4127c | ||
![]() |
1418ec2416 | ||
![]() |
b51978f51c | ||
![]() |
b07361580a | ||
![]() |
6ff45a754d | ||
![]() |
d1b5ebad7d | ||
![]() |
32d2df0f08 | ||
![]() |
f4ce813de9 | ||
![]() |
b44ac994d8 | ||
![]() |
333948814c | ||
![]() |
1a51ad6e01 | ||
![]() |
22a5c11f0d | ||
![]() |
51b22d1ad4 | ||
![]() |
bef5969580 | ||
![]() |
c6bf7bb9cd | ||
![]() |
2a84d92cbf | ||
![]() |
62de36b0da | ||
![]() |
03a9aaeff7 | ||
![]() |
45765e292d | ||
![]() |
6e28a26015 | ||
![]() |
9150bf720d | ||
![]() |
845864679c | ||
![]() |
b3b2149ebb | ||
![]() |
0886dca385 | ||
![]() |
53198ba4a7 | ||
![]() |
a9652ee1fd | ||
![]() |
75caf2f01c | ||
![]() |
65bab2666e | ||
![]() |
6d93ae399a | ||
![]() |
7239c2e31a | ||
![]() |
f269695d4a | ||
![]() |
443af5f760 | ||
![]() |
0e35350160 | ||
![]() |
10bf497cda | ||
![]() |
76eb629fc2 | ||
![]() |
91de738563 | ||
![]() |
43b7ef8110 | ||
![]() |
99ef0b8cb4 | ||
![]() |
0cf13f6393 | ||
![]() |
4a8acfd123 | ||
![]() |
abaffc1908 | ||
![]() |
ea61d5c1a5 | ||
![]() |
9a14931175 | ||
![]() |
165eee102a | ||
![]() |
6900c197cd | ||
![]() |
fe3c66a7c8 | ||
![]() |
0efb4da0ee | ||
![]() |
1d728475e3 | ||
![]() |
827057b9f1 | ||
![]() |
ed7920d61e | ||
![]() |
c0379c8e25 | ||
![]() |
00a0e64fdd | ||
![]() |
0dc60debea | ||
![]() |
c44ae5888c | ||
![]() |
b9495cd1bb | ||
![]() |
bfec381933 | ||
![]() |
2dddb8df69 | ||
![]() |
d30397e9c0 | ||
![]() |
d9597549fd | ||
![]() |
13512b4146 | ||
![]() |
49e546919a | ||
![]() |
586015c2ed | ||
![]() |
4a7e067d1a | ||
![]() |
9bc0b7f183 | ||
![]() |
cd4dfc9861 | ||
![]() |
1716452203 | ||
![]() |
09bdbc1224 | ||
![]() |
978b3a64c5 | ||
![]() |
651547ef20 | ||
![]() |
b4d95977d0 | ||
![]() |
5d8bb897db | ||
![]() |
84c8ecb372 | ||
![]() |
61abe5b948 | ||
![]() |
a5b573eaaa | ||
![]() |
cbb32f82eb | ||
![]() |
ca9334b2df | ||
![]() |
959ed7f866 | ||
![]() |
a5c0411be0 | ||
![]() |
32e1303742 | ||
![]() |
7263b6fe89 | ||
![]() |
46a4070f84 | ||
![]() |
c3c155a1ed | ||
![]() |
b067105660 | ||
![]() |
15ca18848e | ||
![]() |
67c9e2ead6 | ||
![]() |
3681177be4 | ||
![]() |
6eb814ef0b | ||
![]() |
bcc695234c | ||
![]() |
ad16a6fc1b | ||
![]() |
478b7eeb65 | ||
![]() |
151a153dc9 | ||
![]() |
ad131854ca | ||
![]() |
0bd0eb9e59 | ||
![]() |
54827cacb9 | ||
![]() |
e3a4a16507 | ||
![]() |
cf16fd0104 | ||
![]() |
21b00ac6ca | ||
![]() |
57e6f3080c | ||
![]() |
89744100ce | ||
![]() |
a718f9bbfd | ||
![]() |
e81bc4f044 | ||
![]() |
4dbacd79ae | ||
![]() |
ae74d54451 | ||
![]() |
dc316c5669 | ||
![]() |
e9f04256c9 | ||
![]() |
e1aabd70e8 | ||
![]() |
a9dc1b32e0 | ||
![]() |
01d847ae4e | ||
![]() |
61e2c3444a | ||
![]() |
5363b0f810 | ||
![]() |
f0e1a8823e | ||
![]() |
7be5937aa0 | ||
![]() |
8f43055b0e | ||
![]() |
953a81b299 | ||
![]() |
1d34ae7934 | ||
![]() |
2cabb2666b | ||
![]() |
0b59bb1a29 | ||
![]() |
c1e7d74b96 | ||
![]() |
cc262d6595 | ||
![]() |
61d43b118b | ||
![]() |
989d8181dd | ||
![]() |
cffc157d98 | ||
![]() |
2a70619577 | ||
![]() |
b91919bffa | ||
![]() |
fb7a4bf880 | ||
![]() |
4b41799a90 | ||
![]() |
123f39a21b | ||
![]() |
cadab12737 | ||
![]() |
742055c43b | ||
![]() |
fa73b41fa7 | ||
![]() |
a474eafe84 | ||
![]() |
442fcf921c | ||
![]() |
fb0923f3ab | ||
![]() |
5bb943f845 | ||
![]() |
a3109953d0 | ||
![]() |
ff266c8c79 | ||
![]() |
ef2e02098d | ||
![]() |
93598d3a51 | ||
![]() |
53aebcfb1e | ||
![]() |
bb2467d2ac | ||
![]() |
05c063b61d | ||
![]() |
ef1d1303f4 | ||
![]() |
b84ab656d8 | ||
![]() |
edd4b477f8 | ||
![]() |
04fcb33d7e | ||
![]() |
f31d2486c9 | ||
![]() |
7dea682713 | ||
![]() |
7955ddceb2 | ||
![]() |
8a6b254799 | ||
![]() |
94562cb5cf | ||
![]() |
b064c124e7 | ||
![]() |
c7e64f40f9 | ||
![]() |
0f254dca13 | ||
![]() |
e0f2ff36af | ||
![]() |
3546e7b51e | ||
![]() |
5e7c3ed46a | ||
![]() |
13ec1aafa0 | ||
![]() |
f521bce9e6 | ||
![]() |
c78209604c | ||
![]() |
8fe4cfecb6 | ||
![]() |
a5a2df4956 | ||
![]() |
2fa5e4679f | ||
![]() |
57af984e68 | ||
![]() |
442e840a53 | ||
![]() |
3c33f7d294 | ||
![]() |
42a66ad49e | ||
![]() |
2d1d70b3b6 | ||
![]() |
c9217a419a | ||
![]() |
a180395832 | ||
![]() |
3dfcc6b0be | ||
![]() |
cb1df5217e | ||
![]() |
24ef80351c | ||
![]() |
bb878a1ccf | ||
![]() |
4daea7d7e6 | ||
![]() |
3b20747192 | ||
![]() |
403e30feba | ||
![]() |
f58c73b7f1 | ||
![]() |
2a8477cbda | ||
![]() |
f5bee7b691 | ||
![]() |
8c077a7373 | ||
![]() |
4e07b51460 | ||
![]() |
44294e1a88 | ||
![]() |
25a0a68cde | ||
![]() |
3e259021d0 | ||
![]() |
f760a9d0c2 | ||
![]() |
f69facc842 | ||
![]() |
e17638bc06 | ||
![]() |
399c0d337a | ||
![]() |
856eb479e4 | ||
![]() |
1c7de1d668 | ||
![]() |
8a8f24f93e | ||
![]() |
e76dba0f84 | ||
![]() |
aababe1a87 | ||
![]() |
436b0624e7 | ||
![]() |
0a37d1c15c | ||
![]() |
793269731d | ||
![]() |
b69a4fe8b5 | ||
![]() |
665d84f40a | ||
![]() |
4734b390a5 | ||
![]() |
50d0721c39 | ||
![]() |
9079f15f52 | ||
![]() |
60b460d594 | ||
![]() |
98f42d9b3b | ||
![]() |
23adcb544b | ||
![]() |
e6b24d2e3c | ||
![]() |
ea3e736a14 | ||
![]() |
a5c39b829a | ||
![]() |
1ec333ee5a | ||
![]() |
bbae93aa16 | ||
![]() |
be1dcb7264 | ||
![]() |
4a1e6dcc32 | ||
![]() |
f644a4ea78 | ||
![]() |
85b7405963 | ||
![]() |
c854f436bf | ||
![]() |
e5be8b7f67 | ||
![]() |
906ae730e9 | ||
![]() |
92df7747b2 | ||
![]() |
0ee8f5efe3 | ||
![]() |
4b5b0b065d | ||
![]() |
15cf8d2a6d | ||
![]() |
ef0ba9483f | ||
![]() |
70500cf21e | ||
![]() |
a7da6cf172 | ||
![]() |
ae76ae4025 | ||
![]() |
9614ec4c6a | ||
![]() |
c4e90b810d | ||
![]() |
887ce3377e | ||
![]() |
6ef47249ab | ||
![]() |
3a0df56605 | ||
![]() |
98cdee7f03 | ||
![]() |
b3e2a6a860 | ||
![]() |
55410f026b | ||
![]() |
f2611f64ac | ||
![]() |
d788bd8323 | ||
![]() |
9eb108f13e | ||
![]() |
eebd64bedb | ||
![]() |
21504f1329 | ||
![]() |
ff6bae936d | ||
![]() |
62523c815e | ||
![]() |
0f5465c5da | ||
![]() |
e4cba70008 | ||
![]() |
692b993eee | ||
![]() |
35e3a479cd | ||
![]() |
bb7ff27d04 | ||
![]() |
0acc5e33b3 | ||
![]() |
cb5187fd8d | ||
![]() |
160c6e6554 | ||
![]() |
a173179b03 | ||
![]() |
e73497e4b7 | ||
![]() |
835ef01a70 | ||
![]() |
a1335aecfb | ||
![]() |
c553312fd5 | ||
![]() |
3adc7ca22a | ||
![]() |
441e603bc0 | ||
![]() |
7511df61b3 | ||
![]() |
91d3d2ad1f | ||
![]() |
6692b618ea | ||
![]() |
2052149dc1 | ||
![]() |
7b8237afae | ||
![]() |
859a984ec8 | ||
![]() |
89932b325d | ||
![]() |
dac85757b3 | ||
![]() |
3b0cec9db6 | ||
![]() |
17749bb14a | ||
![]() |
c56dd4172e | ||
![]() |
d2335485f2 | ||
![]() |
cf69dd644a | ||
![]() |
8df6af62d7 | ||
![]() |
3c3bb70b01 | ||
![]() |
d8a4eaf026 | ||
![]() |
2402010d24 | ||
![]() |
16c804106a | ||
![]() |
b1ef9361f3 | ||
![]() |
766a26128d | ||
![]() |
5b2dce6cf6 | ||
![]() |
bee9be534c | ||
![]() |
4b49331d97 | ||
![]() |
f9513ca802 | ||
![]() |
3de13a4d9e | ||
![]() |
8a7df954e5 | ||
![]() |
3706b53e65 | ||
![]() |
8a8aaf3297 | ||
![]() |
41a5639711 | ||
![]() |
5d8f9f1a5a | ||
![]() |
5124cd4b77 | ||
![]() |
0cbf66996f | ||
![]() |
e922fdc5d0 | ||
![]() |
0addbaa9a8 | ||
![]() |
8176fb7bad | ||
![]() |
baae3592d3 | ||
![]() |
6a40e18193 | ||
![]() |
2cdb6b811f | ||
![]() |
8a8aa1337b | ||
![]() |
3fe5647a15 | ||
![]() |
fec1245811 | ||
![]() |
ccab6eb7c4 | ||
![]() |
c9f6e2e257 | ||
![]() |
f0d3a4e4b7 | ||
![]() |
41295e0c4d | ||
![]() |
2abd0265c8 | ||
![]() |
1e09ccb4d9 | ||
![]() |
11e1d04dd1 | ||
![]() |
f140f5f14b | ||
![]() |
5898534c23 | ||
![]() |
7836336689 | ||
![]() |
f96865c2cb | ||
![]() |
e475893fd7 | ||
![]() |
75a37adcd1 | ||
![]() |
c3b1070b83 | ||
![]() |
339ca7accf | ||
![]() |
0b02e8116c | ||
![]() |
8f973661f4 | ||
![]() |
c5a73a5c19 | ||
![]() |
6a90340b14 | ||
![]() |
46abbfe224 | ||
![]() |
145d4e4bd5 | ||
![]() |
b3ba79a3ba | ||
![]() |
c69db035ee | ||
![]() |
60a7eaf2bb | ||
![]() |
3f43567c8f | ||
![]() |
e690f6d487 | ||
![]() |
3d4b4e04c5 | ||
![]() |
62dd8f35c0 | ||
![]() |
1468dfd6b6 | ||
![]() |
40e92721c1 | ||
![]() |
204e940dcb | ||
![]() |
98aa9bd3fe | ||
![]() |
041531e96d | ||
![]() |
c2a188f7fe | ||
![]() |
1a1d37a2d0 | ||
![]() |
214649ec20 | ||
![]() |
e3866eeb29 | ||
![]() |
20db216275 | ||
![]() |
f404fe0570 | ||
![]() |
bef4361736 | ||
![]() |
aa991b62f4 | ||
![]() |
8dfe0f4373 | ||
![]() |
ffedb79670 | ||
![]() |
0e23935455 | ||
![]() |
4f62320e7b | ||
![]() |
aee3bd3a80 | ||
![]() |
c992b89b2f | ||
![]() |
fc5c9647d8 | ||
![]() |
3a238e9d4b | ||
![]() |
9d9fea49ca | ||
![]() |
e21131d67e | ||
![]() |
1f02d0f6d0 | ||
![]() |
830fde8007 | ||
![]() |
c44ce77e95 | ||
![]() |
ab318ef99e | ||
![]() |
c86c2661af | ||
![]() |
dabb222511 | ||
![]() |
ef13b3a36c | ||
![]() |
6fb9081394 | ||
![]() |
1ba38b3902 | ||
![]() |
dc06a132bc | ||
![]() |
644b4f88ac | ||
![]() |
c97197b61a | ||
![]() |
3e97d29bcf | ||
![]() |
a5ea214553 | ||
![]() |
91c6ae229e | ||
![]() |
e18f4c843a | ||
![]() |
0f103d5853 | ||
![]() |
56f10e238b | ||
![]() |
5baa2e9069 | ||
![]() |
7bf83371d5 | ||
![]() |
36c575023e | ||
![]() |
7eadc74f6c | ||
![]() |
3ad06c406c | ||
![]() |
c68e37a8c4 | ||
![]() |
e66496eae7 | ||
![]() |
e6b951c62a | ||
![]() |
5279226f36 | ||
![]() |
31b552ab51 | ||
![]() |
f5e53cd60f | ||
![]() |
4a48f59d27 | ||
![]() |
bc2c63bf1f | ||
![]() |
b56a757f2e | ||
![]() |
4692ed4b4a | ||
![]() |
615bbcae74 | ||
![]() |
7737c6aee1 | ||
![]() |
f7c0499158 | ||
![]() |
9ebcefee00 | ||
![]() |
b18b5c4f43 | ||
![]() |
4752b0772f | ||
![]() |
957e319649 | ||
![]() |
a8978a0d4d | ||
![]() |
10712c5ec0 | ||
![]() |
83c39f57f0 | ||
![]() |
173757cfa2 | ||
![]() |
c6be73dba2 | ||
![]() |
ccf293906a | ||
![]() |
0f4c0b95e2 | ||
![]() |
82973e7608 | ||
![]() |
c011bccc45 | ||
![]() |
8473caf5a6 | ||
![]() |
85b038525b | ||
![]() |
51a5c3c664 | ||
![]() |
d6cda9df0a | ||
![]() |
ca7d09d1cb | ||
![]() |
4ab478c49c | ||
![]() |
1a1c1fd0da | ||
![]() |
370951ab67 | ||
![]() |
a0632a572a | ||
![]() |
10601e7760 | ||
![]() |
088ce9c2ad | ||
![]() |
e1a69b97db | ||
![]() |
a2fd45bb95 | ||
![]() |
01ddd8eaa8 | ||
![]() |
22fa57b82c | ||
![]() |
92a51ca546 | ||
![]() |
6a9234e634 | ||
![]() |
e8d062a95a | ||
![]() |
3394d64f6c | ||
![]() |
0fd5a277ed | ||
![]() |
8eef2818fa | ||
![]() |
a15703d5af | ||
![]() |
34d8165edd | ||
![]() |
1759add2b6 | ||
![]() |
dd80f1b997 | ||
![]() |
90ff602ecd | ||
![]() |
0099ff1321 |
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -11,8 +11,9 @@
|
||||
*.bat text eol=crlf
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
chromeos/** binary
|
||||
tools/** binary
|
||||
*.jar binary
|
||||
*.exe binary
|
||||
*.apk binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@@ -2,9 +2,11 @@ out
|
||||
*.zip
|
||||
*.jks
|
||||
*.apk
|
||||
config.prop
|
||||
update.sh
|
||||
|
||||
# Built binaries
|
||||
ziptools/zipadjust
|
||||
native/out
|
||||
|
||||
# Android Studio / Gradle
|
||||
*.iml
|
||||
@@ -13,4 +15,3 @@ ziptools/zipadjust
|
||||
/.idea
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
|
42
.gitmodules
vendored
42
.gitmodules
vendored
@@ -1,27 +1,27 @@
|
||||
[submodule "jni/selinux"]
|
||||
path = core/jni/external/selinux
|
||||
[submodule "selinux"]
|
||||
path = native/jni/external/selinux
|
||||
url = https://github.com/topjohnwu/selinux.git
|
||||
[submodule "jni/su"]
|
||||
path = core/jni/su
|
||||
url = https://github.com/topjohnwu/MagiskSU.git
|
||||
[submodule "jni/magiskpolicy"]
|
||||
path = core/jni/magiskpolicy
|
||||
url = https://github.com/topjohnwu/magiskpolicy.git
|
||||
[submodule "MagiskManager"]
|
||||
path = app
|
||||
url = https://github.com/topjohnwu/MagiskManager.git
|
||||
[submodule "jni/busybox"]
|
||||
path = core/jni/external/busybox
|
||||
[submodule "busybox"]
|
||||
path = native/jni/external/busybox
|
||||
url = https://github.com/topjohnwu/ndk-busybox.git
|
||||
[submodule "jni/external/dtc"]
|
||||
path = core/jni/external/dtc
|
||||
[submodule "dtc"]
|
||||
path = native/jni/external/dtc
|
||||
url = https://github.com/dgibson/dtc
|
||||
[submodule "jni/external/lz4"]
|
||||
path = core/jni/external/lz4
|
||||
[submodule "lz4"]
|
||||
path = native/jni/external/lz4
|
||||
url = https://github.com/lz4/lz4.git
|
||||
[submodule "jni/external/bzip2"]
|
||||
path = core/jni/external/bzip2
|
||||
[submodule "bzip2"]
|
||||
path = native/jni/external/bzip2
|
||||
url = https://github.com/nemequ/bzip2.git
|
||||
[submodule "jni/external/xz"]
|
||||
path = core/jni/external/xz
|
||||
[submodule "xz"]
|
||||
path = native/jni/external/xz
|
||||
url = https://github.com/xz-mirror/xz.git
|
||||
[submodule "nanopb"]
|
||||
path = native/jni/external/nanopb
|
||||
url = https://github.com/nanopb/nanopb.git
|
||||
[submodule "mincrypt"]
|
||||
path = native/jni/external/mincrypt
|
||||
url = https://github.com/topjohnwu/mincrypt.git
|
||||
[submodule "termux-elf-cleaner"]
|
||||
path = tools/termux-elf-cleaner
|
||||
url = https://github.com/termux/termux-elf-cleaner.git
|
||||
|
132
README.MD
132
README.MD
@@ -1,81 +1,75 @@
|
||||
# Magisk
|
||||
|
||||
## How to build 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)
|
||||
|
||||
#### Building has been tested on 3 major platforms: macOS, Ubuntu, Windows 10
|
||||
## Introduction
|
||||
|
||||
### Environment Requirements
|
||||
Magisk is a suite of open source tools for customizing Android, supporting devices higher than Android 4.2 (API 17). It covers the fundamental parts for Android customization: root, boot scripts, SELinux patches, AVB2.0 / dm-verity / forceencrypt removals etc.
|
||||
|
||||
1. A 64-bit machine: `cmake` for Android is only available in 64-bit
|
||||
2. Python 3.5+: run `build.py` script
|
||||
3. Java Development Kit (JDK) 8: Compile Magisk Manager and sign zips
|
||||
4. Latest Android SDK: `ANDROID_HOME` environment variable should point to the Android SDK folder
|
||||
5. Android NDK: Install NDK along with SDK (`$ANDROID_HOME/ndk-bundle`), or specify custom path `ANDROID_NDK`
|
||||
6. (Windows Only) Python package Colorama: Install with `pip install colorama`, used for ANSI color codes
|
||||
7. (Unix only) C compiler: Build `zipadjust`. Windows users can use the pre-built `zipadjust.exe`
|
||||
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).
|
||||
|
||||
### Instructions and Notes
|
||||
1. Magisk can be built with the latest NDK (r16 as of writing), however binaries released officially will be built with NDK r10e due to ELF incompatibilities with the binaries built from the newer NDKs.
|
||||
2. The easiest way to setup the environment is by importing this folder as an Android Studio project. The IDE will download required components and construct the environment for you. You still have to set the `ANDROID_HOME` environment variable to point to the SDK path.
|
||||
3. 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`
|
||||
4. Build everything with `build.py`, don't directly call `gradlew` or `ndk-build`, since most requires special setup / dependencies.
|
||||
5. By default, `build.py` will build binaries and Magisk Manager in debug mode. If you want to build Magisk Manager in release mode (through the flag `--release`), you will need to place a Java Keystore file at `release_signature.jks` to sign Magisk Manager's APK. For more information, check out [Google's Official Documentation](https://developer.android.com/studio/publish/app-signing.html#signing-manually).
|
||||
## 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 are 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](https://github.com/topjohnwu/Magisk/issues) or directly in the thread.
|
||||
|
||||
## Building Environment Requirements
|
||||
|
||||
- Python 3: run `build.py` script
|
||||
- Java Development Kit (JDK) 8: Compile Magisk Manager and sign zips
|
||||
- Latest Android SDK: set `ANDROID_HOME` environment variable to the path to Android SDK
|
||||
- Android NDK: Install NDK along with SDK (`$ANDROID_HOME/ndk-bundle`), or optionally specify a custom path `ANDROID_NDK_HOME`
|
||||
- (Windows Only) Python package Colorama: Install with `pip install colorama`, used for ANSI color codes
|
||||
|
||||
## Building Notes and Instructions
|
||||
|
||||
- Clone sources with submodules: `git clone --recurse-submodules https://github.com/topjohnwu/Magisk.git`
|
||||
- Building is supported on macOS, Linux, and Windows. Official releases are built and tested with [FrankeNDK](https://github.com/topjohnwu/FrankeNDK); point `ANDROID_NDK_HOME` to FrankeNDK if you want to use it for compiling.
|
||||
- Set configurations in `config.prop`. A sample file `config.prop.sample` is provided as an example.
|
||||
- Run `build.py` with argument `-h` to see the built-in help message. The `-h` option also works for each supported actions, e.g. `./build.py binary -h`
|
||||
- By default, `build.py` build binaries and Magisk Manager in debug mode. If you want to build Magisk Manager in release mode (via the `-r, --release` flag), you need a Java Keystore file `release-key.jks` (only `JKS` format is supported) to sign APKs and zips. For more information, check out [Google's Official Documentation](https://developer.android.com/studio/publish/app-signing.html#signing-manually).
|
||||
|
||||
## Translations
|
||||
|
||||
Default string resources for Magisk Manager are scattered throughout
|
||||
|
||||
- `app/src/main/res/values/strings.xml`
|
||||
- `stub/src/main/res/values/strings.xml`
|
||||
- `shared/src/main/res/values/strings.xml`
|
||||
|
||||
Translate each and place them in the respective locations (`<module>/src/main/res/values-<lang>/strings.xml`).
|
||||
|
||||
## Signature Verification
|
||||
|
||||
Official release zips and APKs are signed with my personal private key. You can verify the key certificate to make sure the binaries you downloaded are not manipulated in anyway.
|
||||
|
||||
``` bash
|
||||
# Use the keytool command from JDK to print certificates
|
||||
keytool -printcert -jarfile <APK or Magisk zip>
|
||||
|
||||
# The output should contain the following signature
|
||||
Owner: CN=John Wu, L=Taipei, C=TW
|
||||
Issuer: CN=John Wu, L=Taipei, C=TW
|
||||
Serial number: 50514879
|
||||
Valid from: Sun Aug 14 13:23:44 EDT 2016 until: Tue Jul 21 13:23:44 EDT 2116
|
||||
Certificate fingerprints:
|
||||
MD5: CE:DA:68:C1:E1:74:71:0A:EF:58:89:7D:AE:6E:AB:4F
|
||||
SHA1: DC:0F:2B:61:CB:D7:E9:D3:DB:BE:06:0B:2B:87:0D:46:BB:06:02:11
|
||||
SHA256: B4:CB:83:B4:DA:D9:9F:99:7D:BE:87:2F:01:3A:A1:6C:14:EE:C4:1D:16:70:21:F3:71:F7:E1:33:0F:27:3E:E6
|
||||
Signature algorithm name: SHA256withRSA
|
||||
Version: 3
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
```
|
||||
Magisk, including all git submodules are free software:
|
||||
you can redistribute it and/or modify it under the terms of the
|
||||
GNU General Public License as published by the Free Software Foundation,
|
||||
either version 3 of the License, or (at your option) any later version.
|
||||
Magisk, including all git submodules are free software:
|
||||
you can redistribute it and/or modify it under the terms of the
|
||||
GNU General Public License as published by the Free Software Foundation,
|
||||
either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
||||
**MagiskManager** (`app`)
|
||||
|
||||
* Copyright 2016-2018, John Wu (@topjohnwu)
|
||||
* All contributors and translators on Github
|
||||
|
||||
**MagiskSU** (`core/jni/su`)
|
||||
|
||||
* Copyright 2016-2018, John Wu (@topjohnwu)
|
||||
* Copyright 2015, Pierre-Hugues Husson (phh@phh.me)
|
||||
* Copyright 2013, Koushik Dutta (@koush)
|
||||
* Copyright 2010, Adam Shanks (@ChainsDD)
|
||||
* Copyright 2008, Zinx Verituse (@zinxv)
|
||||
|
||||
**MagiskPolicy** (`core/jni/magiskpolicy`)
|
||||
|
||||
* Copyright 2016-2018, John Wu (@topjohnwu)
|
||||
* Copyright 2015, Pierre-Hugues Husson (phh@phh.me)
|
||||
* Copyright 2015, Joshua Brindle (@joshua_brindle)
|
||||
|
||||
**MagiskHide** (`core/jni/magiskhide`)
|
||||
|
||||
* Copyright 2016-2018, John Wu (@topjohnwu)
|
||||
* Copyright 2016, Pierre-Hugues Husson (phh@phh.me)
|
||||
|
||||
**resetprop** (`core/jni/resetprop`)
|
||||
|
||||
* Copyright 2016-2018 John Wu (@topjohnwu)
|
||||
* Copyright 2016 nkk71 (nkk71x@gmail.com)
|
||||
|
||||
**External Dependencies** (`core/jni/external`)
|
||||
|
||||
* Makefile for busybox, generated by [ndk-busybox-kitchen](https://github.com/topjohnwu/ndk-busybox-kitchen)
|
||||
* Each dependencies has its own license/copyright information in each subdirectory.
|
||||
All of them are either GPL or GPL compatible.
|
||||
|
||||
**Others Not Mentioned**
|
||||
|
||||
* Copyright 2016-2018, John Wu (@topjohnwu)
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
1
app
1
app
Submodule app deleted from 759e905c3c
11
app/.gitignore
vendored
Normal file
11
app/.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
.idea/
|
||||
/build
|
||||
app/release
|
||||
*.hprof
|
||||
.externalNativeBuild/
|
||||
public.certificate.x509.pem
|
||||
private.key.pk8
|
||||
*.apk
|
133
app/build.gradle
Normal file
133
app/build.gradle
Normal file
@@ -0,0 +1,133 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
kapt {
|
||||
correctErrorTypes = true
|
||||
useBuildCache = true
|
||||
mapDiagnosticLocations = true
|
||||
javacOptions {
|
||||
option("-Xmaxerrs", 1000)
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
applicationId 'com.topjohnwu.magisk'
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
multiDexEnabled true
|
||||
versionName props['appVersion']
|
||||
versionCode props['appVersionCode'] as Integer
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
|
||||
'proguard-rules.pro', 'proguard-kotlin.pro'
|
||||
}
|
||||
}
|
||||
|
||||
dataBinding {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude '/META-INF/*.version'
|
||||
exclude '/META-INF/*.kotlin_module'
|
||||
exclude '/META-INF/rxkotlin.properties'
|
||||
exclude '/androidsupportmultidexversion.txt'
|
||||
exclude '/org/bouncycastle/**'
|
||||
exclude '/kotlin/**'
|
||||
exclude '/kotlinx/**'
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
implementation project(':shared')
|
||||
implementation project(':signing')
|
||||
|
||||
implementation 'com.github.topjohnwu:jtar:1.0.0'
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
implementation 'com.ncapdevi:frag-nav:3.2.0'
|
||||
implementation 'com.github.pwittchen:reactivenetwork-rx2:3.0.6'
|
||||
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.2.13'
|
||||
implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:${vKotlin}"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${vKotlin}"
|
||||
|
||||
def vBAdapt = '3.1.1'
|
||||
def bindingAdapter = 'me.tatarka.bindingcollectionadapter2:bindingcollectionadapter'
|
||||
implementation "${bindingAdapter}:${vBAdapt}"
|
||||
implementation "${bindingAdapter}-recyclerview:${vBAdapt}"
|
||||
|
||||
def vMarkwon = '4.1.1'
|
||||
implementation "io.noties.markwon:core:${vMarkwon}"
|
||||
implementation "io.noties.markwon:html:${vMarkwon}"
|
||||
implementation "io.noties.markwon:image:${vMarkwon}"
|
||||
implementation 'com.caverock:androidsvg:1.4'
|
||||
|
||||
def vLibsu = '2.5.1'
|
||||
implementation "com.github.topjohnwu.libsu:core:${vLibsu}"
|
||||
implementation "com.github.topjohnwu.libsu:io:${vLibsu}"
|
||||
|
||||
def vKoin = "2.0.1"
|
||||
implementation "org.koin:koin-core:${vKoin}"
|
||||
implementation "org.koin:koin-android:${vKoin}"
|
||||
implementation "org.koin:koin-androidx-viewmodel:${vKoin}"
|
||||
|
||||
def vRetrofit = '2.6.2'
|
||||
implementation "com.squareup.retrofit2:retrofit:${vRetrofit}"
|
||||
implementation "com.squareup.retrofit2:converter-moshi:${vRetrofit}"
|
||||
implementation "com.squareup.retrofit2:converter-scalars:${vRetrofit}"
|
||||
implementation "com.squareup.retrofit2:adapter-rxjava2:${vRetrofit}"
|
||||
|
||||
def vOkHttp = '3.12.6'
|
||||
implementation "com.squareup.okhttp3:okhttp:${vOkHttp}"
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:${vOkHttp}"
|
||||
|
||||
def vMoshi = "1.8.0"
|
||||
implementation "com.squareup.moshi:moshi:${vMoshi}"
|
||||
|
||||
def vKotshi = "2.0.1"
|
||||
implementation "se.ansman.kotshi:api:${vKotshi}"
|
||||
kapt "se.ansman.kotshi:compiler:${vKotshi}"
|
||||
|
||||
modules {
|
||||
module('androidx.room:room-runtime') {
|
||||
replacedBy('com.github.topjohnwu:room-runtime')
|
||||
}
|
||||
}
|
||||
def vRoom = "2.2.0"
|
||||
implementation "com.github.topjohnwu:room-runtime:${vRoom}"
|
||||
kapt "androidx.room:room-compiler:${vRoom}"
|
||||
|
||||
def vNav = "2.1.0"
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$vNav"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$vNav"
|
||||
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha03'
|
||||
implementation 'androidx.preference:preference:1.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0-beta05'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.work:work-runtime:2.2.0'
|
||||
implementation 'androidx.transition:transition:1.2.0'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.core:core-ktx:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.1.0-beta01'
|
||||
}
|
20
app/proguard-kotlin.pro
Normal file
20
app/proguard-kotlin.pro
Normal file
@@ -0,0 +1,20 @@
|
||||
## So every class is case insensitive to avoid some bizare problems
|
||||
-dontusemixedcaseclassnames
|
||||
|
||||
## If reflection issues come up uncomment this, that should temporarily fix it
|
||||
#-keep class kotlin.** { *; }
|
||||
#-keep class kotlin.Metadata { *; }
|
||||
#-keepclassmembers class kotlin.Metadata {
|
||||
# public <methods>;
|
||||
#}
|
||||
|
||||
## Never warn about Kotlin, it should work as-is
|
||||
-dontwarn kotlin.**
|
||||
|
||||
## Removes runtime null checks - doesn't really matter if it crashes on kotlin or java NPE
|
||||
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
||||
static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
|
||||
}
|
||||
|
||||
## Useless option for dex
|
||||
-dontpreverify
|
47
app/proguard-rules.pro
vendored
Normal file
47
app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /Users/topjohnwu/Library/Android/sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# 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 *;
|
||||
#}
|
||||
|
||||
# Snet
|
||||
-keepclassmembers class com.topjohnwu.magisk.utils.SafetyNetHelper { *; }
|
||||
-keep,allowobfuscation interface com.topjohnwu.magisk.utils.SafetyNetHelper$Callback
|
||||
-keepclassmembers class * implements com.topjohnwu.magisk.utils.SafetyNetHelper$Callback {
|
||||
void onResponse(int);
|
||||
}
|
||||
|
||||
# Keep all fragment constructors
|
||||
-keepclassmembers class * extends androidx.fragment.app.Fragment {
|
||||
public <init>(...);
|
||||
}
|
||||
|
||||
# DelegateWorker
|
||||
-keep,allowobfuscation class * extends com.topjohnwu.magisk.base.DelegateWorker
|
||||
|
||||
# BootSigner
|
||||
-keepclassmembers class com.topjohnwu.signing.BootSigner { *; }
|
||||
|
||||
# Strip logging
|
||||
-assumenosideeffects class timber.log.Timber.Tree { *; }
|
||||
|
||||
# Excessive obfuscation
|
||||
-repackageclasses 'a'
|
||||
-allowaccessmodification
|
||||
|
||||
# QOL
|
||||
-dontnote **
|
||||
-dontwarn com.caverock.androidsvg.**
|
||||
-dontwarn ru.noties.markwon.**
|
79
app/src/main/AndroidManifest.xml
Normal file
79
app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,79 @@
|
||||
<?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.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
|
||||
<application
|
||||
android:name="a.e"
|
||||
android:allowBackup="true"
|
||||
android:theme="@style/MagiskTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="UnusedAttribute,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.f"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:screenOrientation="nosensor"
|
||||
android:theme="@style/MagiskTheme.Flashing" />
|
||||
|
||||
<!-- Superuser -->
|
||||
|
||||
<activity
|
||||
android:name="a.m"
|
||||
android:directBootAware="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false"
|
||||
android:theme="@style/MagiskTheme.SU" />
|
||||
|
||||
<!-- Receiver -->
|
||||
|
||||
<receiver
|
||||
android:name="a.h"
|
||||
android:directBootAware="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
||||
</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>
|
||||
|
||||
<!-- Service -->
|
||||
|
||||
<service android:name="a.j"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- Hardcode GMS version -->
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.version"
|
||||
android:value="12451000" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
13
app/src/main/java/a/a.java
Normal file
13
app/src/main/java/a/a.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.utils.PatchAPK;
|
||||
import com.topjohnwu.signing.BootSigner;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
@Keep
|
||||
public class a extends BootSigner {
|
||||
public static boolean patchAPK(String in, String out, String pkg) {
|
||||
return PatchAPK.patch(in, out, pkg);
|
||||
}
|
||||
}
|
7
app/src/main/java/a/b.java
Normal file
7
app/src/main/java/a/b.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.ui.MainActivity;
|
||||
|
||||
public class b extends MainActivity {
|
||||
/* stub */
|
||||
}
|
7
app/src/main/java/a/c.java
Normal file
7
app/src/main/java/a/c.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.ui.SplashActivity;
|
||||
|
||||
public class c extends SplashActivity {
|
||||
/* stub */
|
||||
}
|
7
app/src/main/java/a/e.java
Normal file
7
app/src/main/java/a/e.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.App;
|
||||
|
||||
public class e extends App {
|
||||
/* stub */
|
||||
}
|
7
app/src/main/java/a/f.java
Normal file
7
app/src/main/java/a/f.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.ui.flash.FlashActivity;
|
||||
|
||||
public class f extends FlashActivity {
|
||||
/* stub */
|
||||
}
|
15
app/src/main/java/a/g.java
Normal file
15
app/src/main/java/a/g.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package a;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.topjohnwu.magisk.model.update.UpdateCheckService;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
public class g extends w<UpdateCheckService> {
|
||||
/* Stub */
|
||||
public g(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
||||
super(context, workerParams);
|
||||
}
|
||||
}
|
7
app/src/main/java/a/h.java
Normal file
7
app/src/main/java/a/h.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.model.receiver.GeneralReceiver;
|
||||
|
||||
public class h extends GeneralReceiver {
|
||||
/* stub */
|
||||
}
|
7
app/src/main/java/a/j.java
Normal file
7
app/src/main/java/a/j.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.model.download.DownloadService;
|
||||
|
||||
public class j extends DownloadService {
|
||||
/* stub */
|
||||
}
|
7
app/src/main/java/a/m.java
Normal file
7
app/src/main/java/a/m.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity;
|
||||
|
||||
public class m extends SuRequestActivity {
|
||||
/* stub */
|
||||
}
|
42
app/src/main/java/a/w.java
Normal file
42
app/src/main/java/a/w.java
Normal file
@@ -0,0 +1,42 @@
|
||||
package a;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
import com.topjohnwu.magisk.base.DelegateWorker;
|
||||
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
|
||||
public abstract class w<T extends DelegateWorker> extends Worker {
|
||||
|
||||
/* Wrapper class to workaround Proguard -keep class * extends Worker */
|
||||
|
||||
private T base;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
w(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
||||
super(context, workerParams);
|
||||
try {
|
||||
base = ((Class<T>) ((ParameterizedType) getClass().getGenericSuperclass())
|
||||
.getActualTypeArguments()[0]).newInstance();
|
||||
base.attachWorker(this);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Result doWork() {
|
||||
if (base == null)
|
||||
return Result.failure();
|
||||
return base.doWork();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopped() {
|
||||
if (base != null)
|
||||
base.onStopped();
|
||||
}
|
||||
}
|
59
app/src/main/java/com/topjohnwu/magisk/App.kt
Normal file
59
app/src/main/java/com/topjohnwu/magisk/App.kt
Normal file
@@ -0,0 +1,59 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.multidex.MultiDex
|
||||
import androidx.room.Room
|
||||
import androidx.work.impl.WorkDatabase
|
||||
import androidx.work.impl.WorkDatabase_Impl
|
||||
import com.topjohnwu.magisk.data.database.RepoDatabase
|
||||
import com.topjohnwu.magisk.data.database.RepoDatabase_Impl
|
||||
import com.topjohnwu.magisk.di.ActivityTracker
|
||||
import com.topjohnwu.magisk.di.koinModules
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.utils.LocaleManager
|
||||
import com.topjohnwu.magisk.utils.RootUtils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.context.startKoin
|
||||
import timber.log.Timber
|
||||
|
||||
open class App : Application() {
|
||||
|
||||
init {
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER or Shell.FLAG_USE_MAGISK_BUSYBOX)
|
||||
Shell.Config.verboseLogging(BuildConfig.DEBUG)
|
||||
Shell.Config.addInitializers(RootUtils::class.java)
|
||||
Shell.Config.setTimeout(2)
|
||||
Room.setFactory {
|
||||
when (it) {
|
||||
WorkDatabase::class.java -> WorkDatabase_Impl()
|
||||
RepoDatabase::class.java -> RepoDatabase_Impl()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base)
|
||||
if (BuildConfig.DEBUG)
|
||||
MultiDex.install(base)
|
||||
Timber.plant(Timber.DebugTree())
|
||||
|
||||
startKoin {
|
||||
androidContext(this@App)
|
||||
modules(koinModules)
|
||||
}
|
||||
|
||||
registerActivityLifecycleCallbacks(get<ActivityTracker>())
|
||||
LocaleManager.setLocale(this)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
LocaleManager.setLocale(this)
|
||||
}
|
||||
}
|
26
app/src/main/java/com/topjohnwu/magisk/ClassMap.kt
Normal file
26
app/src/main/java/com/topjohnwu/magisk/ClassMap.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import com.topjohnwu.magisk.model.download.DownloadService
|
||||
import com.topjohnwu.magisk.model.receiver.GeneralReceiver
|
||||
import com.topjohnwu.magisk.model.update.UpdateCheckService
|
||||
import com.topjohnwu.magisk.ui.MainActivity
|
||||
import com.topjohnwu.magisk.ui.SplashActivity
|
||||
import com.topjohnwu.magisk.ui.flash.FlashActivity
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
||||
|
||||
object ClassMap {
|
||||
private val map = mapOf(
|
||||
App::class.java to a.e::class.java,
|
||||
MainActivity::class.java to a.b::class.java,
|
||||
SplashActivity::class.java to a.c::class.java,
|
||||
FlashActivity::class.java to a.f::class.java,
|
||||
UpdateCheckService::class.java to a.g::class.java,
|
||||
GeneralReceiver::class.java to a.h::class.java,
|
||||
DownloadService::class.java to a.j::class.java,
|
||||
SuRequestActivity::class.java to a.m::class.java
|
||||
)
|
||||
|
||||
operator fun <T : Class<*>>get(c: Class<*>): T {
|
||||
return map.getOrElse(c) { throw IllegalArgumentException() } as T
|
||||
}
|
||||
}
|
208
app/src/main/java/com/topjohnwu/magisk/Config.kt
Normal file
208
app/src/main/java/com/topjohnwu/magisk/Config.kt
Normal file
@@ -0,0 +1,208 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Environment
|
||||
import android.util.Xml
|
||||
import androidx.core.content.edit
|
||||
import com.topjohnwu.magisk.data.database.SettingsDao
|
||||
import com.topjohnwu.magisk.data.database.StringDao
|
||||
import com.topjohnwu.magisk.data.repository.DBConfig
|
||||
import com.topjohnwu.magisk.di.Protected
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.magisk.extensions.packageName
|
||||
import com.topjohnwu.magisk.model.preference.PreferenceModel
|
||||
import com.topjohnwu.magisk.utils.FingerprintHelper
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import java.io.File
|
||||
|
||||
object Config : PreferenceModel, DBConfig {
|
||||
|
||||
override val stringDao: StringDao by inject()
|
||||
override val settingsDao: SettingsDao by inject()
|
||||
override val context: Context by inject(Protected)
|
||||
|
||||
object Key {
|
||||
// db configs
|
||||
const val ROOT_ACCESS = "root_access"
|
||||
const val SU_MULTIUSER_MODE = "multiuser_mode"
|
||||
const val SU_MNT_NS = "mnt_ns"
|
||||
const val SU_MANAGER = "requester"
|
||||
const val SU_FINGERPRINT = "su_fingerprint"
|
||||
|
||||
// prefs
|
||||
const val SU_REQUEST_TIMEOUT = "su_request_timeout"
|
||||
const val SU_AUTO_RESPONSE = "su_auto_response"
|
||||
const val SU_NOTIFICATION = "su_notification"
|
||||
const val SU_REAUTH = "su_reauth"
|
||||
const val CHECK_UPDATES = "check_update"
|
||||
const val UPDATE_CHANNEL = "update_channel"
|
||||
const val CUSTOM_CHANNEL = "custom_channel"
|
||||
const val LOCALE = "locale"
|
||||
const val DARK_THEME = "dark_theme"
|
||||
const val REPO_ORDER = "repo_order"
|
||||
const val SHOW_SYSTEM_APP = "show_system"
|
||||
const val DOWNLOAD_PATH = "download_path"
|
||||
|
||||
// system state
|
||||
const val MAGISKHIDE = "magiskhide"
|
||||
const val COREONLY = "disable"
|
||||
}
|
||||
|
||||
object Value {
|
||||
// Update channels
|
||||
const val DEFAULT_CHANNEL = -1
|
||||
const val STABLE_CHANNEL = 0
|
||||
const val BETA_CHANNEL = 1
|
||||
const val CUSTOM_CHANNEL = 2
|
||||
const val CANARY_CHANNEL = 3
|
||||
const val CANARY_DEBUG_CHANNEL = 4
|
||||
|
||||
// root access mode
|
||||
const val ROOT_ACCESS_DISABLED = 0
|
||||
const val ROOT_ACCESS_APPS_ONLY = 1
|
||||
const val ROOT_ACCESS_ADB_ONLY = 2
|
||||
const val ROOT_ACCESS_APPS_AND_ADB = 3
|
||||
|
||||
// su multiuser
|
||||
const val MULTIUSER_MODE_OWNER_ONLY = 0
|
||||
const val MULTIUSER_MODE_OWNER_MANAGED = 1
|
||||
const val MULTIUSER_MODE_USER = 2
|
||||
|
||||
// su mnt ns
|
||||
const val NAMESPACE_MODE_GLOBAL = 0
|
||||
const val NAMESPACE_MODE_REQUESTER = 1
|
||||
const val NAMESPACE_MODE_ISOLATE = 2
|
||||
|
||||
// su notification
|
||||
const val NO_NOTIFICATION = 0
|
||||
const val NOTIFICATION_TOAST = 1
|
||||
|
||||
// su auto response
|
||||
const val SU_PROMPT = 0
|
||||
const val SU_AUTO_DENY = 1
|
||||
const val SU_AUTO_ALLOW = 2
|
||||
|
||||
// su timeout
|
||||
val TIMEOUT_LIST = intArrayOf(0, -1, 10, 20, 30, 60)
|
||||
|
||||
// repo order
|
||||
const val ORDER_NAME = 0
|
||||
const val ORDER_DATE = 1
|
||||
}
|
||||
|
||||
private val defaultChannel =
|
||||
if (Utils.isCanary) Value.CANARY_DEBUG_CHANNEL
|
||||
else Value.DEFAULT_CHANNEL
|
||||
|
||||
var downloadPath by preference(Key.DOWNLOAD_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||
var repoOrder by preference(Key.REPO_ORDER, Value.ORDER_DATE)
|
||||
|
||||
var suDefaultTimeout by preferenceStrInt(Key.SU_REQUEST_TIMEOUT, 10)
|
||||
var suAutoReponse by preferenceStrInt(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT)
|
||||
var suNotification by preferenceStrInt(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST)
|
||||
var updateChannel by preferenceStrInt(Key.UPDATE_CHANNEL, defaultChannel)
|
||||
|
||||
var darkTheme by preference(Key.DARK_THEME, true)
|
||||
var suReAuth by preference(Key.SU_REAUTH, false)
|
||||
var checkUpdate by preference(Key.CHECK_UPDATES, true)
|
||||
var magiskHide by preference(Key.MAGISKHIDE, true)
|
||||
var coreOnly by preference(Key.COREONLY, false)
|
||||
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
|
||||
|
||||
var customChannelUrl by preference(Key.CUSTOM_CHANNEL, "")
|
||||
var locale by preference(Key.LOCALE, "")
|
||||
|
||||
var rootMode by dbSettings(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB)
|
||||
var suMntNamespaceMode by dbSettings(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER)
|
||||
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
|
||||
var suFingerprint by dbSettings(Key.SU_FINGERPRINT, false)
|
||||
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
||||
|
||||
// Always return a path in external storage where we can write
|
||||
val downloadDirectory get() =
|
||||
Utils.ensureDownloadPath(downloadPath) ?: get<Context>().getExternalFilesDir(null)!!
|
||||
|
||||
fun initialize() = prefs.edit {
|
||||
parsePrefs(this)
|
||||
|
||||
if (!prefs.contains(Key.UPDATE_CHANNEL))
|
||||
putString(Key.UPDATE_CHANNEL, defaultChannel.toString())
|
||||
|
||||
// Get actual state
|
||||
putBoolean(Key.COREONLY, Const.MAGISK_DISABLE_FILE.exists())
|
||||
|
||||
// Write database configs
|
||||
putString(Key.ROOT_ACCESS, rootMode.toString())
|
||||
putString(Key.SU_MNT_NS, suMntNamespaceMode.toString())
|
||||
putString(Key.SU_MULTIUSER_MODE, suMultiuserMode.toString())
|
||||
putBoolean(Key.SU_FINGERPRINT, FingerprintHelper.useFingerprint())
|
||||
}
|
||||
|
||||
private fun parsePrefs(editor: SharedPreferences.Editor) = editor.apply {
|
||||
val config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS)
|
||||
if (config.exists()) runCatching {
|
||||
val input = SuFileInputStream(config).buffered()
|
||||
val parser = Xml.newPullParser()
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
|
||||
parser.setInput(input, "UTF-8")
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.START_TAG, null, "map")
|
||||
while (parser.next() != XmlPullParser.END_TAG) {
|
||||
if (parser.eventType != XmlPullParser.START_TAG)
|
||||
continue
|
||||
val key: String = parser.getAttributeValue(null, "name")
|
||||
fun value() = parser.getAttributeValue(null, "value")!!
|
||||
when (parser.name) {
|
||||
"string" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "string")
|
||||
putString(key, parser.nextText())
|
||||
parser.require(XmlPullParser.END_TAG, null, "string")
|
||||
}
|
||||
"boolean" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "boolean")
|
||||
putBoolean(key, value().toBoolean())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "boolean")
|
||||
}
|
||||
"int" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "int")
|
||||
putInt(key, value().toInt())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "int")
|
||||
}
|
||||
"long" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "long")
|
||||
putLong(key, value().toLong())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "long")
|
||||
}
|
||||
"float" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "int")
|
||||
putFloat(key, value().toFloat())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "int")
|
||||
}
|
||||
else -> parser.next()
|
||||
}
|
||||
}
|
||||
config.delete()
|
||||
}
|
||||
}
|
||||
|
||||
fun export() {
|
||||
// Flush prefs to disk
|
||||
prefs.edit().commit()
|
||||
val xml = File(
|
||||
"${get<Context>(Protected).filesDir.parent}/shared_prefs",
|
||||
"${packageName}_preferences.xml"
|
||||
)
|
||||
Shell.su("cat $xml > /data/adb/${Const.MANAGER_CONFIGS}").exec()
|
||||
}
|
||||
|
||||
}
|
78
app/src/main/java/com/topjohnwu/magisk/Const.kt
Normal file
78
app/src/main/java/com/topjohnwu/magisk/Const.kt
Normal file
@@ -0,0 +1,78 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import android.os.Process
|
||||
import java.io.File
|
||||
|
||||
object Const {
|
||||
|
||||
// Paths
|
||||
const val MAGISK_PATH = "/sbin/.magisk/img"
|
||||
var MAGISK_DISABLE_FILE = File("xxx")
|
||||
const val TMP_FOLDER_PATH = "/dev/tmp"
|
||||
const val MAGISK_LOG = "/cache/magisk.log"
|
||||
|
||||
// Versions
|
||||
const val SNET_EXT_VER = 13
|
||||
const val SNET_REVISION = "a6c47f86f10b310358afa9dbe837037dd5d561df"
|
||||
const val BOOTCTL_REVISION = "a6c47f86f10b310358afa9dbe837037dd5d561df"
|
||||
|
||||
// Misc
|
||||
const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
||||
const val MAGISK_INSTALL_LOG_FILENAME = "magisk_install_log_%s.log"
|
||||
const val MANAGER_CONFIGS = ".tmp.magisk.config"
|
||||
val USER_ID = Process.myUid() / 100000
|
||||
|
||||
object MagiskVersion {
|
||||
const val MIN_SUPPORT = 18000
|
||||
}
|
||||
|
||||
object ID {
|
||||
const val FETCH_ZIP = 2
|
||||
const val SELECT_BOOT = 3
|
||||
|
||||
// notifications
|
||||
const val MAGISK_UPDATE_NOTIFICATION_ID = 4
|
||||
const val APK_UPDATE_NOTIFICATION_ID = 5
|
||||
const val DTBO_NOTIFICATION_ID = 7
|
||||
const val HIDE_MANAGER_NOTIFICATION_ID = 8
|
||||
const val UPDATE_NOTIFICATION_CHANNEL = "update"
|
||||
const val PROGRESS_NOTIFICATION_CHANNEL = "progress"
|
||||
const val CHECK_MAGISK_UPDATE_WORKER_ID = "magisk_update"
|
||||
}
|
||||
|
||||
object Url {
|
||||
const val ZIP_URL = "https://github.com/Magisk-Modules-Repo/%s/archive/master.zip"
|
||||
const val PAYPAL_URL = "https://www.paypal.me/topjohnwu"
|
||||
const val PATREON_URL = "https://www.patreon.com/topjohnwu"
|
||||
const val TWITTER_URL = "https://twitter.com/topjohnwu"
|
||||
const val XDA_THREAD = "http://forum.xda-developers.com/showthread.php?t=3432382"
|
||||
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
|
||||
|
||||
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com/"
|
||||
const val GITHUB_API_URL = "https://api.github.com/users/Magisk-Modules-Repo/"
|
||||
}
|
||||
|
||||
object Key {
|
||||
// others
|
||||
const val LINK_KEY = "Link"
|
||||
const val IF_NONE_MATCH = "If-None-Match"
|
||||
const val ETAG_KEY = "ETag"
|
||||
// intents
|
||||
const val OPEN_SECTION = "section"
|
||||
const val INTENT_SET_APP = "app_json"
|
||||
const val FLASH_ACTION = "action"
|
||||
const val FLASH_DATA = "additional_data"
|
||||
const val DISMISS_ID = "dismiss_id"
|
||||
const val BROADCAST_MANAGER_UPDATE = "manager_update"
|
||||
const val BROADCAST_REBOOT = "reboot"
|
||||
}
|
||||
|
||||
object Value {
|
||||
const val FLASH_ZIP = "flash"
|
||||
const val PATCH_FILE = "patch"
|
||||
const val FLASH_MAGISK = "magisk"
|
||||
const val FLASH_INACTIVE_SLOT = "slot"
|
||||
const val UNINSTALL = "uninstall"
|
||||
}
|
||||
|
||||
}
|
26
app/src/main/java/com/topjohnwu/magisk/Info.kt
Normal file
26
app/src/main/java/com/topjohnwu/magisk/Info.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import com.topjohnwu.magisk.model.entity.UpdateInfo
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
|
||||
object Info {
|
||||
|
||||
var magiskVersionCode = -1
|
||||
|
||||
var magiskVersionString = ""
|
||||
|
||||
var remote = UpdateInfo()
|
||||
|
||||
var keepVerity = false
|
||||
var keepEnc = false
|
||||
var recovery = false
|
||||
|
||||
fun loadMagiskInfo() {
|
||||
runCatching {
|
||||
magiskVersionString = ShellUtils.fastCmd("magisk -v").split(":".toRegex())[0]
|
||||
magiskVersionCode = ShellUtils.fastCmd("magisk -V").toInt()
|
||||
Config.magiskHide = Shell.su("magiskhide --status").exec().isSuccess
|
||||
}
|
||||
}
|
||||
}
|
123
app/src/main/java/com/topjohnwu/magisk/base/BaseActivity.kt
Normal file
123
app/src/main/java/com/topjohnwu/magisk/base/BaseActivity.kt
Normal file
@@ -0,0 +1,123 @@
|
||||
package com.topjohnwu.magisk.base
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
|
||||
import com.topjohnwu.magisk.extensions.set
|
||||
import com.topjohnwu.magisk.model.events.EventHandler
|
||||
import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder
|
||||
import com.topjohnwu.magisk.utils.LocaleManager
|
||||
import com.topjohnwu.magisk.utils.currentLocale
|
||||
import kotlin.random.Random
|
||||
|
||||
typealias RequestCallback = BaseActivity<*, *>.(Int, Intent?) -> Unit
|
||||
|
||||
abstract class BaseActivity<ViewModel : BaseViewModel, Binding : ViewDataBinding> :
|
||||
AppCompatActivity(), EventHandler {
|
||||
|
||||
protected lateinit var binding: Binding
|
||||
protected abstract val layoutRes: Int
|
||||
protected abstract val viewModel: ViewModel
|
||||
protected open val snackbarView get() = binding.root
|
||||
protected open val navHostId: Int = 0
|
||||
protected open val defaultPosition: Int = 0
|
||||
|
||||
private val resultCallbacks by lazy { SparseArrayCompat<RequestCallback>() }
|
||||
|
||||
init {
|
||||
val theme = if (Config.darkTheme) {
|
||||
AppCompatDelegate.MODE_NIGHT_YES
|
||||
} else {
|
||||
AppCompatDelegate.MODE_NIGHT_NO
|
||||
}
|
||||
AppCompatDelegate.setDefaultNightMode(theme)
|
||||
}
|
||||
|
||||
override fun applyOverrideConfiguration(config: Configuration?) {
|
||||
// Force applying our preferred local
|
||||
config?.setLocale(currentLocale)
|
||||
super.applyOverrideConfiguration(config)
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(LocaleManager.getLocaleContext(base))
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
viewModel.viewEvents.observe(this, viewEventObserver)
|
||||
|
||||
binding = DataBindingUtil.setContentView<Binding>(this, layoutRes).apply {
|
||||
setVariable(BR.viewModel, viewModel)
|
||||
lifecycleOwner = this@BaseActivity
|
||||
}
|
||||
}
|
||||
|
||||
fun withPermissions(vararg permissions: String, builder: PermissionRequestBuilder.() -> Unit) {
|
||||
val request = PermissionRequestBuilder().apply(builder).build()
|
||||
val ungranted = permissions.filter {
|
||||
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
if (ungranted.isEmpty()) {
|
||||
request.onSuccess()
|
||||
} else {
|
||||
val requestCode = Random.nextInt(256, 512)
|
||||
resultCallbacks[requestCode] = { result, _ ->
|
||||
if (result > 0)
|
||||
request.onSuccess()
|
||||
else
|
||||
request.onFailure()
|
||||
}
|
||||
ActivityCompat.requestPermissions(this, ungranted.toTypedArray(), requestCode)
|
||||
}
|
||||
}
|
||||
|
||||
fun withExternalRW(builder: PermissionRequestBuilder.() -> Unit) {
|
||||
withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, builder = builder)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
var success = true
|
||||
for (res in grantResults) {
|
||||
if (res != PackageManager.PERMISSION_GRANTED) {
|
||||
success = false
|
||||
break
|
||||
}
|
||||
}
|
||||
resultCallbacks[requestCode]?.apply {
|
||||
resultCallbacks.remove(requestCode)
|
||||
invoke(this@BaseActivity, if (success) 1 else -1, null)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
resultCallbacks[requestCode]?.apply {
|
||||
resultCallbacks.remove(requestCode)
|
||||
invoke(this@BaseActivity, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
fun startActivityForResult(intent: Intent, requestCode: Int, listener: RequestCallback) {
|
||||
resultCallbacks[requestCode] = listener
|
||||
startActivityForResult(intent, requestCode)
|
||||
}
|
||||
|
||||
}
|
50
app/src/main/java/com/topjohnwu/magisk/base/BaseFragment.kt
Normal file
50
app/src/main/java/com/topjohnwu/magisk/base/BaseFragment.kt
Normal file
@@ -0,0 +1,50 @@
|
||||
package com.topjohnwu.magisk.base
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
|
||||
import com.topjohnwu.magisk.model.events.EventHandler
|
||||
import com.topjohnwu.magisk.model.events.ViewEvent
|
||||
|
||||
abstract class BaseFragment<ViewModel : BaseViewModel, Binding : ViewDataBinding> :
|
||||
Fragment(), EventHandler {
|
||||
|
||||
protected val activity get() = requireActivity() as BaseActivity<*, *>
|
||||
protected lateinit var binding: Binding
|
||||
protected abstract val layoutRes: Int
|
||||
protected abstract val viewModel: ViewModel
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
viewModel.viewEvents.observe(this, viewEventObserver)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
binding = DataBindingUtil.inflate<Binding>(inflater, layoutRes, container, false).apply {
|
||||
setVariable(BR.viewModel, viewModel)
|
||||
lifecycleOwner = this@BaseFragment
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onEventDispatched(event: ViewEvent) {
|
||||
super.onEventDispatched(event)
|
||||
activity.onEventDispatched(event)
|
||||
}
|
||||
|
||||
open fun onBackPressed(): Boolean = false
|
||||
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
package com.topjohnwu.magisk.base
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.preference.*
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
abstract class BasePreferenceFragment : PreferenceFragmentCompat(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
protected val prefs: SharedPreferences by inject()
|
||||
protected val activity get() = requireActivity() as BaseActivity<*, *>
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val v = super.onCreateView(inflater, container, savedInstanceState)
|
||||
prefs.registerOnSharedPreferenceChangeListener(this)
|
||||
return v
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
prefs.unregisterOnSharedPreferenceChangeListener(this)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun setAllPreferencesToAvoidHavingExtraSpace(preference: Preference) {
|
||||
preference.isIconSpaceReserved = false
|
||||
if (preference is PreferenceGroup)
|
||||
for (i in 0 until preference.preferenceCount)
|
||||
setAllPreferencesToAvoidHavingExtraSpace(preference.getPreference(i))
|
||||
}
|
||||
|
||||
override fun setPreferenceScreen(preferenceScreen: PreferenceScreen?) {
|
||||
if (preferenceScreen != null)
|
||||
setAllPreferencesToAvoidHavingExtraSpace(preferenceScreen)
|
||||
super.setPreferenceScreen(preferenceScreen)
|
||||
}
|
||||
|
||||
override fun onCreateAdapter(preferenceScreen: PreferenceScreen?): RecyclerView.Adapter<*> =
|
||||
object : PreferenceGroupAdapter(preferenceScreen) {
|
||||
@SuppressLint("RestrictedApi")
|
||||
override fun onPreferenceHierarchyChange(preference: Preference?) {
|
||||
if (preference != null)
|
||||
setAllPreferencesToAvoidHavingExtraSpace(preference)
|
||||
super.onPreferenceHierarchyChange(preference)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
package com.topjohnwu.magisk.base
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Network
|
||||
import android.net.Uri
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.work.Data
|
||||
import androidx.work.ListenableWorker
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import java.util.*
|
||||
|
||||
abstract class DelegateWorker {
|
||||
|
||||
private lateinit var worker: ListenableWorker
|
||||
|
||||
val applicationContext: Context
|
||||
get() = worker.applicationContext
|
||||
|
||||
val id: UUID
|
||||
get() = worker.id
|
||||
|
||||
val inputData: Data
|
||||
get() = worker.inputData
|
||||
|
||||
val tags: Set<String>
|
||||
get() = worker.tags
|
||||
|
||||
val triggeredContentUris: List<Uri>
|
||||
@RequiresApi(24)
|
||||
get() = worker.triggeredContentUris
|
||||
|
||||
val triggeredContentAuthorities: List<String>
|
||||
@RequiresApi(24)
|
||||
get() = worker.triggeredContentAuthorities
|
||||
|
||||
val network: Network?
|
||||
@RequiresApi(28)
|
||||
get() = worker.network
|
||||
|
||||
val runAttemptCount: Int
|
||||
get() = worker.runAttemptCount
|
||||
|
||||
val isStopped: Boolean
|
||||
get() = worker.isStopped
|
||||
|
||||
abstract fun doWork(): ListenableWorker.Result
|
||||
|
||||
fun onStopped() {}
|
||||
|
||||
fun attachWorker(w: ListenableWorker) {
|
||||
worker = w
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun startWork(): ListenableFuture<ListenableWorker.Result> {
|
||||
return worker.startWork()
|
||||
}
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
package com.topjohnwu.magisk.base.viewmodel
|
||||
|
||||
import android.app.Activity
|
||||
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
|
||||
import com.topjohnwu.magisk.extensions.doOnSubscribeUi
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.model.events.BackPressEvent
|
||||
import com.topjohnwu.magisk.model.events.PermissionEvent
|
||||
import com.topjohnwu.magisk.model.events.ViewActionEvent
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
|
||||
|
||||
abstract class BaseViewModel(
|
||||
initialState: State = State.LOADING
|
||||
) : LoadingViewModel(initialState) {
|
||||
|
||||
val isConnected = KObservableField(false)
|
||||
|
||||
init {
|
||||
ReactiveNetwork.observeNetworkConnectivity(get())
|
||||
.subscribeK { isConnected.value = it.available() }
|
||||
.add()
|
||||
}
|
||||
|
||||
fun withView(action: Activity.() -> Unit) {
|
||||
ViewActionEvent(action).publish()
|
||||
}
|
||||
|
||||
fun withPermissions(vararg permissions: String): Observable<Boolean> {
|
||||
val subject = PublishSubject.create<Boolean>()
|
||||
return subject.doOnSubscribeUi { PermissionEvent(permissions.toList(), subject).publish() }
|
||||
}
|
||||
|
||||
fun back() = BackPressEvent().publish()
|
||||
|
||||
}
|
@@ -0,0 +1,78 @@
|
||||
package com.topjohnwu.magisk.base.viewmodel
|
||||
|
||||
import androidx.databinding.Bindable
|
||||
import com.topjohnwu.magisk.BR
|
||||
import io.reactivex.*
|
||||
|
||||
abstract class LoadingViewModel(defaultState: State = State.LOADING) :
|
||||
StatefulViewModel<LoadingViewModel.State>(defaultState) {
|
||||
|
||||
val loading @Bindable get() = state == State.LOADING
|
||||
val loaded @Bindable get() = state == State.LOADED
|
||||
val loadingFailed @Bindable get() = state == State.LOADING_FAILED
|
||||
|
||||
@Deprecated(
|
||||
"Direct access is recommended since 0.2. This access method will be removed in 1.0",
|
||||
ReplaceWith("state = State.LOADING", "com.topjohnwu.magisk.base.viewmodel.LoadingViewModel.State"),
|
||||
DeprecationLevel.WARNING
|
||||
)
|
||||
fun setLoading() {
|
||||
state = State.LOADING
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Direct access is recommended since 0.2. This access method will be removed in 1.0",
|
||||
ReplaceWith("state = State.LOADED", "com.topjohnwu.magisk.base.viewmodel.LoadingViewModel.State"),
|
||||
DeprecationLevel.WARNING
|
||||
)
|
||||
fun setLoaded() {
|
||||
state = State.LOADED
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Direct access is recommended since 0.2. This access method will be removed in 1.0",
|
||||
ReplaceWith("state = State.LOADING_FAILED", "com.topjohnwu.magisk.base.viewmodel.LoadingViewModel.State"),
|
||||
DeprecationLevel.WARNING
|
||||
)
|
||||
fun setLoadingFailed() {
|
||||
state = State.LOADING_FAILED
|
||||
}
|
||||
|
||||
override fun notifyStateChanged() {
|
||||
notifyPropertyChanged(BR.loading)
|
||||
notifyPropertyChanged(BR.loaded)
|
||||
notifyPropertyChanged(BR.loadingFailed)
|
||||
}
|
||||
|
||||
enum class State {
|
||||
LOADED, LOADING, LOADING_FAILED
|
||||
}
|
||||
|
||||
//region Rx
|
||||
protected fun <T> Observable<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
|
||||
doOnSubscribe { viewModel.state = State.LOADING }
|
||||
.doOnError { viewModel.state = State.LOADING_FAILED }
|
||||
.doOnNext { if (allowFinishing) viewModel.state = State.LOADED }
|
||||
|
||||
protected fun <T> Single<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
|
||||
doOnSubscribe { viewModel.state = State.LOADING }
|
||||
.doOnError { viewModel.state = State.LOADING_FAILED }
|
||||
.doOnSuccess { if (allowFinishing) viewModel.state = State.LOADED }
|
||||
|
||||
protected fun <T> Maybe<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
|
||||
doOnSubscribe { viewModel.state = State.LOADING }
|
||||
.doOnError { viewModel.state = State.LOADING_FAILED }
|
||||
.doOnComplete { if (allowFinishing) viewModel.state = State.LOADED }
|
||||
.doOnSuccess { if (allowFinishing) viewModel.state = State.LOADED }
|
||||
|
||||
protected fun <T> Flowable<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
|
||||
doOnSubscribe { viewModel.state = State.LOADING }
|
||||
.doOnError { viewModel.state = State.LOADING_FAILED }
|
||||
.doOnNext { if (allowFinishing) viewModel.state = State.LOADED }
|
||||
|
||||
protected fun Completable.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
|
||||
doOnSubscribe { viewModel.state = State.LOADING }
|
||||
.doOnError { viewModel.state = State.LOADING_FAILED }
|
||||
.doOnComplete { if (allowFinishing) viewModel.state = State.LOADED }
|
||||
//endregion
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
package com.topjohnwu.magisk.base.viewmodel
|
||||
|
||||
import androidx.databinding.Observable
|
||||
import androidx.databinding.PropertyChangeRegistry
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
/**
|
||||
* Copy of [android.databinding.BaseObservable] which extends [ViewModel]
|
||||
*/
|
||||
abstract class ObservableViewModel : TeanityViewModel(), Observable {
|
||||
|
||||
@Transient
|
||||
private var callbacks: PropertyChangeRegistry? = null
|
||||
|
||||
@Synchronized
|
||||
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
|
||||
if (callbacks == null) {
|
||||
callbacks = PropertyChangeRegistry()
|
||||
}
|
||||
callbacks?.add(callback)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
|
||||
callbacks?.remove(callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies listeners that all properties of this instance have changed.
|
||||
*/
|
||||
@Synchronized
|
||||
fun notifyChange() {
|
||||
callbacks?.notifyCallbacks(this, 0, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies listeners that a specific property has changed. The getter for the property
|
||||
* that changes should be marked with [android.databinding.Bindable] to generate a field in
|
||||
* `BR` to be used as `fieldId`.
|
||||
*
|
||||
* @param fieldId The generated BR id for the Bindable field.
|
||||
*/
|
||||
fun notifyPropertyChanged(fieldId: Int) {
|
||||
callbacks?.notifyCallbacks(this, fieldId, null)
|
||||
}
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
package com.topjohnwu.magisk.base.viewmodel
|
||||
|
||||
abstract class StatefulViewModel<State : Enum<*>>(
|
||||
val defaultState: State
|
||||
) : ObservableViewModel() {
|
||||
|
||||
var state: State = defaultState
|
||||
set(value) {
|
||||
field = value
|
||||
notifyStateChanged()
|
||||
}
|
||||
|
||||
open fun notifyStateChanged() = Unit
|
||||
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
package com.topjohnwu.magisk.base.viewmodel
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.topjohnwu.magisk.model.events.SimpleViewEvent
|
||||
import com.topjohnwu.magisk.model.events.ViewEvent
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.disposables.Disposable
|
||||
|
||||
abstract class TeanityViewModel : ViewModel() {
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
private val _viewEvents = MutableLiveData<ViewEvent>()
|
||||
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
fun <Event : ViewEvent> Event.publish() {
|
||||
_viewEvents.value = this
|
||||
}
|
||||
|
||||
fun Int.publish() {
|
||||
_viewEvents.value = SimpleViewEvent(this)
|
||||
}
|
||||
|
||||
fun Disposable.add() {
|
||||
disposables.add(this)
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import com.topjohnwu.magisk.data.database.base.*
|
||||
import com.topjohnwu.magisk.model.entity.MagiskLog
|
||||
import com.topjohnwu.magisk.model.entity.toLog
|
||||
import com.topjohnwu.magisk.model.entity.toMap
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class LogDao : BaseDao() {
|
||||
|
||||
override val table = DatabaseDefinition.Table.LOG
|
||||
|
||||
fun deleteOutdated(
|
||||
suTimeout: Long = TimeUnit.DAYS.toMillis(14)
|
||||
) = query<Delete> {
|
||||
condition {
|
||||
lessThan("time", suTimeout.toString())
|
||||
}
|
||||
}.ignoreElement()
|
||||
|
||||
fun deleteAll() = query<Delete> {}.ignoreElement()
|
||||
|
||||
fun fetchAll() = query<Select> {
|
||||
orderBy("time", Order.DESC)
|
||||
}.flattenAsFlowable { it }
|
||||
.map { it.toLog() }
|
||||
.toList()
|
||||
|
||||
fun put(log: MagiskLog) = query<Insert> {
|
||||
values(log.toMap())
|
||||
}.ignoreElement()
|
||||
|
||||
}
|
@@ -0,0 +1,75 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.data.database.base.*
|
||||
import com.topjohnwu.magisk.extensions.now
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||
import com.topjohnwu.magisk.model.entity.toMap
|
||||
import com.topjohnwu.magisk.model.entity.toPolicy
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
class PolicyDao(
|
||||
private val context: Context
|
||||
) : BaseDao() {
|
||||
|
||||
override val table: String = DatabaseDefinition.Table.POLICY
|
||||
|
||||
fun deleteOutdated(
|
||||
nowSeconds: Long = TimeUnit.MILLISECONDS.toSeconds(now)
|
||||
) = query<Delete> {
|
||||
condition {
|
||||
greaterThan("until", "0")
|
||||
and {
|
||||
lessThan("until", nowSeconds.toString())
|
||||
}
|
||||
or {
|
||||
lessThan("until", "0")
|
||||
}
|
||||
}
|
||||
}.ignoreElement()
|
||||
|
||||
fun delete(packageName: String) = query<Delete> {
|
||||
condition {
|
||||
equals("package_name", packageName)
|
||||
}
|
||||
}.ignoreElement()
|
||||
|
||||
fun delete(uid: Int) = query<Delete> {
|
||||
condition {
|
||||
equals("uid", uid)
|
||||
}
|
||||
}.ignoreElement()
|
||||
|
||||
fun fetch(uid: Int) = query<Select> {
|
||||
condition {
|
||||
equals("uid", uid)
|
||||
}
|
||||
}.map { it.first().toPolicySafe() }
|
||||
|
||||
fun update(policy: MagiskPolicy) = query<Replace> {
|
||||
values(policy.toMap())
|
||||
}.ignoreElement()
|
||||
|
||||
fun fetchAll() = query<Select> {
|
||||
condition {
|
||||
equals("uid/100000", Const.USER_ID)
|
||||
}
|
||||
}.map { it.mapNotNull { it.toPolicySafe() } }
|
||||
|
||||
|
||||
private fun Map<String, String>.toPolicySafe(): MagiskPolicy? {
|
||||
return runCatching { toPolicy(context.packageManager) }.getOrElse {
|
||||
Timber.e(it)
|
||||
if (it is PackageManager.NameNotFoundException) {
|
||||
val uid = getOrElse("uid") { null } ?: return null
|
||||
delete(uid).subscribe()
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,72 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import androidx.room.*
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||
|
||||
@Dao
|
||||
abstract class RepoDao {
|
||||
|
||||
val repoIDList get() = getRepoID().map { it.id }
|
||||
|
||||
val repos: List<Repo> get() = when (Config.repoOrder) {
|
||||
Config.Value.ORDER_NAME -> getReposNameOrder()
|
||||
else -> getReposDateOrder()
|
||||
}
|
||||
|
||||
var etagKey: String
|
||||
set(etag) = addEtagRaw(RepoEtag(0, etag))
|
||||
get() = etagRaw()?.key.orEmpty()
|
||||
|
||||
fun clear() {
|
||||
clearRepos()
|
||||
clearEtag()
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM repos ORDER BY last_update DESC")
|
||||
protected abstract fun getReposDateOrder(): List<Repo>
|
||||
|
||||
@Query("SELECT * FROM repos ORDER BY name COLLATE NOCASE")
|
||||
protected abstract fun getReposNameOrder(): List<Repo>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract fun addRepo(repo: Repo)
|
||||
|
||||
@Query("SELECT * FROM repos WHERE id = :id")
|
||||
abstract fun getRepo(id: String): Repo?
|
||||
|
||||
@Query("SELECT id FROM repos")
|
||||
protected abstract fun getRepoID(): List<RepoID>
|
||||
|
||||
@Delete
|
||||
abstract fun removeRepo(repo: Repo)
|
||||
|
||||
@Query("DELETE FROM repos WHERE id = :id")
|
||||
abstract fun removeRepo(id: String)
|
||||
|
||||
@Query("DELETE FROM repos WHERE id IN (:idList)")
|
||||
abstract fun removeRepos(idList: Collection<String>)
|
||||
|
||||
@Query("SELECT * FROM etag")
|
||||
protected abstract fun etagRaw(): RepoEtag?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
protected abstract fun addEtagRaw(etag: RepoEtag)
|
||||
|
||||
@Query("DELETE FROM repos")
|
||||
protected abstract fun clearRepos()
|
||||
|
||||
@Query("DELETE FROM etag")
|
||||
protected abstract fun clearEtag()
|
||||
}
|
||||
|
||||
data class RepoID(
|
||||
@PrimaryKey val id: String
|
||||
)
|
||||
|
||||
@Entity(tableName = "etag")
|
||||
data class RepoEtag(
|
||||
@PrimaryKey val id: Int,
|
||||
val key: String
|
||||
)
|
||||
|
@@ -0,0 +1,11 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||
|
||||
@Database(version = 6, entities = [Repo::class, RepoEtag::class])
|
||||
abstract class RepoDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun repoDao() : RepoDao
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import com.topjohnwu.magisk.data.database.base.*
|
||||
|
||||
class SettingsDao : BaseDao() {
|
||||
|
||||
override val table = DatabaseDefinition.Table.SETTINGS
|
||||
|
||||
fun delete(key: String) = query<Delete> {
|
||||
condition { equals("key", key) }
|
||||
}.ignoreElement()
|
||||
|
||||
fun put(key: String, value: Int) = query<Replace> {
|
||||
values("key" to key, "value" to value)
|
||||
}.ignoreElement()
|
||||
|
||||
fun fetch(key: String, default: Int = -1) = query<Select> {
|
||||
fields("value")
|
||||
condition { equals("key", key) }
|
||||
}.map { it.firstOrNull()?.values?.firstOrNull()?.toIntOrNull() ?: default }
|
||||
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import com.topjohnwu.magisk.data.database.base.*
|
||||
|
||||
class StringDao : BaseDao() {
|
||||
|
||||
override val table = DatabaseDefinition.Table.STRINGS
|
||||
|
||||
fun delete(key: String) = query<Delete> {
|
||||
condition { equals("key", key) }
|
||||
}.ignoreElement()
|
||||
|
||||
fun put(key: String, value: String) = query<Replace> {
|
||||
values("key" to key, "value" to value)
|
||||
}.ignoreElement()
|
||||
|
||||
fun fetch(key: String, default: String = "") = query<Select> {
|
||||
fields("value")
|
||||
condition { equals("key", key) }
|
||||
}.map { it.firstOrNull()?.values?.firstOrNull() ?: default }
|
||||
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
package com.topjohnwu.magisk.data.database.base
|
||||
|
||||
abstract class BaseDao {
|
||||
|
||||
abstract val table: String
|
||||
|
||||
inline fun <reified Builder : MagiskQueryBuilder> query(builder: Builder.() -> Unit) =
|
||||
Builder::class.java.newInstance()
|
||||
.apply { table = this@BaseDao.table }
|
||||
.apply(builder)
|
||||
.toString()
|
||||
.let { MagiskQuery(it) }
|
||||
.query()
|
||||
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package com.topjohnwu.magisk.data.database.base
|
||||
|
||||
import androidx.annotation.AnyThread
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Single
|
||||
|
||||
object DatabaseDefinition {
|
||||
|
||||
object Table {
|
||||
const val POLICY = "policies"
|
||||
const val LOG = "logs"
|
||||
const val SETTINGS = "settings"
|
||||
const val STRINGS = "strings"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
fun MagiskQuery.query() = query.su()
|
||||
|
||||
fun String.suRaw() = Single.fromCallable { Shell.su(this).exec().out }
|
||||
fun String.su() = suRaw().map { it.toMap() }
|
||||
|
||||
fun List<String>.toMap() = map { it.split(Regex("\\|")) }
|
||||
.map { it.toMapInternal() }
|
||||
|
||||
private fun List<String>.toMapInternal() = map { it.split("=", limit = 2) }
|
||||
.filter { it.size == 2 }
|
||||
.map { Pair(it[0], it[1]) }
|
||||
.toMap()
|
@@ -0,0 +1,5 @@
|
||||
package com.topjohnwu.magisk.data.database.base
|
||||
|
||||
inline class MagiskQuery(private val _query: String) {
|
||||
val query get() = "magisk --sqlite '$_query'"
|
||||
}
|
@@ -0,0 +1,155 @@
|
||||
package com.topjohnwu.magisk.data.database.base
|
||||
|
||||
import androidx.annotation.StringDef
|
||||
import com.topjohnwu.magisk.data.database.base.Order.Companion.ASC
|
||||
import com.topjohnwu.magisk.data.database.base.Order.Companion.DESC
|
||||
|
||||
interface MagiskQueryBuilder {
|
||||
|
||||
val requestType: String
|
||||
var table: String
|
||||
|
||||
companion object {
|
||||
inline operator fun <reified Builder : MagiskQueryBuilder> invoke(builder: Builder.() -> Unit): MagiskQuery =
|
||||
Builder::class.java.newInstance()
|
||||
.apply(builder)
|
||||
.toString()
|
||||
.let {
|
||||
MagiskQuery(it)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Delete : MagiskQueryBuilder {
|
||||
override val requestType: String = "DELETE FROM"
|
||||
override var table = ""
|
||||
|
||||
private var condition = ""
|
||||
|
||||
fun condition(builder: Condition.() -> Unit) {
|
||||
condition = Condition().apply(builder).toString()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return listOf(requestType, table, condition).joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
class Select : MagiskQueryBuilder {
|
||||
override val requestType: String get() = "SELECT $fields FROM"
|
||||
override lateinit var table: String
|
||||
|
||||
private var fields = "*"
|
||||
private var condition = ""
|
||||
private var orderField = ""
|
||||
|
||||
fun fields(vararg newFields: String) {
|
||||
if (newFields.isEmpty()) {
|
||||
fields = "*"
|
||||
return
|
||||
}
|
||||
fields = newFields.joinToString(", ")
|
||||
}
|
||||
|
||||
fun condition(builder: Condition.() -> Unit) {
|
||||
condition = Condition().apply(builder).toString()
|
||||
}
|
||||
|
||||
fun orderBy(field: String, @OrderStrict order: String) {
|
||||
orderField = "ORDER BY $field $order"
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return listOf(requestType, table, condition, orderField).joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
class Replace : Insert() {
|
||||
override val requestType: String = "REPLACE INTO"
|
||||
}
|
||||
|
||||
open class Insert : MagiskQueryBuilder {
|
||||
override val requestType: String = "INSERT INTO"
|
||||
override lateinit var table: String
|
||||
|
||||
private val keys get() = _values.keys.joinToString(",")
|
||||
private val values get() = _values.values.joinToString(",") {
|
||||
when (it) {
|
||||
is Boolean -> if (it) "1" else "0"
|
||||
is Number -> it.toString()
|
||||
else -> "\"$it\""
|
||||
}
|
||||
}
|
||||
private var _values: Map<String, Any> = mapOf()
|
||||
|
||||
fun values(vararg pairs: Pair<String, Any>) {
|
||||
_values = pairs.toMap()
|
||||
}
|
||||
|
||||
fun values(values: Map<String, Any>) {
|
||||
_values = values
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return listOf(requestType, table, "($keys) VALUES($values)").joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
class Condition {
|
||||
|
||||
private val conditionWord = "WHERE %s"
|
||||
private var condition: String = ""
|
||||
|
||||
fun equals(field: String, value: Any) {
|
||||
condition = when (value) {
|
||||
is String -> "$field=\"$value\""
|
||||
else -> "$field=$value"
|
||||
}
|
||||
}
|
||||
|
||||
fun greaterThan(field: String, value: String) {
|
||||
condition = "$field > $value"
|
||||
}
|
||||
|
||||
fun lessThan(field: String, value: String) {
|
||||
condition = "$field < $value"
|
||||
}
|
||||
|
||||
fun greaterOrEqualTo(field: String, value: String) {
|
||||
condition = "$field >= $value"
|
||||
}
|
||||
|
||||
fun lessOrEqualTo(field: String, value: String) {
|
||||
condition = "$field <= $value"
|
||||
}
|
||||
|
||||
fun and(builder: Condition.() -> Unit) {
|
||||
condition = "($condition AND ${Condition().apply(builder).condition})"
|
||||
}
|
||||
|
||||
fun or(builder: Condition.() -> Unit) {
|
||||
condition = "($condition OR ${Condition().apply(builder).condition})"
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return conditionWord.format(condition)
|
||||
}
|
||||
}
|
||||
|
||||
class Order {
|
||||
|
||||
@set:OrderStrict
|
||||
var order = DESC
|
||||
var field = ""
|
||||
|
||||
companion object {
|
||||
const val ASC = "ASC"
|
||||
const val DESC = "DESC"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@StringDef(ASC, DESC)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class OrderStrict
|
@@ -0,0 +1,81 @@
|
||||
package com.topjohnwu.magisk.data.network
|
||||
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.model.entity.UpdateInfo
|
||||
import com.topjohnwu.magisk.tasks.GithubRepoInfo
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Single
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.adapter.rxjava2.Result
|
||||
import retrofit2.http.*
|
||||
|
||||
interface GithubRawServices {
|
||||
|
||||
//region topjohnwu/magisk_files
|
||||
|
||||
@GET("$MAGISK_FILES/master/stable.json")
|
||||
fun fetchStableUpdate(): Single<UpdateInfo>
|
||||
|
||||
@GET("$MAGISK_FILES/master/beta.json")
|
||||
fun fetchBetaUpdate(): Single<UpdateInfo>
|
||||
|
||||
@GET("$MAGISK_FILES/canary/release.json")
|
||||
fun fetchCanaryUpdate(): Single<UpdateInfo>
|
||||
|
||||
@GET("$MAGISK_FILES/canary/debug.json")
|
||||
fun fetchCanaryDebugUpdate(): Single<UpdateInfo>
|
||||
|
||||
@GET
|
||||
fun fetchCustomUpdate(@Url url: String): Single<UpdateInfo>
|
||||
|
||||
@GET("$MAGISK_FILES/{$REVISION}/snet.jar")
|
||||
@Streaming
|
||||
fun fetchSafetynet(@Path(REVISION) revision: String = Const.SNET_REVISION): Single<ResponseBody>
|
||||
|
||||
@GET("$MAGISK_FILES/{$REVISION}/bootctl")
|
||||
@Streaming
|
||||
fun fetchBootctl(@Path(REVISION) revision: String = Const.BOOTCTL_REVISION): Single<ResponseBody>
|
||||
|
||||
@GET("$MAGISK_MASTER/scripts/module_installer.sh")
|
||||
@Streaming
|
||||
fun fetchInstaller(): Single<ResponseBody>
|
||||
|
||||
@GET("$MAGISK_MODULES/{$MODULE}/master/{$FILE}")
|
||||
fun fetchModuleInfo(@Path(MODULE) id: String, @Path(FILE) file: String): Single<String>
|
||||
|
||||
//endregion
|
||||
|
||||
/**
|
||||
* This method shall be used exclusively for fetching files from urls from previous requests.
|
||||
* Him, who uses it in a wrong way, shall die in an eternal flame.
|
||||
* */
|
||||
@GET
|
||||
@Streaming
|
||||
fun fetchFile(@Url url: String): Single<ResponseBody>
|
||||
|
||||
@GET
|
||||
fun fetchString(@Url url: String): Single<String>
|
||||
|
||||
|
||||
companion object {
|
||||
private const val REVISION = "revision"
|
||||
private const val MODULE = "module"
|
||||
private const val FILE = "file"
|
||||
|
||||
|
||||
private const val MAGISK_FILES = "topjohnwu/magisk_files"
|
||||
private const val MAGISK_MASTER = "topjohnwu/Magisk/master"
|
||||
private const val MAGISK_MODULES = "Magisk-Modules-Repo"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface GithubApiServices {
|
||||
|
||||
@GET("repos")
|
||||
fun fetchRepos(@Query("page") page: Int,
|
||||
@Header(Const.Key.IF_NONE_MATCH) etag: String,
|
||||
@Query("sort") sort: String = "pushed",
|
||||
@Query("per_page") count: Int = 100): Flowable<Result<List<GithubRepoInfo>>>
|
||||
|
||||
}
|
@@ -0,0 +1,106 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.data.database.SettingsDao
|
||||
import com.topjohnwu.magisk.data.database.StringDao
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
interface DBConfig {
|
||||
val settingsDao: SettingsDao
|
||||
val stringDao: StringDao
|
||||
|
||||
fun dbSettings(
|
||||
name: String,
|
||||
default: Int
|
||||
) = DBSettingsValue(name, default)
|
||||
|
||||
fun dbSettings(
|
||||
name: String,
|
||||
default: Boolean
|
||||
) = DBBoolSettings(name, default)
|
||||
|
||||
fun dbStrings(
|
||||
name: String,
|
||||
default: String,
|
||||
sync: Boolean = false
|
||||
) = DBStringsValue(name, default, sync)
|
||||
|
||||
}
|
||||
|
||||
class DBSettingsValue(
|
||||
private val name: String,
|
||||
private val default: Int
|
||||
) : ReadWriteProperty<DBConfig, Int> {
|
||||
|
||||
private var value: Int? = null
|
||||
|
||||
@Synchronized
|
||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Int {
|
||||
if (value == null)
|
||||
value = thisRef.settingsDao.fetch(name, default).blockingGet()
|
||||
return value!!
|
||||
}
|
||||
|
||||
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Int) {
|
||||
synchronized(this) {
|
||||
this.value = value
|
||||
}
|
||||
thisRef.settingsDao.put(name, value)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
class DBBoolSettings(
|
||||
name: String,
|
||||
default: Boolean
|
||||
) : ReadWriteProperty<DBConfig, Boolean> {
|
||||
|
||||
val base = DBSettingsValue(name, if (default) 1 else 0)
|
||||
|
||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Boolean
|
||||
= base.getValue(thisRef, property) != 0
|
||||
|
||||
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Boolean) =
|
||||
base.setValue(thisRef, property, if (value) 1 else 0)
|
||||
}
|
||||
|
||||
class DBStringsValue(
|
||||
private val name: String,
|
||||
private val default: String,
|
||||
private val sync: Boolean
|
||||
) : ReadWriteProperty<DBConfig, String> {
|
||||
|
||||
private var value: String? = null
|
||||
|
||||
@Synchronized
|
||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): String {
|
||||
if (value == null)
|
||||
value = thisRef.stringDao.fetch(name, default).blockingGet()
|
||||
return value!!
|
||||
}
|
||||
|
||||
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: String) {
|
||||
synchronized(this) {
|
||||
this.value = value
|
||||
}
|
||||
if (value.isEmpty()) {
|
||||
if (sync) {
|
||||
thisRef.stringDao.delete(name).blockingAwait()
|
||||
} else {
|
||||
thisRef.stringDao.delete(name)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
} else {
|
||||
if (sync) {
|
||||
thisRef.stringDao.put(name, value).blockingAwait()
|
||||
} else {
|
||||
thisRef.stringDao.put(name, value)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.data.database.LogDao
|
||||
import com.topjohnwu.magisk.data.database.base.suRaw
|
||||
import com.topjohnwu.magisk.extensions.toSingle
|
||||
import com.topjohnwu.magisk.model.entity.MagiskLog
|
||||
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
class LogRepository(
|
||||
private val logDao: LogDao
|
||||
) {
|
||||
|
||||
fun fetchLogs() = logDao.fetchAll()
|
||||
.map { it.sortByDescending { it.date.time }; it }
|
||||
.map { it.wrap() }
|
||||
|
||||
fun fetchMagiskLogs() = "tail -n 5000 ${Const.MAGISK_LOG}".suRaw()
|
||||
.filter { it.isNotEmpty() }
|
||||
|
||||
fun clearLogs() = logDao.deleteAll()
|
||||
fun clearOutdated() = logDao.deleteOutdated()
|
||||
|
||||
fun clearMagiskLogs() = Shell.su("echo -n > " + Const.MAGISK_LOG)
|
||||
.toSingle()
|
||||
.map { it.exec() }
|
||||
|
||||
fun put(log: MagiskLog) = logDao.put(log)
|
||||
|
||||
private fun List<MagiskLog>.wrap(): List<WrappedMagiskLog> {
|
||||
val day = TimeUnit.DAYS.toMillis(1)
|
||||
return groupBy { it.date.time / day }
|
||||
.map { WrappedMagiskLog(it.key * day, it.value) }
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.magisk.data.database.base.su
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.extensions.getLabel
|
||||
import com.topjohnwu.magisk.extensions.packageName
|
||||
import com.topjohnwu.magisk.extensions.toSingle
|
||||
import com.topjohnwu.magisk.model.entity.HideAppInfo
|
||||
import com.topjohnwu.magisk.model.entity.HideTarget
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Single
|
||||
|
||||
class MagiskRepository(
|
||||
private val apiRaw: GithubRawServices,
|
||||
private val packageManager: PackageManager
|
||||
) {
|
||||
|
||||
fun fetchSafetynet() = apiRaw.fetchSafetynet()
|
||||
|
||||
fun fetchUpdate() = when (Config.updateChannel) {
|
||||
Config.Value.DEFAULT_CHANNEL, Config.Value.STABLE_CHANNEL -> apiRaw.fetchStableUpdate()
|
||||
Config.Value.BETA_CHANNEL -> apiRaw.fetchBetaUpdate()
|
||||
Config.Value.CANARY_CHANNEL -> apiRaw.fetchCanaryUpdate()
|
||||
Config.Value.CANARY_DEBUG_CHANNEL -> apiRaw.fetchCanaryDebugUpdate()
|
||||
Config.Value.CUSTOM_CHANNEL -> apiRaw.fetchCustomUpdate(Config.customChannelUrl)
|
||||
else -> throw IllegalArgumentException()
|
||||
}.flatMap {
|
||||
// If remote version is lower than current installed, try switching to beta
|
||||
if (it.magisk.versionCode < Info.magiskVersionCode
|
||||
&& Config.updateChannel == Config.Value.DEFAULT_CHANNEL) {
|
||||
Config.updateChannel = Config.Value.BETA_CHANNEL
|
||||
apiRaw.fetchBetaUpdate()
|
||||
} else {
|
||||
Single.just(it)
|
||||
}
|
||||
}.doOnSuccess { Info.remote = it }
|
||||
|
||||
fun fetchApps() =
|
||||
Single.fromCallable { packageManager.getInstalledApplications(0) }
|
||||
.flattenAsFlowable { it }
|
||||
.filter { it.enabled && !blacklist.contains(it.packageName) }
|
||||
.map {
|
||||
val label = it.getLabel(packageManager)
|
||||
val icon = it.loadIcon(packageManager)
|
||||
HideAppInfo(it, label, icon)
|
||||
}
|
||||
.filter { it.processes.isNotEmpty() }
|
||||
.toList()
|
||||
|
||||
fun fetchHideTargets() = Shell.su("magiskhide --ls").toSingle()
|
||||
.map { it.exec().out }
|
||||
.flattenAsFlowable { it }
|
||||
.map { HideTarget(it) }
|
||||
.toList()
|
||||
|
||||
fun toggleHide(isEnabled: Boolean, packageName: String, process: String) =
|
||||
"magiskhide --%s %s %s".format(isEnabled.state, packageName, process).su().ignoreElement()
|
||||
|
||||
private val Boolean.state get() = if (this) "add" else "rm"
|
||||
|
||||
companion object {
|
||||
private val blacklist by lazy { listOf(
|
||||
packageName,
|
||||
"android",
|
||||
"com.android.chrome",
|
||||
"com.chrome.beta",
|
||||
"com.chrome.dev",
|
||||
"com.chrome.canary",
|
||||
"com.android.webview",
|
||||
"com.google.android.webview"
|
||||
) }
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||
|
||||
class StringRepository(
|
||||
private val api: GithubRawServices
|
||||
) {
|
||||
|
||||
fun getString(url: String) = api.fetchString(url)
|
||||
|
||||
fun getMetadata(repo: Repo) = api.fetchModuleInfo(repo.id, "module.prop")
|
||||
|
||||
fun getReadme(repo: Repo) = api.fetchModuleInfo(repo.id, "README.md")
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.databinding.BindingAdapter
|
||||
|
||||
@BindingAdapter("gone")
|
||||
fun setGone(view: View, gone: Boolean) {
|
||||
view.isGone = gone
|
||||
}
|
||||
|
||||
@BindingAdapter("invisible")
|
||||
fun setInvisible(view: View, invisible: Boolean) {
|
||||
view.isInvisible = invisible
|
||||
}
|
||||
|
||||
@BindingAdapter("goneUnless")
|
||||
fun setGoneUnless(view: View, goneUnless: Boolean) {
|
||||
setGone(view, goneUnless.not())
|
||||
}
|
||||
|
||||
@BindingAdapter("invisibleUnless")
|
||||
fun setInvisibleUnless(view: View, invisibleUnless: Boolean) {
|
||||
setInvisible(view, invisibleUnless.not())
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.graphics.drawable.InsetDrawable
|
||||
import androidx.databinding.BindingAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.magisk.extensions.startEndToLeftRight
|
||||
import com.topjohnwu.magisk.extensions.toPx
|
||||
import com.topjohnwu.magisk.utils.KItemDecoration
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@BindingAdapter(
|
||||
"dividerColor",
|
||||
"dividerHorizontal",
|
||||
"dividerSize",
|
||||
"dividerAfterLast",
|
||||
"dividerMarginStart",
|
||||
"dividerMarginEnd",
|
||||
"dividerMarginTop",
|
||||
"dividerMarginBottom",
|
||||
requireAll = false
|
||||
)
|
||||
fun setDivider(
|
||||
view: RecyclerView,
|
||||
color: Int,
|
||||
horizontal: Boolean,
|
||||
_size: Float,
|
||||
_afterLast: Boolean?,
|
||||
marginStartF: Float,
|
||||
marginEndF: Float,
|
||||
marginTopF: Float,
|
||||
marginBottomF: Float
|
||||
) {
|
||||
val orientation = if (horizontal) RecyclerView.HORIZONTAL else RecyclerView.VERTICAL
|
||||
val size = if (_size > 0) _size.roundToInt() else 1.toPx()
|
||||
val (width, height) = if (horizontal) size to 1 else 1 to size
|
||||
val afterLast = _afterLast ?: true
|
||||
|
||||
val marginStart = marginStartF.roundToInt()
|
||||
val marginEnd = marginEndF.roundToInt()
|
||||
val marginTop = marginTopF.roundToInt()
|
||||
val marginBottom = marginBottomF.roundToInt()
|
||||
val (marginLeft, marginRight) = view.context.startEndToLeftRight(marginStart, marginEnd)
|
||||
|
||||
val drawable = GradientDrawable().apply {
|
||||
setSize(width, height)
|
||||
shape = GradientDrawable.RECTANGLE
|
||||
setColor(color)
|
||||
}.let {
|
||||
InsetDrawable(it, marginLeft, marginTop, marginRight, marginBottom)
|
||||
}
|
||||
|
||||
val decoration = KItemDecoration(view.context, orientation)
|
||||
.setDeco(drawable)
|
||||
.apply { showAfterLast = afterLast }
|
||||
view.addItemDecoration(decoration)
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
|
||||
|
||||
open class BindingBoundAdapter : BindingRecyclerViewAdapter<RvItem>() {
|
||||
|
||||
override fun onBindBinding(binding: ViewDataBinding, variableId: Int, layoutRes: Int, position: Int, item: RvItem) {
|
||||
super.onBindBinding(binding, variableId, layoutRes, position, item)
|
||||
|
||||
item.onBindingBound(binding)
|
||||
}
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
||||
|
||||
abstract class RvItem {
|
||||
|
||||
abstract val layoutRes: Int
|
||||
|
||||
@CallSuper
|
||||
open fun bind(binding: ItemBinding<*>) {
|
||||
binding.set(BR.item, layoutRes)
|
||||
}
|
||||
|
||||
/**
|
||||
* This callback is useful if you want to manipulate your views directly.
|
||||
* If you want to use this callback, you must set [me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter]
|
||||
* on your RecyclerView and call it from there. You can use [BindingBoundAdapter] for your convenience.
|
||||
*/
|
||||
open fun onBindingBound(binding: ViewDataBinding) {}
|
||||
}
|
||||
|
||||
abstract class ComparableRvItem<in T> : RvItem() {
|
||||
|
||||
abstract fun itemSameAs(other: T): Boolean
|
||||
abstract fun contentSameAs(other: T): Boolean
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
open fun genericItemSameAs(other: Any): Boolean = other::class == this::class && itemSameAs(other as T)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
open fun genericContentSameAs(other: Any): Boolean = other::class == this::class && contentSameAs(other as T)
|
||||
|
||||
companion object {
|
||||
val callback = object : DiffObservableList.Callback<ComparableRvItem<*>> {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: ComparableRvItem<*>,
|
||||
newItem: ComparableRvItem<*>
|
||||
) = oldItem.genericItemSameAs(newItem)
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: ComparableRvItem<*>,
|
||||
newItem: ComparableRvItem<*>
|
||||
) = oldItem.genericContentSameAs(newItem)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.topjohnwu.magisk.utils.RxBus
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val SUTimeout = named("su_timeout")
|
||||
val Protected = named("protected")
|
||||
|
||||
val applicationModule = module {
|
||||
single { RxBus() }
|
||||
factory { get<Context>().resources }
|
||||
factory { get<Context>().packageManager }
|
||||
factory(Protected) { createDEContext(get()) }
|
||||
single(SUTimeout) { get<Context>(Protected).getSharedPreferences("su_timeout", 0) }
|
||||
single { PreferenceManager.getDefaultSharedPreferences(get<Context>(Protected)) }
|
||||
single { ActivityTracker() }
|
||||
factory { get<ActivityTracker>().foreground ?: NullActivity }
|
||||
}
|
||||
|
||||
private fun createDEContext(context: Context): Context {
|
||||
return if (Build.VERSION.SDK_INT >= 24)
|
||||
context.createDeviceProtectedStorageContext()
|
||||
else context
|
||||
}
|
||||
|
||||
class ActivityTracker : Application.ActivityLifecycleCallbacks {
|
||||
|
||||
@Volatile
|
||||
var foreground: Activity? = null
|
||||
|
||||
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
||||
|
||||
override fun onActivityStarted(activity: Activity) {}
|
||||
|
||||
@Synchronized
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
foreground = activity
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
foreground = null
|
||||
}
|
||||
|
||||
override fun onActivityStopped(activity: Activity) {}
|
||||
|
||||
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {}
|
||||
}
|
||||
|
||||
@SuppressLint("Registered")
|
||||
object NullActivity : Activity()
|
23
app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt
Normal file
23
app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt
Normal file
@@ -0,0 +1,23 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.topjohnwu.magisk.data.database.*
|
||||
import com.topjohnwu.magisk.tasks.RepoUpdater
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
val databaseModule = module {
|
||||
single { LogDao() }
|
||||
single { PolicyDao(get()) }
|
||||
single { SettingsDao() }
|
||||
single { StringDao() }
|
||||
single { createRepoDatabase(get()) }
|
||||
single { get<RepoDatabase>().repoDao() }
|
||||
single { RepoUpdater(get(), get()) }
|
||||
}
|
||||
|
||||
fun createRepoDatabase(context: Context) =
|
||||
Room.databaseBuilder(context, RepoDatabase::class.java, "repo.db")
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
9
app/src/main/java/com/topjohnwu/magisk/di/Modules.kt
Normal file
9
app/src/main/java/com/topjohnwu/magisk/di/Modules.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
val koinModules = listOf(
|
||||
applicationModule,
|
||||
networkingModule,
|
||||
databaseModule,
|
||||
repositoryModule,
|
||||
viewModelModules
|
||||
)
|
@@ -0,0 +1,83 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.data.network.GithubApiServices
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.net.Networking
|
||||
import com.topjohnwu.magisk.net.NoSSLv3SocketFactory
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.html.HtmlPlugin
|
||||
import io.noties.markwon.image.ImagesPlugin
|
||||
import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.koin.dsl.module
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.converter.scalars.ScalarsConverterFactory
|
||||
import se.ansman.kotshi.KotshiJsonAdapterFactory
|
||||
|
||||
val networkingModule = module {
|
||||
single { createOkHttpClient(get()) }
|
||||
single { createRetrofit(get()) }
|
||||
single { createApiService<GithubRawServices>(get(), Const.Url.GITHUB_RAW_URL) }
|
||||
single { createApiService<GithubApiServices>(get(), Const.Url.GITHUB_API_URL) }
|
||||
single { createMarkwon(get(), get()) }
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun createOkHttpClient(context: Context): OkHttpClient {
|
||||
val builder = OkHttpClient.Builder()
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.HEADERS
|
||||
}
|
||||
builder.addInterceptor(httpLoggingInterceptor)
|
||||
}
|
||||
|
||||
if (!Networking.init(context)) {
|
||||
builder.sslSocketFactory(NoSSLv3SocketFactory())
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun createMoshiConverterFactory(): MoshiConverterFactory {
|
||||
val moshi = Moshi.Builder()
|
||||
.add(KotshiJsonAdapterFactory)
|
||||
.build()
|
||||
return MoshiConverterFactory.create(moshi)
|
||||
}
|
||||
|
||||
fun createRetrofit(okHttpClient: OkHttpClient): Retrofit.Builder {
|
||||
return Retrofit.Builder()
|
||||
.addConverterFactory(ScalarsConverterFactory.create())
|
||||
.addConverterFactory(createMoshiConverterFactory())
|
||||
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
|
||||
.client(okHttpClient)
|
||||
}
|
||||
|
||||
@KotshiJsonAdapterFactory
|
||||
abstract class JsonAdapterFactory : JsonAdapter.Factory
|
||||
|
||||
inline fun <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseUrl: String): T {
|
||||
return retrofitBuilder
|
||||
.baseUrl(baseUrl)
|
||||
.build()
|
||||
.create(T::class.java)
|
||||
}
|
||||
|
||||
fun createMarkwon(context: Context, okHttpClient: OkHttpClient): Markwon {
|
||||
return Markwon.builder(context)
|
||||
.usePlugin(HtmlPlugin.create())
|
||||
.usePlugin(ImagesPlugin.create {
|
||||
it.addSchemeHandler(OkHttpNetworkSchemeHandler.create(okHttpClient))
|
||||
})
|
||||
.build()
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
||||
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
||||
import com.topjohnwu.magisk.data.repository.StringRepository
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
val repositoryModule = module {
|
||||
single { MagiskRepository(get(), get()) }
|
||||
single { LogRepository(get()) }
|
||||
single { StringRepository(get()) }
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import android.net.Uri
|
||||
import com.topjohnwu.magisk.ui.MainViewModel
|
||||
import com.topjohnwu.magisk.ui.flash.FlashViewModel
|
||||
import com.topjohnwu.magisk.ui.hide.HideViewModel
|
||||
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
||||
import com.topjohnwu.magisk.ui.log.LogViewModel
|
||||
import com.topjohnwu.magisk.ui.module.ModuleViewModel
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
val viewModelModules = module {
|
||||
viewModel { MainViewModel() }
|
||||
viewModel { HomeViewModel(get()) }
|
||||
viewModel { SuperuserViewModel(get(), get(), get(), get()) }
|
||||
viewModel { HideViewModel(get(), get()) }
|
||||
viewModel { ModuleViewModel(get(), get(), get()) }
|
||||
viewModel { LogViewModel(get(), get()) }
|
||||
viewModel { (action: String, file: Uri, additional: Uri) ->
|
||||
FlashViewModel(action, file, additional, get())
|
||||
}
|
||||
viewModel { SuRequestViewModel(get(), get(), get(SUTimeout), get()) }
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import androidx.databinding.Observable
|
||||
import androidx.databinding.ObservableBoolean
|
||||
import androidx.databinding.ObservableField
|
||||
import androidx.databinding.ObservableInt
|
||||
|
||||
fun <T> ObservableField<T>.addOnPropertyChangedCallback(
|
||||
removeAfterChanged: Boolean = false,
|
||||
callback: (T?) -> Unit
|
||||
) {
|
||||
addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
||||
callback(get())
|
||||
if (removeAfterChanged) removeOnPropertyChangedCallback(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun ObservableInt.addOnPropertyChangedCallback(
|
||||
removeAfterChanged: Boolean = false,
|
||||
callback: (Int) -> Unit
|
||||
) {
|
||||
addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
||||
callback(get())
|
||||
if (removeAfterChanged) removeOnPropertyChangedCallback(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun ObservableBoolean.addOnPropertyChangedCallback(
|
||||
removeAfterChanged: Boolean = false,
|
||||
callback: (Boolean) -> Unit
|
||||
) {
|
||||
addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
||||
callback(get())
|
||||
if (removeAfterChanged) removeOnPropertyChangedCallback(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
inline fun <T> ObservableField<T>.update(block: (T?) -> Unit) {
|
||||
set(get().apply(block))
|
||||
}
|
||||
|
||||
inline fun <T> ObservableField<T>.updateNonNull(block: (T) -> Unit) {
|
||||
update {
|
||||
it ?: return@update
|
||||
block(it)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun ObservableInt.update(block: (Int) -> Unit) {
|
||||
set(get().apply(block))
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.content.res.Resources
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun Int.toDp(): Int = ceil(this / Resources.getSystem().displayMetrics.density).roundToInt()
|
||||
|
||||
fun Int.toPx(): Int = (this * Resources.getSystem().displayMetrics.density).roundToInt()
|
@@ -0,0 +1,6 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
|
||||
fun ui(body: () -> Unit) = Handler(Looper.getMainLooper()).post(body)
|
201
app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt
Normal file
201
app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt
Normal file
@@ -0,0 +1,201 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import androidx.databinding.ObservableField
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
import io.reactivex.*
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposables
|
||||
import io.reactivex.functions.BiFunction
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import androidx.databinding.Observable as BindingObservable
|
||||
|
||||
fun <T> Observable<T>.applySchedulers(
|
||||
subscribeOn: Scheduler = Schedulers.io(),
|
||||
observeOn: Scheduler = AndroidSchedulers.mainThread()
|
||||
): Observable<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
|
||||
|
||||
fun <T> Flowable<T>.applySchedulers(
|
||||
subscribeOn: Scheduler = Schedulers.io(),
|
||||
observeOn: Scheduler = AndroidSchedulers.mainThread()
|
||||
): Flowable<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
|
||||
|
||||
fun <T> Single<T>.applySchedulers(
|
||||
subscribeOn: Scheduler = Schedulers.io(),
|
||||
observeOn: Scheduler = AndroidSchedulers.mainThread()
|
||||
): Single<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
|
||||
|
||||
fun <T> Maybe<T>.applySchedulers(
|
||||
subscribeOn: Scheduler = Schedulers.io(),
|
||||
observeOn: Scheduler = AndroidSchedulers.mainThread()
|
||||
): Maybe<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
|
||||
|
||||
fun Completable.applySchedulers(
|
||||
subscribeOn: Scheduler = Schedulers.io(),
|
||||
observeOn: Scheduler = AndroidSchedulers.mainThread()
|
||||
): Completable = this.subscribeOn(subscribeOn).observeOn(observeOn)
|
||||
|
||||
/*=== ALIASES FOR OBSERVABLES ===*/
|
||||
|
||||
typealias OnCompleteListener = () -> Unit
|
||||
typealias OnSuccessListener<T> = (T) -> Unit
|
||||
typealias OnErrorListener = (Throwable) -> Unit
|
||||
|
||||
/*=== ALIASES FOR OBSERVABLES ===*/
|
||||
|
||||
fun <T> Observable<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {},
|
||||
onNext: OnSuccessListener<T> = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onNext, onError, onComplete)
|
||||
|
||||
fun <T> Single<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onNext: OnSuccessListener<T> = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onNext, onError)
|
||||
|
||||
fun <T> Maybe<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {},
|
||||
onNext: OnSuccessListener<T> = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onNext, onError, onComplete)
|
||||
|
||||
fun <T> Flowable<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {},
|
||||
onNext: OnSuccessListener<T> = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onNext, onError, onComplete)
|
||||
|
||||
fun Completable.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onComplete, onError)
|
||||
|
||||
|
||||
fun <T> Observable<out T>.updateBy(
|
||||
field: KObservableField<T?>
|
||||
) = doOnNextUi { field.value = it }
|
||||
.doOnErrorUi { field.value = null }
|
||||
|
||||
fun <T> Single<out T>.updateBy(
|
||||
field: KObservableField<T?>
|
||||
) = doOnSuccessUi { field.value = it }
|
||||
.doOnErrorUi { field.value = null }
|
||||
|
||||
fun <T> Maybe<out T>.updateBy(
|
||||
field: KObservableField<T?>
|
||||
) = doOnSuccessUi { field.value = it }
|
||||
.doOnErrorUi { field.value = null }
|
||||
.doOnComplete { field.value = field.value }
|
||||
|
||||
fun <T> Flowable<out T>.updateBy(
|
||||
field: KObservableField<T?>
|
||||
) = doOnNextUi { field.value = it }
|
||||
.doOnErrorUi { field.value = null }
|
||||
|
||||
fun Completable.updateBy(
|
||||
field: KObservableField<Boolean>
|
||||
) = doOnCompleteUi { field.value = true }
|
||||
.doOnErrorUi { field.value = false }
|
||||
|
||||
|
||||
fun <T> Observable<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { ui { body() } }
|
||||
|
||||
fun <T> Single<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { ui { body() } }
|
||||
|
||||
fun <T> Maybe<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { ui { body() } }
|
||||
|
||||
fun <T> Flowable<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { ui { body() } }
|
||||
|
||||
fun Completable.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { ui { body() } }
|
||||
|
||||
|
||||
fun <T> Observable<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { ui { body(it) } }
|
||||
|
||||
fun <T> Single<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { ui { body(it) } }
|
||||
|
||||
fun <T> Maybe<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { ui { body(it) } }
|
||||
|
||||
fun <T> Flowable<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { ui { body(it) } }
|
||||
|
||||
fun Completable.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { ui { body(it) } }
|
||||
|
||||
|
||||
fun <T> Observable<T>.doOnNextUi(body: (T) -> Unit) =
|
||||
doOnNext { ui { body(it) } }
|
||||
|
||||
fun <T> Flowable<T>.doOnNextUi(body: (T) -> Unit) =
|
||||
doOnNext { ui { body(it) } }
|
||||
|
||||
fun <T> Single<T>.doOnSuccessUi(body: (T) -> Unit) =
|
||||
doOnSuccess { ui { body(it) } }
|
||||
|
||||
fun <T> Maybe<T>.doOnSuccessUi(body: (T) -> Unit) =
|
||||
doOnSuccess { ui { body(it) } }
|
||||
|
||||
fun <T> Maybe<T>.doOnCompleteUi(body: () -> Unit) =
|
||||
doOnComplete { ui { body() } }
|
||||
|
||||
fun Completable.doOnCompleteUi(body: () -> Unit) =
|
||||
doOnComplete { ui { body() } }
|
||||
|
||||
|
||||
fun <T, R> Observable<List<T>>.mapList(
|
||||
transformer: (T) -> R
|
||||
) = flatMapIterable { it }
|
||||
.map(transformer)
|
||||
.toList()
|
||||
|
||||
fun <T, R> Single<List<T>>.mapList(
|
||||
transformer: (T) -> R
|
||||
) = flattenAsFlowable { it }
|
||||
.map(transformer)
|
||||
.toList()
|
||||
|
||||
fun <T, R> Maybe<List<T>>.mapList(
|
||||
transformer: (T) -> R
|
||||
) = flattenAsFlowable { it }
|
||||
.map(transformer)
|
||||
.toList()
|
||||
|
||||
fun <T, R> Flowable<List<T>>.mapList(
|
||||
transformer: (T) -> R
|
||||
) = flatMapIterable { it }
|
||||
.map(transformer)
|
||||
.toList()
|
||||
|
||||
fun <T> ObservableField<T>.toObservable(): Observable<T> {
|
||||
val observableField = this
|
||||
return Observable.create { emitter ->
|
||||
observableField.get()?.let { emitter.onNext(it) }
|
||||
|
||||
val callback = object : BindingObservable.OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: BindingObservable?, propertyId: Int) {
|
||||
observableField.get()?.let { emitter.onNext(it) }
|
||||
}
|
||||
}
|
||||
observableField.addOnPropertyChangedCallback(callback)
|
||||
emitter.setDisposable(Disposables.fromAction {
|
||||
observableField.removeOnPropertyChangedCallback(callback)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : Any> T.toSingle() = Single.just(this)
|
||||
|
||||
fun <T1, T2, R> zip(t1: Single<T1>, t2: Single<T2>, zipper: (T1, T2) -> R) =
|
||||
Single.zip(t1, t2, BiFunction<T1, T2, R> { rt1, rt2 -> zipper(rt1, rt2) })
|
126
app/src/main/java/com/topjohnwu/magisk/extensions/Snackbar.kt
Normal file
126
app/src/main/java/com/topjohnwu/magisk/extensions/Snackbar.kt
Normal file
@@ -0,0 +1,126 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
||||
fun AppCompatActivity.snackbar(
|
||||
view: View,
|
||||
@StringRes messageRes: Int,
|
||||
length: Int = Snackbar.LENGTH_SHORT,
|
||||
f: Snackbar.() -> Unit = {}
|
||||
) {
|
||||
snackbar(view, getString(messageRes), length, f)
|
||||
}
|
||||
|
||||
fun AppCompatActivity.snackbar(
|
||||
view: View,
|
||||
message: String,
|
||||
length: Int = Snackbar.LENGTH_SHORT,
|
||||
f: Snackbar.() -> Unit = {}
|
||||
) = Snackbar.make(view, message, length)
|
||||
.apply(f)
|
||||
.show()
|
||||
|
||||
fun Fragment.snackbar(
|
||||
view: View,
|
||||
@StringRes messageRes: Int,
|
||||
length: Int = Snackbar.LENGTH_SHORT,
|
||||
f: Snackbar.() -> Unit = {}
|
||||
) {
|
||||
snackbar(view, getString(messageRes), length, f)
|
||||
}
|
||||
|
||||
fun Fragment.snackbar(
|
||||
view: View,
|
||||
message: String,
|
||||
length: Int = Snackbar.LENGTH_SHORT,
|
||||
f: Snackbar.() -> Unit = {}
|
||||
) = Snackbar.make(view, message, length)
|
||||
.apply(f)
|
||||
.show()
|
||||
|
||||
fun Snackbar.action(init: KSnackbar.() -> Unit) = apply {
|
||||
val config = KSnackbar().apply(init)
|
||||
|
||||
setAction(config.title(context), config.onClickListener)
|
||||
|
||||
when {
|
||||
config.hasValidColor -> setActionTextColor(config.color(context) ?: return@apply)
|
||||
config.hasValidColorStateList -> setActionTextColor(config.colorStateList(context) ?: return@apply)
|
||||
}
|
||||
}
|
||||
|
||||
class KSnackbar {
|
||||
var colorRes: Int = -1
|
||||
var colorStateListRes: Int = -1
|
||||
|
||||
var title: CharSequence = ""
|
||||
var titleRes: Int = -1
|
||||
|
||||
internal var onClickListener: (View) -> Unit = {}
|
||||
internal val hasValidColor get() = colorRes != -1
|
||||
internal val hasValidColorStateList get() = colorStateListRes != -1
|
||||
|
||||
fun onClicked(listener: (View) -> Unit) {
|
||||
onClickListener = listener
|
||||
}
|
||||
|
||||
internal fun title(context: Context) = if (title.isBlank()) context.getString(titleRes) else title
|
||||
internal fun colorStateList(context: Context) = context.colorStateListCompat(colorStateListRes)
|
||||
internal fun color(context: Context) = context.colorCompat(colorRes)
|
||||
}
|
||||
|
||||
@Deprecated("Kotlin DSL version is preferred", ReplaceWith("action {}"))
|
||||
fun Snackbar.action(
|
||||
@StringRes actionRes: Int,
|
||||
@ColorRes colorRes: Int? = null,
|
||||
listener: (View) -> Unit
|
||||
) {
|
||||
view.resources.getString(actionRes)
|
||||
colorRes?.let { ContextCompat.getColor(view.context, colorRes) }
|
||||
action {}
|
||||
}
|
||||
|
||||
@Deprecated("Kotlin DSL version is preferred", ReplaceWith("action {}"))
|
||||
fun Snackbar.action(action: String, @ColorInt color: Int? = null, listener: (View) -> Unit) {
|
||||
setAction(action, listener)
|
||||
color?.let { setActionTextColor(color) }
|
||||
}
|
||||
|
||||
fun Snackbar.textColorRes(@ColorRes colorRes: Int) {
|
||||
textColor(context.colorCompat(colorRes) ?: return)
|
||||
}
|
||||
|
||||
fun Snackbar.textColor(@ColorInt color: Int) {
|
||||
val tv = view.findViewById<TextView>(com.google.android.material.R.id.snackbar_text)
|
||||
tv.setTextColor(color)
|
||||
}
|
||||
|
||||
fun Snackbar.backgroundColorRes(@ColorRes colorRes: Int) {
|
||||
backgroundColor(context.colorCompat(colorRes) ?: return)
|
||||
}
|
||||
|
||||
fun Snackbar.backgroundColor(@ColorInt color: Int) {
|
||||
ViewCompat.setBackgroundTintList(
|
||||
view,
|
||||
ColorStateList.valueOf(color)
|
||||
)
|
||||
}
|
||||
|
||||
fun Snackbar.alert() {
|
||||
textColor(0xF44336)
|
||||
}
|
||||
|
||||
fun Snackbar.success() {
|
||||
textColor(0x4CAF50)
|
||||
}
|
159
app/src/main/java/com/topjohnwu/magisk/extensions/XAndroid.kt
Normal file
159
app/src/main/java/com/topjohnwu/magisk/extensions/XAndroid.kt
Normal file
@@ -0,0 +1,159 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.ComponentInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.*
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.OpenableColumns
|
||||
import android.view.View
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import com.topjohnwu.magisk.utils.FileProvider
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.utils.currentLocale
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
val packageName: String get() = get<Context>().packageName
|
||||
|
||||
val PackageInfo.processes
|
||||
get() = activities?.processNames.orEmpty() +
|
||||
services?.processNames.orEmpty() +
|
||||
receivers?.processNames.orEmpty() +
|
||||
providers?.processNames.orEmpty()
|
||||
|
||||
val Array<out ComponentInfo>.processNames get() = mapNotNull { it.processName }
|
||||
|
||||
val ApplicationInfo.packageInfo: PackageInfo?
|
||||
get() {
|
||||
val pm: PackageManager by inject()
|
||||
|
||||
return try {
|
||||
val request = GET_ACTIVITIES or
|
||||
GET_SERVICES or
|
||||
GET_RECEIVERS or
|
||||
GET_PROVIDERS
|
||||
pm.getPackageInfo(packageName, request)
|
||||
} catch (e1: Exception) {
|
||||
try {
|
||||
pm.activities(packageName).apply {
|
||||
services = pm.services(packageName)
|
||||
receivers = pm.receivers(packageName)
|
||||
providers = pm.providers(packageName)
|
||||
}
|
||||
} catch (e2: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val Uri.fileName: String
|
||||
get() {
|
||||
var name: String? = null
|
||||
get<Context>().contentResolver.query(this, null, null, null, null)?.use { c ->
|
||||
val nameIndex = c.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
if (nameIndex != -1) {
|
||||
c.moveToFirst()
|
||||
name = c.getString(nameIndex)
|
||||
}
|
||||
}
|
||||
if (name == null && path != null) {
|
||||
val idx = path!!.lastIndexOf('/')
|
||||
name = path!!.substring(idx + 1)
|
||||
}
|
||||
return name.orEmpty()
|
||||
}
|
||||
|
||||
fun PackageManager.activities(packageName: String) =
|
||||
getPackageInfo(packageName, GET_ACTIVITIES)
|
||||
|
||||
fun PackageManager.services(packageName: String) =
|
||||
getPackageInfo(packageName, GET_SERVICES).services
|
||||
|
||||
fun PackageManager.receivers(packageName: String) =
|
||||
getPackageInfo(packageName, GET_RECEIVERS).receivers
|
||||
|
||||
fun PackageManager.providers(packageName: String) =
|
||||
getPackageInfo(packageName, GET_PROVIDERS).providers
|
||||
|
||||
fun Context.rawResource(id: Int) = resources.openRawResource(id)
|
||||
|
||||
fun Context.readUri(uri: Uri) =
|
||||
contentResolver.openInputStream(uri) ?: throw FileNotFoundException()
|
||||
|
||||
fun Intent.startActivity(context: Context) = context.startActivity(this)
|
||||
|
||||
fun File.provide(context: Context = get()): Uri {
|
||||
return FileProvider.getUriForFile(context, context.packageName + ".provider", this)
|
||||
}
|
||||
|
||||
fun File.mv(destination: File) {
|
||||
inputStream().writeTo(destination)
|
||||
deleteRecursively()
|
||||
}
|
||||
|
||||
fun String.toFile() = File(this)
|
||||
|
||||
fun Intent.chooser(title: String = "Pick an app") = Intent.createChooser(this, title)
|
||||
|
||||
fun Context.cachedFile(name: String) = File(cacheDir, name)
|
||||
|
||||
fun <Result> Cursor.toList(transformer: (Cursor) -> Result): List<Result> {
|
||||
val out = mutableListOf<Result>()
|
||||
while (moveToNext()) out.add(transformer(this))
|
||||
return out
|
||||
}
|
||||
|
||||
fun ApplicationInfo.getLabel(pm: PackageManager): String {
|
||||
runCatching {
|
||||
if (labelRes > 0) {
|
||||
val res = pm.getResourcesForApplication(this)
|
||||
val config = Configuration()
|
||||
config.setLocale(currentLocale)
|
||||
res.updateConfiguration(config, res.displayMetrics)
|
||||
return res.getString(labelRes)
|
||||
}
|
||||
}
|
||||
|
||||
return loadLabel(pm).toString()
|
||||
}
|
||||
|
||||
fun Intent.exists(packageManager: PackageManager) = resolveActivity(packageManager) != null
|
||||
|
||||
fun Context.colorCompat(@ColorRes id: Int) = try {
|
||||
ContextCompat.getColor(this, id)
|
||||
} catch (e: Resources.NotFoundException) {
|
||||
null
|
||||
}
|
||||
|
||||
fun Context.colorStateListCompat(@ColorRes id: Int) = try {
|
||||
ContextCompat.getColorStateList(this, id)
|
||||
} catch (e: Resources.NotFoundException) {
|
||||
null
|
||||
}
|
||||
|
||||
fun Context.drawableCompat(@DrawableRes id: Int) = ContextCompat.getDrawable(this, id)
|
||||
/**
|
||||
* Pass [start] and [end] dimensions, function will return left and right
|
||||
* with respect to RTL layout direction
|
||||
*/
|
||||
fun Context.startEndToLeftRight(start: Int, end: Int): Pair<Int, Int> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 &&
|
||||
resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
|
||||
) {
|
||||
return end to start
|
||||
}
|
||||
return start to end
|
||||
}
|
||||
|
||||
fun Context.openUrl(url: String) = Utils.openLink(this, url.toUri())
|
@@ -0,0 +1,8 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
|
||||
|
||||
fun KObservableField<Boolean>.toggle() {
|
||||
value = !value
|
||||
}
|
103
app/src/main/java/com/topjohnwu/magisk/extensions/XJava.kt
Normal file
103
app/src/main/java/com/topjohnwu/magisk/extensions/XJava.kt
Normal file
@@ -0,0 +1,103 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.net.toFile
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.*
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import kotlin.NoSuchElementException
|
||||
|
||||
fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
|
||||
var entry: ZipEntry? = nextEntry
|
||||
while (entry != null) {
|
||||
callback(entry)
|
||||
entry = nextEntry
|
||||
}
|
||||
}
|
||||
|
||||
fun Uri.writeTo(file: File) = toFile().copyTo(file)
|
||||
|
||||
fun InputStream.writeTo(file: File) =
|
||||
withStreams(this, file.outputStream()) { reader, writer -> reader.copyTo(writer) }
|
||||
|
||||
inline fun <In : InputStream, Out : OutputStream> withStreams(
|
||||
inStream: In,
|
||||
outStream: Out,
|
||||
withBoth: (In, Out) -> Unit
|
||||
) {
|
||||
inStream.use { reader ->
|
||||
outStream.use { writer ->
|
||||
withBoth(reader, writer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T, R> List<T>.firstMap(mapper: (T) -> R?): R {
|
||||
for (item: T in this) {
|
||||
return mapper(item) ?: continue
|
||||
}
|
||||
throw NoSuchElementException("Collection contains no element matching the predicate.")
|
||||
}
|
||||
|
||||
fun String.langTagToLocale(): Locale {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
return Locale.forLanguageTag(this)
|
||||
} else {
|
||||
val tok = split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
if (tok.isEmpty()) {
|
||||
return Locale("")
|
||||
}
|
||||
val language = when (tok[0]) {
|
||||
"und" -> "" // Undefined
|
||||
"fil" -> "tl" // Filipino
|
||||
else -> tok[0]
|
||||
}
|
||||
if (language.length != 2 && language.length != 3)
|
||||
return Locale("")
|
||||
if (tok.size == 1)
|
||||
return Locale(language)
|
||||
val country = tok[1]
|
||||
|
||||
return if (country.length != 2 && country.length != 3) Locale(language)
|
||||
else Locale(language, country)
|
||||
}
|
||||
}
|
||||
|
||||
fun Locale.toLangTag(): String {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
return toLanguageTag()
|
||||
} else {
|
||||
var language = language
|
||||
var country = country
|
||||
var variant = variant
|
||||
when {
|
||||
language.isEmpty() || !language.matches("\\p{Alpha}{2,8}".toRegex()) ->
|
||||
language = "und" // Follow the Locale#toLanguageTag() implementation
|
||||
language == "iw" -> language = "he" // correct deprecated "Hebrew"
|
||||
language == "in" -> language = "id" // correct deprecated "Indonesian"
|
||||
language == "ji" -> language = "yi" // correct deprecated "Yiddish"
|
||||
}
|
||||
// ensure valid country code, if not well formed, it's omitted
|
||||
|
||||
// variant subtags that begin with a letter must be at least 5 characters long
|
||||
// ensure valid country code, if not well formed, it's omitted
|
||||
if (!country.matches("\\p{Alpha}{2}|\\p{Digit}{3}".toRegex())) {
|
||||
country = ""
|
||||
}
|
||||
|
||||
// variant subtags that begin with a letter must be at least 5 characters long
|
||||
if (!variant.matches("\\p{Alnum}{5,8}|\\p{Digit}\\p{Alnum}{3}".toRegex())) {
|
||||
variant = ""
|
||||
}
|
||||
val tag = StringBuilder(language)
|
||||
if (country.isNotEmpty())
|
||||
tag.append('-').append(country)
|
||||
if (variant.isNotEmpty())
|
||||
tag.append('-').append(variant)
|
||||
return tag.toString()
|
||||
}
|
||||
}
|
17
app/src/main/java/com/topjohnwu/magisk/extensions/XKoin.kt
Normal file
17
app/src/main/java/com/topjohnwu/magisk/extensions/XKoin.kt
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import org.koin.core.context.GlobalContext
|
||||
import org.koin.core.parameter.ParametersDefinition
|
||||
import org.koin.core.qualifier.Qualifier
|
||||
|
||||
fun getKoin() = GlobalContext.get().koin
|
||||
|
||||
inline fun <reified T> inject(
|
||||
qualifier: Qualifier? = null,
|
||||
noinline parameters: ParametersDefinition? = null
|
||||
) = lazy { get<T>(qualifier, parameters) }
|
||||
|
||||
inline fun <reified T> get(
|
||||
qualifier: Qualifier? = null,
|
||||
noinline parameters: ParametersDefinition? = null
|
||||
): T = getKoin().get(qualifier, parameters)
|
83
app/src/main/java/com/topjohnwu/magisk/extensions/XList.kt
Normal file
83
app/src/main/java/com/topjohnwu/magisk/extensions/XList.kt
Normal file
@@ -0,0 +1,83 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import androidx.databinding.ObservableList
|
||||
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||
import io.reactivex.disposables.Disposable
|
||||
|
||||
fun <T> MutableList<T>.update(newList: List<T>) {
|
||||
clear()
|
||||
addAll(newList)
|
||||
}
|
||||
|
||||
fun List<String>.toShellCmd(): String {
|
||||
val sb = StringBuilder()
|
||||
for (s in this) {
|
||||
if (s.contains(" ")) {
|
||||
sb.append('"').append(s).append('"')
|
||||
} else {
|
||||
sb.append(s)
|
||||
}
|
||||
sb.append(' ')
|
||||
}
|
||||
sb.deleteCharAt(sb.length - 1)
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
fun <T1, T2> ObservableList<T1>.sendUpdatesTo(
|
||||
target: DiffObservableList<T2>,
|
||||
mapper: (List<T1>) -> List<T2>
|
||||
) = addOnListChangedCallback(object :
|
||||
ObservableList.OnListChangedCallback<ObservableList<T1>>() {
|
||||
override fun onChanged(sender: ObservableList<T1>?) {
|
||||
updateAsync(sender ?: return)
|
||||
}
|
||||
|
||||
override fun onItemRangeRemoved(sender: ObservableList<T1>?, p0: Int, p1: Int) {
|
||||
updateAsync(sender ?: return)
|
||||
}
|
||||
|
||||
override fun onItemRangeMoved(sender: ObservableList<T1>?, p0: Int, p1: Int, p2: Int) {
|
||||
updateAsync(sender ?: return)
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(sender: ObservableList<T1>?, p0: Int, p1: Int) {
|
||||
updateAsync(sender ?: return)
|
||||
}
|
||||
|
||||
override fun onItemRangeChanged(sender: ObservableList<T1>?, p0: Int, p1: Int) {
|
||||
updateAsync(sender ?: return)
|
||||
}
|
||||
|
||||
private var updater: Disposable? = null
|
||||
|
||||
private fun updateAsync(sender: List<T1>) {
|
||||
updater?.dispose()
|
||||
updater = sender.toSingle()
|
||||
.map { mapper(it) }
|
||||
.map { it to target.calculateDiff(it) }
|
||||
.subscribeK { target.update(it.first, it.second) }
|
||||
}
|
||||
})
|
||||
|
||||
fun <T1> ObservableList<T1>.copyNewInputInto(
|
||||
target: MutableList<T1>
|
||||
) = addOnListChangedCallback(object : ObservableList.OnListChangedCallback<ObservableList<T1>>() {
|
||||
override fun onChanged(p0: ObservableList<T1>?) = Unit
|
||||
override fun onItemRangeRemoved(p0: ObservableList<T1>?, p1: Int, p2: Int) = Unit
|
||||
override fun onItemRangeMoved(p0: ObservableList<T1>?, p1: Int, p2: Int, p3: Int) = Unit
|
||||
override fun onItemRangeChanged(p0: ObservableList<T1>?, p1: Int, p2: Int) = Unit
|
||||
override fun onItemRangeInserted(
|
||||
sender: ObservableList<T1>?,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
val positionEnd = positionStart + itemCount
|
||||
val addedValues = sender?.slice(positionStart until positionEnd).orEmpty()
|
||||
target.addAll(addedValues)
|
||||
}
|
||||
})
|
||||
|
||||
operator fun <E> SparseArrayCompat<E>.set(key: Int, value: E) {
|
||||
put(key, value)
|
||||
}
|
14
app/src/main/java/com/topjohnwu/magisk/extensions/XSU.kt
Normal file
14
app/src/main/java/com/topjohnwu/magisk/extensions/XSU.kt
Normal file
@@ -0,0 +1,14 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
||||
import java.io.File
|
||||
|
||||
fun reboot(reason: String = if (Info.recovery) "recovery" else "") {
|
||||
Shell.su("/system/bin/svc power reboot $reason || /system/bin/reboot $reason").submit()
|
||||
}
|
||||
|
||||
fun File.suOutputStream() = SuFileOutputStream(this)
|
||||
fun File.suInputStream() = SuFileInputStream(this)
|
27
app/src/main/java/com/topjohnwu/magisk/extensions/XString.kt
Normal file
27
app/src/main/java/com/topjohnwu/magisk/extensions/XString.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.content.res.Resources
|
||||
|
||||
val specialChars = arrayOf('!', '@', '#', '$', '%', '&', '?')
|
||||
|
||||
fun String.replaceRandomWithSpecial(): String {
|
||||
var random: Char
|
||||
do {
|
||||
random = random()
|
||||
} while (random == '.')
|
||||
return replace(random, specialChars.random())
|
||||
}
|
||||
|
||||
fun StringBuilder.appendIf(condition: Boolean, builder: StringBuilder.() -> Unit) =
|
||||
if (condition) apply(builder) else this
|
||||
|
||||
fun Int.res(vararg args: Any): String {
|
||||
val resources: Resources by inject()
|
||||
return resources.getString(this, *args)
|
||||
}
|
||||
|
||||
fun String.trimEmptyToNull(): String? = if (isBlank()) null else this
|
||||
|
||||
fun String.legalFilename() = replace(" ", "_").replace("'", "").replace("\"", "")
|
||||
.replace("$", "").replace("`", "").replace("*", "").replace("/", "_")
|
||||
.replace("#", "").replace("@", "").replace("\\", "_")
|
20
app/src/main/java/com/topjohnwu/magisk/extensions/XTime.kt
Normal file
20
app/src/main/java/com/topjohnwu/magisk/extensions/XTime.kt
Normal file
@@ -0,0 +1,20 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import com.topjohnwu.magisk.utils.currentLocale
|
||||
import java.text.DateFormat
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
val now get() = System.currentTimeMillis()
|
||||
|
||||
fun Long.toTime(format: DateFormat) = format.format(this).orEmpty()
|
||||
fun String.toTime(format: DateFormat) = try {
|
||||
format.parse(this)?.time ?: -1
|
||||
} catch (e: ParseException) {
|
||||
-1L
|
||||
}
|
||||
|
||||
val timeFormatFull by lazy { SimpleDateFormat("yyyy/MM/dd_HH:mm:ss", currentLocale) }
|
||||
val timeFormatStandard by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", currentLocale) }
|
||||
val timeFormatMedium by lazy { DateFormat.getDateInstance(DateFormat.MEDIUM, currentLocale) }
|
||||
val timeFormatTime by lazy { SimpleDateFormat("h:mm a", currentLocale) }
|
14
app/src/main/java/com/topjohnwu/magisk/extensions/XView.kt
Normal file
14
app/src/main/java/com/topjohnwu/magisk/extensions/XView.kt
Normal file
@@ -0,0 +1,14 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
|
||||
fun View.setOnViewReadyListener(callback: () -> Unit) = addOnGlobalLayoutListener(true, callback)
|
||||
|
||||
fun View.addOnGlobalLayoutListener(oneShot: Boolean = false, callback: () -> Unit) =
|
||||
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
|
||||
override fun onGlobalLayout() {
|
||||
if (oneShot) viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
callback()
|
||||
}
|
||||
})
|
@@ -0,0 +1,37 @@
|
||||
package com.topjohnwu.magisk.model.binding
|
||||
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
import com.topjohnwu.magisk.model.entity.recycler.LenientRvItem
|
||||
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
|
||||
|
||||
class BindingAdapter : BindingRecyclerViewAdapter<ComparableRvItem<*>>() {
|
||||
|
||||
private var recyclerView: RecyclerView? = null
|
||||
|
||||
override fun onBindBinding(
|
||||
binding: ViewDataBinding,
|
||||
variableId: Int,
|
||||
layoutRes: Int,
|
||||
position: Int,
|
||||
item: ComparableRvItem<*>
|
||||
) {
|
||||
super.onBindBinding(binding, variableId, layoutRes, position, item)
|
||||
|
||||
when (item) {
|
||||
is LenientRvItem -> {
|
||||
val recycler = recyclerView ?: return
|
||||
item.onBindingBound(binding)
|
||||
item.onBindingBound(binding, recycler)
|
||||
}
|
||||
else -> item.onBindingBound(binding)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onAttachedToRecyclerView(recyclerView)
|
||||
this.recyclerView = recyclerView
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,155 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.topjohnwu.magisk.ClassMap
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.extensions.chooser
|
||||
import com.topjohnwu.magisk.extensions.exists
|
||||
import com.topjohnwu.magisk.extensions.provide
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.*
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.Flash.Secondary
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*
|
||||
import com.topjohnwu.magisk.ui.flash.FlashActivity
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import org.koin.core.get
|
||||
import java.io.File
|
||||
import kotlin.random.Random.Default.nextInt
|
||||
|
||||
/* More of a facade for [RemoteFileService], but whatever... */
|
||||
@SuppressLint("Registered")
|
||||
open class DownloadService : RemoteFileService() {
|
||||
|
||||
private val context get() = this
|
||||
private val File.type
|
||||
get() = MimeTypeMap.getSingleton()
|
||||
.getMimeTypeFromExtension(extension)
|
||||
?: "resource/folder"
|
||||
|
||||
override fun onFinished(subject: DownloadSubject, id: Int) = when (subject) {
|
||||
is Magisk -> onFinishedInternal(subject, id)
|
||||
is Module -> onFinishedInternal(subject, id)
|
||||
is Manager -> onFinishedInternal(subject, id)
|
||||
}
|
||||
|
||||
private fun onFinishedInternal(
|
||||
subject: Magisk,
|
||||
id: Int
|
||||
) = when (val conf = subject.configuration) {
|
||||
Uninstall -> FlashActivity.uninstall(this, subject.file, id)
|
||||
is Patch -> FlashActivity.patch(this, subject.file, conf.fileUri, id)
|
||||
is Flash -> FlashActivity.flash(this, subject.file, conf is Secondary, id)
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
private fun onFinishedInternal(
|
||||
subject: Module,
|
||||
id: Int
|
||||
) = when (subject.configuration) {
|
||||
is Flash -> FlashActivity.install(this, subject.file, id)
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
private fun onFinishedInternal(
|
||||
subject: Manager,
|
||||
id: Int
|
||||
) {
|
||||
remove(id)
|
||||
when (subject.configuration) {
|
||||
is APK.Upgrade -> APKInstall.install(this, subject.file)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
override fun NotificationCompat.Builder.addActions(subject: DownloadSubject)
|
||||
= when (subject) {
|
||||
is Magisk -> addActionsInternal(subject)
|
||||
is Module -> addActionsInternal(subject)
|
||||
is Manager -> addActionsInternal(subject)
|
||||
}
|
||||
|
||||
private fun NotificationCompat.Builder.addActionsInternal(subject: Magisk)
|
||||
= when (val conf = subject.configuration) {
|
||||
Download -> this.apply {
|
||||
fileIntent(subject.file.parentFile!!)
|
||||
.takeIf { it.exists(get()) }
|
||||
?.let { addAction(0, R.string.download_open_parent, it.chooser()) }
|
||||
fileIntent(subject.file)
|
||||
.takeIf { it.exists(get()) }
|
||||
?.let { addAction(0, R.string.download_open_self, it.chooser()) }
|
||||
}
|
||||
Uninstall -> setContentIntent(FlashActivity.uninstallIntent(context, subject.file))
|
||||
is Flash -> setContentIntent(FlashActivity.flashIntent(context, subject.file, conf is Secondary))
|
||||
is Patch -> setContentIntent(FlashActivity.patchIntent(context, subject.file, conf.fileUri))
|
||||
else -> this
|
||||
}
|
||||
|
||||
private fun NotificationCompat.Builder.addActionsInternal(subject: Module)
|
||||
= when (subject.configuration) {
|
||||
Download -> this.apply {
|
||||
fileIntent(subject.file.parentFile!!)
|
||||
.takeIf { it.exists(get()) }
|
||||
?.let { addAction(0, R.string.download_open_parent, it.chooser()) }
|
||||
fileIntent(subject.file)
|
||||
.takeIf { it.exists(get()) }
|
||||
?.let { addAction(0, R.string.download_open_self, it.chooser()) }
|
||||
}
|
||||
is Flash -> setContentIntent(FlashActivity.installIntent(context, subject.file))
|
||||
else -> this
|
||||
}
|
||||
|
||||
private fun NotificationCompat.Builder.addActionsInternal(subject: Manager)
|
||||
= when (subject.configuration) {
|
||||
APK.Upgrade -> setContentIntent(APKInstall.installIntent(context, subject.file))
|
||||
else -> this
|
||||
}
|
||||
|
||||
@Suppress("ReplaceSingleLineLet")
|
||||
private fun NotificationCompat.Builder.setContentIntent(intent: Intent) =
|
||||
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
||||
.let { setContentIntent(it) }
|
||||
|
||||
@Suppress("ReplaceSingleLineLet")
|
||||
private fun NotificationCompat.Builder.addAction(icon: Int, title: Int, intent: Intent) =
|
||||
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
||||
.let { addAction(icon, getString(title), it) }
|
||||
|
||||
// ---
|
||||
|
||||
private fun fileIntent(file: File): Intent {
|
||||
return Intent(Intent.ACTION_VIEW)
|
||||
.setDataAndType(file.provide(this), file.type)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
|
||||
class Builder {
|
||||
lateinit var subject: DownloadSubject
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
inline operator fun invoke(context: Context, argBuilder: Builder.() -> Unit) {
|
||||
val app = context.applicationContext
|
||||
val builder = Builder().apply(argBuilder)
|
||||
val intent = Intent(app, ClassMap[DownloadService::class.java])
|
||||
.putExtra(ARG_URL, builder.subject)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
app.startForegroundService(intent)
|
||||
} else {
|
||||
app.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
|
||||
import android.content.ComponentName
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.ClassMap
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.APK.Restore
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.APK.Upgrade
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.magisk.ui.SplashActivity
|
||||
import com.topjohnwu.magisk.utils.DynamicClassLoader
|
||||
import com.topjohnwu.magisk.utils.PatchAPK
|
||||
import com.topjohnwu.magisk.utils.RootUtils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
private fun RemoteFileService.patchPackage(apk: File, id: Int) {
|
||||
if (packageName != BuildConfig.APPLICATION_ID) {
|
||||
update(id) { notification ->
|
||||
notification.setProgress(0, 0, true)
|
||||
.setProgress(0, 0, true)
|
||||
.setContentTitle(getString(R.string.hide_manager_title))
|
||||
.setContentText("")
|
||||
}
|
||||
val patched = File(apk.parent, "patched.apk")
|
||||
try {
|
||||
// Try using the new APK to patch itself
|
||||
val loader = DynamicClassLoader(apk)
|
||||
loader.loadClass("a.a")
|
||||
.getMethod("patchAPK", String::class.java, String::class.java, String::class.java)
|
||||
.invoke(null, apk.path, patched.path, packageName)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
// Fallback to use the current implementation
|
||||
PatchAPK.patch(apk.path, patched.path, packageName)
|
||||
}
|
||||
apk.delete()
|
||||
patched.renameTo(apk)
|
||||
}
|
||||
}
|
||||
|
||||
private fun RemoteFileService.restore(apk: File, id: Int) {
|
||||
update(id) { notification ->
|
||||
notification.setProgress(0, 0, true)
|
||||
.setProgress(0, 0, true)
|
||||
.setContentTitle(getString(R.string.restore_img_msg))
|
||||
.setContentText("")
|
||||
}
|
||||
Config.export()
|
||||
// Make it world readable
|
||||
apk.setReadable(true, false)
|
||||
if (Shell.su("pm install $apk").exec().isSuccess) {
|
||||
val component = ComponentName(BuildConfig.APPLICATION_ID,
|
||||
ClassMap.get<Class<*>>(SplashActivity::class.java).name)
|
||||
RootUtils.rmAndLaunch(packageName, component)
|
||||
}
|
||||
}
|
||||
|
||||
fun RemoteFileService.handleAPK(subject: DownloadSubject.Manager)
|
||||
= when (subject.configuration) {
|
||||
is Upgrade -> patchPackage(subject.file, subject.hashCode())
|
||||
is Restore -> restore(subject.file, subject.hashCode())
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
|
||||
import com.topjohnwu.magisk.extensions.withStreams
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
fun InputStream.toModule(file: File, installer: InputStream) {
|
||||
|
||||
val input = ZipInputStream(buffered())
|
||||
val output = ZipOutputStream(file.outputStream().buffered())
|
||||
|
||||
withStreams(input, output) { zin, zout ->
|
||||
zout.putNextEntry(ZipEntry("META-INF/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
|
||||
installer.copyTo(zout)
|
||||
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
|
||||
zout.write("#MAGISK\n".toByteArray(charset("UTF-8")))
|
||||
|
||||
var off = -1
|
||||
var entry: ZipEntry? = zin.nextEntry
|
||||
while (entry != null) {
|
||||
if (off < 0) {
|
||||
off = entry.name.indexOf('/') + 1
|
||||
}
|
||||
|
||||
val path = entry.name.substring(off)
|
||||
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
|
||||
zout.putNextEntry(ZipEntry(path))
|
||||
if (!entry.isDirectory) {
|
||||
zin.copyTo(zout)
|
||||
}
|
||||
}
|
||||
|
||||
entry = zin.nextEntry
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,87 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import org.koin.core.KoinComponent
|
||||
import java.util.*
|
||||
import kotlin.random.Random.Default.nextInt
|
||||
|
||||
abstract class NotificationService : Service(), KoinComponent {
|
||||
|
||||
abstract val defaultNotification: NotificationCompat.Builder
|
||||
|
||||
private val manager by lazy { NotificationManagerCompat.from(this) }
|
||||
private val hasNotifications get() = notifications.isNotEmpty()
|
||||
|
||||
private val notifications =
|
||||
Collections.synchronizedMap(mutableMapOf<Int, NotificationCompat.Builder>())
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
notifications.forEach { cancel(it.key) }
|
||||
notifications.clear()
|
||||
}
|
||||
|
||||
// --
|
||||
|
||||
fun update(
|
||||
id: Int,
|
||||
body: (NotificationCompat.Builder) -> Unit = {}
|
||||
) {
|
||||
val notification = notifications.getOrPut(id) { defaultNotification }
|
||||
|
||||
notify(id, notification.also(body).build())
|
||||
|
||||
if (notifications.size == 1) {
|
||||
updateForeground()
|
||||
}
|
||||
}
|
||||
|
||||
protected fun finishNotify(
|
||||
id: Int,
|
||||
editBody: (NotificationCompat.Builder) -> NotificationCompat.Builder? = { null }
|
||||
) : Int {
|
||||
val currentNotification = remove(id)?.run(editBody)
|
||||
|
||||
var newId = -1
|
||||
currentNotification?.let {
|
||||
newId = nextInt(Int.MAX_VALUE)
|
||||
notify(newId, it.build())
|
||||
}
|
||||
|
||||
if (!hasNotifications) {
|
||||
stopSelf()
|
||||
}
|
||||
return newId
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
private fun notify(id: Int, notification: Notification) {
|
||||
manager.notify(id, notification)
|
||||
}
|
||||
|
||||
private fun cancel(id: Int) {
|
||||
manager.cancel(id)
|
||||
}
|
||||
|
||||
protected fun remove(id: Int) = notifications.remove(id).also {
|
||||
cancel(id)
|
||||
updateForeground()
|
||||
}
|
||||
|
||||
private fun updateForeground() {
|
||||
if (hasNotifications)
|
||||
startForeground(notifications.keys.first(), notifications.values.first().build())
|
||||
else
|
||||
stopForeground(true)
|
||||
}
|
||||
|
||||
// --
|
||||
|
||||
override fun onBind(p0: Intent?): IBinder? = null
|
||||
}
|
@@ -0,0 +1,118 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.di.NullActivity
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.extensions.writeTo
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*
|
||||
import com.topjohnwu.magisk.utils.ProgressInputStream
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import io.reactivex.Completable
|
||||
import okhttp3.ResponseBody
|
||||
import org.koin.android.ext.android.inject
|
||||
import timber.log.Timber
|
||||
import java.io.InputStream
|
||||
|
||||
abstract class RemoteFileService : NotificationService() {
|
||||
|
||||
private val service: GithubRawServices by inject()
|
||||
|
||||
override val defaultNotification: NotificationCompat.Builder
|
||||
get() = Notifications.progress(this, "")
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
intent?.getParcelableExtra<DownloadSubject>(ARG_URL)?.let { start(it) }
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
private fun start(subject: DownloadSubject) = checkExisting(subject)
|
||||
.onErrorResumeNext { download(subject) }
|
||||
.doOnSubscribe { update(subject.hashCode()) { it.setContentTitle(subject.title) } }
|
||||
.subscribeK(onError = {
|
||||
Timber.e(it)
|
||||
finishNotify(subject.hashCode()) { notification ->
|
||||
notification.setContentText(getString(R.string.download_file_error))
|
||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||
.setOngoing(false)
|
||||
}
|
||||
}) {
|
||||
val newId = finishNotify(subject)
|
||||
if (get<Activity>() !is NullActivity) {
|
||||
onFinished(subject, newId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkExisting(subject: DownloadSubject) = Completable.fromAction {
|
||||
check(subject is Magisk) { "Download cache is disabled" }
|
||||
|
||||
subject.file.also {
|
||||
check(it.exists() && ShellUtils.checkSum("MD5", it, subject.magisk.md5)) {
|
||||
"The given file does not match checksum"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun download(subject: DownloadSubject) = service.fetchFile(subject.url)
|
||||
.map { it.toStream(subject.hashCode()) }
|
||||
.flatMapCompletable { stream ->
|
||||
when (subject) {
|
||||
is Module -> service.fetchInstaller()
|
||||
.doOnSuccess { stream.toModule(subject.file, it.byteStream()) }
|
||||
.ignoreElement()
|
||||
else -> Completable.fromAction { stream.writeTo(subject.file) }
|
||||
}
|
||||
}.doOnComplete {
|
||||
if (subject is Manager)
|
||||
handleAPK(subject)
|
||||
}
|
||||
|
||||
private fun ResponseBody.toStream(id: Int): InputStream {
|
||||
val maxRaw = contentLength()
|
||||
val max = maxRaw / 1_000_000f
|
||||
|
||||
return ProgressInputStream(byteStream()) {
|
||||
val progress = it / 1_000_000f
|
||||
update(id) { notification ->
|
||||
if (maxRaw > 0) {
|
||||
notification
|
||||
.setProgress(maxRaw.toInt(), it.toInt(), false)
|
||||
.setContentText("%.2f / %.2f MB".format(progress, max))
|
||||
} else {
|
||||
notification.setContentText("%.2f MB / ??".format(progress))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun finishNotify(subject: DownloadSubject) = finishNotify(subject.hashCode()) {
|
||||
it.addActions(subject)
|
||||
.setContentText(getString(R.string.download_complete))
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setProgress(0, 0, false)
|
||||
.setOngoing(false)
|
||||
.setAutoCancel(true)
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
|
||||
@Throws(Throwable::class)
|
||||
protected abstract fun onFinished(subject: DownloadSubject, id: Int)
|
||||
|
||||
protected abstract fun NotificationCompat.Builder.addActions(subject: DownloadSubject)
|
||||
: NotificationCompat.Builder
|
||||
|
||||
companion object {
|
||||
const val ARG_URL = "arg_url"
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.graphics.drawable.Drawable
|
||||
import com.topjohnwu.magisk.extensions.packageInfo
|
||||
import com.topjohnwu.magisk.extensions.processes
|
||||
|
||||
class HideAppInfo(
|
||||
val info: ApplicationInfo,
|
||||
val name: String,
|
||||
val icon: Drawable
|
||||
) {
|
||||
|
||||
val processes = info.packageInfo?.processes?.distinct() ?: listOf(info.packageName)
|
||||
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
class HideTarget(line: String) {
|
||||
|
||||
private val split = line.split(Regex("\\|"), 2)
|
||||
|
||||
val packageName = split[0]
|
||||
val process = split.getOrElse(1) { packageName }
|
||||
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import com.topjohnwu.magisk.extensions.timeFormatTime
|
||||
import com.topjohnwu.magisk.extensions.toTime
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.ALLOW
|
||||
import java.util.*
|
||||
|
||||
data class MagiskLog(
|
||||
val fromUid: Int,
|
||||
val toUid: Int,
|
||||
val fromPid: Int,
|
||||
val packageName: String,
|
||||
val appName: String,
|
||||
val command: String,
|
||||
val action: Boolean,
|
||||
val date: Date
|
||||
) {
|
||||
val timeString = date.time.toTime(timeFormatTime)
|
||||
}
|
||||
|
||||
data class WrappedMagiskLog(
|
||||
val time: Long,
|
||||
val items: List<MagiskLog>
|
||||
)
|
||||
|
||||
fun Map<String, String>.toLog(): MagiskLog {
|
||||
return MagiskLog(
|
||||
fromUid = get("from_uid")?.toIntOrNull() ?: -1,
|
||||
toUid = get("to_uid")?.toIntOrNull() ?: -1,
|
||||
fromPid = get("from_pid")?.toIntOrNull() ?: -1,
|
||||
packageName = get("package_name").orEmpty(),
|
||||
appName = get("app_name").orEmpty(),
|
||||
command = get("command").orEmpty(),
|
||||
action = get("action")?.toIntOrNull() != 0,
|
||||
date = get("time")?.toLongOrNull()?.toDate() ?: Date()
|
||||
)
|
||||
}
|
||||
|
||||
fun Long.toDate() = Date(this)
|
||||
|
||||
fun MagiskLog.toMap() = mapOf(
|
||||
"from_uid" to fromUid,
|
||||
"to_uid" to toUid,
|
||||
"from_pid" to fromPid,
|
||||
"package_name" to packageName,
|
||||
"app_name" to appName,
|
||||
"command" to command,
|
||||
"action" to action,
|
||||
"time" to date.time
|
||||
)
|
||||
|
||||
fun MagiskPolicy.toLog(
|
||||
toUid: Int,
|
||||
fromPid: Int,
|
||||
command: String,
|
||||
date: Date
|
||||
) = MagiskLog(uid, toUid, fromPid, packageName, appName, command, policy == ALLOW, date)
|
@@ -0,0 +1,69 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.extensions.getLabel
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.INTERACTIVE
|
||||
|
||||
|
||||
data class MagiskPolicy(
|
||||
val uid: Int,
|
||||
val packageName: String,
|
||||
val appName: String,
|
||||
val policy: Int = INTERACTIVE,
|
||||
val until: Long = -1L,
|
||||
val logging: Boolean = true,
|
||||
val notification: Boolean = true,
|
||||
val applicationInfo: ApplicationInfo
|
||||
) {
|
||||
|
||||
companion object {
|
||||
const val INTERACTIVE = 0
|
||||
const val DENY = 1
|
||||
const val ALLOW = 2
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun MagiskPolicy.toMap() = mapOf(
|
||||
"uid" to uid,
|
||||
"package_name" to packageName,
|
||||
"policy" to policy,
|
||||
"until" to until,
|
||||
"logging" to logging,
|
||||
"notification" to notification
|
||||
)
|
||||
|
||||
@Throws(PackageManager.NameNotFoundException::class)
|
||||
fun Map<String, String>.toPolicy(pm: PackageManager): MagiskPolicy {
|
||||
val uid = get("uid")?.toIntOrNull() ?: -1
|
||||
val packageName = get("package_name").orEmpty()
|
||||
val info = pm.getApplicationInfo(packageName, 0)
|
||||
|
||||
if (info.uid != uid)
|
||||
throw PackageManager.NameNotFoundException()
|
||||
|
||||
return MagiskPolicy(
|
||||
uid = uid,
|
||||
packageName = packageName,
|
||||
policy = get("policy")?.toIntOrNull() ?: INTERACTIVE,
|
||||
until = get("until")?.toLongOrNull() ?: -1L,
|
||||
logging = get("logging")?.toIntOrNull() != 0,
|
||||
notification = get("notification")?.toIntOrNull() != 0,
|
||||
applicationInfo = info,
|
||||
appName = info.getLabel(pm)
|
||||
)
|
||||
}
|
||||
|
||||
@Throws(PackageManager.NameNotFoundException::class)
|
||||
fun Int.toPolicy(pm: PackageManager): MagiskPolicy {
|
||||
val pkg = pm.getPackagesForUid(this)?.firstOrNull()
|
||||
?: throw PackageManager.NameNotFoundException()
|
||||
val info = pm.getApplicationInfo(pkg, 0)
|
||||
return MagiskPolicy(
|
||||
uid = this,
|
||||
packageName = pkg,
|
||||
applicationInfo = info,
|
||||
appName = info.loadLabel(pm).toString()
|
||||
)
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import se.ansman.kotshi.JsonSerializable
|
||||
|
||||
@JsonSerializable
|
||||
data class UpdateInfo(
|
||||
val app: ManagerJson = ManagerJson(),
|
||||
val uninstaller: UninstallerJson = UninstallerJson(),
|
||||
val magisk: MagiskJson = MagiskJson()
|
||||
)
|
||||
|
||||
@JsonSerializable
|
||||
data class UninstallerJson(
|
||||
val link: String = ""
|
||||
)
|
||||
|
||||
@JsonSerializable
|
||||
data class MagiskJson(
|
||||
val version: String = "",
|
||||
val versionCode: Int = -1,
|
||||
val link: String = "",
|
||||
val note: String = "",
|
||||
val md5: String = ""
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
@JsonSerializable
|
||||
data class ManagerJson(
|
||||
val version: String = "",
|
||||
val versionCode: Int = -1,
|
||||
val link: String = "",
|
||||
val note: String = ""
|
||||
) : Parcelable
|
@@ -0,0 +1,37 @@
|
||||
package com.topjohnwu.magisk.model.entity.internal
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
sealed class Configuration : Parcelable {
|
||||
|
||||
sealed class Flash : Configuration() {
|
||||
|
||||
@Parcelize
|
||||
object Primary : Flash()
|
||||
|
||||
@Parcelize
|
||||
object Secondary : Flash()
|
||||
|
||||
}
|
||||
|
||||
sealed class APK : Configuration() {
|
||||
|
||||
@Parcelize
|
||||
object Upgrade : APK()
|
||||
|
||||
@Parcelize
|
||||
object Restore : APK()
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
object Download : Configuration()
|
||||
|
||||
@Parcelize
|
||||
object Uninstall : Configuration()
|
||||
|
||||
@Parcelize
|
||||
data class Patch(val fileUri: Uri) : Configuration()
|
||||
|
||||
}
|
@@ -0,0 +1,106 @@
|
||||
package com.topjohnwu.magisk.model.entity.internal
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.magisk.extensions.cachedFile
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.model.entity.MagiskJson
|
||||
import com.topjohnwu.magisk.model.entity.ManagerJson
|
||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||
import kotlinx.android.parcel.IgnoredOnParcel
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import java.io.File
|
||||
|
||||
sealed class DownloadSubject : Parcelable {
|
||||
|
||||
abstract val url: String
|
||||
abstract val file: File
|
||||
open val title: String get() = file.name
|
||||
|
||||
@Parcelize
|
||||
data class Module(
|
||||
val module: Repo,
|
||||
val configuration: Configuration
|
||||
) : DownloadSubject() {
|
||||
override val url: String get() = module.zipUrl
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
File(Config.downloadDirectory, module.downloadFilename)
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Manager(
|
||||
val configuration: Configuration.APK
|
||||
) : DownloadSubject() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val manager: ManagerJson = Info.remote.app
|
||||
|
||||
override val title: String
|
||||
get() = "MagiskManager-v${manager.version}(${manager.versionCode})"
|
||||
|
||||
override val url: String
|
||||
get() = manager.link
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
get<Context>().cachedFile("manager.apk")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sealed class Magisk : DownloadSubject() {
|
||||
|
||||
abstract val configuration: Configuration
|
||||
val magisk: MagiskJson = Info.remote.magisk
|
||||
|
||||
@Parcelize
|
||||
protected data class Flash(
|
||||
override val configuration: Configuration
|
||||
) : Magisk() {
|
||||
override val url: String get() = magisk.link
|
||||
override val title: String get() = "Magisk-v${magisk.version}(${magisk.versionCode})"
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
get<Context>().cachedFile("magisk.zip")
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
protected class Uninstall : Magisk() {
|
||||
override val configuration: Configuration get() = Configuration.Uninstall
|
||||
override val url: String get() = Info.remote.uninstaller.link
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
get<Context>().cachedFile("uninstall.zip")
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
protected class Download : Magisk() {
|
||||
override val configuration: Configuration get() = Configuration.Download
|
||||
override val url: String get() = magisk.link
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
File(Config.downloadDirectory, "Magisk-v${magisk.version}(${magisk.versionCode}).zip")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
operator fun invoke(configuration: Configuration) = when (configuration) {
|
||||
Configuration.Download -> Download()
|
||||
Configuration.Uninstall -> Uninstall()
|
||||
else -> Flash(configuration)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
package com.topjohnwu.magisk.model.entity.module
|
||||
|
||||
abstract class BaseModule : Comparable<BaseModule> {
|
||||
abstract var id: String
|
||||
protected set
|
||||
abstract var name: String
|
||||
protected set
|
||||
abstract var author: String
|
||||
protected set
|
||||
abstract var version: String
|
||||
protected set
|
||||
abstract var versionCode: Int
|
||||
protected set
|
||||
abstract var description: String
|
||||
protected set
|
||||
|
||||
@Throws(NumberFormatException::class)
|
||||
protected fun parseProps(props: List<String>) {
|
||||
for (line in props) {
|
||||
val prop = line.split("=".toRegex(), 2).map { it.trim() }
|
||||
if (prop.size != 2)
|
||||
continue
|
||||
|
||||
val key = prop[0]
|
||||
val value = prop[1]
|
||||
if (key.isEmpty() || key[0] == '#')
|
||||
continue
|
||||
|
||||
when (key) {
|
||||
"id" -> id = value
|
||||
"name" -> name = value
|
||||
"version" -> version = value
|
||||
"versionCode" -> versionCode = value.toInt()
|
||||
"author" -> author = value
|
||||
"description" -> description = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override operator fun compareTo(other: BaseModule) = name.compareTo(other.name, true)
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
package com.topjohnwu.magisk.model.entity.module
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
|
||||
class Module(path: String) : BaseModule() {
|
||||
override var id: String = ""
|
||||
override var name: String = ""
|
||||
override var author: String = ""
|
||||
override var version: String = ""
|
||||
override var versionCode: Int = -1
|
||||
override var description: String = ""
|
||||
|
||||
private val removeFile: SuFile = SuFile(path, "remove")
|
||||
private val disableFile: SuFile = SuFile(path, "disable")
|
||||
private val updateFile: SuFile = SuFile(path, "update")
|
||||
|
||||
val updated: Boolean = updateFile.exists()
|
||||
|
||||
var enable: Boolean = !disableFile.exists()
|
||||
set(enable) {
|
||||
field = if (enable) {
|
||||
disableFile.delete()
|
||||
} else {
|
||||
!disableFile.createNewFile()
|
||||
}
|
||||
}
|
||||
|
||||
var remove: Boolean = removeFile.exists()
|
||||
set(remove) {
|
||||
field = if (remove) {
|
||||
removeFile.createNewFile()
|
||||
} else {
|
||||
!removeFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
runCatching {
|
||||
parseProps(Shell.su("dos2unix < $path/module.prop").exec().out)
|
||||
}
|
||||
|
||||
if (id.isEmpty()) {
|
||||
val sep = path.lastIndexOf('/')
|
||||
id = path.substring(sep + 1)
|
||||
}
|
||||
|
||||
if (name.isEmpty()) {
|
||||
name = id
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@WorkerThread
|
||||
fun loadModules(): List<Module> {
|
||||
val moduleList = mutableListOf<Module>()
|
||||
val path = SuFile(Const.MAGISK_PATH)
|
||||
val modules =
|
||||
path.listFiles { _, name -> name != "lost+found" && name != ".core" }.orEmpty()
|
||||
for (file in modules) {
|
||||
if (file.isFile) continue
|
||||
val module = Module(Const.MAGISK_PATH + "/" + file.name)
|
||||
moduleList.add(module)
|
||||
}
|
||||
return moduleList.sortedBy { it.name }
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
package com.topjohnwu.magisk.model.entity.module
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.data.repository.StringRepository
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.legalFilename
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
||||
@Entity(tableName = "repos")
|
||||
@Parcelize
|
||||
data class Repo(
|
||||
@PrimaryKey override var id: String,
|
||||
override var name: String,
|
||||
override var author: String,
|
||||
override var version: String,
|
||||
override var versionCode: Int,
|
||||
override var description: String,
|
||||
var last_update: Long
|
||||
) : BaseModule(), Parcelable {
|
||||
|
||||
private val stringRepo: StringRepository get() = get()
|
||||
|
||||
val lastUpdate get() = Date(last_update)
|
||||
|
||||
val lastUpdateString: String get() = dateFormat.format(lastUpdate)
|
||||
|
||||
val downloadFilename: String get() = "$name-$version($versionCode).zip".legalFilename()
|
||||
|
||||
val readme get() = stringRepo.getReadme(this)
|
||||
|
||||
val zipUrl: String get() = Const.Url.ZIP_URL.format(id)
|
||||
|
||||
constructor(id: String) : this(id, "", "", "", -1, "", 0)
|
||||
|
||||
@Throws(IllegalRepoException::class)
|
||||
fun update() {
|
||||
val props = runCatching {
|
||||
stringRepo.getMetadata(this).blockingGet()
|
||||
.orEmpty().split("\\n".toRegex()).dropLastWhile { it.isEmpty() }
|
||||
}.getOrElse {
|
||||
throw IllegalRepoException("Repo [$id] module.prop download error: " + it.message)
|
||||
}
|
||||
|
||||
props.runCatching {
|
||||
parseProps(this)
|
||||
}.onFailure {
|
||||
throw IllegalRepoException("Repo [$id] parse error: " + it.message)
|
||||
}
|
||||
|
||||
if (versionCode < 0) {
|
||||
throw IllegalRepoException("Repo [$id] does not contain versionCode")
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IllegalRepoException::class)
|
||||
fun update(lastUpdate: Date) {
|
||||
last_update = lastUpdate.time
|
||||
update()
|
||||
}
|
||||
|
||||
class IllegalRepoException(message: String) : Exception(message)
|
||||
|
||||
companion object {
|
||||
val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)!!
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.magisk.R
|
||||
|
||||
class ConsoleRvItem(val item: String) : LenientRvItem<ConsoleRvItem>() {
|
||||
override val layoutRes: Int = R.layout.item_console
|
||||
|
||||
override fun onBindingBound(binding: ViewDataBinding, recyclerView: RecyclerView) {
|
||||
val view = binding.root as TextView
|
||||
view.measure(0, 0)
|
||||
val desiredWidth = view.measuredWidth
|
||||
|
||||
view.updateLayoutParams { width = desiredWidth }
|
||||
|
||||
if (recyclerView.width < desiredWidth) {
|
||||
recyclerView.requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
override fun contentSameAs(other: ConsoleRvItem) = itemSameAs(other)
|
||||
override fun itemSameAs(other: ConsoleRvItem) = item == other.item
|
||||
}
|
@@ -0,0 +1,92 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.magisk.extensions.toggle
|
||||
import com.topjohnwu.magisk.model.entity.HideAppInfo
|
||||
import com.topjohnwu.magisk.model.entity.HideTarget
|
||||
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
|
||||
import com.topjohnwu.magisk.model.events.HideProcessEvent
|
||||
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
import com.topjohnwu.magisk.utils.RxBus
|
||||
|
||||
class HideRvItem(val item: HideAppInfo, targets: List<HideTarget>) :
|
||||
ComparableRvItem<HideRvItem>() {
|
||||
|
||||
override val layoutRes: Int = R.layout.item_hide_app
|
||||
|
||||
val packageName = item.info.packageName.orEmpty()
|
||||
val items = DiffObservableList(callback).also {
|
||||
val items = item.processes.map {
|
||||
val isHidden = targets.any { target ->
|
||||
packageName == target.packageName && it == target.process
|
||||
}
|
||||
HideProcessRvItem(packageName, it, isHidden)
|
||||
}
|
||||
it.update(items)
|
||||
}
|
||||
val isHiddenState = KObservableField(currentState)
|
||||
val isExpanded = KObservableField(false)
|
||||
|
||||
private val itemsProcess get() = items.filterIsInstance<HideProcessRvItem>()
|
||||
|
||||
private val currentState
|
||||
get() = when (itemsProcess.count { it.isHidden.value }) {
|
||||
items.size -> IndeterminateState.CHECKED
|
||||
in 1 until items.size -> IndeterminateState.INDETERMINATE
|
||||
else -> IndeterminateState.UNCHECKED
|
||||
}
|
||||
|
||||
init {
|
||||
itemsProcess.forEach {
|
||||
it.isHidden.addOnPropertyChangedCallback { isHiddenState.value = currentState }
|
||||
}
|
||||
}
|
||||
|
||||
fun toggle() {
|
||||
val desiredState = when (isHiddenState.value) {
|
||||
IndeterminateState.INDETERMINATE,
|
||||
IndeterminateState.UNCHECKED -> true
|
||||
IndeterminateState.CHECKED -> false
|
||||
}
|
||||
itemsProcess.forEach { it.isHidden.value = desiredState }
|
||||
isHiddenState.value = currentState
|
||||
}
|
||||
|
||||
fun toggleExpansion() {
|
||||
if (items.size <= 1) return
|
||||
isExpanded.toggle()
|
||||
}
|
||||
|
||||
override fun contentSameAs(other: HideRvItem): Boolean = items.all { other.items.contains(it) }
|
||||
override fun itemSameAs(other: HideRvItem): Boolean = item.info == other.item.info
|
||||
|
||||
}
|
||||
|
||||
class HideProcessRvItem(
|
||||
val packageName: String,
|
||||
val process: String,
|
||||
isHidden: Boolean
|
||||
) : ComparableRvItem<HideProcessRvItem>() {
|
||||
|
||||
override val layoutRes: Int = R.layout.item_hide_process
|
||||
|
||||
val isHidden = KObservableField(isHidden)
|
||||
|
||||
private val rxBus: RxBus by inject()
|
||||
|
||||
init {
|
||||
this.isHidden.addOnPropertyChangedCallback {
|
||||
rxBus.post(HideProcessEvent(this@HideProcessRvItem))
|
||||
}
|
||||
}
|
||||
|
||||
fun toggle() = isHidden.toggle()
|
||||
|
||||
override fun contentSameAs(other: HideProcessRvItem): Boolean = itemSameAs(other)
|
||||
override fun itemSameAs(other: HideProcessRvItem): Boolean =
|
||||
packageName == other.packageName && process == other.process
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
|
||||
/**
|
||||
* This item addresses issues where enclosing recycler has to be invalidated or generally
|
||||
* manipulated with. This shouldn't be however necessary for 99.9% of use-cases. Refrain from using
|
||||
* this item as it provides virtually no additional functionality. Stick with ComparableRvItem.
|
||||
* */
|
||||
abstract class LenientRvItem<in T> : ComparableRvItem<T>() {
|
||||
|
||||
open fun onBindingBound(binding: ViewDataBinding, recyclerView: RecyclerView) {}
|
||||
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
import com.topjohnwu.magisk.extensions.timeFormatMedium
|
||||
import com.topjohnwu.magisk.extensions.toTime
|
||||
import com.topjohnwu.magisk.extensions.toggle
|
||||
import com.topjohnwu.magisk.model.entity.MagiskLog
|
||||
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
|
||||
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
|
||||
class LogRvItem : ComparableRvItem<LogRvItem>() {
|
||||
override val layoutRes: Int = R.layout.item_page_log
|
||||
|
||||
val items = DiffObservableList(callback)
|
||||
|
||||
fun update(list: List<LogItemRvItem>) {
|
||||
list.firstOrNull()?.isExpanded?.value = true
|
||||
items.update(list)
|
||||
}
|
||||
|
||||
//two of these will never be present, safe to assume it's unique
|
||||
override fun contentSameAs(other: LogRvItem): Boolean = false
|
||||
|
||||
override fun itemSameAs(other: LogRvItem): Boolean = false
|
||||
}
|
||||
|
||||
class LogItemRvItem(
|
||||
item: WrappedMagiskLog
|
||||
) : ComparableRvItem<LogItemRvItem>() {
|
||||
override val layoutRes: Int = R.layout.item_superuser_log
|
||||
|
||||
val date = item.time.toTime(timeFormatMedium)
|
||||
val items: List<ComparableRvItem<*>> = item.items.map { LogItemEntryRvItem(it) }
|
||||
val isExpanded = KObservableField(false)
|
||||
|
||||
fun toggle() = isExpanded.toggle()
|
||||
|
||||
override fun contentSameAs(other: LogItemRvItem): Boolean = items
|
||||
.any { !other.items.contains(it) }
|
||||
|
||||
override fun itemSameAs(other: LogItemRvItem): Boolean = date == other.date
|
||||
}
|
||||
|
||||
class LogItemEntryRvItem(val item: MagiskLog) : ComparableRvItem<LogItemEntryRvItem>() {
|
||||
override val layoutRes: Int = R.layout.item_superuser_log_entry
|
||||
|
||||
val isExpanded = KObservableField(false)
|
||||
|
||||
fun toggle() = isExpanded.toggle()
|
||||
|
||||
override fun contentSameAs(other: LogItemEntryRvItem) = item.fromUid == other.item.fromUid &&
|
||||
item.toUid == other.item.toUid &&
|
||||
item.fromPid == other.item.fromPid &&
|
||||
item.packageName == other.item.packageName &&
|
||||
item.command == other.item.command &&
|
||||
item.action == other.item.action &&
|
||||
item.date == other.item.date
|
||||
|
||||
override fun itemSameAs(other: LogItemEntryRvItem) = item.appName == other.item.appName
|
||||
}
|
||||
|
||||
class MagiskLogRvItem : ComparableRvItem<MagiskLogRvItem>() {
|
||||
override val layoutRes: Int = R.layout.item_page_magisk_log
|
||||
|
||||
val items = DiffObservableList(callback)
|
||||
|
||||
fun update(list: List<ConsoleRvItem>) {
|
||||
items.update(list)
|
||||
}
|
||||
|
||||
//two of these will never be present, safe to assume it's unique
|
||||
override fun contentSameAs(other: MagiskLogRvItem): Boolean = false
|
||||
|
||||
override fun itemSameAs(other: MagiskLogRvItem): Boolean = false
|
||||
}
|
@@ -0,0 +1,75 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.annotation.StringRes
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.toggle
|
||||
import com.topjohnwu.magisk.model.entity.module.Module
|
||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
|
||||
class ModuleRvItem(val item: Module) : ComparableRvItem<ModuleRvItem>() {
|
||||
|
||||
override val layoutRes: Int = R.layout.item_module
|
||||
|
||||
val lastActionNotice = KObservableField("")
|
||||
val isChecked = KObservableField(item.enable)
|
||||
val isDeletable = KObservableField(item.remove)
|
||||
|
||||
init {
|
||||
isChecked.addOnPropertyChangedCallback {
|
||||
when (it) {
|
||||
true -> {
|
||||
item.enable = true
|
||||
notice(R.string.disable_file_removed)
|
||||
}
|
||||
false -> {
|
||||
item.enable = false
|
||||
notice(R.string.disable_file_created)
|
||||
}
|
||||
}
|
||||
}
|
||||
isDeletable.addOnPropertyChangedCallback {
|
||||
when (it) {
|
||||
true -> {
|
||||
item.remove = true
|
||||
notice(R.string.remove_file_created)
|
||||
}
|
||||
false -> {
|
||||
item.remove = false
|
||||
notice(R.string.remove_file_deleted)
|
||||
}
|
||||
}
|
||||
}
|
||||
when {
|
||||
item.updated -> notice(R.string.update_file_created)
|
||||
item.remove -> notice(R.string.remove_file_created)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggle() = isChecked.toggle()
|
||||
fun toggleDelete() = isDeletable.toggle()
|
||||
|
||||
private fun notice(@StringRes info: Int) {
|
||||
lastActionNotice.value = get<Resources>().getString(info)
|
||||
}
|
||||
|
||||
override fun contentSameAs(other: ModuleRvItem): Boolean = item.version == other.item.version
|
||||
&& item.versionCode == other.item.versionCode
|
||||
&& item.description == other.item.description
|
||||
&& item.name == other.item.name
|
||||
|
||||
override fun itemSameAs(other: ModuleRvItem): Boolean = item.id == other.item.id
|
||||
}
|
||||
|
||||
class RepoRvItem(val item: Repo) : ComparableRvItem<RepoRvItem>() {
|
||||
|
||||
override val layoutRes: Int = R.layout.item_repo
|
||||
|
||||
override fun contentSameAs(other: RepoRvItem): Boolean = item == other.item
|
||||
|
||||
override fun itemSameAs(other: RepoRvItem): Boolean = item.id == other.item.id
|
||||
}
|
@@ -0,0 +1,52 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.magisk.extensions.toggle
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||
import com.topjohnwu.magisk.model.events.PolicyEnableEvent
|
||||
import com.topjohnwu.magisk.model.events.PolicyUpdateEvent
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
import com.topjohnwu.magisk.utils.RxBus
|
||||
|
||||
class PolicyRvItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvItem<PolicyRvItem>() {
|
||||
|
||||
override val layoutRes: Int = R.layout.item_policy
|
||||
|
||||
val isExpanded = KObservableField(false)
|
||||
val isEnabled = KObservableField(item.policy == MagiskPolicy.ALLOW)
|
||||
val shouldNotify = KObservableField(item.notification)
|
||||
val shouldLog = KObservableField(item.logging)
|
||||
|
||||
fun toggle() = isExpanded.toggle()
|
||||
|
||||
private val rxBus: RxBus by inject()
|
||||
|
||||
private val currentStateItem
|
||||
get() = item.copy(
|
||||
policy = if (isEnabled.value) MagiskPolicy.ALLOW else MagiskPolicy.DENY,
|
||||
notification = shouldNotify.value,
|
||||
logging = shouldLog.value
|
||||
)
|
||||
|
||||
init {
|
||||
isEnabled.addOnPropertyChangedCallback {
|
||||
it ?: return@addOnPropertyChangedCallback
|
||||
rxBus.post(PolicyEnableEvent(this@PolicyRvItem, it))
|
||||
}
|
||||
shouldNotify.addOnPropertyChangedCallback {
|
||||
it ?: return@addOnPropertyChangedCallback
|
||||
rxBus.post(PolicyUpdateEvent.Notification(currentStateItem))
|
||||
}
|
||||
shouldLog.addOnPropertyChangedCallback {
|
||||
it ?: return@addOnPropertyChangedCallback
|
||||
rxBus.post(PolicyUpdateEvent.Log(currentStateItem))
|
||||
}
|
||||
}
|
||||
|
||||
override fun contentSameAs(other: PolicyRvItem): Boolean = itemSameAs(other)
|
||||
override fun itemSameAs(other: PolicyRvItem): Boolean = item.uid == other.item.uid
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
|
||||
class SectionRvItem(val text: String) : ComparableRvItem<SectionRvItem>() {
|
||||
override val layoutRes: Int = R.layout.item_section
|
||||
|
||||
override fun contentSameAs(other: SectionRvItem) = itemSameAs(other)
|
||||
override fun itemSameAs(other: SectionRvItem) = text == other.text
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
|
||||
class SpinnerRvItem(val item: String) : ComparableRvItem<SpinnerRvItem>() {
|
||||
|
||||
override val layoutRes: Int = R.layout.item_spinner
|
||||
|
||||
override fun contentSameAs(other: SpinnerRvItem) = itemSameAs(other)
|
||||
override fun itemSameAs(other: SpinnerRvItem) = item == other.item
|
||||
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
package com.topjohnwu.magisk.model.entity.state
|
||||
|
||||
enum class IndeterminateState {
|
||||
CHECKED, INDETERMINATE, UNCHECKED
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
package com.topjohnwu.magisk.model.events
|
||||
|
||||
internal interface EventHandler {
|
||||
|
||||
/**
|
||||
* Called for all [ViewEvent]s published by associated viewModel.
|
||||
* For [SimpleViewEvent]s, both this and [onSimpleEventDispatched]
|
||||
* methods are called - you can choose the way how you handle them.
|
||||
*/
|
||||
fun onEventDispatched(event: ViewEvent) {}
|
||||
|
||||
/**
|
||||
* Called for all [SimpleViewEvent]s published by associated viewModel.
|
||||
* Both this and [onEventDispatched] methods are called - you can choose
|
||||
* the way how you handle them.
|
||||
*/
|
||||
fun onSimpleEventDispatched(event: Int) {}
|
||||
|
||||
val viewEventObserver get() = ViewEventObserver {
|
||||
onEventDispatched(it)
|
||||
if (it is SimpleViewEvent) {
|
||||
onSimpleEventDispatched(it.event)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package com.topjohnwu.magisk.model.events
|
||||
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||
import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem
|
||||
import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem
|
||||
import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem
|
||||
import com.topjohnwu.magisk.utils.RxBus
|
||||
|
||||
class HideProcessEvent(val item: HideProcessRvItem) : RxBus.Event
|
||||
|
||||
class PolicyEnableEvent(val item: PolicyRvItem, val enable: Boolean) : RxBus.Event
|
||||
sealed class PolicyUpdateEvent(val item: MagiskPolicy) : RxBus.Event {
|
||||
class Notification(item: MagiskPolicy) : PolicyUpdateEvent(item)
|
||||
class Log(item: MagiskPolicy) : PolicyUpdateEvent(item)
|
||||
}
|
||||
|
||||
class ModuleUpdatedEvent(val item: ModuleRvItem) : RxBus.Event
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user