Compare commits
6817 Commits
db-version
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c5fa250331 | ||
![]() |
a51588ddd2 | ||
![]() |
b066c40176 | ||
![]() |
bbc2c6b35a | ||
![]() |
42b0b576d1 | ||
![]() |
39f680bb24 | ||
![]() |
19ffd2b491 | ||
![]() |
a8efa1ac07 | ||
![]() |
240b7af585 | ||
![]() |
345915cdf4 | ||
![]() |
d7fb1e0044 | ||
![]() |
0d8018b10e | ||
![]() |
d9b443429d | ||
![]() |
e698f4f8a3 | ||
![]() |
c27e1a697e | ||
![]() |
d549fb905d | ||
![]() |
c758cb60d9 | ||
![]() |
eab5ef59b9 | ||
![]() |
c0a699e21e | ||
![]() |
242662d02a | ||
![]() |
e1ca1552f7 | ||
![]() |
93a160b40d | ||
![]() |
5fad229dbe | ||
![]() |
43a809490c | ||
![]() |
a02ba42cf9 | ||
![]() |
3ca9d47aae | ||
![]() |
c3e81e0f45 | ||
![]() |
c7fd1f186b | ||
![]() |
ed2b119f33 | ||
![]() |
465e92f8bc | ||
![]() |
09c7b21fd8 | ||
![]() |
6671683d08 | ||
![]() |
9e6d931e3b | ||
![]() |
64c349e39a | ||
![]() |
bea8b77538 | ||
![]() |
480e98144d | ||
![]() |
7037bc3e9c | ||
![]() |
4e241f9b1b | ||
![]() |
79ea0a08c3 | ||
![]() |
e02b8cd80e | ||
![]() |
ba56933e87 | ||
![]() |
45ad90bfbc | ||
![]() |
35f6e25d18 | ||
![]() |
b031cf3d07 | ||
![]() |
3821b9e6d6 | ||
![]() |
21354b76ff | ||
![]() |
602c203495 | ||
![]() |
0ead57926c | ||
![]() |
002b01823e | ||
![]() |
45ed83501e | ||
![]() |
2c69c5ccf3 | ||
![]() |
e52b8a27d6 | ||
![]() |
2dc8deca59 | ||
![]() |
25392dd8bd | ||
![]() |
60a8eccb5f | ||
![]() |
6cf9e9d105 | ||
![]() |
5489f0a089 | ||
![]() |
cc12359ba6 | ||
![]() |
173aa6792a | ||
![]() |
72c0c6f4a6 | ||
![]() |
5e8948b534 | ||
![]() |
19b7b57b43 | ||
![]() |
ae37d587d4 | ||
![]() |
0f635d6e80 | ||
![]() |
eeb5f5f2ec | ||
![]() |
4342d87b37 | ||
![]() |
27ddf42897 | ||
![]() |
b642b14d5a | ||
![]() |
b2a9854e1a | ||
![]() |
9d738f0d9d | ||
![]() |
d32d866393 | ||
![]() |
f09434869e | ||
![]() |
f12f6694c4 | ||
![]() |
6a3d989778 | ||
![]() |
de1b05d307 | ||
![]() |
16ab3672cd | ||
![]() |
108b9e4bf4 | ||
![]() |
82d1fe8815 | ||
![]() |
ecc017845d | ||
![]() |
6b2157fc1c | ||
![]() |
b8b4da7ac9 | ||
![]() |
0b3edc812c | ||
![]() |
30521f00cd | ||
![]() |
4eea2cb4e1 | ||
![]() |
aa2537eb6d | ||
![]() |
1747a0a737 | ||
![]() |
60c41783cc | ||
![]() |
3d53b22e37 | ||
![]() |
719a599c41 | ||
![]() |
10111b2beb | ||
![]() |
1fd3b308e9 | ||
![]() |
ee5c2b6632 | ||
![]() |
6155bdbe20 | ||
![]() |
8ffbd0b44d | ||
![]() |
a5d678b72e | ||
![]() |
e48c49ce0c | ||
![]() |
7993b04cf4 | ||
![]() |
ca54511cf6 | ||
![]() |
fc463810f6 | ||
![]() |
a1369cdd67 | ||
![]() |
3178624b4b | ||
![]() |
2530487483 | ||
![]() |
03c16e3f1b | ||
![]() |
fee35fe285 | ||
![]() |
b310032cf5 | ||
![]() |
07aae0674f | ||
![]() |
cbea1539f8 | ||
![]() |
90d514ac6a | ||
![]() |
97bc77b03a | ||
![]() |
02ee182508 | ||
![]() |
6e2b258eee | ||
![]() |
5a183d27d1 | ||
![]() |
b52c7ca39a | ||
![]() |
e677d815d4 | ||
![]() |
471d2b86c7 | ||
![]() |
aaf08fea0c | ||
![]() |
37275e2c7c | ||
![]() |
723a4996fb | ||
![]() |
cb53c8bbd6 | ||
![]() |
cc37dab980 | ||
![]() |
24ed554b1d | ||
![]() |
deea4bd696 | ||
![]() |
7db4456aae | ||
![]() |
98c204e74e | ||
![]() |
028d5f5a4f | ||
![]() |
4716e77e6d | ||
![]() |
c1f8862128 | ||
![]() |
05564afdec | ||
![]() |
b1856022c6 | ||
![]() |
b201a4ed0c | ||
![]() |
a34c6e316b | ||
![]() |
f6a630c5c9 | ||
![]() |
6bc82c3cff | ||
![]() |
a0e33276b6 | ||
![]() |
42754339db | ||
![]() |
a8a6dfc802 | ||
![]() |
29c608b176 | ||
![]() |
2be3c0509b | ||
![]() |
86f0891321 | ||
![]() |
72757fd653 | ||
![]() |
a534f37efb | ||
![]() |
246936ede7 | ||
![]() |
dd69feac52 | ||
![]() |
36fc7a4eed | ||
![]() |
fb5c273c8e | ||
![]() |
d8f9aa7bd7 | ||
![]() |
5b2af1b0c8 | ||
![]() |
97ee9bf5fe | ||
![]() |
4f789c560d | ||
![]() |
aca7faa3fa | ||
![]() |
422bba662f | ||
![]() |
b2687ca1d6 | ||
![]() |
edff08c3c2 | ||
![]() |
e7014bf7ac | ||
![]() |
5c8c46f3ad | ||
![]() |
6b73ed74ed | ||
![]() |
7912f12cf5 | ||
![]() |
001a337994 | ||
![]() |
f7895cea87 | ||
![]() |
f91728b081 | ||
![]() |
c9fbb97018 | ||
![]() |
18a43ac471 | ||
![]() |
908921e978 | ||
![]() |
8773d6205c | ||
![]() |
a505850110 | ||
![]() |
d9a86d4c16 | ||
![]() |
5acc87c405 | ||
![]() |
f39fc1386a | ||
![]() |
1758c2c9f1 | ||
![]() |
35cc9bce42 | ||
![]() |
cf94cfb543 | ||
![]() |
9d5af90c61 | ||
![]() |
755588202f | ||
![]() |
5af693265f | ||
![]() |
1cd02f02e0 | ||
![]() |
4bf168eedd | ||
![]() |
04ab3aefa4 | ||
![]() |
f8f48e1be4 | ||
![]() |
b5ab4eb978 | ||
![]() |
1a1a06b499 | ||
![]() |
96cf03b277 | ||
![]() |
528eecb63c | ||
![]() |
6f91a60cb2 | ||
![]() |
98b3b74610 | ||
![]() |
4817ec1411 | ||
![]() |
6443b9fecf | ||
![]() |
9419f9d943 | ||
![]() |
4bdc45b105 | ||
![]() |
54712a10b4 | ||
![]() |
46da689cda | ||
![]() |
b5fc3e07e0 | ||
![]() |
477024c79e | ||
![]() |
8fd6d0a4fb | ||
![]() |
ababe82a37 | ||
![]() |
548fe432a7 | ||
![]() |
3e372bc9f9 | ||
![]() |
21303adb27 | ||
![]() |
e7eba5f332 | ||
![]() |
0f91aff535 | ||
![]() |
f850c9b73d | ||
![]() |
8666eabde8 | ||
![]() |
f051b710df | ||
![]() |
dc39730c0b | ||
![]() |
6ecf410f6f | ||
![]() |
3555f18de1 | ||
![]() |
149ae6aa95 | ||
![]() |
f423a04f08 | ||
![]() |
86613d08d5 | ||
![]() |
afb6928bb1 | ||
![]() |
826852b729 | ||
![]() |
e7f4e8b8d3 | ||
![]() |
d6e4645d08 | ||
![]() |
28a88172f3 | ||
![]() |
b3dcaf2721 | ||
![]() |
98442b6938 | ||
![]() |
3ee4823354 | ||
![]() |
a4027e0f73 | ||
![]() |
437db5fab3 | ||
![]() |
dbdc8e38d5 | ||
![]() |
6ab33ce442 | ||
![]() |
06620d6c52 | ||
![]() |
dfa6edf9ed | ||
![]() |
9f54ff09f8 | ||
![]() |
c843950e5f | ||
![]() |
6fa6e5710b | ||
![]() |
58130712e6 | ||
![]() |
3d84a1f488 | ||
![]() |
1d0e533643 | ||
![]() |
1b218c8515 | ||
![]() |
7391dbbf64 | ||
![]() |
17d9aeabeb | ||
![]() |
aca17d551c | ||
![]() |
e69a7f7556 | ||
![]() |
d49653cb33 | ||
![]() |
5aafe23c7a | ||
![]() |
5b14043201 | ||
![]() |
9c0141345f | ||
![]() |
ba794c1116 | ||
![]() |
904a7ef3ac | ||
![]() |
909cedba75 | ||
![]() |
70c7d6dace | ||
![]() |
2fa0ac280c | ||
![]() |
7c3cf560bc | ||
![]() |
4bd129f1d3 | ||
![]() |
6a8a9fd323 | ||
![]() |
d2728ea23e | ||
![]() |
e256cf9892 | ||
![]() |
70ca7b184e | ||
![]() |
29f5d0c8b3 | ||
![]() |
96637c71e6 | ||
![]() |
1f570e38d9 | ||
![]() |
49e69c9ec1 | ||
![]() |
d7ace10735 | ||
![]() |
41094bdaf3 | ||
![]() |
bcbc1940bf | ||
![]() |
17cceab5a1 | ||
![]() |
51eaaf6c94 | ||
![]() |
82b9f21fc0 | ||
![]() |
191b3e5eab | ||
![]() |
a7a6679573 | ||
![]() |
09614a75ea | ||
![]() |
9c2d061283 | ||
![]() |
b97f1f7955 | ||
![]() |
6d14eb4562 | ||
![]() |
ce4375f7c7 | ||
![]() |
1e090e2440 | ||
![]() |
c1d8b944b3 | ||
![]() |
f0ddc16aea | ||
![]() |
8e8a7c0b74 | ||
![]() |
d558d396ed | ||
![]() |
a207798f5c | ||
![]() |
d1e80bb067 | ||
![]() |
1b594fa830 | ||
![]() |
b5d59349f3 | ||
![]() |
e9bdbb2662 | ||
![]() |
363f5baf39 | ||
![]() |
ee606b84a5 | ||
![]() |
7318d500ba | ||
![]() |
e010f2324c | ||
![]() |
5af3f54272 | ||
![]() |
03eef2d0c0 | ||
![]() |
bdd84a74f1 | ||
![]() |
857540cf8b | ||
![]() |
ea15f8ef97 | ||
![]() |
2685c3de52 | ||
![]() |
12ebb865fc | ||
![]() |
d3be7d692d | ||
![]() |
ce7d241196 | ||
![]() |
eb6ab1ec0a | ||
![]() |
fc8321de17 | ||
![]() |
2a31b0dc61 | ||
![]() |
5fe5754a2d | ||
![]() |
2d9d0c30b1 | ||
![]() |
bde60282f1 | ||
![]() |
6710b74477 | ||
![]() |
bb8fce0272 | ||
![]() |
08b45d3518 | ||
![]() |
1d99cc0f7e | ||
![]() |
8b21c7c1fe | ||
![]() |
fa827e4edb | ||
![]() |
a34216e39f | ||
![]() |
3c3a9c92d4 | ||
![]() |
1a787fe502 | ||
![]() |
509f975fcb | ||
![]() |
9e222c77e0 | ||
![]() |
06863bd8f0 | ||
![]() |
4d19ac29da | ||
![]() |
2bf5f6830f | ||
![]() |
b8d39c9a50 | ||
![]() |
0bc06daa5d | ||
![]() |
d0f64fd44d | ||
![]() |
59c4422ba3 | ||
![]() |
4eaf98c3f0 | ||
![]() |
7cb7b93ccc | ||
![]() |
72a972aa25 | ||
![]() |
161f4248c8 | ||
![]() |
7ca8939889 | ||
![]() |
ef4d1c616f | ||
![]() |
ac35df82ca | ||
![]() |
e4326c12b4 | ||
![]() |
e5205358b4 | ||
![]() |
0bcb7e6bca | ||
![]() |
cd76e5816b | ||
![]() |
679b0f05a7 | ||
![]() |
64470b3991 | ||
![]() |
d85a26c89e | ||
![]() |
825f5f09b5 | ||
![]() |
4a6db7c6b0 | ||
![]() |
0435f6b89c | ||
![]() |
06b3de011e | ||
![]() |
e20f11d528 | ||
![]() |
33ea00507b | ||
![]() |
295fe1793f | ||
![]() |
f8fbc37083 | ||
![]() |
a700b09e53 | ||
![]() |
9b9ead6301 | ||
![]() |
3b6255d8a4 | ||
![]() |
75d43f13b8 | ||
![]() |
51bad7d319 | ||
![]() |
71c7ba4b80 | ||
![]() |
e3c22ca370 | ||
![]() |
6ea5204a22 | ||
![]() |
63ecaf5e04 | ||
![]() |
b80efee952 | ||
![]() |
0eea69d785 | ||
![]() |
a33c6eeb2d | ||
![]() |
0b5883bacb | ||
![]() |
24570a5538 | ||
![]() |
7965f95940 | ||
![]() |
438aea3400 | ||
![]() |
57e4dc8757 | ||
![]() |
9c92916b2b | ||
![]() |
089abd8d6d | ||
![]() |
8cb1522033 | ||
![]() |
4cf206e7e9 | ||
![]() |
d1a2873f3d | ||
![]() |
3adf7ab0fb | ||
![]() |
a6a5e3cc9b | ||
![]() |
6e95bbe380 | ||
![]() |
b21dbb8646 | ||
![]() |
b6c439e0f9 | ||
![]() |
19349ea032 | ||
![]() |
8c2fa955ce | ||
![]() |
6fa5ff6608 | ||
![]() |
292d3219d6 | ||
![]() |
3c22024d94 | ||
![]() |
3afd269a9c | ||
![]() |
4662c0bdaa | ||
![]() |
2975d4c09f | ||
![]() |
018e3221a7 | ||
![]() |
38e4b05e56 | ||
![]() |
a1827f6266 | ||
![]() |
725c82c1b0 | ||
![]() |
30b6c28a52 | ||
![]() |
ed86a2fb4f | ||
![]() |
ee1a794680 | ||
![]() |
dc314963f9 | ||
![]() |
47e8e43318 | ||
![]() |
efe757be07 | ||
![]() |
69fc823beb | ||
![]() |
3a36bb5c2e | ||
![]() |
64e99cf90f | ||
![]() |
611974ecbf | ||
![]() |
36cb4e8a4c | ||
![]() |
a6a4147005 | ||
![]() |
8c8c79d68a | ||
![]() |
857bc5c29e | ||
![]() |
a4e66540c2 | ||
![]() |
801682500a | ||
![]() |
5d24061cfd | ||
![]() |
8ddc28524f | ||
![]() |
7743588f35 | ||
![]() |
e526f67228 | ||
![]() |
6f81c482c6 | ||
![]() |
7f9f1d771b | ||
![]() |
e4f44025f1 | ||
![]() |
20cccf4832 | ||
![]() |
91909a887c | ||
![]() |
04b69a9e2b | ||
![]() |
3d4055d1e8 | ||
![]() |
0e38288705 | ||
![]() |
22cffbb0d8 | ||
![]() |
a0c809f0ad | ||
![]() |
be9b6515cd | ||
![]() |
2a4c9f0fcb | ||
![]() |
e9b03e5a2a | ||
![]() |
f38450fc19 | ||
![]() |
1c8a56e314 | ||
![]() |
c3c31ed033 | ||
![]() |
646f2c8e9f | ||
![]() |
9c3176852e | ||
![]() |
8856f19277 | ||
![]() |
d96dda0519 | ||
![]() |
a81140be47 | ||
![]() |
3cb6cc747b | ||
![]() |
5187b88a08 | ||
![]() |
8cd82e0287 | ||
![]() |
4caab6a135 | ||
![]() |
b9efb143be | ||
![]() |
c927f37013 | ||
![]() |
bdbb95110b | ||
![]() |
5c7be1e852 | ||
![]() |
a11d214039 | ||
![]() |
cb1b4330ef | ||
![]() |
57cee93647 | ||
![]() |
19215db243 | ||
![]() |
3d9a68c1e3 | ||
![]() |
e35335d59c | ||
![]() |
fbbf78dcf8 | ||
![]() |
e757c14148 | ||
![]() |
4737b32a60 | ||
![]() |
2086f50d05 | ||
![]() |
e2979f632d | ||
![]() |
34b24a5b60 | ||
![]() |
e95e99018a | ||
![]() |
e6819e7f12 | ||
![]() |
8f64b45e79 | ||
![]() |
9eba243092 | ||
![]() |
06dbd048af | ||
![]() |
2e10c7ba03 | ||
![]() |
fa7e7fccdc | ||
![]() |
ab8e7935b0 | ||
![]() |
7c81b1ad15 | ||
![]() |
df66d127c2 | ||
![]() |
c72a315872 | ||
![]() |
83b7d7ff67 | ||
![]() |
c5056a8167 | ||
![]() |
c697501f8f | ||
![]() |
06b6f8794f | ||
![]() |
474bbdfdbd | ||
![]() |
69b845c34f | ||
![]() |
99a5262730 | ||
![]() |
97aaf5a067 | ||
![]() |
94dbaab83d | ||
![]() |
ec9ce60813 | ||
![]() |
95a2803185 | ||
![]() |
42cdeddd01 | ||
![]() |
dc8e662b7b | ||
![]() |
4ed6afb532 | ||
![]() |
e24fd389c2 | ||
![]() |
f188e0df71 | ||
![]() |
d4f9e91938 | ||
![]() |
95e1cd4a89 | ||
![]() |
e6529d9ff1 | ||
![]() |
58379f68cf | ||
![]() |
658ce1cfdd | ||
![]() |
9f443e8562 | ||
![]() |
0cc38b3d0a | ||
![]() |
29c8ed09a0 | ||
![]() |
ef22161f91 | ||
![]() |
164adf3cf3 | ||
![]() |
25a61583ac | ||
![]() |
4fc0a3d313 | ||
![]() |
626d924e03 | ||
![]() |
069dc9de15 | ||
![]() |
9e6900452b | ||
![]() |
9be0dc3414 | ||
![]() |
4eb5146f17 | ||
![]() |
3147fd9646 | ||
![]() |
344e5923af | ||
![]() |
1df2613b7c | ||
![]() |
0738c4dfee | ||
![]() |
6134a2ee0a | ||
![]() |
11b375531a | ||
![]() |
292ac90e8e | ||
![]() |
151446adff | ||
![]() |
bd219210c0 | ||
![]() |
1ac266ac1e | ||
![]() |
ac7b4dcbc9 | ||
![]() |
ce7c14e4a2 | ||
![]() |
9c7648a03d | ||
![]() |
faa27d181e | ||
![]() |
7d9c12ecf9 | ||
![]() |
90bd663ad9 | ||
![]() |
a77feac6f0 | ||
![]() |
3d39d1ee4a | ||
![]() |
99b14f5a54 | ||
![]() |
73e189613d | ||
![]() |
e36e1b0aee | ||
![]() |
42dbdef5f1 | ||
![]() |
67bf515a17 | ||
![]() |
aa453227af | ||
![]() |
810445c298 | ||
![]() |
fc88599132 | ||
![]() |
64fb1f17a3 | ||
![]() |
3a15230eba | ||
![]() |
13614a916b | ||
![]() |
6b50a963b8 | ||
![]() |
797a006b02 | ||
![]() |
1d10bf16d2 | ||
![]() |
054d6318e9 | ||
![]() |
560b178c09 | ||
![]() |
bdbf48185d | ||
![]() |
5581a84788 | ||
![]() |
fc97db723a | ||
![]() |
d4134ab03f | ||
![]() |
9d57cd2076 | ||
![]() |
ee22e1710b | ||
![]() |
d59bfd5af4 | ||
![]() |
c986a70a08 | ||
![]() |
fb442d82db | ||
![]() |
865f4a7c1f | ||
![]() |
5a0cfb93ba | ||
![]() |
b2b7a27377 | ||
![]() |
0225805e88 | ||
![]() |
3218a5e0ae | ||
![]() |
6497d8f8ba | ||
![]() |
62aea8a02a | ||
![]() |
12568dfd31 | ||
![]() |
182200659d | ||
![]() |
5acde313b6 | ||
![]() |
2bc0a847ed | ||
![]() |
d35d44f4bf | ||
![]() |
56039cbdbb | ||
![]() |
6e9385b190 | ||
![]() |
cf4429e341 | ||
![]() |
9ac9eca6ce | ||
![]() |
bfa281e9ac | ||
![]() |
6ef94ef9cd | ||
![]() |
e7ae54590e | ||
![]() |
342c5b3fa6 | ||
![]() |
55547291c7 | ||
![]() |
58429b81f7 | ||
![]() |
e33751ef3b | ||
![]() |
34abd85289 | ||
![]() |
9c4c4e9883 | ||
![]() |
897f4b0f8f | ||
![]() |
d14a5200f1 | ||
![]() |
81ba7148d3 | ||
![]() |
0835b777a9 | ||
![]() |
1b428087cf | ||
![]() |
be3ac52000 | ||
![]() |
413e2672c6 | ||
![]() |
331d4a174d | ||
![]() |
7248375a3a | ||
![]() |
f4ae1aacc7 | ||
![]() |
8f4263097c | ||
![]() |
ae4b6a8b7c | ||
![]() |
d00108ba68 | ||
![]() |
6887e09d88 | ||
![]() |
8209bf9a0b | ||
![]() |
6af66abf54 | ||
![]() |
6fd7970ca5 | ||
![]() |
bd45f0fdc5 | ||
![]() |
043ed328d2 | ||
![]() |
fcd3fadc9e | ||
![]() |
fb8922b252 | ||
![]() |
8b9c624d75 | ||
![]() |
5254a6f1aa | ||
![]() |
6a0cd0a14a | ||
![]() |
07f3e778d3 | ||
![]() |
2f0994d16f | ||
![]() |
038eeadca6 | ||
![]() |
5ffb6636a3 | ||
![]() |
9a0ae69bbd | ||
![]() |
554eb27a81 | ||
![]() |
7777272313 | ||
![]() |
0ebb0b1b40 | ||
![]() |
aefaf38cee | ||
![]() |
a2858b7e8b | ||
![]() |
3b318e4b6f | ||
![]() |
a92eb3c899 | ||
![]() |
1924f38bc6 | ||
![]() |
c438611030 | ||
![]() |
dd5153e445 | ||
![]() |
b6f8f8d6b7 | ||
![]() |
6c241c8e4a | ||
![]() |
d5d9aa897c | ||
![]() |
89d6c475f7 | ||
![]() |
de5b35e776 | ||
![]() |
2fa3ff166b | ||
![]() |
4b28ca9591 | ||
![]() |
7b0e2b3ec9 | ||
![]() |
607d27a139 | ||
![]() |
5bf7a33903 | ||
![]() |
0b25365330 | ||
![]() |
a9b37a533e | ||
![]() |
16095c8bfd | ||
![]() |
4640146842 | ||
![]() |
f51527befa | ||
![]() |
2b57f49734 | ||
![]() |
a9ac8ba6ce | ||
![]() |
fd954168ce | ||
![]() |
9ea4024082 | ||
![]() |
3ab5b5f670 | ||
![]() |
8951996831 | ||
![]() |
3a67065878 | ||
![]() |
4cb57eda8f | ||
![]() |
b310c9267f | ||
![]() |
ecd4face33 | ||
![]() |
ba65044889 | ||
![]() |
c2796d95ac | ||
![]() |
f1f8d1b741 | ||
![]() |
83e64f0c61 | ||
![]() |
1f7ee0ba9b | ||
![]() |
e1e086f8b7 | ||
![]() |
d84ffac02d | ||
![]() |
1001fca4f2 | ||
![]() |
a69aa26e9c | ||
![]() |
cdc7b41fb5 | ||
![]() |
f81e7cdca5 | ||
![]() |
ea3b84b6e3 | ||
![]() |
686494fc3a | ||
![]() |
551a869c7e | ||
![]() |
9f37e03e91 | ||
![]() |
3bfe3cd440 | ||
![]() |
f2f75f8129 | ||
![]() |
a4ed1ab5ee | ||
![]() |
1b0525b0bd | ||
![]() |
e89df0b6dc | ||
![]() |
c88d696d64 | ||
![]() |
c305ffbbe1 | ||
![]() |
3ab35a8e92 | ||
![]() |
af3577afc9 | ||
![]() |
829372a18b | ||
![]() |
58e83039ac | ||
![]() |
d7b0356e69 | ||
![]() |
ccbc6c9e8c | ||
![]() |
9830c2685f | ||
![]() |
154d40fd16 | ||
![]() |
aecc0ed234 | ||
![]() |
7096460141 | ||
![]() |
10f64237af | ||
![]() |
9219d20a82 | ||
![]() |
cb1041ddf1 | ||
![]() |
26850ba40e | ||
![]() |
10d848768b | ||
![]() |
b240f9575d | ||
![]() |
c157c3f047 | ||
![]() |
3b2b9ae1df | ||
![]() |
15a024b06e | ||
![]() |
c0344c1eed | ||
![]() |
021d5cc1ff | ||
![]() |
b316eab85d | ||
![]() |
8cf267ea6c | ||
![]() |
4bb158ef77 | ||
![]() |
5a0092d42e | ||
![]() |
fe45b33851 | ||
![]() |
0797d82d67 | ||
![]() |
adc62211aa | ||
![]() |
33fb22eae1 | ||
![]() |
138b78572c | ||
![]() |
3406edefcd | ||
![]() |
87d4779c2d | ||
![]() |
c8dcb6260c | ||
![]() |
6730d6097e | ||
![]() |
d0b8da7322 | ||
![]() |
1b300e1273 | ||
![]() |
d33a1851a3 | ||
![]() |
62ab323fff | ||
![]() |
fa6fc5ef5c | ||
![]() |
52796f786e | ||
![]() |
998c56e721 | ||
![]() |
f7f15df0a9 | ||
![]() |
d3b041cb86 | ||
![]() |
ba8c150a72 | ||
![]() |
9bc61e07c8 | ||
![]() |
f996061712 | ||
![]() |
b92bf6700c | ||
![]() |
a1bc1c1613 | ||
![]() |
80a5b4066b | ||
![]() |
0c8bdf9cc8 | ||
![]() |
16d0fc9a4c | ||
![]() |
11709ca2b3 | ||
![]() |
759d324985 | ||
![]() |
48f1d50917 | ||
![]() |
3f8bcc38f2 | ||
![]() |
cf3822012f | ||
![]() |
3a3011a5e0 | ||
![]() |
61b0d3f875 | ||
![]() |
1e7596ecb6 | ||
![]() |
0e8656cd70 | ||
![]() |
f6e57ea946 | ||
![]() |
bd1da011dc | ||
![]() |
a0e64f1085 | ||
![]() |
d362cb33c8 | ||
![]() |
a374b114e6 | ||
![]() |
18c82fcb2a | ||
![]() |
2f5276c4c7 | ||
![]() |
02749f36ff | ||
![]() |
3a005cbf52 | ||
![]() |
b34d736592 | ||
![]() |
dce093cf8b | ||
![]() |
4710ce460e | ||
![]() |
25333469ff | ||
![]() |
ed491ed609 | ||
![]() |
aeedf5f396 | ||
![]() |
61ff42d74e | ||
![]() |
b5c62dbc79 | ||
![]() |
4ba21b82d3 | ||
![]() |
35403599e4 | ||
![]() |
1a61016851 | ||
![]() |
9a38fac9ed | ||
![]() |
f739b82735 | ||
![]() |
04a1f48144 | ||
![]() |
c7944d1274 | ||
![]() |
27d17207e0 | ||
![]() |
0e2505b3a9 | ||
![]() |
11fdbb0a8e | ||
![]() |
fdfb4b8140 | ||
![]() |
6665459ae9 | ||
![]() |
43879ea35e | ||
![]() |
351460d0af | ||
![]() |
9740a2a014 | ||
![]() |
95a2f6c969 | ||
![]() |
824cd13c99 | ||
![]() |
157a963b22 | ||
![]() |
7de0e12142 | ||
![]() |
88cb6f7fc9 | ||
![]() |
77a42538ac | ||
![]() |
e436485cf8 | ||
![]() |
54b591b7e9 | ||
![]() |
d2a5e0e987 | ||
![]() |
35aac2727a | ||
![]() |
7246e71679 | ||
![]() |
c70b4b5ac7 | ||
![]() |
c9e76a9641 | ||
![]() |
c623a08727 | ||
![]() |
10a586100e | ||
![]() |
2396a32bb4 | ||
![]() |
dbbedc7ab1 | ||
![]() |
17ce6110c2 | ||
![]() |
f4409bad02 | ||
![]() |
1c6ab58a30 | ||
![]() |
a8f1a58f2d | ||
![]() |
48c520e343 | ||
![]() |
9fd0a433e4 | ||
![]() |
708a6d8dbd | ||
![]() |
e0cb3f8afc | ||
![]() |
2021f43761 | ||
![]() |
a44829d33f | ||
![]() |
7b8c12bc78 | ||
![]() |
33683cf870 | ||
![]() |
be5bdf3219 | ||
![]() |
dc93686926 | ||
![]() |
d02ea22572 | ||
![]() |
0c43c66b12 | ||
![]() |
dfec1f305b | ||
![]() |
91e04bd9e8 | ||
![]() |
8e95dcb63c | ||
![]() |
1614ad7c31 | ||
![]() |
fa50b8f1a1 | ||
![]() |
9ec5dbcf82 | ||
![]() |
13208de90a | ||
![]() |
1607df0d8c | ||
![]() |
fbb609824c | ||
![]() |
1b31ffa093 | ||
![]() |
f58bf3eb50 | ||
![]() |
7ba4e02754 | ||
![]() |
7e77a65a24 | ||
![]() |
5320dbbddc | ||
![]() |
cb44a26d77 | ||
![]() |
f86869487f | ||
![]() |
b67e146b44 | ||
![]() |
fc571f14bd | ||
![]() |
215dd00550 | ||
![]() |
d5b08cb8c4 | ||
![]() |
1be81fc813 | ||
![]() |
857a7f7567 | ||
![]() |
850b1cf414 | ||
![]() |
fcc1656595 | ||
![]() |
1ee0d7568b | ||
![]() |
5ba756ba5b | ||
![]() |
fa209c457f | ||
![]() |
44c284d723 | ||
![]() |
2701961175 | ||
![]() |
319b615da0 | ||
![]() |
8a0b7eca12 | ||
![]() |
64e1ec12b9 | ||
![]() |
22c98036c1 | ||
![]() |
f6b3c4bbe2 | ||
![]() |
f089b9df08 | ||
![]() |
14996c1eca | ||
![]() |
0034e1faff | ||
![]() |
0ed4fb8e60 | ||
![]() |
06b839d14f | ||
![]() |
1fde5bd355 | ||
![]() |
930f7e2c13 | ||
![]() |
78e81fb781 | ||
![]() |
8f81bf0cb9 | ||
![]() |
360e61118d | ||
![]() |
d67db78997 | ||
![]() |
cefd706aeb | ||
![]() |
c784274b98 | ||
![]() |
f9663a5ac6 | ||
![]() |
352b6fc7c1 | ||
![]() |
59ad42a73b | ||
![]() |
38374e1d32 | ||
![]() |
18592150c1 | ||
![]() |
e2a75f69ee | ||
![]() |
f9e4506f2d | ||
![]() |
d0604cb886 | ||
![]() |
600842c5ff | ||
![]() |
986c9d0689 | ||
![]() |
eddc99879b | ||
![]() |
c226523ecc | ||
![]() |
78a69e7703 | ||
![]() |
0a9e58cb2a | ||
![]() |
69ced9d114 | ||
![]() |
a4c3df6c24 | ||
![]() |
2f6dfa02d0 | ||
![]() |
e66fdeb8ef | ||
![]() |
d62eba1a00 | ||
![]() |
73f1725da9 | ||
![]() |
aea5452904 | ||
![]() |
864453bb5d | ||
![]() |
757476dc9b | ||
![]() |
dfca502ac4 | ||
![]() |
adf62a5b1f | ||
![]() |
993611e90f | ||
![]() |
3b14604589 | ||
![]() |
73ffd6453b | ||
![]() |
a6d1e18ee9 | ||
![]() |
5c0ffea1ef | ||
![]() |
770d80340b | ||
![]() |
c9178d1a08 | ||
![]() |
23bd3b81dd | ||
![]() |
1061929fb4 | ||
![]() |
35d2047536 | ||
![]() |
82e07f59d9 | ||
![]() |
fbf964f1a3 | ||
![]() |
a28c62e77e | ||
![]() |
36dbdfb8c0 | ||
![]() |
a7c7f83d18 | ||
![]() |
0270cdbccd | ||
![]() |
b3220091e4 | ||
![]() |
78dbde3870 | ||
![]() |
33949c2ee5 | ||
![]() |
2eb652c23a | ||
![]() |
e71f139be5 | ||
![]() |
a071b756a4 | ||
![]() |
49942cf2fb | ||
![]() |
9673ac3102 | ||
![]() |
977a923573 | ||
![]() |
23cd5a0b3b | ||
![]() |
ebdad96b0e | ||
![]() |
35fa77cfe7 | ||
![]() |
2498c127b4 | ||
![]() |
3ddbd00976 | ||
![]() |
4a5bee3e84 | ||
![]() |
56c05933a2 | ||
![]() |
bc6e5e8433 | ||
![]() |
7b5d7f8fed | ||
![]() |
941d8a0b8b | ||
![]() |
a500660a41 | ||
![]() |
a1701ca8c0 | ||
![]() |
4ac2f93238 | ||
![]() |
87d2db3a80 | ||
![]() |
82bca90009 | ||
![]() |
4031df22f4 | ||
![]() |
a0b9150bb3 | ||
![]() |
3a2ab0baa6 | ||
![]() |
a965deb51e | ||
![]() |
527917393e | ||
![]() |
80693c48ff | ||
![]() |
f2a625aa75 | ||
![]() |
3f0361ce79 | ||
![]() |
983593688c | ||
![]() |
32be04285b | ||
![]() |
80408b95d7 | ||
![]() |
fa2cb96577 | ||
![]() |
e1070c0755 | ||
![]() |
4eb86f3e5a | ||
![]() |
d312b80916 | ||
![]() |
f4812c5a1a | ||
![]() |
7e33726a74 | ||
![]() |
41edad08d4 | ||
![]() |
6a06e9ea7c | ||
![]() |
b963130a30 | ||
![]() |
335d7f3faa | ||
![]() |
cbc7c841ff | ||
![]() |
444f593ce6 | ||
![]() |
0533bb8588 | ||
![]() |
17ee32e0b6 | ||
![]() |
cd84aa846c | ||
![]() |
9fb7685fe3 | ||
![]() |
ab13ba34dc | ||
![]() |
1304a6603d | ||
![]() |
204b136fa3 | ||
![]() |
d028baff66 | ||
![]() |
3960a1e6c5 | ||
![]() |
5d49a8a325 | ||
![]() |
65526288a2 | ||
![]() |
65ca378f74 | ||
![]() |
063787ee6e | ||
![]() |
056b47934d | ||
![]() |
4b83d4ee8a | ||
![]() |
e3b1d937b3 | ||
![]() |
41e613ba1b | ||
![]() |
0f9e015f72 | ||
![]() |
3cd77a50ff | ||
![]() |
075d4ee9e5 | ||
![]() |
2a28ede418 | ||
![]() |
83a65b7f95 | ||
![]() |
3a0cce4185 | ||
![]() |
29d920cb72 | ||
![]() |
cafbf4783c | ||
![]() |
f9835f295a | ||
![]() |
d58052d325 | ||
![]() |
f4b52b2753 | ||
![]() |
10af09a911 | ||
![]() |
b3f24ba34d | ||
![]() |
03600c347c | ||
![]() |
bc36e92731 | ||
![]() |
b64b938f78 | ||
![]() |
d1d71e4900 | ||
![]() |
03189986d5 | ||
![]() |
da8d02afcc | ||
![]() |
7cd42ce499 | ||
![]() |
6c4315f9a1 | ||
![]() |
02b613a08e | ||
![]() |
881a7e59c6 | ||
![]() |
827e2b2e04 | ||
![]() |
5de957de0d | ||
![]() |
b0b3657269 | ||
![]() |
3312a1d44b | ||
![]() |
90d803dc7b | ||
![]() |
2cd2f82394 | ||
![]() |
9fca42e252 | ||
![]() |
3f64fd4930 | ||
![]() |
765f6b840f | ||
![]() |
10808e2fea | ||
![]() |
531494e571 | ||
![]() |
cd635459ad | ||
![]() |
cc3d874dc4 | ||
![]() |
0d84836153 | ||
![]() |
a8b75e8243 | ||
![]() |
92327cbc99 | ||
![]() |
1c0957ce77 | ||
![]() |
c987e6ad4c | ||
![]() |
c7f3910472 | ||
![]() |
3b67aa1a60 | ||
![]() |
cd23d5cd85 | ||
![]() |
1cfae44aec | ||
![]() |
e0fd1ec771 | ||
![]() |
2ac9100eea | ||
![]() |
3ff70a8b6a | ||
![]() |
e6d99a6887 | ||
![]() |
a2848520fa | ||
![]() |
14c4cdafc9 | ||
![]() |
1048e44996 | ||
![]() |
03576a656c | ||
![]() |
1e5994d74b | ||
![]() |
a2b6a30e11 | ||
![]() |
74a5a5a400 | ||
![]() |
42409f7c71 | ||
![]() |
6d5d6b5ef8 | ||
![]() |
0ad00f9576 | ||
![]() |
2e275db9b2 | ||
![]() |
6fff5599de | ||
![]() |
3a3c2583ff | ||
![]() |
30d16a8862 | ||
![]() |
5b8d85a4da | ||
![]() |
05347d5cbc | ||
![]() |
fae735c468 | ||
![]() |
ee1f4d3dc4 | ||
![]() |
cc9796127d | ||
![]() |
e2d7199f3f | ||
![]() |
ab81295639 | ||
![]() |
a7fa47e3e9 | ||
![]() |
390f0a8d96 | ||
![]() |
25262e4463 | ||
![]() |
f7c757bf33 | ||
![]() |
432a7882fd | ||
![]() |
1379d1e365 | ||
![]() |
e42718b11f | ||
![]() |
dca8023d7d | ||
![]() |
6375dccb06 | ||
![]() |
4d28b52419 | ||
![]() |
053ffab395 | ||
![]() |
975013700a | ||
![]() |
eabb1ba950 | ||
![]() |
b143a76466 | ||
![]() |
6b7d82dba7 | ||
![]() |
103098d31c | ||
![]() |
aecf4fdc17 | ||
![]() |
984fe6ad2b | ||
![]() |
69e85ed202 | ||
![]() |
bfd6959c34 | ||
![]() |
ae06166d18 | ||
![]() |
666d8cc7d7 | ||
![]() |
38e6103c0f | ||
![]() |
947945dc92 | ||
![]() |
c829d232a3 | ||
![]() |
573c9a3d0a | ||
![]() |
745839e9ed | ||
![]() |
bf75b6bbcb | ||
![]() |
047988895c | ||
![]() |
47974be2a1 | ||
![]() |
0204ce5106 | ||
![]() |
8941188d3c | ||
![]() |
1b7b368e98 | ||
![]() |
7badadcdae | ||
![]() |
d926014538 | ||
![]() |
76eb99d03a | ||
![]() |
f93768b94f | ||
![]() |
eab406ee1f | ||
![]() |
f605b76d3d | ||
![]() |
f6ea6f3763 | ||
![]() |
00c437f6a7 | ||
![]() |
0188758d9f | ||
![]() |
fddc369c6c | ||
![]() |
dca5c73ffd | ||
![]() |
2bf36381e5 | ||
![]() |
a37808a9d7 | ||
![]() |
3796f19cd0 | ||
![]() |
d9ba782d97 | ||
![]() |
dcdfb863d2 | ||
![]() |
9eb863e3da | ||
![]() |
e33eb7cc83 | ||
![]() |
a57a01687f | ||
![]() |
b17c4b473f | ||
![]() |
d0b868025e | ||
![]() |
13b7ae99fb | ||
![]() |
03e56ab389 | ||
![]() |
2536af8cad | ||
![]() |
0e32304952 | ||
![]() |
ad37ca43c4 | ||
![]() |
9abe99d2b9 | ||
![]() |
b074934e3e | ||
![]() |
d21c43e97d | ||
![]() |
cbe264ec2f | ||
![]() |
b3c11263ae | ||
![]() |
7abf7b92b0 | ||
![]() |
6957488974 | ||
![]() |
a1c08a2453 | ||
![]() |
159c0023e7 | ||
![]() |
ea10fe3549 | ||
![]() |
117113e266 | ||
![]() |
fb757a2c01 | ||
![]() |
4f368db1db | ||
![]() |
346db627ba | ||
![]() |
bc78eba6ba | ||
![]() |
09835721f2 | ||
![]() |
7c0cb2064e | ||
![]() |
4312deb4c8 | ||
![]() |
9994728c37 | ||
![]() |
c9040e2cf4 | ||
![]() |
d538760ad1 | ||
![]() |
4253f49e79 | ||
![]() |
a2583f0255 | ||
![]() |
6393923274 | ||
![]() |
96229f01e8 | ||
![]() |
65463781d0 | ||
![]() |
8ee5fa75e4 | ||
![]() |
36961c2139 | ||
![]() |
887f53cf95 | ||
![]() |
df818b0aed | ||
![]() |
f5a5805610 | ||
![]() |
d233b03029 | ||
![]() |
915aeb6c54 | ||
![]() |
e9c26d0e60 | ||
![]() |
ce37822bb7 | ||
![]() |
9fe882294d | ||
![]() |
e7b8eee0b6 | ||
![]() |
1e29c281c6 | ||
![]() |
bfcc5c974e | ||
![]() |
18d6110a31 | ||
![]() |
ebd63bad5e | ||
![]() |
40f157a817 | ||
![]() |
3848ec28c1 | ||
![]() |
aed8748600 | ||
![]() |
89c2409841 | ||
![]() |
2e45e7326b | ||
![]() |
7ca55dd04d | ||
![]() |
cee8711797 | ||
![]() |
b0212d3182 | ||
![]() |
9e500ef30b | ||
![]() |
2072cebfca | ||
![]() |
636b0886d8 | ||
![]() |
5566f4d8fa | ||
![]() |
8822962f85 | ||
![]() |
4e2a22247a | ||
![]() |
084e41c60f | ||
![]() |
e913fc2bd1 | ||
![]() |
d8be3ab597 | ||
![]() |
d3b216e036 | ||
![]() |
c4afbdb563 | ||
![]() |
fe3cc58da9 | ||
![]() |
01061e0dc1 | ||
![]() |
752dce0938 | ||
![]() |
5d078d0fb6 | ||
![]() |
380b366d33 | ||
![]() |
d45d187628 | ||
![]() |
fe92b4afc6 | ||
![]() |
ad9074232d | ||
![]() |
2c882741e7 | ||
![]() |
ec35f0c998 | ||
![]() |
47f8e0bc89 | ||
![]() |
63a67698ac | ||
![]() |
44ae615b29 | ||
![]() |
14739c301f | ||
![]() |
2e5beb2f96 | ||
![]() |
05a225b432 | ||
![]() |
4260539374 | ||
![]() |
6c9afd823e | ||
![]() |
ca5996c5b3 | ||
![]() |
23b296a012 | ||
![]() |
525f99b056 | ||
![]() |
1ce70d3703 | ||
![]() |
474073b48c | ||
![]() |
02669ea31f | ||
![]() |
842db03d8a | ||
![]() |
ef90fd2dfd | ||
![]() |
69642d88f7 | ||
![]() |
32aad6b562 | ||
![]() |
15d349f863 | ||
![]() |
43c7574228 | ||
![]() |
790b5ab27d | ||
![]() |
1e00910a42 | ||
![]() |
dc80851536 | ||
![]() |
ac286dac56 | ||
![]() |
165457d27e | ||
![]() |
14033a7975 | ||
![]() |
45eab6598e | ||
![]() |
9bc78ed297 | ||
![]() |
df3a09dd2b | ||
![]() |
ec6f9cef2b | ||
![]() |
254bd6a4db | ||
![]() |
679820c0b2 | ||
![]() |
9db9f32bf1 | ||
![]() |
b11ac7a64a | ||
![]() |
abd61208d4 | ||
![]() |
ca81518f74 | ||
![]() |
d406a5d66c | ||
![]() |
bac0ae8f25 | ||
![]() |
89140d5334 | ||
![]() |
4c352aeadc | ||
![]() |
3b53af0657 | ||
![]() |
72f5398b79 | ||
![]() |
d762e187dc | ||
![]() |
515b5b08f0 | ||
![]() |
3812331166 | ||
![]() |
b400df3ac3 | ||
![]() |
301c2fff2d | ||
![]() |
7ca4ee6539 | ||
![]() |
736341b34e | ||
![]() |
fd625ca8d4 | ||
![]() |
5dd866d39e | ||
![]() |
067d39f72d | ||
![]() |
88f3a68f83 | ||
![]() |
292950898e | ||
![]() |
1f565d7638 | ||
![]() |
0851fea8eb | ||
![]() |
3ea365a1f5 | ||
![]() |
6d3a7d4537 | ||
![]() |
23fc17ba5f | ||
![]() |
c652718a4a | ||
![]() |
1b670df1fd | ||
![]() |
36f29bc6a8 | ||
![]() |
2289a07483 | ||
![]() |
4258315286 | ||
![]() |
c2070fb76f | ||
![]() |
f69b38aad5 | ||
![]() |
14e006b924 | ||
![]() |
7acc0cd4c3 | ||
![]() |
051d58acaf | ||
![]() |
6e8c4eb1bf | ||
![]() |
7f35420e37 | ||
![]() |
18a7690758 | ||
![]() |
0727787713 | ||
![]() |
74c3c24f0a | ||
![]() |
e02bc4134a | ||
![]() |
fea778793c | ||
![]() |
ddbe93aeb4 | ||
![]() |
da66949b9e | ||
![]() |
effcf4bfa9 | ||
![]() |
46472ba7a4 | ||
![]() |
79e7e78e7f | ||
![]() |
5b610798c2 | ||
![]() |
11e0c1926c | ||
![]() |
6c1375bf3a | ||
![]() |
e7979fca48 | ||
![]() |
e98393f092 | ||
![]() |
289976667d | ||
![]() |
779a0122fd | ||
![]() |
910f5da81a | ||
![]() |
d91fbe7b0e | ||
![]() |
035a89e5f6 | ||
![]() |
39f23f6876 | ||
![]() |
9dc9a23a41 | ||
![]() |
89f5127f6d | ||
![]() |
bc14e62e46 | ||
![]() |
f0158063fb | ||
![]() |
ad3fd26756 | ||
![]() |
b5d94b7476 | ||
![]() |
5851ea73e0 | ||
![]() |
5ddc287ab3 | ||
![]() |
a9aafa080d | ||
![]() |
1e1ea03bc3 | ||
![]() |
d5f2e26ea7 | ||
![]() |
014fb0b99d | ||
![]() |
69ce8dbe8c | ||
![]() |
ea3b47f705 | ||
![]() |
85410504da | ||
![]() |
9fc1ecd5a4 | ||
![]() |
866a7276a4 | ||
![]() |
c5a6445239 | ||
![]() |
1cc14b3499 | ||
![]() |
dffc8e5925 | ||
![]() |
86908ceeaa | ||
![]() |
8d60f40e98 | ||
![]() |
200548d6ba | ||
![]() |
dbfe267540 | ||
![]() |
291e4c4c3c | ||
![]() |
9d6c95d51d | ||
![]() |
fe59522666 | ||
![]() |
6a8f5fb4a7 | ||
![]() |
568abe9f3b | ||
![]() |
68cc241bc0 | ||
![]() |
9aa73b95e4 | ||
![]() |
40c52821fa | ||
![]() |
a8fc3d2b2c | ||
![]() |
fac36457ea | ||
![]() |
826d70f2c9 | ||
![]() |
1d359f82ce | ||
![]() |
c0c5721f6a | ||
![]() |
d62c0cf723 | ||
![]() |
a71489a102 | ||
![]() |
67af661640 | ||
![]() |
af1040443e | ||
![]() |
5cce64e153 | ||
![]() |
738a4be9fd | ||
![]() |
5a3e9f2f58 | ||
![]() |
f76f9357d7 | ||
![]() |
0d9a203447 | ||
![]() |
080f93eb9c | ||
![]() |
306786c9e5 | ||
![]() |
ad093d49e6 | ||
![]() |
b1fd5395b6 | ||
![]() |
951375f526 | ||
![]() |
b8dba454e3 | ||
![]() |
854941363d | ||
![]() |
003c448bba | ||
![]() |
dbe65f0365 | ||
![]() |
c5daf1981a | ||
![]() |
d7453d878d | ||
![]() |
3915abccc0 | ||
![]() |
8be69b7fc4 | ||
![]() |
28b5744fe9 | ||
![]() |
4073c26132 | ||
![]() |
95787b637e | ||
![]() |
fe7d836f28 | ||
![]() |
5c64e722b8 | ||
![]() |
aa1ea98244 | ||
![]() |
2147f952cd | ||
![]() |
a4c19b3dc8 | ||
![]() |
2903a0f059 | ||
![]() |
bf4ead6470 | ||
![]() |
0f2fbc9ed1 | ||
![]() |
a1e1417e42 | ||
![]() |
f9998e15ea | ||
![]() |
b89967d566 | ||
![]() |
bc50ffa2cc | ||
![]() |
dac403731c | ||
![]() |
d2c5dc76de | ||
![]() |
52a1719fa9 | ||
![]() |
04335a9c20 | ||
![]() |
5a41ca85d9 | ||
![]() |
cb1627d666 | ||
![]() |
c4906e0002 | ||
![]() |
307abc5706 | ||
![]() |
623e27931a | ||
![]() |
1c881baa9c | ||
![]() |
4f771c4add | ||
![]() |
d2fea72ed9 | ||
![]() |
272a0e3f27 | ||
![]() |
593ce5284c | ||
![]() |
66e909d606 | ||
![]() |
06b9abcee3 | ||
![]() |
9a8eb5d495 | ||
![]() |
94e441cc9c | ||
![]() |
ada263feca | ||
![]() |
9ed4fe8c64 | ||
![]() |
934eb06ff1 | ||
![]() |
81139c40fa | ||
![]() |
43d3653753 | ||
![]() |
0322e87d18 | ||
![]() |
d201851784 | ||
![]() |
f21e4b5931 | ||
![]() |
56ddbf2056 | ||
![]() |
275a674d26 | ||
![]() |
64ff1c472e | ||
![]() |
727e9ed5dd | ||
![]() |
c7c16131e9 | ||
![]() |
5f14628524 | ||
![]() |
edb2b838eb | ||
![]() |
0aab6bc422 | ||
![]() |
3378c0a333 | ||
![]() |
3a5f96e0b3 | ||
![]() |
7f22c3c221 | ||
![]() |
288577407d | ||
![]() |
04298f8886 | ||
![]() |
d794c5a77c | ||
![]() |
6b0a784a26 | ||
![]() |
e346d2351b | ||
![]() |
4ea775be00 | ||
![]() |
a0b318c383 | ||
![]() |
19a0428944 | ||
![]() |
80bab5611b | ||
![]() |
51431d59ee | ||
![]() |
496a3a2938 | ||
![]() |
59fe365ed6 | ||
![]() |
e3cfe3690d | ||
![]() |
99591c050c | ||
![]() |
90d4821e99 | ||
![]() |
4f1855c8a1 | ||
![]() |
3862f941e6 | ||
![]() |
dcb1c3accd | ||
![]() |
765bd2892d | ||
![]() |
ff111bd6af | ||
![]() |
8eea0f6c34 | ||
![]() |
5d21f0bc86 | ||
![]() |
bba0d5b753 | ||
![]() |
42f73fb54b | ||
![]() |
49c32a4392 | ||
![]() |
07131e981b | ||
![]() |
5e1d95c361 | ||
![]() |
05bbe94b47 | ||
![]() |
dc59d48be3 | ||
![]() |
3adfb65c69 | ||
![]() |
eacce08a13 | ||
![]() |
7ed7cf3bcb | ||
![]() |
886cd0b1bf | ||
![]() |
3a992c10d2 | ||
![]() |
72d33ffe3d | ||
![]() |
bd93c5743f | ||
![]() |
8da7fc5103 | ||
![]() |
7ce7b46ed2 | ||
![]() |
9897c97ef8 | ||
![]() |
6a18ba1b85 | ||
![]() |
b1569795a2 | ||
![]() |
c3ec29df93 | ||
![]() |
a81a61be61 | ||
![]() |
2b1f851617 | ||
![]() |
0f08a66696 | ||
![]() |
14b4a7e00a | ||
![]() |
dd14b9e315 | ||
![]() |
89422e9c8f | ||
![]() |
cf9a6b851d | ||
![]() |
afe6de94a0 | ||
![]() |
9323ccdfd1 | ||
![]() |
9522012fe1 | ||
![]() |
07971080c4 | ||
![]() |
b632066ca7 | ||
![]() |
683a990da8 | ||
![]() |
b5de4898b1 | ||
![]() |
8e8d4a4af6 | ||
![]() |
181f2ec292 | ||
![]() |
6847dc656a | ||
![]() |
a8fbb64f5a | ||
![]() |
e30e5004f1 | ||
![]() |
bb597537f2 | ||
![]() |
1ed99835a0 | ||
![]() |
1587505389 | ||
![]() |
251391f051 | ||
![]() |
074f72f58a | ||
![]() |
c0123eb01b | ||
![]() |
e33da7200b | ||
![]() |
ac406c5323 | ||
![]() |
c64fb61855 | ||
![]() |
8276ab004e | ||
![]() |
57b587dfe3 | ||
![]() |
aebf7efc10 | ||
![]() |
421270ad5f | ||
![]() |
66ac4bcec3 | ||
![]() |
8f854c2c6e | ||
![]() |
71884c16b1 | ||
![]() |
eb12bc1645 | ||
![]() |
9a534b8dea | ||
![]() |
ed798c393b | ||
![]() |
e7c8abbeed | ||
![]() |
84d1b75445 | ||
![]() |
bc3c0da115 | ||
![]() |
e690fbb061 | ||
![]() |
3c185d6d64 | ||
![]() |
22773f3de3 | ||
![]() |
11ff830d3d | ||
![]() |
77b0bdf453 | ||
![]() |
97ad4ddc1f | ||
![]() |
f064e33de9 | ||
![]() |
16a5ac32b7 | ||
![]() |
5ceaa95a34 | ||
![]() |
12728d6101 | ||
![]() |
7bc7fa9288 | ||
![]() |
1deec1c9b3 | ||
![]() |
26c1ef3033 | ||
![]() |
9c8cc20a80 | ||
![]() |
9a04ce4332 | ||
![]() |
d111d0a0d8 | ||
![]() |
a4e80383f9 | ||
![]() |
823ddcaca8 | ||
![]() |
4cc5d58480 | ||
![]() |
e3a24a042d | ||
![]() |
079cf2e903 | ||
![]() |
09abc0734e | ||
![]() |
cf5b9520fb | ||
![]() |
77a4ad4cc9 | ||
![]() |
178371bf54 | ||
![]() |
71920f6082 | ||
![]() |
bce35ef865 | ||
![]() |
7fbfa46e18 | ||
![]() |
309694458b | ||
![]() |
f799d24f1d | ||
![]() |
e5121dc508 | ||
![]() |
945dbdbd3c | ||
![]() |
8ce4882c1d | ||
![]() |
b264688385 | ||
![]() |
e4537a4271 | ||
![]() |
928042540f | ||
![]() |
da9eba94eb | ||
![]() |
d82023b943 | ||
![]() |
7f9bfd38a1 | ||
![]() |
fc474ddf58 | ||
![]() |
4595517aba | ||
![]() |
43e5ab7eb2 | ||
![]() |
0a306a4df2 | ||
![]() |
c97424f054 | ||
![]() |
43ee48ac2b | ||
![]() |
20ebc00e5e | ||
![]() |
9c8c3380c1 | ||
![]() |
30c4a26145 | ||
![]() |
4b30b42e16 | ||
![]() |
0a4ee45083 | ||
![]() |
3868b81587 | ||
![]() |
c031f49ede | ||
![]() |
38e0b8d29d | ||
![]() |
e0f346d4b4 | ||
![]() |
ae0c712a3e | ||
![]() |
59befbd355 | ||
![]() |
d8e8cc82f1 | ||
![]() |
8c5263c5c5 | ||
![]() |
f95af36140 | ||
![]() |
69e2ca4283 | ||
![]() |
0e5dd45859 | ||
![]() |
90c3baf5af | ||
![]() |
1d1f489d85 | ||
![]() |
c4b0955c96 | ||
![]() |
f9bc219073 | ||
![]() |
1571e28f68 | ||
![]() |
ac1a5e0ad8 | ||
![]() |
9380a6fd0f | ||
![]() |
89ce8cb5a9 | ||
![]() |
d6eb7ece89 | ||
![]() |
09ffe21969 | ||
![]() |
6a12fd011a | ||
![]() |
2d2a36bab7 | ||
![]() |
a24a98aaf2 | ||
![]() |
a2addd22a6 | ||
![]() |
f7049a3295 | ||
![]() |
fc27122dae | ||
![]() |
28863cf88f | ||
![]() |
083e3ead75 | ||
![]() |
0e6b4acabf | ||
![]() |
148d1cdc8a | ||
![]() |
9cddc0c990 | ||
![]() |
2b512d96c5 | ||
![]() |
b0ee5b120c | ||
![]() |
a589f53e30 | ||
![]() |
f1a27c72ef | ||
![]() |
b8dc569809 | ||
![]() |
551b577bca | ||
![]() |
ea9697b48e | ||
![]() |
1ea4d0a330 | ||
![]() |
568ef56247 | ||
![]() |
85ed0d46ab | ||
![]() |
24a4da9d89 | ||
![]() |
eb651c9996 | ||
![]() |
9dd64b8cbc | ||
![]() |
09a7ec3fd3 | ||
![]() |
5f660501e3 | ||
![]() |
42934a6ec9 | ||
![]() |
c1db27715c | ||
![]() |
589a29386c | ||
![]() |
a912c3ea3e | ||
![]() |
3f00b2a4fb | ||
![]() |
fef3309714 | ||
![]() |
ce1756db62 | ||
![]() |
f6a7029fed | ||
![]() |
04468d4576 | ||
![]() |
071bca951e | ||
![]() |
d191d10621 | ||
![]() |
255fe37dc1 | ||
![]() |
eb023887a5 | ||
![]() |
409846e199 | ||
![]() |
6dd4523d3c | ||
![]() |
1788546267 | ||
![]() |
ec58141379 | ||
![]() |
9f3a32d986 | ||
![]() |
aace086da4 | ||
![]() |
e8264d7dbb | ||
![]() |
b9b7dab2c4 | ||
![]() |
7eb53518aa | ||
![]() |
b529e10c4a | ||
![]() |
95c375ac26 | ||
![]() |
dd08655d43 | ||
![]() |
5a6d2df3ac | ||
![]() |
dc19b11ae1 | ||
![]() |
59d0f7d6be | ||
![]() |
324cb2998b | ||
![]() |
27e0eaad9e | ||
![]() |
895166e9db | ||
![]() |
f5a5260e3e | ||
![]() |
9e2882451f | ||
![]() |
3193b7f93a | ||
![]() |
a96621c6ec | ||
![]() |
51135e5684 | ||
![]() |
03507804b6 | ||
![]() |
adc1c80b77 | ||
![]() |
b150a01706 | ||
![]() |
b17a40c215 | ||
![]() |
e500299c24 | ||
![]() |
907dc03253 | ||
![]() |
c56e5262d3 | ||
![]() |
d386183f1a | ||
![]() |
4127e235e2 | ||
![]() |
10b9dc7f1e | ||
![]() |
d11c67ead5 | ||
![]() |
1c7e4aac29 | ||
![]() |
11e8a3e2b6 | ||
![]() |
71e87043a2 | ||
![]() |
f10e6f8afe | ||
![]() |
66a7fd81bf | ||
![]() |
2a9a3bddae | ||
![]() |
ae136c8342 | ||
![]() |
93efab2d12 | ||
![]() |
c937cbf297 | ||
![]() |
9ddd5bb1a7 | ||
![]() |
2bb49d7dde | ||
![]() |
fe22a849ff | ||
![]() |
62f1a60949 | ||
![]() |
dd56dc6bca | ||
![]() |
de837284ba | ||
![]() |
8b30179ac2 | ||
![]() |
9fbc1d0d66 | ||
![]() |
d5738f19a2 | ||
![]() |
13c3781b7e | ||
![]() |
9e41200fb9 | ||
![]() |
385d337235 | ||
![]() |
6ff3a422c4 | ||
![]() |
054bdb5cd4 | ||
![]() |
c4265fe5cb | ||
![]() |
070d9af9a8 | ||
![]() |
3b711ea571 | ||
![]() |
6b655b8c04 | ||
![]() |
7e14bf4c10 | ||
![]() |
acefc4ca59 | ||
![]() |
5701cfe34b | ||
![]() |
037c6ec3f6 | ||
![]() |
6647bc465d | ||
![]() |
766247c1d3 | ||
![]() |
9eb6e3fc3e | ||
![]() |
37b310575a | ||
![]() |
834997c5cd | ||
![]() |
9a1162c556 | ||
![]() |
e68de76185 | ||
![]() |
da4c97c4c2 | ||
![]() |
6345195d41 | ||
![]() |
a5ba847df3 | ||
![]() |
c6904bcd74 | ||
![]() |
add211ff67 | ||
![]() |
795dd0dbf7 | ||
![]() |
efd577b36b | ||
![]() |
aead26f02c | ||
![]() |
9cfe8ef091 | ||
![]() |
6b6ab02c9c | ||
![]() |
6db0fa6387 | ||
![]() |
ab8948eb5a | ||
![]() |
9ea6afef33 | ||
![]() |
db5ac3bdfc | ||
![]() |
c291b8f0f8 | ||
![]() |
e02a5987d2 | ||
![]() |
20a1b4c83d | ||
![]() |
20c93c64d8 | ||
![]() |
8ce08c5920 | ||
![]() |
0fb65f6921 | ||
![]() |
558dde5f77 | ||
![]() |
2ddf94a9f0 | ||
![]() |
e450569a69 | ||
![]() |
7493118a92 | ||
![]() |
9d8232472f | ||
![]() |
990d05dc0f | ||
![]() |
fd7acd6304 | ||
![]() |
2f038d81e0 | ||
![]() |
897a35aa76 | ||
![]() |
896d4a788f | ||
![]() |
4a684182ca | ||
![]() |
0e6e839ed7 | ||
![]() |
e75a1a7005 | ||
![]() |
81b32120e9 | ||
![]() |
667197eeac | ||
![]() |
9bb7463863 | ||
![]() |
8fb43b29b2 | ||
![]() |
32296910df | ||
![]() |
ae56d5c97c | ||
![]() |
e5d25ae8d0 | ||
![]() |
6f88dcdbf5 | ||
![]() |
0e368c6d6e | ||
![]() |
3d05363c31 | ||
![]() |
8cae4da748 | ||
![]() |
15aaa12e25 | ||
![]() |
e215c26517 | ||
![]() |
21b10f65c7 | ||
![]() |
0af7c18750 | ||
![]() |
66530358b8 | ||
![]() |
10e275037b | ||
![]() |
d64a55e013 | ||
![]() |
2cf2fe5341 | ||
![]() |
91c84a8588 | ||
![]() |
8de89cc6dd | ||
![]() |
0d1a53863e | ||
![]() |
fc65502ce4 | ||
![]() |
e3b26b76f6 | ||
![]() |
47929fa4b3 | ||
![]() |
03619dea3e | ||
![]() |
b6c69f7548 | ||
![]() |
2194dbd649 | ||
![]() |
73388ddf8c | ||
![]() |
738216c205 | ||
![]() |
f1e5653601 | ||
![]() |
e4c9d1d522 | ||
![]() |
ddb85befa6 | ||
![]() |
f56fbf4dcb | ||
![]() |
57a00938a0 | ||
![]() |
e0d6371147 | ||
![]() |
e8ec045c55 | ||
![]() |
8ad951317a | ||
![]() |
cdf419d1fe | ||
![]() |
49e1e0bf17 | ||
![]() |
af7b94ff4c | ||
![]() |
8e0c355534 | ||
![]() |
a0bc2335ef | ||
![]() |
6c6e3ad82d | ||
![]() |
8f393d9f43 | ||
![]() |
3a501c9eac | ||
![]() |
fe37695751 | ||
![]() |
90acd75b90 | ||
![]() |
109a927a68 | ||
![]() |
8f8aa44f43 | ||
![]() |
395efa2e3f | ||
![]() |
7d808b6e73 | ||
![]() |
a2361df854 | ||
![]() |
e49d8c92df | ||
![]() |
d4ebed134d | ||
![]() |
fb98c75210 | ||
![]() |
6184bf6813 | ||
![]() |
9cd3d81783 | ||
![]() |
94e87d83c5 | ||
![]() |
2aa72ff2ac | ||
![]() |
6876088ede | ||
![]() |
01280d65a5 | ||
![]() |
5e75bc060c | ||
![]() |
8feb735140 | ||
![]() |
1c50e28910 | ||
![]() |
bda755584f | ||
![]() |
d1cbbe72d7 | ||
![]() |
2ff47ed5c3 | ||
![]() |
4bf1a4fa2b | ||
![]() |
a1edfdfc8d | ||
![]() |
ab767ce621 | ||
![]() |
b75fc8e011 | ||
![]() |
e374a3da5a | ||
![]() |
25016fdb2f | ||
![]() |
2040d885f2 | ||
![]() |
34c788681b | ||
![]() |
339afa1e72 | ||
![]() |
048d5531be | ||
![]() |
0d386b824f | ||
![]() |
e44ca193dd | ||
![]() |
8503a625b5 | ||
![]() |
ac7916298a | ||
![]() |
1cb9754545 | ||
![]() |
1771f2ce9f | ||
![]() |
36c0505115 | ||
![]() |
0b3e52a924 | ||
![]() |
e05e306006 | ||
![]() |
ca42217217 | ||
![]() |
9d398746fa | ||
![]() |
42424ad902 | ||
![]() |
277cd3c992 | ||
![]() |
01abcc2f4d | ||
![]() |
a912eebe5b | ||
![]() |
a12425cb92 | ||
![]() |
f7986073a8 | ||
![]() |
6996bb6968 | ||
![]() |
0952f3ba71 | ||
![]() |
862e985cc4 | ||
![]() |
be560f7179 | ||
![]() |
6c66697762 | ||
![]() |
ab8f405dd2 | ||
![]() |
955edb49cd | ||
![]() |
d74e63c784 | ||
![]() |
35a2ec5687 | ||
![]() |
c7dcb92a20 | ||
![]() |
54a080f7f7 | ||
![]() |
edb9fcd044 | ||
![]() |
5295b6f0d1 | ||
![]() |
6bcc1f2e68 | ||
![]() |
11c28bff2c | ||
![]() |
4ff6afad1b | ||
![]() |
02f10ea632 | ||
![]() |
c2d9749105 | ||
![]() |
1fe0d8f4b7 | ||
![]() |
d9781d735c | ||
![]() |
5b11898ba3 | ||
![]() |
61a452e7cd | ||
![]() |
0d1cdf84f9 | ||
![]() |
bfb67061b2 | ||
![]() |
6f7a8db019 | ||
![]() |
4cdbee7d3b | ||
![]() |
c2fce47975 | ||
![]() |
1bd4f9f4e7 | ||
![]() |
aa87a02584 | ||
![]() |
56cbcd386c | ||
![]() |
bae8d2718e | ||
![]() |
40e6437602 | ||
![]() |
f17b5061fd | ||
![]() |
c9241ae720 | ||
![]() |
7043627a3a | ||
![]() |
460720c7f3 | ||
![]() |
b1c3d64748 | ||
![]() |
d57b00ab7d | ||
![]() |
2c5fe8ffbe | ||
![]() |
519101a8a4 | ||
![]() |
ea166fbc81 | ||
![]() |
74cfbb9658 | ||
![]() |
6cb54e8821 | ||
![]() |
4d9f3ffa29 | ||
![]() |
2e802fc6e4 | ||
![]() |
8d62f2a27a | ||
![]() |
6a3de3aab4 | ||
![]() |
9ed2539ea5 | ||
![]() |
407e545a24 | ||
![]() |
4292832736 | ||
![]() |
d8d043125b | ||
![]() |
5771908f90 | ||
![]() |
654d2ca40e | ||
![]() |
9e2800a9b9 | ||
![]() |
f1f56abd0f | ||
![]() |
06e922a35e | ||
![]() |
1a7c73064f | ||
![]() |
654c5313de | ||
![]() |
91b070675d | ||
![]() |
5ffec23b2f | ||
![]() |
a5d642911d | ||
![]() |
74a2b25dfc | ||
![]() |
c87f6a64f9 | ||
![]() |
ea80704598 | ||
![]() |
90748a3685 | ||
![]() |
f159432bf1 | ||
![]() |
7f314d7781 | ||
![]() |
9e4e435ae5 | ||
![]() |
71e190ad2b | ||
![]() |
0f4caf79b8 | ||
![]() |
40d80bd41f | ||
![]() |
55c2cbf9c2 | ||
![]() |
0d5d071eac | ||
![]() |
6b0653a488 | ||
![]() |
14d80cf1c9 | ||
![]() |
5aca70c786 | ||
![]() |
53894db911 | ||
![]() |
1f4b6e483a | ||
![]() |
a8af84ecbc | ||
![]() |
b13394fdb0 | ||
![]() |
ab87755b20 | ||
![]() |
361281aa3b | ||
![]() |
1c89076393 | ||
![]() |
c7e692e9e5 | ||
![]() |
99b3152381 | ||
![]() |
a41d64df4a | ||
![]() |
67e90bd1e7 | ||
![]() |
da856786ba | ||
![]() |
54eaa273b6 | ||
![]() |
bc6fba88fa | ||
![]() |
3878b781fd | ||
![]() |
3cb34aa4b0 | ||
![]() |
c8f804d0f6 | ||
![]() |
6f7fdec4eb | ||
![]() |
d5d3abe2a3 | ||
![]() |
e55b121489 | ||
![]() |
00da6080c7 | ||
![]() |
f3974898af | ||
![]() |
ca577dc65a | ||
![]() |
c81e3f3d02 | ||
![]() |
e6c8cfa3f8 | ||
![]() |
4bcceeaf74 | ||
![]() |
2a0150a45e | ||
![]() |
ae7973f5c4 | ||
![]() |
724234ef09 | ||
![]() |
63812715aa | ||
![]() |
93083c6111 | ||
![]() |
9103e49e20 | ||
![]() |
5ae563ae51 | ||
![]() |
3034afedeb | ||
![]() |
b10c8e5c79 | ||
![]() |
5043a081a7 | ||
![]() |
8ab5487daa | ||
![]() |
3fcd880848 | ||
![]() |
36b51c506f | ||
![]() |
b1f0f43546 | ||
![]() |
76150db0c2 | ||
![]() |
2cab890e34 | ||
![]() |
fb8cbd04c3 | ||
![]() |
e76aa0390d | ||
![]() |
3a5ecc5e8e | ||
![]() |
94b70608d0 | ||
![]() |
80734891a6 | ||
![]() |
7b66ad4920 | ||
![]() |
e6fb837af0 | ||
![]() |
34ac465438 | ||
![]() |
07ada4e6b2 | ||
![]() |
fe260d931c | ||
![]() |
3785aecc26 | ||
![]() |
94e5519d23 | ||
![]() |
9c7d97a379 | ||
![]() |
5946d198b0 | ||
![]() |
f8225f3122 | ||
![]() |
ce934d3846 | ||
![]() |
d0d0c1d62c | ||
![]() |
801d1693a7 | ||
![]() |
f15c85146d | ||
![]() |
50bce42940 | ||
![]() |
c04a867b9b | ||
![]() |
40635c36e5 | ||
![]() |
16d15a358d | ||
![]() |
ba2f1e1919 | ||
![]() |
4ff4f8056f | ||
![]() |
b3bd4ff2f7 | ||
![]() |
40fdccf262 | ||
![]() |
518537f23a | ||
![]() |
96e10d7bf1 | ||
![]() |
9f839bcca7 | ||
![]() |
b15fff082e | ||
![]() |
8903a089ba | ||
![]() |
71f7762521 | ||
![]() |
15b0736252 | ||
![]() |
7b679ef57c | ||
![]() |
898c731fcc | ||
![]() |
4e4dd2385b | ||
![]() |
a3d9850a42 | ||
![]() |
14474aed01 | ||
![]() |
e00fe0dcda | ||
![]() |
5c6c54cadf | ||
![]() |
dd2f43dcde | ||
![]() |
dd16906982 | ||
![]() |
a2597ae3e8 | ||
![]() |
404796a9fa | ||
![]() |
dfc6524fc1 | ||
![]() |
2a9c3fee5b | ||
![]() |
e923427232 | ||
![]() |
6fbdd1210e | ||
![]() |
3a2c10f077 | ||
![]() |
5ec9789e73 | ||
![]() |
38a3ccda17 | ||
![]() |
9e0de9ac69 | ||
![]() |
e36d7719b3 | ||
![]() |
7d9f5e880c | ||
![]() |
5f3e952958 | ||
![]() |
552da24d30 | ||
![]() |
d917d6d007 | ||
![]() |
6a86762cd2 | ||
![]() |
e7bc6f1fd7 | ||
![]() |
7ce639f4dd | ||
![]() |
ce8d089dc7 | ||
![]() |
9b8f60c4fe | ||
![]() |
9a33a751a1 | ||
![]() |
2615c461f2 | ||
![]() |
045fc1a35e | ||
![]() |
fa1331139f | ||
![]() |
b9c247e2b1 | ||
![]() |
3fd1b055b3 | ||
![]() |
eb77f72cd2 | ||
![]() |
21e3124b5f | ||
![]() |
c42d7164cf | ||
![]() |
59ec2a7751 | ||
![]() |
36c76070e5 | ||
![]() |
94818e36bf | ||
![]() |
eac85e725f | ||
![]() |
c327cd788b | ||
![]() |
2cba07b117 | ||
![]() |
714a44ad5d | ||
![]() |
b0d11abc4e | ||
![]() |
601e6f6e45 | ||
![]() |
c770d4ef18 | ||
![]() |
f07e5c040c | ||
![]() |
444ecd024b | ||
![]() |
570b532bd6 | ||
![]() |
f90b030e76 | ||
![]() |
3fca7c7153 | ||
![]() |
57e4315e18 | ||
![]() |
73ad5bd6b3 | ||
![]() |
6b4b85d396 | ||
![]() |
9cb53e93d0 | ||
![]() |
615e559ce1 | ||
![]() |
f181e41338 | ||
![]() |
86e128b7e8 | ||
![]() |
b523ecc969 | ||
![]() |
1413c35342 | ||
![]() |
1e6fb13ebc | ||
![]() |
af509e431e | ||
![]() |
00cfaa572f | ||
![]() |
facf251cd1 | ||
![]() |
3c14d273b2 | ||
![]() |
fbf7e98df9 | ||
![]() |
5bd276c804 | ||
![]() |
7ba1966538 | ||
![]() |
98aea01272 | ||
![]() |
b5adf7d0ef | ||
![]() |
36d699a6a6 | ||
![]() |
4fa86f548b | ||
![]() |
ddb694380b | ||
![]() |
e7f428afce | ||
![]() |
d06630f287 | ||
![]() |
4a0dd8abe7 | ||
![]() |
c1656f61a7 | ||
![]() |
af32e4ac85 | ||
![]() |
661aebb75f | ||
![]() |
688057b3e7 | ||
![]() |
2c6ed51a35 | ||
![]() |
9bec554d2f | ||
![]() |
48e438d510 | ||
![]() |
fa9a4ce608 | ||
![]() |
cb5923e03b | ||
![]() |
ce19d60e7b | ||
![]() |
8b37ee5863 | ||
![]() |
427f6f7bbf | ||
![]() |
ae2df2a836 | ||
![]() |
2d0168215e | ||
![]() |
258d2a41c3 | ||
![]() |
67a5156641 | ||
![]() |
2b8bfd4aa6 | ||
![]() |
c83c8301e6 | ||
![]() |
06f42f864f | ||
![]() |
3f3ff5ffd7 | ||
![]() |
401d094af1 | ||
![]() |
651f395738 | ||
![]() |
5f2b053b1c | ||
![]() |
df08e84e78 | ||
![]() |
195aaae7e5 | ||
![]() |
a88b9c924e | ||
![]() |
70c9121995 | ||
![]() |
2f0cb30ad0 | ||
![]() |
89e04cc078 | ||
![]() |
759c3b90fc | ||
![]() |
b3d90cd1b6 | ||
![]() |
dd48103516 | ||
![]() |
34381f9cfb | ||
![]() |
5e6ff06e0e | ||
![]() |
9a8f343fd9 | ||
![]() |
10d4a8fd8b | ||
![]() |
a5acdc3db8 | ||
![]() |
44fcfd36f9 | ||
![]() |
b10fa425b5 | ||
![]() |
504286d44b | ||
![]() |
efa1106d39 | ||
![]() |
6e64438fa6 | ||
![]() |
2a3aaacf23 | ||
![]() |
b19eb1f91d | ||
![]() |
28fb229ad0 | ||
![]() |
cd31976a58 | ||
![]() |
0260afcc99 | ||
![]() |
e3961a8c00 | ||
![]() |
be11306a45 | ||
![]() |
d0e8d2da01 | ||
![]() |
5de555d0b4 | ||
![]() |
20b75d5edc | ||
![]() |
f57cafbdb1 | ||
![]() |
aee621f3f5 | ||
![]() |
cc540301d1 | ||
![]() |
57b2032e30 | ||
![]() |
ccac3c41ff | ||
![]() |
51b22d50b6 | ||
![]() |
df65905d19 | ||
![]() |
35471db83c | ||
![]() |
72fcc3d2c5 | ||
![]() |
32e5471d6c | ||
![]() |
ce0dfa48f9 | ||
![]() |
f32ed1eaed | ||
![]() |
f52194ef7e | ||
![]() |
806580acd3 | ||
![]() |
dbfac03b52 | ||
![]() |
7c1e2ee09c | ||
![]() |
52d09e6418 | ||
![]() |
23822cc247 | ||
![]() |
9ffab6db0c | ||
![]() |
e272badf8e | ||
![]() |
9cf98ae158 | ||
![]() |
50056317df | ||
![]() |
29a3016bd4 | ||
![]() |
10db3bc057 | ||
![]() |
a74c68afd4 | ||
![]() |
016e65375e | ||
![]() |
068e0d1a7e | ||
![]() |
a7ebb7827a | ||
![]() |
40d6ffeb49 | ||
![]() |
29c70d998e | ||
![]() |
215db81802 | ||
![]() |
5547f12527 | ||
![]() |
978f4a2928 | ||
![]() |
b9144cc95d | ||
![]() |
64863bb1e1 | ||
![]() |
b4620bd0b6 | ||
![]() |
8df7781eed | ||
![]() |
74bf125ccb | ||
![]() |
cf7b786054 | ||
![]() |
80915589ee | ||
![]() |
dffac4e797 | ||
![]() |
6055874d9d | ||
![]() |
86cc977746 | ||
![]() |
a0624639fc | ||
![]() |
6bb8d5e92b | ||
![]() |
70370f5d88 | ||
![]() |
c76f7addd1 | ||
![]() |
6ad3604a0e | ||
![]() |
1ef1128cf7 | ||
![]() |
7d45781611 | ||
![]() |
3bf167e85f | ||
![]() |
6b484f4f01 | ||
![]() |
80b5addf62 | ||
![]() |
6d1fc68ff1 | ||
![]() |
b419b78c94 | ||
![]() |
1facc7d07c | ||
![]() |
216695285a | ||
![]() |
d80f589b01 | ||
![]() |
2d9cbc5455 | ||
![]() |
1cd2e62505 | ||
![]() |
31b6bbcd3e | ||
![]() |
bee7de476b | ||
![]() |
32601baf5c | ||
![]() |
10e2bf2a7c | ||
![]() |
f1d28d5768 | ||
![]() |
7436274685 | ||
![]() |
4766ed2868 | ||
![]() |
5c2e9305a8 | ||
![]() |
30b00156db | ||
![]() |
833d3f40fd | ||
![]() |
8a0abdd841 | ||
![]() |
532d1dfc72 | ||
![]() |
8600ce8d8a | ||
![]() |
96fb01ca52 | ||
![]() |
a4d35244bc | ||
![]() |
3c341b7e4b | ||
![]() |
f32b26d5c3 | ||
![]() |
782cf9917f | ||
![]() |
c031939379 | ||
![]() |
0982e8dd2d | ||
![]() |
a566890963 | ||
![]() |
c800cb9d5c | ||
![]() |
f8b18a373f | ||
![]() |
959c5c0b84 | ||
![]() |
583dde51db | ||
![]() |
93885371cc | ||
![]() |
e72348b2d9 | ||
![]() |
0159020c2e | ||
![]() |
0f98e1e125 | ||
![]() |
333d0a5ea3 | ||
![]() |
b11cc6ab28 | ||
![]() |
bb37a013a8 | ||
![]() |
c95c2ed283 | ||
![]() |
82f5e4ff31 | ||
![]() |
1e0200fa30 | ||
![]() |
2daf741c4a | ||
![]() |
3fa90e4666 | ||
![]() |
606ef45bcd | ||
![]() |
54ed573070 | ||
![]() |
b79c6a9780 | ||
![]() |
29658259fa | ||
![]() |
887c55247a | ||
![]() |
6448491751 | ||
![]() |
8a8ca2e6f7 | ||
![]() |
6d011c3895 | ||
![]() |
d8879dd425 | ||
![]() |
2fe7faed6e | ||
![]() |
a639edb59c | ||
![]() |
d3cb775101 | ||
![]() |
6260c11efc | ||
![]() |
95b138b5d5 | ||
![]() |
f83094d9fe | ||
![]() |
94c4374bf7 | ||
![]() |
6067c15085 | ||
![]() |
3b65cd38e7 | ||
![]() |
34c17c7e74 | ||
![]() |
b5e6db76a7 | ||
![]() |
6fe1fa581f | ||
![]() |
d6ab0ddde1 | ||
![]() |
e3d47fbe81 | ||
![]() |
c60c606dcf | ||
![]() |
1959657092 | ||
![]() |
c61024e070 | ||
![]() |
720822e260 | ||
![]() |
aab46c8ab2 | ||
![]() |
bb1292586b | ||
![]() |
0f64fd30c6 | ||
![]() |
b13e27f759 | ||
![]() |
9786fd2550 | ||
![]() |
96def8adca | ||
![]() |
4a5ad0a33d | ||
![]() |
88e567354e | ||
![]() |
dd0f791e3f | ||
![]() |
b1c435d3a4 | ||
![]() |
0922953bca | ||
![]() |
3938146f93 | ||
![]() |
a09b1ecb58 | ||
![]() |
f748d1e5fc | ||
![]() |
55aa8e9aa6 | ||
![]() |
69f6560ba8 | ||
![]() |
eda48c09c7 | ||
![]() |
ec2b82f827 | ||
![]() |
81f725f9f8 | ||
![]() |
8f170ddb13 | ||
![]() |
8b074e3a30 | ||
![]() |
91b67a9b39 | ||
![]() |
0983a6a0b0 | ||
![]() |
a170b054dd | ||
![]() |
512789aafa | ||
![]() |
5ddedcf486 | ||
![]() |
1ef92f55cd | ||
![]() |
2efdbfb007 | ||
![]() |
0c5ba631cf | ||
![]() |
1d4c6161dc | ||
![]() |
a8e87c8f0a | ||
![]() |
0508dc25a3 | ||
![]() |
1095469cd0 | ||
![]() |
ed0ab169fb | ||
![]() |
8e729ce86b | ||
![]() |
704456e18c | ||
![]() |
685cd4cfa2 | ||
![]() |
95487cdd74 | ||
![]() |
b6820ecdf8 | ||
![]() |
1c632199b3 | ||
![]() |
1295e9ff53 | ||
![]() |
0e4b06fd1b | ||
![]() |
e83553dafa | ||
![]() |
c8eeab200c | ||
![]() |
5bc1f00f7c | ||
![]() |
2a0ffd4ed2 | ||
![]() |
89480b0b57 | ||
![]() |
f03de08201 | ||
![]() |
9b8eaec0fe | ||
![]() |
ccd24acf2b | ||
![]() |
65d98abaf0 | ||
![]() |
fb219efaab | ||
![]() |
8025004578 | ||
![]() |
389beee6a2 | ||
![]() |
351712a836 | ||
![]() |
46e286df96 | ||
![]() |
76050de2f4 | ||
![]() |
7ae2aac589 | ||
![]() |
100e1c03db | ||
![]() |
b8caa06979 | ||
![]() |
eccae1aa8f | ||
![]() |
d98b6c3077 | ||
![]() |
f61ee5edfa | ||
![]() |
d74d640388 | ||
![]() |
61ebef9e79 | ||
![]() |
9384b6904e | ||
![]() |
7a313262e9 | ||
![]() |
f1a8f7c0d4 | ||
![]() |
5001356f7f | ||
![]() |
945c51bd80 | ||
![]() |
012cf26fc2 | ||
![]() |
24e0db226d | ||
![]() |
7f209f4994 | ||
![]() |
ccb34538af | ||
![]() |
abf01ba42d | ||
![]() |
b682b337ed | ||
![]() |
143469cc8b | ||
![]() |
94be88c2ff | ||
![]() |
1129234341 | ||
![]() |
5056f612ad | ||
![]() |
92f94ef195 | ||
![]() |
a2c436fd65 | ||
![]() |
50b4aac263 | ||
![]() |
810533cb1f | ||
![]() |
cdc0b8b1ea | ||
![]() |
3fbf77c276 | ||
![]() |
d4d9707631 | ||
![]() |
427d0d0aa0 | ||
![]() |
4265b45f18 | ||
![]() |
4149cf7e8c | ||
![]() |
44d984786f | ||
![]() |
ac7169262e | ||
![]() |
4c6dd8d5ac | ||
![]() |
cd5ba9d8f2 | ||
![]() |
b86723be44 | ||
![]() |
6970f4a94a | ||
![]() |
2a6c700648 | ||
![]() |
bcc29ef6c9 | ||
![]() |
540bd4741f | ||
![]() |
c8aac78321 | ||
![]() |
75d3e47572 | ||
![]() |
89a933cd84 | ||
![]() |
6be865f910 | ||
![]() |
3ebb64b120 | ||
![]() |
257c3da9b4 | ||
![]() |
dd87ba9efc | ||
![]() |
61fb63d0f9 | ||
![]() |
1d51385fc6 | ||
![]() |
e38d02a59c | ||
![]() |
30e498b049 | ||
![]() |
f3a58980f4 | ||
![]() |
5f26a78527 | ||
![]() |
c3f46bb5fb | ||
![]() |
dc46a22f67 | ||
![]() |
7e767d0f44 | ||
![]() |
7eb304cfec | ||
![]() |
6d8f29f8da | ||
![]() |
41ae4867d9 | ||
![]() |
e0dafd0d7c | ||
![]() |
da9f8724cd | ||
![]() |
8576a22705 | ||
![]() |
4b0eb32d30 | ||
![]() |
d2e96f8b55 | ||
![]() |
f579eecfb3 | ||
![]() |
b71173392d | ||
![]() |
5e612a67ff | ||
![]() |
0f05e95b2a | ||
![]() |
bf2e04cd2a | ||
![]() |
f17eace301 | ||
![]() |
be87ac7293 | ||
![]() |
f0438a67b1 | ||
![]() |
5e91bb7892 | ||
![]() |
245b918d6e | ||
![]() |
e064bdda56 | ||
![]() |
02707e9a4b | ||
![]() |
87064e771f | ||
![]() |
a160476a14 | ||
![]() |
fba11b8aaf | ||
![]() |
2a577af5a2 | ||
![]() |
b3c3527b04 | ||
![]() |
bb7bb2c4fe | ||
![]() |
dfcb4a15f4 | ||
![]() |
e310810f22 | ||
![]() |
4a35e48ae7 | ||
![]() |
de75c1fef5 | ||
![]() |
fe626e3fd8 | ||
![]() |
18270aba86 | ||
![]() |
24512f6465 | ||
![]() |
58a10a9ab6 | ||
![]() |
461fe4292b | ||
![]() |
669e9f6573 | ||
![]() |
b4938879d0 | ||
![]() |
859357368c | ||
![]() |
a86b0657ff | ||
![]() |
314e55057f | ||
![]() |
ecd70c94b8 | ||
![]() |
fa859466da | ||
![]() |
a35ee4df30 | ||
![]() |
695394fe71 | ||
![]() |
85349fb59b | ||
![]() |
b69a26463c | ||
![]() |
784733cd76 | ||
![]() |
b5ae78cf4d | ||
![]() |
25897df85b | ||
![]() |
1d4f4ce01d | ||
![]() |
32c17e5f72 | ||
![]() |
13e2eddb6a | ||
![]() |
8bececfa58 | ||
![]() |
dc61360824 | ||
![]() |
fd298d26fb | ||
![]() |
2bd70fc6b5 | ||
![]() |
382ee652b8 | ||
![]() |
63cf0742b7 | ||
![]() |
0551b0d1fc | ||
![]() |
5f64985b34 | ||
![]() |
4e544e61fb | ||
![]() |
7424220c02 | ||
![]() |
9b20142fd9 | ||
![]() |
073cb199fc | ||
![]() |
504854547b | ||
![]() |
1fc8828122 | ||
![]() |
caa1ead689 | ||
![]() |
5d17676a01 | ||
![]() |
91a03be6f4 | ||
![]() |
3a79a9b07f | ||
![]() |
bd796136f7 | ||
![]() |
a5caea24e7 | ||
![]() |
1906dd2f67 | ||
![]() |
eed9effb0e | ||
![]() |
e1743c70f6 | ||
![]() |
7db7cca716 | ||
![]() |
445c275abe | ||
![]() |
9cc66e035a | ||
![]() |
dacebceff6 | ||
![]() |
42fa371e1b | ||
![]() |
d65273cdb9 | ||
![]() |
f4766a93c5 | ||
![]() |
6cb3cf1627 | ||
![]() |
71337a49b3 | ||
![]() |
3a3c170781 | ||
![]() |
595f72d5b2 | ||
![]() |
ac1dce24d2 | ||
![]() |
620affa239 | ||
![]() |
80259d00ba | ||
![]() |
d42d83321b | ||
![]() |
5144aedec9 | ||
![]() |
ba96819e40 | ||
![]() |
dd93df9ef2 | ||
![]() |
ef717437a9 | ||
![]() |
68e0f16b7c | ||
![]() |
fc2093a9a6 | ||
![]() |
5739706cdd | ||
![]() |
5e4095f3f1 | ||
![]() |
15b27ef1f6 | ||
![]() |
3ad8467135 | ||
![]() |
679e933bfd | ||
![]() |
eb8af46a94 | ||
![]() |
aa2d791531 | ||
![]() |
709919ada6 | ||
![]() |
2bfa642046 | ||
![]() |
a27d2804f3 | ||
![]() |
75d13d60ae | ||
![]() |
6651117f90 | ||
![]() |
0730d3c676 | ||
![]() |
312bc9f503 | ||
![]() |
2550329ab5 | ||
![]() |
4dd27ef727 | ||
![]() |
2dbeb60666 | ||
![]() |
fdd2a18199 | ||
![]() |
0d874a7ee8 | ||
![]() |
f8c81a47d9 | ||
![]() |
8bfecc89fb | ||
![]() |
a48a92385e | ||
![]() |
d65d64f6f8 | ||
![]() |
7d95f80608 | ||
![]() |
6b1bb92282 | ||
![]() |
3250603000 | ||
![]() |
e77281b99b | ||
![]() |
e9cdc5e952 | ||
![]() |
9be13fd2ab | ||
![]() |
f3c48f8d6b | ||
![]() |
393db2ba86 | ||
![]() |
0f8bec59e1 | ||
![]() |
a3a0c0a15d | ||
![]() |
3b41287cf7 | ||
![]() |
618f83bb23 | ||
![]() |
2d377453d9 | ||
![]() |
a71eb243fa | ||
![]() |
c93cd5a3e6 | ||
![]() |
2d89f8665a | ||
![]() |
f30b73ed17 | ||
![]() |
c6a1e15bc7 | ||
![]() |
6dc49eef0f | ||
![]() |
419c48a234 | ||
![]() |
166171eb32 | ||
![]() |
e331913f6d | ||
![]() |
5f94beb25d | ||
![]() |
c69df9fb8c | ||
![]() |
11152925c5 | ||
![]() |
c5f837f8ab | ||
![]() |
31a52c3d48 | ||
![]() |
a07e2b2db3 | ||
![]() |
45d130f072 | ||
![]() |
5091366b2d | ||
![]() |
dcfeb47bc6 | ||
![]() |
fbd7a7cfb0 | ||
![]() |
d4b04c187d | ||
![]() |
8497b76a21 | ||
![]() |
2bc8c3ee36 | ||
![]() |
9049418e8d | ||
![]() |
e6d77460b7 | ||
![]() |
5a4ffa7cac | ||
![]() |
8e830ed102 | ||
![]() |
745bd5bc38 | ||
![]() |
972869f27d | ||
![]() |
9392878b02 | ||
![]() |
555352e947 | ||
![]() |
f41104a306 | ||
![]() |
bad714d60e | ||
![]() |
78f35db060 | ||
![]() |
1bd9a73dbc | ||
![]() |
e26748e0e0 | ||
![]() |
5bde27daa8 | ||
![]() |
8c3441939f | ||
![]() |
571e05398f | ||
![]() |
488b9b02f9 | ||
![]() |
99f4ae9864 | ||
![]() |
e62f695a48 | ||
![]() |
18057abdb1 | ||
![]() |
2523134485 | ||
![]() |
300677fb9c | ||
![]() |
57664eb086 | ||
![]() |
7e7835c182 | ||
![]() |
2b24c810fa | ||
![]() |
490b757113 | ||
![]() |
86e8df852e | ||
![]() |
dcf395ad9a | ||
![]() |
6c247e3201 | ||
![]() |
2a7fe78483 | ||
![]() |
0180e754fe | ||
![]() |
a430c17950 | ||
![]() |
7dbf03c435 | ||
![]() |
c5a1b11315 | ||
![]() |
06bcf75f9b | ||
![]() |
0d8b0c7fd4 | ||
![]() |
cbf3133e43 | ||
![]() |
339deb97d9 | ||
![]() |
8f680bc1aa | ||
![]() |
89001ac1f5 | ||
![]() |
2fc8060a5d | ||
![]() |
2a40e082e6 | ||
![]() |
acbf563724 | ||
![]() |
d37e25db21 | ||
![]() |
9acc5a2310 | ||
![]() |
bf4b0d89a1 | ||
![]() |
677fd3a522 | ||
![]() |
41f85f3c9d | ||
![]() |
caac895442 | ||
![]() |
6b42b802b3 | ||
![]() |
b7b8865325 | ||
![]() |
ba84bbb9ea | ||
![]() |
ee398167bb | ||
![]() |
8ba7882a98 | ||
![]() |
b9277f52e7 | ||
![]() |
5b52835097 | ||
![]() |
4096c1e1b6 | ||
![]() |
b9334e4507 | ||
![]() |
3d195c9dc0 | ||
![]() |
b7a20bbf01 | ||
![]() |
df20d2df8d | ||
![]() |
827235b6d7 | ||
![]() |
66d3a50564 | ||
![]() |
5de66eae15 | ||
![]() |
b658c0b44e | ||
![]() |
945c6f24e6 | ||
![]() |
427d7bcbfa | ||
![]() |
ef230f749c | ||
![]() |
d0cf621314 | ||
![]() |
61ab88f8ea | ||
![]() |
2fbb102f62 | ||
![]() |
1b75a4fd3a | ||
![]() |
b1ac0ff351 | ||
![]() |
79ede18259 | ||
![]() |
41d7e830d3 | ||
![]() |
7ac3ff293e | ||
![]() |
a67393f65f | ||
![]() |
02fad57f0b | ||
![]() |
93d5fc171e | ||
![]() |
31473c4395 | ||
![]() |
eabec87a4c | ||
![]() |
6eaf0360d8 | ||
![]() |
3d5d20e082 | ||
![]() |
b4d22bb4ce | ||
![]() |
b28057a6ee | ||
![]() |
963996b412 | ||
![]() |
444ba43ac0 | ||
![]() |
3484d6f1dd | ||
![]() |
f5810073ea | ||
![]() |
bb47d1e41e | ||
![]() |
53a17e6146 | ||
![]() |
ee1871f07e | ||
![]() |
7b737b7ba6 | ||
![]() |
ab66bfa78b | ||
![]() |
e40ce864ce | ||
![]() |
e6ba0de8b2 | ||
![]() |
d54788aafa | ||
![]() |
c095a85c3d | ||
![]() |
204ac3cca9 | ||
![]() |
edf84c533d | ||
![]() |
5a292b8e77 | ||
![]() |
0409a7dcd6 | ||
![]() |
28b7d69e60 | ||
![]() |
189b2277ba | ||
![]() |
b092d52403 | ||
![]() |
655a30c309 | ||
![]() |
f7d9be9cd5 | ||
![]() |
de149cf589 | ||
![]() |
bb96cdeff9 | ||
![]() |
b729f4dc84 | ||
![]() |
d0444dafca | ||
![]() |
82eb50c2fe | ||
![]() |
aa472ba764 | ||
![]() |
b95a330ccf | ||
![]() |
44ffaa37d6 | ||
![]() |
aae0a57dfe | ||
![]() |
833ae329e4 | ||
![]() |
72d7d902e0 | ||
![]() |
318bbfc140 | ||
![]() |
5b6e515a06 | ||
![]() |
5b182ef38d | ||
![]() |
9e33dcf0db | ||
![]() |
1dbf5704b6 | ||
![]() |
116cb88b81 | ||
![]() |
48fd6d287d | ||
![]() |
a0015cda32 | ||
![]() |
63cad0d754 | ||
![]() |
701d927e7a | ||
![]() |
f66781eac6 | ||
![]() |
9967ed9923 | ||
![]() |
4fd84a57d0 | ||
![]() |
37a2856a1d | ||
![]() |
db120c0f1f | ||
![]() |
feeb8c17ad | ||
![]() |
76fca35c2e | ||
![]() |
f6c669783d | ||
![]() |
edc9941e8b | ||
![]() |
dc6eccc218 | ||
![]() |
4621de34f8 | ||
![]() |
793b62878b | ||
![]() |
087e86b312 | ||
![]() |
d70d597d81 | ||
![]() |
dfadffd65b | ||
![]() |
07cd5e82fc | ||
![]() |
f617402f32 | ||
![]() |
a656e8e133 | ||
![]() |
b3ed64ddaf | ||
![]() |
2a9fefd54e | ||
![]() |
b69587ca65 | ||
![]() |
7d1fac2729 | ||
![]() |
8e2a099e51 | ||
![]() |
ee7055e118 | ||
![]() |
be727ae7c0 | ||
![]() |
7c0c5b2490 | ||
![]() |
702cf2dec8 | ||
![]() |
c5f5eb6a05 | ||
![]() |
ecf0b88c65 | ||
![]() |
e46fccce95 | ||
![]() |
05b4a55c0a | ||
![]() |
18bf71a149 | ||
![]() |
35df6afa53 | ||
![]() |
2c31b9f793 | ||
![]() |
7cf5f2496c | ||
![]() |
371d8ede77 | ||
![]() |
d688831cf5 | ||
![]() |
0506fd4d79 | ||
![]() |
3f125b8e36 | ||
![]() |
e07e4fc223 | ||
![]() |
d71514d628 | ||
![]() |
23e6539589 | ||
![]() |
91eb408280 | ||
![]() |
e965e97623 | ||
![]() |
6002a97a03 | ||
![]() |
876f94e7ca | ||
![]() |
1f3271d72d | ||
![]() |
0fb65d2cfd | ||
![]() |
94e4ded672 | ||
![]() |
7c34c60a41 | ||
![]() |
b848766436 | ||
![]() |
2d9e1ecd4f | ||
![]() |
064be9482d | ||
![]() |
0df1f765e9 | ||
![]() |
09ad7fe3d0 | ||
![]() |
2f4b00dc75 | ||
![]() |
bf8aedd79c | ||
![]() |
7c2125c248 | ||
![]() |
1b390cceca | ||
![]() |
6456f56ced | ||
![]() |
d1014f7b1f | ||
![]() |
186d1dbabb | ||
![]() |
7702e72acb | ||
![]() |
151c83218c | ||
![]() |
eff483d73e | ||
![]() |
2ef9b279a8 | ||
![]() |
38403338da | ||
![]() |
3df626aed1 | ||
![]() |
082b6091fc | ||
![]() |
b99e95304e | ||
![]() |
924f192d97 | ||
![]() |
4200d997a2 | ||
![]() |
c2545aee88 | ||
![]() |
e19cfd2831 | ||
![]() |
3cd4abb510 | ||
![]() |
6c9d4f899e | ||
![]() |
67a29bae8f | ||
![]() |
bd5503b4cd | ||
![]() |
ca4e1c24a3 | ||
![]() |
4e375ca7f8 | ||
![]() |
5e031a5ac1 | ||
![]() |
3558117e3c | ||
![]() |
fe8ce530fe | ||
![]() |
7135cdef48 | ||
![]() |
2e2a6bd419 | ||
![]() |
a12d8e0c6d | ||
![]() |
7e12ae45e3 | ||
![]() |
62a670ba3a | ||
![]() |
1222a9b4c7 | ||
![]() |
676b2b9093 | ||
![]() |
27453855b2 | ||
![]() |
0f5aa55dab | ||
![]() |
a3c5e9631b | ||
![]() |
bbd77c5b79 | ||
![]() |
66612e906c | ||
![]() |
16ddcbd5d8 | ||
![]() |
b7fc904a6a | ||
![]() |
18f8817388 | ||
![]() |
a9d817441f | ||
![]() |
b7bdd40ab4 | ||
![]() |
c7e4a0f3df | ||
![]() |
c7d9235951 | ||
![]() |
1f8330fd06 | ||
![]() |
6c64e1943a | ||
![]() |
3e7b549f48 | ||
![]() |
653f577bf9 | ||
![]() |
2eaf00f9f1 | ||
![]() |
de1cc566a1 | ||
![]() |
e02c0cb555 | ||
![]() |
39e382b810 | ||
![]() |
53417955c9 | ||
![]() |
ef6c03c45d | ||
![]() |
11c42f6a2f | ||
![]() |
0a59c5c6e5 | ||
![]() |
d946678568 | ||
![]() |
4d785bd6bd | ||
![]() |
03168ff99e | ||
![]() |
dc57fd712b | ||
![]() |
bdc9a14c2a | ||
![]() |
add570ab4b | ||
![]() |
50982060ac | ||
![]() |
2586e87524 | ||
![]() |
5febedebd6 | ||
![]() |
561a18ad2b | ||
![]() |
7199e829f3 | ||
![]() |
6c08e054f5 | ||
![]() |
294e1d2821 | ||
![]() |
b1a1c68ad7 | ||
![]() |
69816b5b82 | ||
![]() |
25edfffcbe | ||
![]() |
0d1e00b6cf | ||
![]() |
f8c9d7ec8e | ||
![]() |
06088e2b35 | ||
![]() |
8e168b46a7 | ||
![]() |
1b6dc02513 | ||
![]() |
74795d2f7a | ||
![]() |
7375e09d19 | ||
![]() |
e6c6c28f5f | ||
![]() |
4b70d81e5c | ||
![]() |
2a6dcb63bb | ||
![]() |
6a0b16fc7d | ||
![]() |
1eb224fc5e | ||
![]() |
4cc6e5bc89 | ||
![]() |
e2c82d2943 | ||
![]() |
bae5bdb1f4 | ||
![]() |
96a9681fd1 | ||
![]() |
2272e441c6 | ||
![]() |
86ba3bb6c6 | ||
![]() |
671589d1d6 | ||
![]() |
ccdd8a168c | ||
![]() |
79d526ead1 | ||
![]() |
07bc33d391 | ||
![]() |
c4fd369feb | ||
![]() |
9f50bef04f | ||
![]() |
cb241a23ef | ||
![]() |
b0803432d8 | ||
![]() |
9703589950 | ||
![]() |
471083b8f4 | ||
![]() |
f224483e1b | ||
![]() |
b2d89ec665 | ||
![]() |
67f40367cd | ||
![]() |
3a194026fa | ||
![]() |
670e6be39a | ||
![]() |
4bb7050725 | ||
![]() |
6fae74b36d | ||
![]() |
148d69d9b4 | ||
![]() |
06dff8184b | ||
![]() |
432f45254b | ||
![]() |
db4aa3b519 | ||
![]() |
d844541f6e | ||
![]() |
15d11f6d44 | ||
![]() |
dfff412dfe | ||
![]() |
de15f7d5bb | ||
![]() |
164dc2740c | ||
![]() |
b7ef0bd4e1 | ||
![]() |
cfabd2a2c3 | ||
![]() |
77e3d6a6f1 | ||
![]() |
e9cb347642 | ||
![]() |
6fae06375e | ||
![]() |
4920b5f75f | ||
![]() |
143e5ccc74 | ||
![]() |
0119138f38 | ||
![]() |
131eece46c | ||
![]() |
2fbfcb66b0 | ||
![]() |
93e39d895c | ||
![]() |
17ad6167a3 | ||
![]() |
6c12707720 | ||
![]() |
a9b2ac9388 | ||
![]() |
3c1b66dcd1 | ||
![]() |
c73a65656a | ||
![]() |
97fd3f0bad | ||
![]() |
656b3185b2 | ||
![]() |
8cb277e90d | ||
![]() |
3121a106e0 | ||
![]() |
e6844a5f4f | ||
![]() |
a89323b183 | ||
![]() |
c2685bf14e | ||
![]() |
46adf47fdf | ||
![]() |
ecaf2362f2 | ||
![]() |
b9129e8a35 | ||
![]() |
6891752672 | ||
![]() |
854b19eee6 | ||
![]() |
3d7a4ef8dc | ||
![]() |
46da776c2c | ||
![]() |
8e7096b2a8 | ||
![]() |
16880c638c | ||
![]() |
cfd31e9086 | ||
![]() |
c1af914df8 | ||
![]() |
c43877eed1 | ||
![]() |
78ecba646c | ||
![]() |
49f7248d45 | ||
![]() |
eefb2eb8a6 | ||
![]() |
2353a4e82d | ||
![]() |
6d0bde716e | ||
![]() |
019964387a | ||
![]() |
7f261b339a | ||
![]() |
9d2e981d5e | ||
![]() |
60241f3ae4 | ||
![]() |
9521b9a72c | ||
![]() |
42965701a3 | ||
![]() |
f830d4745d | ||
![]() |
34c741f835 | ||
![]() |
1f035a9696 | ||
![]() |
05d6929eb6 | ||
![]() |
d41b0d330f | ||
![]() |
c5051e8813 | ||
![]() |
3b9f841117 | ||
![]() |
ab5ea39f7c | ||
![]() |
510ec5f7c7 | ||
![]() |
d83c15d0d4 | ||
![]() |
01f27ac404 | ||
![]() |
830fd5e375 | ||
![]() |
1054d57b02 | ||
![]() |
a710eeef3a | ||
![]() |
8c2d922019 | ||
![]() |
9be1936e03 | ||
![]() |
481b96ef17 | ||
![]() |
432eee0f83 | ||
![]() |
655c4bfdd3 | ||
![]() |
5ef5288e3a | ||
![]() |
05f6f08832 | ||
![]() |
62247b867f | ||
![]() |
37e608dbad | ||
![]() |
f83adbef44 | ||
![]() |
74f4883ffc | ||
![]() |
1a799a7dcc | ||
![]() |
f099ae52bf | ||
![]() |
fada693ac9 | ||
![]() |
3efb3a3043 | ||
![]() |
c2d5f57fbd | ||
![]() |
a8e6daadc3 | ||
![]() |
d7fbd868b9 | ||
![]() |
65afc83b16 | ||
![]() |
6f80fbd945 | ||
![]() |
4db31ef975 | ||
![]() |
7b9219e701 | ||
![]() |
4dc18f0a88 | ||
![]() |
5ebe3b2420 | ||
![]() |
e28e29455e | ||
![]() |
8ca795e307 | ||
![]() |
8faf151c90 | ||
![]() |
24a6e9590d | ||
![]() |
50044629ab | ||
![]() |
b0afa3e2fe | ||
![]() |
2ab5c6a1c6 | ||
![]() |
8a69816648 | ||
![]() |
be11a785e5 | ||
![]() |
0b7dc64fb6 | ||
![]() |
b5341efff9 | ||
![]() |
89625ac355 | ||
![]() |
1aea1c9302 | ||
![]() |
c6efdbb20c | ||
![]() |
89e103fc23 | ||
![]() |
cebf5ed915 | ||
![]() |
fb0e453fce | ||
![]() |
5971533b09 | ||
![]() |
bcb2894d4c | ||
![]() |
21fd522e95 | ||
![]() |
a9a37747cb | ||
![]() |
e2ab447315 | ||
![]() |
c69f443506 | ||
![]() |
c3424f9ff3 | ||
![]() |
e34f289246 | ||
![]() |
fb6f61c226 | ||
![]() |
b08dfdcb80 | ||
![]() |
d90c773161 | ||
![]() |
28bcbc548a | ||
![]() |
2a2e475bdc | ||
![]() |
9d026bbdbc | ||
![]() |
f86b65e12a | ||
![]() |
6f58c2a13d | ||
![]() |
38d21cd178 | ||
![]() |
8cfe1d3584 | ||
![]() |
5aa44a4d74 | ||
![]() |
517321356d | ||
![]() |
40643855c4 | ||
![]() |
281dd435e7 | ||
![]() |
9d97546c4f | ||
![]() |
d769dcfc60 | ||
![]() |
7e0ae10e84 | ||
![]() |
0bce5cebe6 | ||
![]() |
2be79c35e7 | ||
![]() |
befdc4a850 | ||
![]() |
61d7ba9f29 | ||
![]() |
8ed62d22e1 | ||
![]() |
b9c429d22d | ||
![]() |
b179aaecff | ||
![]() |
bf8a61765d | ||
![]() |
bb7fca7382 | ||
![]() |
ba503812e4 | ||
![]() |
d4584083af | ||
![]() |
15be01ada9 | ||
![]() |
b177dc2cda | ||
![]() |
53bed0f2e1 | ||
![]() |
836d16393d | ||
![]() |
a32a534957 | ||
![]() |
31fdcba945 | ||
![]() |
80e0f84816 | ||
![]() |
c059b83f2b | ||
![]() |
622a1245dc | ||
![]() |
2d27ba4086 | ||
![]() |
0689a39735 | ||
![]() |
d457a5ca0a | ||
![]() |
03a8151875 | ||
![]() |
343e91280a | ||
![]() |
8f8afdb10d | ||
![]() |
c59899cbff | ||
![]() |
c24ccae02c | ||
![]() |
df25606a29 | ||
![]() |
d4dc0a1f90 | ||
![]() |
d9cb2fc5da | ||
![]() |
d527b26814 | ||
![]() |
60a2ebe9fa | ||
![]() |
43a9a9d229 | ||
![]() |
385a16448c | ||
![]() |
7b77919432 | ||
![]() |
77f3a5feff | ||
![]() |
0826533d40 | ||
![]() |
c275fae9b7 | ||
![]() |
df5db32451 | ||
![]() |
4f73b10230 | ||
![]() |
ce2ac71206 | ||
![]() |
40cc328e98 | ||
![]() |
7659c3a33f | ||
![]() |
d4716e6387 | ||
![]() |
23d40dbca0 | ||
![]() |
9e2f7edff4 | ||
![]() |
c1cf153852 | ||
![]() |
f93a1ba6e4 | ||
![]() |
bded83d8a3 | ||
![]() |
bc1ff7d8c8 | ||
![]() |
1776b1e2c3 | ||
![]() |
e4766645eb | ||
![]() |
7796a3f374 | ||
![]() |
ff946f6a7d | ||
![]() |
a7828bcb9e | ||
![]() |
61e219a02e | ||
![]() |
34efdf1822 | ||
![]() |
3f04606fcc | ||
![]() |
b01a871d9d | ||
![]() |
c65029ae8d | ||
![]() |
b72c418f11 | ||
![]() |
dcf41bcdf2 | ||
![]() |
655030d1b6 | ||
![]() |
e69c04458f | ||
![]() |
2c7489508f | ||
![]() |
09ab4b0051 | ||
![]() |
0c4059d053 | ||
![]() |
c6f7eefddd | ||
![]() |
e8b24bc994 | ||
![]() |
ce946f4257 | ||
![]() |
259dd38ae9 | ||
![]() |
a3de43ff7a | ||
![]() |
84f844109a | ||
![]() |
3139415172 | ||
![]() |
8fbf67c03c | ||
![]() |
13362aad2e | ||
![]() |
045dd7a94c | ||
![]() |
c3b8134be7 | ||
![]() |
2eab1d0e6d | ||
![]() |
a1a7427cd2 | ||
![]() |
92943ebdf3 | ||
![]() |
5e6eed9963 | ||
![]() |
3bc97d931b | ||
![]() |
b8de545179 | ||
![]() |
02dae30a1b | ||
![]() |
9acc34c9ba | ||
![]() |
6ad1f0faef | ||
![]() |
952024768a | ||
![]() |
405279c0ff | ||
![]() |
2164a2662a | ||
![]() |
09af5402e8 | ||
![]() |
f9a860e00c | ||
![]() |
908d5f3a05 | ||
![]() |
7209ab87f5 | ||
![]() |
ffa3a8dbf3 | ||
![]() |
b2d11091a7 | ||
![]() |
d67f23b60c | ||
![]() |
a954b1cd76 | ||
![]() |
258064d495 | ||
![]() |
6e613ad952 | ||
![]() |
fd22ee01e0 | ||
![]() |
48b21626e4 | ||
![]() |
89fa83c8c7 | ||
![]() |
083339f60e | ||
![]() |
a814792594 | ||
![]() |
bfd5c525b0 | ||
![]() |
34e176539b | ||
![]() |
26b9e09f2f | ||
![]() |
55824a491b | ||
![]() |
fff7999aac | ||
![]() |
9bc72ff102 | ||
![]() |
f5e6d73999 | ||
![]() |
53df5473f5 | ||
![]() |
69b58c7646 | ||
![]() |
517301194b | ||
![]() |
b151374a6c | ||
![]() |
f0d4f8f01a | ||
![]() |
226554c026 | ||
![]() |
d8dc1698d6 | ||
![]() |
30701ff9ac | ||
![]() |
1bb82fa077 | ||
![]() |
ba82cfcfc5 | ||
![]() |
04182b64aa | ||
![]() |
eb3c6e6e0b | ||
![]() |
d43b31a701 | ||
![]() |
0b53f69427 | ||
![]() |
238dff5c10 | ||
![]() |
1418a419ea | ||
![]() |
a8509ae833 | ||
![]() |
7b446daf3f | ||
![]() |
a4c187f3e1 | ||
![]() |
04d2d004a3 | ||
![]() |
d2cc4fcb05 | ||
![]() |
40a061ae73 | ||
![]() |
583ea96816 | ||
![]() |
2ad61a4eb7 | ||
![]() |
d00de69974 | ||
![]() |
30d3f8efcc | ||
![]() |
aa945367c9 | ||
![]() |
d2ef357403 | ||
![]() |
7329dc5d8d | ||
![]() |
ef7d8dfce9 | ||
![]() |
a2997747af | ||
![]() |
0b1940c4bf | ||
![]() |
875b0d091f | ||
![]() |
49f20f64b3 | ||
![]() |
1a9f5d60bf | ||
![]() |
c81b2c7fe4 | ||
![]() |
f7e12b4f25 | ||
![]() |
28d683e16a | ||
![]() |
0461860479 | ||
![]() |
7efa62891d | ||
![]() |
55c7a21c90 | ||
![]() |
10cbf1ef7a | ||
![]() |
9e1458f41e | ||
![]() |
1a586511cf | ||
![]() |
c004f1ce10 | ||
![]() |
3ca3a6d699 | ||
![]() |
6ac62d791a | ||
![]() |
a93904d907 | ||
![]() |
bdde162f56 | ||
![]() |
f4c03c6baa | ||
![]() |
25d2659b93 | ||
![]() |
a8d8e65698 | ||
![]() |
601c85103e | ||
![]() |
bc0db92c50 | ||
![]() |
611fd6e5e3 | ||
![]() |
199ab44ec3 | ||
![]() |
6fb1207ae6 | ||
![]() |
34dbf49f6c | ||
![]() |
6f43d4eb18 | ||
![]() |
bfc1060bfb | ||
![]() |
bb4409457b | ||
![]() |
064af8c94c | ||
![]() |
e27ce21b65 | ||
![]() |
0ee14800c3 | ||
![]() |
5e59138082 | ||
![]() |
75eb72c50b | ||
![]() |
2101ec4bbc | ||
![]() |
109c795dd1 | ||
![]() |
81aacac592 | ||
![]() |
79ecffc91c | ||
![]() |
2c1a059da9 | ||
![]() |
2db46e0794 | ||
![]() |
56ccc09f90 | ||
![]() |
3d20a07c8a | ||
![]() |
6a6cede4de | ||
![]() |
d54c138a1d | ||
![]() |
7997024a18 | ||
![]() |
f9f0a0f91c | ||
![]() |
2b088bc438 | ||
![]() |
2c0a5fcb21 | ||
![]() |
42b188a005 | ||
![]() |
2b98f5477b | ||
![]() |
56575c3291 | ||
![]() |
65b16beb61 | ||
![]() |
734741c8f4 | ||
![]() |
ccf763199f | ||
![]() |
a00534c7c7 | ||
![]() |
4f7116fcfb | ||
![]() |
22d9f55afb | ||
![]() |
c50c9f2ea0 | ||
![]() |
1f83cf1eca | ||
![]() |
539455bd16 | ||
![]() |
0847bb6008 | ||
![]() |
6e0ea68bab | ||
![]() |
17c97e577e | ||
![]() |
598d604ca5 | ||
![]() |
9f758c4f86 | ||
![]() |
003b9459da | ||
![]() |
0b09d591ea | ||
![]() |
4e664c3d92 | ||
![]() |
d0a326aa2c | ||
![]() |
d2787d8d09 | ||
![]() |
65f2c0eac7 | ||
![]() |
88661757f6 | ||
![]() |
a67ef806d2 | ||
![]() |
ab72797a41 | ||
![]() |
ab917b6272 | ||
![]() |
2709d00b70 | ||
![]() |
5f09c80914 | ||
![]() |
2e1484bcb2 | ||
![]() |
a0c40bcaae | ||
![]() |
920e6499ab | ||
![]() |
4a92a37df2 | ||
![]() |
46eb6ee3b4 | ||
![]() |
774ca02d22 | ||
![]() |
190cf40ff9 | ||
![]() |
1eb7d389f2 | ||
![]() |
7d2d22cf96 | ||
![]() |
4c42e73243 | ||
![]() |
af24db2d14 | ||
![]() |
a8b84ff815 | ||
![]() |
f566a53efb | ||
![]() |
3b23ea019b | ||
![]() |
e1af82c5f8 | ||
![]() |
7f49c82b62 | ||
![]() |
019e73eb13 | ||
![]() |
18885a66c2 | ||
![]() |
99ad9752c8 | ||
![]() |
5be23b793e | ||
![]() |
05f13df17a | ||
![]() |
b36e581480 | ||
![]() |
d02ea05865 | ||
![]() |
b2d363b947 | ||
![]() |
4a25df8461 | ||
![]() |
0855c0affe | ||
![]() |
f3e88535f9 | ||
![]() |
30bcad963e | ||
![]() |
7061e47a8c | ||
![]() |
5a0b625dc0 | ||
![]() |
46308dcbde | ||
![]() |
d90cb040b1 | ||
![]() |
d3247b9cfd | ||
![]() |
be16137553 | ||
![]() |
7469a1cdab | ||
![]() |
88c60b98f9 | ||
![]() |
56f0505d4a | ||
![]() |
0898db3490 | ||
![]() |
3eb2a5eb96 | ||
![]() |
34450569b3 | ||
![]() |
f355232bd5 | ||
![]() |
99216d923a | ||
![]() |
43303d17cc | ||
![]() |
35996952ef | ||
![]() |
e01ec5f3e8 | ||
![]() |
b799fe8494 | ||
![]() |
5925851f40 | ||
![]() |
7e7ec966ee | ||
![]() |
e14cb9d16a | ||
![]() |
6545a26e31 | ||
![]() |
ab1e869ebe | ||
![]() |
096b7132c4 | ||
![]() |
b9dad4bce6 | ||
![]() |
b852c0dca0 | ||
![]() |
0c6ca09a7d | ||
![]() |
b2b108c39e | ||
![]() |
b2c497e5b9 | ||
![]() |
fdc95c071d | ||
![]() |
f060efb7ba | ||
![]() |
a7a52fbfba | ||
![]() |
2263352ca4 | ||
![]() |
9785536910 | ||
![]() |
354f0a9b53 | ||
![]() |
68f666685f | ||
![]() |
634fe1084a | ||
![]() |
a7a7f77b42 | ||
![]() |
b2d5bcc94a | ||
![]() |
3e3fdd5c07 | ||
![]() |
8757acca1a | ||
![]() |
99f7cab62e | ||
![]() |
bb7cfc14cc | ||
![]() |
19ca68cb30 | ||
![]() |
6f0b33a092 | ||
![]() |
eae81b51ee | ||
![]() |
dd5f84f226 | ||
![]() |
04c7176a2a | ||
![]() |
28853fb2ec | ||
![]() |
8c68849e82 | ||
![]() |
6aba30fe7f | ||
![]() |
3af5416f09 | ||
![]() |
40665e335a | ||
![]() |
d65e72638b | ||
![]() |
63a609fbab | ||
![]() |
a317877120 | ||
![]() |
5d2c2bc6e6 | ||
![]() |
2b2958f89c | ||
![]() |
b72cdff522 | ||
![]() |
ad2059574e | ||
![]() |
c771e9a394 | ||
![]() |
8e2e14d703 | ||
![]() |
189c8bd452 | ||
![]() |
0e70495046 | ||
![]() |
e2e1d3111c | ||
![]() |
2428a89288 | ||
![]() |
53e8061ef3 | ||
![]() |
6f0c9ff88a | ||
![]() |
2350b4e694 | ||
![]() |
d7022dd498 | ||
![]() |
43f380b6b6 | ||
![]() |
a5a90954bc | ||
![]() |
e2256d3d8c | ||
![]() |
a16589eab0 | ||
![]() |
c71590c6fa | ||
![]() |
15181d47f5 | ||
![]() |
d8e83c7c9c | ||
![]() |
72a88583d6 | ||
![]() |
0d4d160407 | ||
![]() |
050d9974b7 | ||
![]() |
663d981c7a | ||
![]() |
4b2aec6d08 | ||
![]() |
88ba2ef5eb | ||
![]() |
6cc8e6143d | ||
![]() |
65c4087b05 | ||
![]() |
470145e611 | ||
![]() |
e1a6c931c6 | ||
![]() |
b8162a1a91 | ||
![]() |
8affa08d11 | ||
![]() |
4c4aef5314 | ||
![]() |
a5e6dad9bf | ||
![]() |
bbac03b4d1 | ||
![]() |
cd9582c990 | ||
![]() |
6eeaf8662a | ||
![]() |
a118cf13f0 | ||
![]() |
e0a1d2384d | ||
![]() |
d062af0975 | ||
![]() |
3a24d21f59 | ||
![]() |
2cc155355b | ||
![]() |
ab02058ece | ||
![]() |
01b8f7f4bf | ||
![]() |
f2a58ad67f | ||
![]() |
c08a2a7bdc | ||
![]() |
efdf328fd3 | ||
![]() |
3ec64d6d82 | ||
![]() |
1d1c1ebb74 | ||
![]() |
e25d26aca0 | ||
![]() |
6d2fdb2843 | ||
![]() |
8ed884887a | ||
![]() |
44a82c4af7 | ||
![]() |
5efa53b466 | ||
![]() |
88c536efb4 | ||
![]() |
486e8e699f | ||
![]() |
45f9379fee | ||
![]() |
97cf69341a | ||
![]() |
6c462713aa | ||
![]() |
c8182d9c01 | ||
![]() |
d35c84b2e1 | ||
![]() |
372b28a71a | ||
![]() |
79df0a1f9b | ||
![]() |
d2291b2134 | ||
![]() |
a08a32020a | ||
![]() |
f102ccff60 | ||
![]() |
c02125db01 | ||
![]() |
cb4edbed44 | ||
![]() |
e69a6d5a8f | ||
![]() |
fc5c41036d | ||
![]() |
723be967ca | ||
![]() |
b90cf7386c | ||
![]() |
5c9dd1a11e | ||
![]() |
6c0a72369c | ||
![]() |
36378acc7b | ||
![]() |
d34a1285e8 | ||
![]() |
28198dddb4 | ||
![]() |
ae78042bda | ||
![]() |
85a8a7d743 | ||
![]() |
ba8c39d3a9 | ||
![]() |
9848db7680 | ||
![]() |
f24c5b6ac7 | ||
![]() |
a5746c03f3 | ||
![]() |
4c7322d917 | ||
![]() |
4598a78bfd | ||
![]() |
bad613fbc1 | ||
![]() |
0433f0eba5 | ||
![]() |
2773db8304 | ||
![]() |
dd134c50bc | ||
![]() |
5e549193f1 | ||
![]() |
f0d9675b55 | ||
![]() |
a3b6185942 | ||
![]() |
3aad107b51 | ||
![]() |
a29d2eb4f9 | ||
![]() |
ab7602c407 | ||
![]() |
e18164acbb | ||
![]() |
8cd48bcbc4 | ||
![]() |
1678223cab | ||
![]() |
1ba6034e19 | ||
![]() |
9b13d98943 | ||
![]() |
cff209cd44 | ||
![]() |
b5b5dd4ede | ||
![]() |
ecd98047da | ||
![]() |
481468372e | ||
![]() |
360f365227 | ||
![]() |
fdc70b0f9d | ||
![]() |
012fe5e224 | ||
![]() |
c4a3b579c4 | ||
![]() |
3657cf3256 | ||
![]() |
49db1a78d7 | ||
![]() |
973174cc7b | ||
![]() |
530144bec6 | ||
![]() |
c9a6cc3051 | ||
![]() |
3aa58bc005 | ||
![]() |
5d58fc0199 | ||
![]() |
632eeff611 | ||
![]() |
1e95f57ef8 | ||
![]() |
1f354a1b3f | ||
![]() |
fde227e889 | ||
![]() |
b30a188498 | ||
![]() |
1599b3c1ae | ||
![]() |
45b083546f | ||
![]() |
b45f7ba27c | ||
![]() |
0577ecfc53 | ||
![]() |
691545f065 | ||
![]() |
3adfbc66aa | ||
![]() |
53cbb26209 | ||
![]() |
fa112ed303 | ||
![]() |
3f53d1bad8 | ||
![]() |
ea0700d406 | ||
![]() |
91cac9fed4 | ||
![]() |
aa848b2617 | ||
![]() |
a1ae08cd1e | ||
![]() |
9e10d46449 | ||
![]() |
a9944f7a22 | ||
![]() |
cbccb910a2 | ||
![]() |
5679880f88 | ||
![]() |
16170d6794 | ||
![]() |
6846d04751 | ||
![]() |
5a6aecad85 | ||
![]() |
cbd6da5267 | ||
![]() |
252330fb86 | ||
![]() |
b855c745e0 | ||
![]() |
20a5f42359 | ||
![]() |
4dc1415035 | ||
![]() |
0614213de0 | ||
![]() |
f6693ab1a1 | ||
![]() |
09829515e8 | ||
![]() |
cbf5914460 | ||
![]() |
40541fadfe | ||
![]() |
c9e3643712 | ||
![]() |
fc98820c93 | ||
![]() |
28dfe970da | ||
![]() |
6204a16024 | ||
![]() |
cd465aae2b | ||
![]() |
b1df9bba01 | ||
![]() |
943e0d0a1c | ||
![]() |
4907e0b289 | ||
![]() |
e77bde2cfa | ||
![]() |
fdec402837 | ||
![]() |
ef21bf973c | ||
![]() |
c6faeea14e | ||
![]() |
7afee40d8f | ||
![]() |
1192410d87 | ||
![]() |
c94d2a1395 | ||
![]() |
ebb6d43cbb | ||
![]() |
6c1b277cab | ||
![]() |
005d109818 | ||
![]() |
fd50a2c730 | ||
![]() |
3f352ae83f | ||
![]() |
626f55b43b | ||
![]() |
9235462e34 | ||
![]() |
2837a235b4 | ||
![]() |
203bcda695 | ||
![]() |
bb88be9403 | ||
![]() |
9637de5e4c | ||
![]() |
125acd6276 | ||
![]() |
4b5481b8f2 | ||
![]() |
5e263c0e0f | ||
![]() |
004c86bc42 | ||
![]() |
903048ffe4 | ||
![]() |
3e3af3bbf3 | ||
![]() |
d47967e03d | ||
![]() |
3c5f8756f4 | ||
![]() |
ba51536b88 | ||
![]() |
0fd1d5ae4c | ||
![]() |
d24f1034f4 | ||
![]() |
3726b2b7fa | ||
![]() |
c61a055ea8 | ||
![]() |
d41cfe85d6 | ||
![]() |
217bde1290 | ||
![]() |
59c25dd998 | ||
![]() |
bfcc706167 | ||
![]() |
a442d539e4 | ||
![]() |
19a033db32 | ||
![]() |
a31fb068ae | ||
![]() |
318f95b51e | ||
![]() |
f4de353900 | ||
![]() |
2e92dc941b | ||
![]() |
321842836c | ||
![]() |
b58138d4a2 | ||
![]() |
ed76576a5c | ||
![]() |
7287acd822 | ||
![]() |
f9bfe2a877 | ||
![]() |
5f3448394c | ||
![]() |
b37d07a829 | ||
![]() |
cbbc3ab60d | ||
![]() |
53e812e198 | ||
![]() |
6a8729e607 | ||
![]() |
dd9b1fc613 | ||
![]() |
7e451c87c7 | ||
![]() |
2804b56d13 | ||
![]() |
485d5e82ed | ||
![]() |
eb14d157db | ||
![]() |
10144f72c9 | ||
![]() |
9fbcc255ab | ||
![]() |
a686529ba5 | ||
![]() |
b3d88aa333 | ||
![]() |
b9b3908dc3 | ||
![]() |
ae6fd96256 | ||
![]() |
52195bb3e9 | ||
![]() |
698c517508 | ||
![]() |
666e853c5c | ||
![]() |
3d182d8e14 | ||
![]() |
c2855c1e8b | ||
![]() |
1c8cba5692 | ||
![]() |
131e7f9dbd | ||
![]() |
94c9114862 | ||
![]() |
b4e0bde57f | ||
![]() |
81910bf749 | ||
![]() |
d3f9cfbdfa | ||
![]() |
ec36f2a1cd | ||
![]() |
6e3b1fde86 | ||
![]() |
2733081b3a | ||
![]() |
30334c3714 | ||
![]() |
cd4700aeed | ||
![]() |
43c063d67e | ||
![]() |
f17482f1eb | ||
![]() |
8d60e70f5d | ||
![]() |
a4458b7cdf | ||
![]() |
19d3d8a1fc | ||
![]() |
0adca3ff2c | ||
![]() |
cc7bf78ed1 | ||
![]() |
2dd053f76b | ||
![]() |
49e0561356 | ||
![]() |
a6e2aaabd8 | ||
![]() |
48ac66b315 | ||
![]() |
bd0efe8ffa | ||
![]() |
f3fffd9f2e | ||
![]() |
6d52bc1022 | ||
![]() |
e38624626c | ||
![]() |
88107cf94e | ||
![]() |
9135026362 | ||
![]() |
c55a53ec69 | ||
![]() |
131978ad02 | ||
![]() |
2ffd4ae428 | ||
![]() |
a8d2b2aff3 | ||
![]() |
47fa5a94b3 | ||
![]() |
942cfb59d6 | ||
![]() |
7fe15e6c7d | ||
![]() |
8af44b1af5 | ||
![]() |
3fae57c360 | ||
![]() |
b6b039aa49 | ||
![]() |
668cc27d29 | ||
![]() |
6fb23d2efa | ||
![]() |
d1ceb84af4 | ||
![]() |
14958d48e3 | ||
![]() |
d1c04de71a | ||
![]() |
8a155aef89 | ||
![]() |
0ea5325b81 | ||
![]() |
315f20df0c | ||
![]() |
7c13cc26fc | ||
![]() |
72c6b86376 | ||
![]() |
3162e93b35 | ||
![]() |
5dd95754cd | ||
![]() |
07a8b7a935 | ||
![]() |
3b649138bd | ||
![]() |
c7fd3f238d | ||
![]() |
64bc13de8a | ||
![]() |
95d8537187 | ||
![]() |
a383c7a457 | ||
![]() |
29689338f9 | ||
![]() |
65cbc9dc10 | ||
![]() |
cd1f59fb29 | ||
![]() |
26f5286f97 | ||
![]() |
ae2def3e0d | ||
![]() |
09eea0d40b | ||
![]() |
ba88bd7060 | ||
![]() |
80b158e7d6 | ||
![]() |
015343f9df | ||
![]() |
d718528466 | ||
![]() |
34aa8ab062 | ||
![]() |
ef403928cf | ||
![]() |
243bb1948f | ||
![]() |
a192389318 | ||
![]() |
d99b357a1f | ||
![]() |
57c63a8e2a | ||
![]() |
5facd1b9a1 | ||
![]() |
16f97125d7 | ||
![]() |
780c3968b5 | ||
![]() |
4fbc327a91 | ||
![]() |
3d59f4f192 | ||
![]() |
b71cb37cad | ||
![]() |
1ac53078ee | ||
![]() |
783d6d1ba6 | ||
![]() |
5dd1cd6ee6 | ||
![]() |
95b7201868 | ||
![]() |
4f67437bc9 | ||
![]() |
89e2c9948a | ||
![]() |
5523a443f8 | ||
![]() |
e285fd6f38 | ||
![]() |
81ff5f3cd2 | ||
![]() |
3f9370371b | ||
![]() |
aae87eb92e | ||
![]() |
f023cd7757 | ||
![]() |
d47e87e53b | ||
![]() |
f3cb1d5bd4 | ||
![]() |
4685d82ee6 | ||
![]() |
679fdb3fb1 | ||
![]() |
eac0df88af | ||
![]() |
05a5041971 | ||
![]() |
174b1d108f | ||
![]() |
6b18ab4204 | ||
![]() |
d40c94ab11 | ||
![]() |
59688c7ff0 | ||
![]() |
c88afd0995 | ||
![]() |
f5db19eba8 | ||
![]() |
7e27edc9b5 | ||
![]() |
1f61259ae3 | ||
![]() |
471447fc10 | ||
![]() |
66d309b848 | ||
![]() |
abb7db0bc2 | ||
![]() |
345d735d8f | ||
![]() |
c1abd09362 | ||
![]() |
f6cc716fac | ||
![]() |
739bd00257 | ||
![]() |
4bed9d67c5 | ||
![]() |
1652c32d51 | ||
![]() |
24ed40bd34 | ||
![]() |
182a63af41 | ||
![]() |
53e74dcdbd | ||
![]() |
4e73d1e5e6 | ||
![]() |
253900e927 | ||
![]() |
660ebc5ec8 | ||
![]() |
60451a050f | ||
![]() |
839ebebd87 | ||
![]() |
4e66bb810f | ||
![]() |
09fd3d188c | ||
![]() |
c7bb93f743 | ||
![]() |
4c7217d796 | ||
![]() |
a987dc0ce5 | ||
![]() |
297208b255 | ||
![]() |
5052266450 | ||
![]() |
99a488dc18 | ||
![]() |
f779118d6b | ||
![]() |
1bb35aced4 | ||
![]() |
2c16ffdefe | ||
![]() |
d2ac7e6eba | ||
![]() |
649647497e | ||
![]() |
fef76eff89 | ||
![]() |
52e4548755 | ||
![]() |
692c5e6917 | ||
![]() |
946b017ab2 | ||
![]() |
2b7a194831 | ||
![]() |
3116094b96 | ||
![]() |
e2f5d97cb2 | ||
![]() |
4e9c8e9e5e | ||
![]() |
7c8ea5c5af | ||
![]() |
2d90a484df | ||
![]() |
7076bb767d | ||
![]() |
a74e951cdf | ||
![]() |
371312ef65 | ||
![]() |
fa7f57a18a | ||
![]() |
e80e445aa5 | ||
![]() |
cf4fedbe13 | ||
![]() |
748352e5a1 | ||
![]() |
90467bf8bf | ||
![]() |
906a26414a | ||
![]() |
d734e584f6 | ||
![]() |
677db72bb3 | ||
![]() |
ae3ea85355 | ||
![]() |
335be87cf8 | ||
![]() |
944d355e29 | ||
![]() |
3ec206b152 | ||
![]() |
de238f3f5f | ||
![]() |
43e902407f | ||
![]() |
e95dec30b5 | ||
![]() |
07cadd862a | ||
![]() |
738a92f5d2 | ||
![]() |
f949c5807f | ||
![]() |
9d2fe4000d | ||
![]() |
7385d320b4 | ||
![]() |
6166d9afc9 | ||
![]() |
984a507ffa | ||
![]() |
9c1b917604 | ||
![]() |
b149cd64fd | ||
![]() |
619c4f284b | ||
![]() |
1aa87ddf76 | ||
![]() |
0b92806327 | ||
![]() |
5f1b7488f0 | ||
![]() |
886c67e452 | ||
![]() |
c5ae43d66c | ||
![]() |
9e0787f23d | ||
![]() |
b994b1c895 | ||
![]() |
13f2e30a40 | ||
![]() |
d6803e1bf4 | ||
![]() |
f860257826 | ||
![]() |
d3a7107a0f | ||
![]() |
2776b86050 | ||
![]() |
592cd0424a | ||
![]() |
4e8e148029 | ||
![]() |
de1d310499 | ||
![]() |
c5f9070370 | ||
![]() |
bb273cd2f5 | ||
![]() |
a2356b05d6 | ||
![]() |
6d2f2d20a8 | ||
![]() |
4ef0642134 | ||
![]() |
08e067c6ba | ||
![]() |
d4bb97db72 | ||
![]() |
5e2da57565 | ||
![]() |
cd1c213fb2 | ||
![]() |
1bfd3425c9 | ||
![]() |
a03629d29d | ||
![]() |
1914f5f3e1 | ||
![]() |
f7688d7f9a | ||
![]() |
1410a720c8 | ||
![]() |
aca94bcb68 | ||
![]() |
c947f24495 | ||
![]() |
68375163f5 | ||
![]() |
1591851273 | ||
![]() |
15005372a2 | ||
![]() |
77376dfa42 | ||
![]() |
810e8eae0d | ||
![]() |
082a2f5fef | ||
![]() |
94f79a6438 | ||
![]() |
2897dcb67e | ||
![]() |
fb3dcb293d | ||
![]() |
3f525c9bae | ||
![]() |
5f623e0c4a | ||
![]() |
f08f8cb53d | ||
![]() |
02d98826a9 | ||
![]() |
d54748ff39 | ||
![]() |
f631d16828 | ||
![]() |
215a493fa6 | ||
![]() |
f9a30d2e1c | ||
![]() |
478538690e | ||
![]() |
3df44ce302 | ||
![]() |
d4d5fb1908 | ||
![]() |
79d2bd120e | ||
![]() |
e1f65cab62 | ||
![]() |
0ab80e4c6a | ||
![]() |
c35d327fa4 | ||
![]() |
b57a57cfc1 | ||
![]() |
d1cdfab67a | ||
![]() |
086ff54b5f | ||
![]() |
30eaf328fd | ||
![]() |
f1a31a7fe3 | ||
![]() |
02b2090e53 | ||
![]() |
014ab2d2b6 | ||
![]() |
f7c043b3fc | ||
![]() |
7fbce1f09c | ||
![]() |
e91ba13601 | ||
![]() |
93ae78b46f | ||
![]() |
2c017e158d | ||
![]() |
c862eb0bd3 | ||
![]() |
507f17e19e | ||
![]() |
2038b7e1cb | ||
![]() |
897ed989b1 | ||
![]() |
23ab7046bc | ||
![]() |
4224d6df81 | ||
![]() |
16a36f212c | ||
![]() |
da1e3b93de | ||
![]() |
8d742b7e30 | ||
![]() |
571dfa8e1d | ||
![]() |
97807b8f0a | ||
![]() |
c7b6313907 | ||
![]() |
d578327385 | ||
![]() |
2d24a54033 | ||
![]() |
611db650d5 | ||
![]() |
f184c97354 | ||
![]() |
b2f7fbd980 | ||
![]() |
da8a142510 | ||
![]() |
ef75f30701 | ||
![]() |
4f6910fca7 | ||
![]() |
f7299c6537 | ||
![]() |
5bb73999ad | ||
![]() |
426e03a649 | ||
![]() |
26d173acdc | ||
![]() |
7eeab77aaf | ||
![]() |
9b96282cbf | ||
![]() |
c7f92d2ee6 | ||
![]() |
d9f1215142 | ||
![]() |
d1d0f13a41 | ||
![]() |
0984a93133 | ||
![]() |
6c84c79397 | ||
![]() |
9a6e2f0c71 | ||
![]() |
69e2546821 | ||
![]() |
00608c1bc3 | ||
![]() |
768b3d7688 | ||
![]() |
cad7a9687d | ||
![]() |
80ed1e7180 | ||
![]() |
e96dcd653d | ||
![]() |
03da7366c9 | ||
![]() |
6363e14f10 | ||
![]() |
f02e478cc4 | ||
![]() |
8ef32eba0c | ||
![]() |
7de5d4d551 | ||
![]() |
9d79d372cd | ||
![]() |
dc9973cdbc | ||
![]() |
2e04d4b82c | ||
![]() |
c432bf1ea0 | ||
![]() |
65e2931fe8 | ||
![]() |
7a8151753b | ||
![]() |
9680ba1694 | ||
![]() |
7fbdfaaae7 | ||
![]() |
93433cff05 | ||
![]() |
23dad31426 | ||
![]() |
103b2265ee | ||
![]() |
a0c20a35c3 | ||
![]() |
0967b79763 | ||
![]() |
5c4d23d2d6 | ||
![]() |
63807a688d | ||
![]() |
7389315dfa | ||
![]() |
43be8f3fd1 | ||
![]() |
81f13279fe | ||
![]() |
62295b72b4 | ||
![]() |
78c0416c84 | ||
![]() |
e75143530f | ||
![]() |
2080d77e6b | ||
![]() |
4a9ed54f42 | ||
![]() |
7f10be18c6 | ||
![]() |
96c36d85c4 | ||
![]() |
f195c34a8b | ||
![]() |
dded004321 | ||
![]() |
08988f2369 | ||
![]() |
67e66a7b0c | ||
![]() |
37ba565f5b | ||
![]() |
2c7033e367 | ||
![]() |
3a1fcfd226 | ||
![]() |
e72ba25fe5 | ||
![]() |
169346ce76 | ||
![]() |
c66b6c52b7 | ||
![]() |
d73dac73ad | ||
![]() |
0214e9e447 | ||
![]() |
44b703a7d2 | ||
![]() |
18b3a05806 | ||
![]() |
3ee64b1bc0 | ||
![]() |
ac151716c9 | ||
![]() |
cff807e191 | ||
![]() |
d6ed2a5e8a | ||
![]() |
00c6db81a7 | ||
![]() |
3112ba75c9 | ||
![]() |
d8c87c3d4b | ||
![]() |
a4161aeb73 | ||
![]() |
41b2e175c9 | ||
![]() |
aaf5bfe1db | ||
![]() |
a96eeccea9 | ||
![]() |
5f6762f543 | ||
![]() |
98130de4ae | ||
![]() |
0baf443b63 | ||
![]() |
d847f2caef | ||
![]() |
1a94c46e87 | ||
![]() |
ee696b4737 | ||
![]() |
2ed7c925f7 | ||
![]() |
a9c73dd278 | ||
![]() |
1192468015 | ||
![]() |
6ce94a22d3 | ||
![]() |
78af79cd4f | ||
![]() |
6311ad5c6b | ||
![]() |
7e53baf4c1 | ||
![]() |
26f8c851ec | ||
![]() |
60bafa168c | ||
![]() |
fad9844d1f | ||
![]() |
8b0b8bd6d9 | ||
![]() |
2da380a04d | ||
![]() |
06a8ff5787 | ||
![]() |
a2cb5f1ccf | ||
![]() |
7ad4941554 | ||
![]() |
6e9437b1f4 | ||
![]() |
afea37450b | ||
![]() |
b0131a7490 | ||
![]() |
3563e586c4 | ||
![]() |
e3ebab9bc3 | ||
![]() |
77052c2b45 | ||
![]() |
cbf1bda433 | ||
![]() |
83ee0c8f0b | ||
![]() |
6fa8477650 | ||
![]() |
6c47ade379 | ||
![]() |
972ef3b078 | ||
![]() |
32fc118b3d | ||
![]() |
90aa73dc40 | ||
![]() |
cf694b6d83 | ||
![]() |
834c08db3c | ||
![]() |
17f712870b | ||
![]() |
dad993f7b8 | ||
![]() |
10904aa9a3 | ||
![]() |
37fe708f2b | ||
![]() |
128d390e9c | ||
![]() |
7bc130d81a | ||
![]() |
f0d6c7c720 | ||
![]() |
023a6d01bd | ||
![]() |
ac8d3ff600 | ||
![]() |
e5bc365b64 | ||
![]() |
ffa6fb92b9 | ||
![]() |
f9b22442ed | ||
![]() |
721dcb00c1 | ||
![]() |
ab248525e0 | ||
![]() |
fdd441c382 | ||
![]() |
9faa84fa90 | ||
![]() |
27d1187216 | ||
![]() |
1505047cb7 | ||
![]() |
d0ca382d80 | ||
![]() |
51e48f4416 | ||
![]() |
7a880fdc33 | ||
![]() |
87a4cfb27c | ||
![]() |
70da6eaa12 | ||
![]() |
e860503141 | ||
![]() |
f1230adfc1 | ||
![]() |
145314a83a | ||
![]() |
50b2e6f7a5 | ||
![]() |
68db3ae353 | ||
![]() |
c746a49b15 | ||
![]() |
f655f49aee | ||
![]() |
857b0ea1c7 | ||
![]() |
5d3d8786e2 | ||
![]() |
32c67d05ec | ||
![]() |
6a0eec1262 | ||
![]() |
7f2a811541 | ||
![]() |
942cfd3cd1 | ||
![]() |
99840b5bba | ||
![]() |
b1aac69989 | ||
![]() |
d902612f41 | ||
![]() |
a484c03816 | ||
![]() |
2074718391 | ||
![]() |
7b70560c9b | ||
![]() |
3c63bd4b1c | ||
![]() |
df39b0fe40 | ||
![]() |
c560463887 | ||
![]() |
2eea94e5ea | ||
![]() |
a8c5d8e765 | ||
![]() |
00c138e42f | ||
![]() |
c240283ad4 | ||
![]() |
7e83189f5b | ||
![]() |
3ebeec0b87 | ||
![]() |
a149ce54db | ||
![]() |
b66810944f | ||
![]() |
bbf7cd81a6 | ||
![]() |
7f1155816e | ||
![]() |
18f3d86b68 | ||
![]() |
e56d604d6f | ||
![]() |
8794611b26 | ||
![]() |
0163d6efa6 | ||
![]() |
a826d1101d | ||
![]() |
ebbb5f3863 | ||
![]() |
560a620a6c | ||
![]() |
3f9881a6f5 | ||
![]() |
8ce4cad7ef | ||
![]() |
734fe262b1 | ||
![]() |
45313c6234 | ||
![]() |
b7a5c6d201 | ||
![]() |
865d537745 | ||
![]() |
0ec9689a6c | ||
![]() |
550f04cc8b | ||
![]() |
cde57efb98 | ||
![]() |
4d8e410176 | ||
![]() |
1abbedc4ab | ||
![]() |
887bb171aa | ||
![]() |
ac22cc5266 | ||
![]() |
ca3e13ebf6 | ||
![]() |
58aa19f392 | ||
![]() |
6d603ada0d | ||
![]() |
8d7b5bff5d | ||
![]() |
132519590f | ||
![]() |
27c9a21764 | ||
![]() |
127c65b78d | ||
![]() |
db798afb67 | ||
![]() |
158668f378 | ||
![]() |
44f39793be | ||
![]() |
74d1c9521d | ||
![]() |
d114f428e7 | ||
![]() |
adcdc417ab | ||
![]() |
77e041d640 | ||
![]() |
721d4a300a | ||
![]() |
49635c224d | ||
![]() |
3265c8634c | ||
![]() |
fa766180bf | ||
![]() |
ee37f26c4d | ||
![]() |
d07c8b5d74 | ||
![]() |
0b8796e56f | ||
![]() |
fc9459d6c5 | ||
![]() |
5e59f812ce | ||
![]() |
2e3d2c84ae | ||
![]() |
20c66a825d | ||
![]() |
d98d59a8d3 | ||
![]() |
15ec420db4 | ||
![]() |
f8ef7cda39 | ||
![]() |
5fd591bbe8 | ||
![]() |
ed09b1af2d | ||
![]() |
22e40bfee1 | ||
![]() |
6b3004160f | ||
![]() |
eb93a38cf5 | ||
![]() |
b86a30d40f | ||
![]() |
c4a1295095 | ||
![]() |
230c06d536 | ||
![]() |
907507d27d | ||
![]() |
cdd0848927 | ||
![]() |
d846e18d7f | ||
![]() |
35c2ef8c1c | ||
![]() |
fd51fad73b | ||
![]() |
514e83e604 | ||
![]() |
9d69098605 | ||
![]() |
0e1584f083 | ||
![]() |
70864e3479 | ||
![]() |
fc0df0dcf4 | ||
![]() |
74713810dd | ||
![]() |
53c8752444 | ||
![]() |
09324bbb78 | ||
![]() |
bfca096775 | ||
![]() |
04914d8d8b | ||
![]() |
37bca5d244 | ||
![]() |
409a50fe92 | ||
![]() |
ac2cfe557d | ||
![]() |
0bf221383b | ||
![]() |
8befba0522 | ||
![]() |
ae0976d24a | ||
![]() |
591b23b5ab | ||
![]() |
74274d21b4 | ||
![]() |
bc3d8a89b6 | ||
![]() |
f033276f67 | ||
![]() |
e021eb5ca7 | ||
![]() |
610ead83d0 | ||
![]() |
5175eacd6d | ||
![]() |
4485d7c2cb | ||
![]() |
fddce692db | ||
![]() |
b4dd9c857e | ||
![]() |
0e76a2e1fe | ||
![]() |
52436350c6 | ||
![]() |
f34237f333 | ||
![]() |
41d54f7e92 | ||
![]() |
6a1ab2b80a | ||
![]() |
c3b47ecd5a | ||
![]() |
c67a60271d | ||
![]() |
38680d84e3 | ||
![]() |
7f6dfb6dfe | ||
![]() |
6ad9bbd367 | ||
![]() |
df9954ba0b | ||
![]() |
29788254dd | ||
![]() |
260b5bb9fb | ||
![]() |
91edad0c31 | ||
![]() |
d38058497e | ||
![]() |
0473ac72da | ||
![]() |
58c23c0a0e | ||
![]() |
d5456e1bbf | ||
![]() |
3a83f80ab6 | ||
![]() |
db2aaeaf22 | ||
![]() |
6a38e18057 | ||
![]() |
858a44173b | ||
![]() |
c67570541b | ||
![]() |
c1f0414b6c | ||
![]() |
406094b56d | ||
![]() |
7ef4e4245b | ||
![]() |
057f6c167c | ||
![]() |
771ac45b8a | ||
![]() |
d54a4b07e5 | ||
![]() |
ec53f4e05c | ||
![]() |
d76d7aa367 | ||
![]() |
ab709e171a | ||
![]() |
9c47f56f03 | ||
![]() |
2019b7a7c3 | ||
![]() |
88b5e284b5 | ||
![]() |
2578e6bdff | ||
![]() |
9d1743af33 | ||
![]() |
acc2a3154f | ||
![]() |
4538cf1ed0 | ||
![]() |
9656de0147 | ||
![]() |
538830d5a3 | ||
![]() |
3fcdfe85bb | ||
![]() |
f9063b5058 | ||
![]() |
f2621dcb55 | ||
![]() |
a1d6917ec7 | ||
![]() |
35764b90c0 | ||
![]() |
ba9f32e9f4 | ||
![]() |
5195281848 | ||
![]() |
4c83b2c0c6 | ||
![]() |
f30cea33e3 | ||
![]() |
a61fb677bc | ||
![]() |
a7d757cdb2 | ||
![]() |
d46efb1d84 | ||
![]() |
cfe2f71e4d | ||
![]() |
c29aff2167 | ||
![]() |
b19861226a | ||
![]() |
8e28172942 | ||
![]() |
1af6dbc19e | ||
![]() |
82ed44fac0 | ||
![]() |
1d44a5f2e6 | ||
![]() |
3b76db0227 | ||
![]() |
fded4c9f2e | ||
![]() |
87d73e2d01 | ||
![]() |
5ed4977b9d | ||
![]() |
d4992831c0 | ||
![]() |
e18e7a7067 | ||
![]() |
8bba6670a0 | ||
![]() |
c34ceeffe4 | ||
![]() |
4c9012557e | ||
![]() |
d5f0a6397f | ||
![]() |
0655abe3b9 | ||
![]() |
8f24759a33 | ||
![]() |
c5aa3f91b1 | ||
![]() |
35e791543a | ||
![]() |
da8c0e0e16 | ||
![]() |
ea6b0340d9 | ||
![]() |
0f64f2c181 | ||
![]() |
7421d33c3a | ||
![]() |
b8ce62e3ef | ||
![]() |
26d60aa9ae | ||
![]() |
80a3faa4d5 | ||
![]() |
b534bba440 | ||
![]() |
25a9360a58 | ||
![]() |
28988b9fdd | ||
![]() |
ccf858f960 | ||
![]() |
3c33e55fd3 | ||
![]() |
683ba8d20f | ||
![]() |
018a8231d6 | ||
![]() |
37ef67e1b8 | ||
![]() |
d3ca6020dc | ||
![]() |
85ba537d95 | ||
![]() |
fbeb6d140f | ||
![]() |
78d15dd4a8 | ||
![]() |
580723fa64 | ||
![]() |
e698e1557c | ||
![]() |
e2b81c6b77 | ||
![]() |
21a78054d5 | ||
![]() |
77074f83e2 | ||
![]() |
90e64badf3 | ||
![]() |
26ce365c22 | ||
![]() |
d6de4eb58b | ||
![]() |
4aa44cb150 | ||
![]() |
91482e05f0 | ||
![]() |
b22c20ece3 | ||
![]() |
a203545e48 | ||
![]() |
d061dafcd7 | ||
![]() |
3af5f55abd | ||
![]() |
9c86655af0 | ||
![]() |
982bf42a4b | ||
![]() |
d43c6b326f | ||
![]() |
ad63e52e50 | ||
![]() |
1323e800b7 | ||
![]() |
209afd6bf2 | ||
![]() |
62b080a8d9 | ||
![]() |
94c0c391da | ||
![]() |
affa569a79 | ||
![]() |
1eedf66d66 | ||
![]() |
d7827a3f07 | ||
![]() |
4ec9cee143 | ||
![]() |
fd03ebd764 | ||
![]() |
26b35723d3 | ||
![]() |
96fe1f9584 | ||
![]() |
38e4b38602 | ||
![]() |
eed0070d4b | ||
![]() |
c5688dcdbf | ||
![]() |
db120133b9 | ||
![]() |
3dd0589b08 | ||
![]() |
cd3a064a5a | ||
![]() |
1505d21781 | ||
![]() |
2ed6110ae5 | ||
![]() |
18f97602e8 | ||
![]() |
ddbd9e2ea9 | ||
![]() |
1eae135929 | ||
![]() |
8364aa15f1 | ||
![]() |
94e3cf85ba | ||
![]() |
b08b97466f | ||
![]() |
91c3278920 | ||
![]() |
e390561c7c | ||
![]() |
1b6fc3da7c | ||
![]() |
5179e032db | ||
![]() |
43a3c2b042 | ||
![]() |
d1b758147b | ||
![]() |
cdecc1604c | ||
![]() |
073052f5cc | ||
![]() |
153a6bdbe7 | ||
![]() |
99f930663d | ||
![]() |
54c4714b9d | ||
![]() |
6b9d6b6be5 | ||
![]() |
d130a2f634 | ||
![]() |
7e86dc5e48 | ||
![]() |
d79fccb2da | ||
![]() |
86ecfa217f | ||
![]() |
1153a93425 | ||
![]() |
5cca8102d7 | ||
![]() |
9fe6545654 | ||
![]() |
1bae2b0132 | ||
![]() |
17c731ef1d | ||
![]() |
a6b804e93a | ||
![]() |
ea31ed3079 | ||
![]() |
6d6aa98c6b | ||
![]() |
e892d55b91 | ||
![]() |
e066ed04dc | ||
![]() |
5e619a8117 | ||
![]() |
ddda82509c | ||
![]() |
7471fed31c | ||
![]() |
714edfb456 | ||
![]() |
247dfc5ecf | ||
![]() |
6a6a37c42e | ||
![]() |
80d8f9f30b | ||
![]() |
88b4e1ff31 | ||
![]() |
f3e5f2adcc | ||
![]() |
f6724413b0 | ||
![]() |
89c53bbd5e | ||
![]() |
1a1ece16cf | ||
![]() |
9195e3c614 | ||
![]() |
044bf0b2f8 | ||
![]() |
4148edeccd | ||
![]() |
401a1d473d | ||
![]() |
9997b0f448 | ||
![]() |
c0d752cce3 | ||
![]() |
d939e9dac9 | ||
![]() |
416c7aff82 | ||
![]() |
d063aa3a08 | ||
![]() |
597e9c42a2 | ||
![]() |
6dbcf95ae7 | ||
![]() |
e57b034a62 | ||
![]() |
81b772c3fd | ||
![]() |
46817b1893 | ||
![]() |
5f08181f7d | ||
![]() |
0646b6473e | ||
![]() |
d9f0c86c2e | ||
![]() |
180179eb3a | ||
![]() |
10ccd5c503 | ||
![]() |
73d24d987e | ||
![]() |
5c85302928 | ||
![]() |
23612570a8 | ||
![]() |
29b6b261a2 | ||
![]() |
8060ac88c3 | ||
![]() |
bd0e9e0a3c | ||
![]() |
71c507bbfd | ||
![]() |
02c8fac906 | ||
![]() |
a028b3f6a2 | ||
![]() |
1cd0bf721a | ||
![]() |
ff3cd9fb11 | ||
![]() |
7f78b46664 | ||
![]() |
c198736cd1 | ||
![]() |
a3d4c87101 | ||
![]() |
df6d1fe625 | ||
![]() |
bf435bce66 | ||
![]() |
f79d14a59c | ||
![]() |
10a8f349a1 | ||
![]() |
b193a10e69 | ||
![]() |
64c0950e76 | ||
![]() |
bf90cf7e9f | ||
![]() |
ac0ab7e211 | ||
![]() |
97cc279d99 | ||
![]() |
5a03f6a786 | ||
![]() |
f7bbf0e282 | ||
![]() |
63d3006ba8 | ||
![]() |
4957bcb812 | ||
![]() |
426e3da139 | ||
![]() |
11a80ced6b | ||
![]() |
fed9e8d103 | ||
![]() |
31e99a1331 | ||
![]() |
d23e68be6b | ||
![]() |
05c6269d53 | ||
![]() |
0e60886f14 | ||
![]() |
bb1c551cb8 | ||
![]() |
9aca7da375 | ||
![]() |
9b01f86fef | ||
![]() |
315f1fa932 | ||
![]() |
b4bf5d6daf | ||
![]() |
16b765a6b8 | ||
![]() |
e09e1dba0a | ||
![]() |
16170c0901 | ||
![]() |
fea2f9b9cf | ||
![]() |
b22674e3e3 | ||
![]() |
6ab5299e40 | ||
![]() |
e3b29f9613 | ||
![]() |
dfa3ba2c99 | ||
![]() |
06c7bb8286 | ||
![]() |
26e0708573 | ||
![]() |
7a220077c5 | ||
![]() |
51cf2db6ac | ||
![]() |
db1f54c352 | ||
![]() |
96187bda4c | ||
![]() |
0df829034c | ||
![]() |
57f5180dae | ||
![]() |
e1510d2329 | ||
![]() |
4ae5045568 | ||
![]() |
ea3c95832e | ||
![]() |
8e193cecff | ||
![]() |
87fb293348 | ||
![]() |
a99767f3a6 | ||
![]() |
c2c8c55862 | ||
![]() |
d24a6b71b9 | ||
![]() |
03c0ef5f65 | ||
![]() |
e1058ba46d | ||
![]() |
d0c8fecfd3 | ||
![]() |
e34192573d | ||
![]() |
ae5fb8e8fc | ||
![]() |
49d01e0ca3 | ||
![]() |
e29b6cbdaf | ||
![]() |
3a8051898a | ||
![]() |
c1d4248723 | ||
![]() |
9efe173380 | ||
![]() |
dd4420965d | ||
![]() |
7ed03bdcba | ||
![]() |
0f27374452 | ||
![]() |
6a0c230493 | ||
![]() |
0a0dc0d596 | ||
![]() |
975ce94246 | ||
![]() |
209ed1dc62 | ||
![]() |
1b3080c07d | ||
![]() |
352b87f1c4 | ||
![]() |
b22466ee66 | ||
![]() |
76cda91e64 | ||
![]() |
892b47c060 | ||
![]() |
c36529f445 | ||
![]() |
feb7a2bd09 | ||
![]() |
038816e32a | ||
![]() |
5779736913 | ||
![]() |
756df4d635 | ||
![]() |
6d7d19433e | ||
![]() |
9baad0fc3e | ||
![]() |
2c2b826640 | ||
![]() |
62885db7c4 | ||
![]() |
60a3804c5a | ||
![]() |
b7eb30a69b | ||
![]() |
70bf49c3ac | ||
![]() |
07eb633f0f | ||
![]() |
25cbf98390 | ||
![]() |
4bbd0d83ef | ||
![]() |
753e983339 | ||
![]() |
a29bf3d9c8 | ||
![]() |
b80bebc8bd | ||
![]() |
8b89b52d2b | ||
![]() |
fa5b7d8099 | ||
![]() |
895cd7bbb1 | ||
![]() |
1a5b60f654 | ||
![]() |
00e832fbe3 | ||
![]() |
985424ac62 | ||
![]() |
ef4221400b | ||
![]() |
0f6e88d1a3 | ||
![]() |
35f73f3786 | ||
![]() |
58205fefec | ||
![]() |
49ffe06576 | ||
![]() |
86fb652fc3 | ||
![]() |
cc759b17a4 | ||
![]() |
59c6e36034 | ||
![]() |
cb0f252d74 | ||
![]() |
829e8f1058 | ||
![]() |
7b5160df47 | ||
![]() |
bfb90ef655 | ||
![]() |
b1be1dffb6 | ||
![]() |
dba8cd45ed | ||
![]() |
0cc6272664 | ||
![]() |
d9c48c28f6 | ||
![]() |
9950ea3ed2 | ||
![]() |
1ae1ae7368 | ||
![]() |
c24cd89028 | ||
![]() |
9727dc505c | ||
![]() |
2067f15b27 | ||
![]() |
637d3dddc4 | ||
![]() |
3f068929e8 | ||
![]() |
2ecf18dc7f | ||
![]() |
e20188b804 | ||
![]() |
62f6fd3d44 | ||
![]() |
9a0ba186e6 | ||
![]() |
9a9e5c8668 | ||
![]() |
c8e69c794a | ||
![]() |
fee37d88cf | ||
![]() |
6010a6136b | ||
![]() |
4a77b057bd | ||
![]() |
7c99b6ce91 | ||
![]() |
1e740cf72a | ||
![]() |
30a013a4d1 | ||
![]() |
aef0d8be87 | ||
![]() |
88c2774692 | ||
![]() |
2ecb519a56 | ||
![]() |
f67527a86e | ||
![]() |
aff3995475 | ||
![]() |
2154d37dc8 | ||
![]() |
676d173cc9 | ||
![]() |
ab00d49737 | ||
![]() |
4f8fe4e967 | ||
![]() |
30bc2cf8d5 | ||
![]() |
8405ba7d87 | ||
![]() |
1e79b6d1d5 | ||
![]() |
098241c981 | ||
![]() |
04e730c6bb | ||
![]() |
f2212e33d7 | ||
![]() |
4f34013885 | ||
![]() |
b6654f5077 | ||
![]() |
959fe9f2e3 | ||
![]() |
77ee4296a5 | ||
![]() |
096b9c20d1 | ||
![]() |
a3c91e4eae | ||
![]() |
ab33eccaa2 | ||
![]() |
c5bf2a131b | ||
![]() |
8a6a62833b | ||
![]() |
6969dcb90e | ||
![]() |
90290f830a | ||
![]() |
71f641860a | ||
![]() |
b22b39ea26 | ||
![]() |
ad6a8e5b4e | ||
![]() |
1d951e7689 | ||
![]() |
b34853a776 | ||
![]() |
cc0adcc5ad | ||
![]() |
9a2d390279 | ||
![]() |
b989ef3ecc | ||
![]() |
e50a12731d | ||
![]() |
72b3bda941 | ||
![]() |
1a54c61a50 | ||
![]() |
7286bafa23 | ||
![]() |
2cbaa3eaed | ||
![]() |
b9342e909b | ||
![]() |
9dceb60786 | ||
![]() |
69fdaf874e | ||
![]() |
ecff5b1832 | ||
![]() |
20666ba824 | ||
![]() |
35c65fbffa | ||
![]() |
6af08494a2 | ||
![]() |
e29ae6de1a | ||
![]() |
46c066468a | ||
![]() |
0df276eefc | ||
![]() |
caabec7ef9 | ||
![]() |
0c0342b12f | ||
![]() |
e82382fe94 | ||
![]() |
ee7761e1af | ||
![]() |
0b3b32dab3 | ||
![]() |
3426b3bb2d | ||
![]() |
97b60d937d | ||
![]() |
6957721479 | ||
![]() |
34838fd0dc | ||
![]() |
31313bc9ee | ||
![]() |
4e81aba841 | ||
![]() |
51676bf255 | ||
![]() |
34ac4319c0 | ||
![]() |
b6e2f2d691 | ||
![]() |
11e2fb7540 | ||
![]() |
5d048a7c53 | ||
![]() |
9fc1aba8bc | ||
![]() |
04e7b8d7ab | ||
![]() |
52ada0131a | ||
![]() |
14a79b5577 | ||
![]() |
d951ccd221 | ||
![]() |
682cf58aa9 | ||
![]() |
7581d8443e | ||
![]() |
3afe248159 | ||
![]() |
04a2c4b70d | ||
![]() |
41b2ad6a9d | ||
![]() |
405e411200 | ||
![]() |
c3fd3984b8 | ||
![]() |
7458dff6e9 | ||
![]() |
0324631a4e | ||
![]() |
bb5665ab23 | ||
![]() |
5c9e314c9b | ||
![]() |
e841d63ea9 | ||
![]() |
f0d72a3ec4 | ||
![]() |
32413a2753 | ||
![]() |
f7a0063495 | ||
![]() |
e0c09853b9 | ||
![]() |
ad4e53098a | ||
![]() |
b533bc2112 | ||
![]() |
3bbcca9966 | ||
![]() |
96ffb5f0fd | ||
![]() |
7fdcd706fd | ||
![]() |
950854318b | ||
![]() |
080527bf2e | ||
![]() |
817cac3002 | ||
![]() |
3e44ab2474 | ||
![]() |
1a5bd84fad | ||
![]() |
447dbe73ea | ||
![]() |
3df93940c8 | ||
![]() |
eefbee969e | ||
![]() |
bb9426763c | ||
![]() |
40092a07dd | ||
![]() |
4412b0a557 | ||
![]() |
dd9a7207ac | ||
![]() |
94b30a54a6 | ||
![]() |
47196234fe | ||
![]() |
5307d56ee5 | ||
![]() |
12d5c5c7b4 | ||
![]() |
002d356ece | ||
![]() |
94410c5dbc | ||
![]() |
3ad429aa6b | ||
![]() |
f388f32fcf | ||
![]() |
012cdce37b | ||
![]() |
938c992023 | ||
![]() |
1c179848b7 | ||
![]() |
0685c16efe | ||
![]() |
f794d1e7a5 | ||
![]() |
8d1e20b7fd | ||
![]() |
b6218c6d05 | ||
![]() |
c6a8bd2139 | ||
![]() |
c74e272de7 | ||
![]() |
6483106a46 | ||
![]() |
0bd995a731 | ||
![]() |
d242440fe3 | ||
![]() |
ca9e390e60 | ||
![]() |
81db276f8d | ||
![]() |
6db5da2cf7 | ||
![]() |
c5a36f34f2 | ||
![]() |
41d2c69d2e | ||
![]() |
41fbbd9825 | ||
![]() |
04e8117c3e | ||
![]() |
079e704375 | ||
![]() |
7c2b7ef280 | ||
![]() |
4bfae52a9e | ||
![]() |
b4560fc217 | ||
![]() |
501d3d3176 | ||
![]() |
96091b00d5 | ||
![]() |
214fd32552 | ||
![]() |
9170e2af91 | ||
![]() |
2c1b13620f | ||
![]() |
6c8e726aad | ||
![]() |
dd0f9dcccf | ||
![]() |
e599300620 | ||
![]() |
92713acdce | ||
![]() |
42b6176cff | ||
![]() |
e085e0392b | ||
![]() |
1a4c2ca408 | ||
![]() |
927e05ba8d | ||
![]() |
7f2efc7267 | ||
![]() |
97eea866aa | ||
![]() |
1ad69adf4e | ||
![]() |
0bb921adad | ||
![]() |
9475d1634c | ||
![]() |
da6fd323fd | ||
![]() |
56d9ccd737 | ||
![]() |
8b114326f5 | ||
![]() |
960b67e950 | ||
![]() |
d32c866bb4 | ||
![]() |
04e548aec4 | ||
![]() |
35acb141c9 | ||
![]() |
fb3b0a0281 | ||
![]() |
adca9a051b | ||
![]() |
30b6d6df90 | ||
![]() |
2ceeda40b7 | ||
![]() |
8307015d8d | ||
![]() |
3311043e80 | ||
![]() |
6797f51481 | ||
![]() |
a9457e30a6 | ||
![]() |
a962ba9523 | ||
![]() |
71da35d3fd | ||
![]() |
7bc2622b94 | ||
![]() |
cbb526d68b | ||
![]() |
2191cba844 | ||
![]() |
e8093a66f0 | ||
![]() |
70cb5cd94a | ||
![]() |
1a60a7cfb7 | ||
![]() |
5b688d3c23 | ||
![]() |
f349437334 | ||
![]() |
9f7380691b | ||
![]() |
abf4528d02 | ||
![]() |
0b2aac253b | ||
![]() |
b1a1bedc68 | ||
![]() |
68de2356c5 | ||
![]() |
8bf0b1df91 | ||
![]() |
41cfc67d4a | ||
![]() |
127be67c79 | ||
![]() |
dfe72b30ce | ||
![]() |
9586c159a3 | ||
![]() |
bbeef3cd8d | ||
![]() |
ed978ba785 | ||
![]() |
52e9e79fad | ||
![]() |
7b8f577fd4 | ||
![]() |
8d4e785185 | ||
![]() |
603a5b25e4 | ||
![]() |
f1c3e5ee32 | ||
![]() |
0f17901d9d | ||
![]() |
65e8087211 | ||
![]() |
1e8bbbd1f9 | ||
![]() |
8396eba8d9 | ||
![]() |
b60afa2dc0 | ||
![]() |
6b573db3f4 | ||
![]() |
65f292708c | ||
![]() |
2249ad59da | ||
![]() |
caa91186cc | ||
![]() |
6ee3dba3ea | ||
![]() |
b5f62c03cb | ||
![]() |
48b7096523 | ||
![]() |
bd68caa152 | ||
![]() |
638daf83cf | ||
![]() |
d97319efb7 | ||
![]() |
7bbcbf25a6 | ||
![]() |
f5352eaf28 | ||
![]() |
3f8875ab9c | ||
![]() |
174e37e4e0 | ||
![]() |
38cc95b035 | ||
![]() |
0a13cf5c63 | ||
![]() |
561d7833d1 | ||
![]() |
04ea84640e | ||
![]() |
3eb758f1b2 | ||
![]() |
95b53706f9 | ||
![]() |
793788eadf | ||
![]() |
5736c70dc8 | ||
![]() |
2211dc38bb | ||
![]() |
2d62c94b5f | ||
![]() |
5a8c68b8f8 | ||
![]() |
2b0e6dade1 | ||
![]() |
7c6888b5d4 | ||
![]() |
a47e32d40f | ||
![]() |
d8a624b397 | ||
![]() |
1f973a7c88 | ||
![]() |
b05ed99c74 | ||
![]() |
42b7687cf9 | ||
![]() |
999fa6eb21 | ||
![]() |
bc283304df | ||
![]() |
7dd239d4bf | ||
![]() |
9225309191 | ||
![]() |
17f5fc67ec | ||
![]() |
89607eb58a | ||
![]() |
995036f4e4 | ||
![]() |
a6b416e7c9 | ||
![]() |
cebf82e7e3 | ||
![]() |
b204377a4e | ||
![]() |
714fbc8f2a | ||
![]() |
37e23e8d50 | ||
![]() |
7cfc2ea004 | ||
![]() |
fd8355e872 | ||
![]() |
469f09c0b3 | ||
![]() |
17fdaa116c | ||
![]() |
8d75e87c6b | ||
![]() |
2bbde8c705 | ||
![]() |
d1bdd7789b | ||
![]() |
d40781584b | ||
![]() |
a6199e7603 | ||
![]() |
77306e85e7 | ||
![]() |
c4699df82a | ||
![]() |
b94e2e3648 | ||
![]() |
5c4de9a166 | ||
![]() |
c8bf53fd3b | ||
![]() |
a052dc2ff1 | ||
![]() |
ff9871f86f | ||
![]() |
7875d8622e | ||
![]() |
36e0479967 | ||
![]() |
a0ad3ee07d | ||
![]() |
d7d13b26e0 | ||
![]() |
dda3ea15b7 | ||
![]() |
12ed92aa01 | ||
![]() |
bed363e6fa | ||
![]() |
194dff8ab8 | ||
![]() |
ee57a7b683 | ||
![]() |
d7a30b4c34 | ||
![]() |
19ae4a76c2 | ||
![]() |
d911aa2424 | ||
![]() |
0164adc386 | ||
![]() |
f2b1583239 | ||
![]() |
9bb0719a86 | ||
![]() |
312d559e81 | ||
![]() |
ef23e36ef1 | ||
![]() |
eea66e390b | ||
![]() |
ebb991d1c6 | ||
![]() |
8d87031f81 | ||
![]() |
2509f6f869 | ||
![]() |
fd48b504c8 | ||
![]() |
34ac244fdf | ||
![]() |
fc50fea18c | ||
![]() |
1daae076ec | ||
![]() |
3235ddd120 | ||
![]() |
e87d4ac1e8 | ||
![]() |
c8f495ec16 | ||
![]() |
b3ae79e5c2 | ||
![]() |
f9feab57cc | ||
![]() |
2cc6661979 | ||
![]() |
4b2089b790 | ||
![]() |
6f6bd44a38 | ||
![]() |
77f0dbb65a | ||
![]() |
f5906f320f | ||
![]() |
dbd69444f1 | ||
![]() |
4ecb253c80 | ||
![]() |
fe3a739fe3 | ||
![]() |
998d99eee0 | ||
![]() |
be1b68153f | ||
![]() |
ae6f71f482 | ||
![]() |
03e98dbb10 | ||
![]() |
e856a9be3b | ||
![]() |
c57c73ec92 | ||
![]() |
8f6af2be55 | ||
![]() |
d77fce9d80 | ||
![]() |
6de7d9f3f0 | ||
![]() |
3c9df2fc74 | ||
![]() |
f27823adc8 | ||
![]() |
178eabfd01 | ||
![]() |
8f5a1850e9 | ||
![]() |
2b0f2cfc67 | ||
![]() |
50215356ae | ||
![]() |
9b3ede7a81 | ||
![]() |
12f98a64e4 | ||
![]() |
80e9ff9994 | ||
![]() |
12386570ff | ||
![]() |
07f528352a | ||
![]() |
d1d81d85fa | ||
![]() |
c06c115f4d | ||
![]() |
d535d9c6ae | ||
![]() |
5046213f3c | ||
![]() |
3434d71569 | ||
![]() |
38a19c305d | ||
![]() |
5f06910cf9 | ||
![]() |
8b15fb4cb1 | ||
![]() |
d3f7e9555e | ||
![]() |
c18900c242 | ||
![]() |
2d75c57a20 | ||
![]() |
6d579368af | ||
![]() |
c0fc70c7c0 | ||
![]() |
24ffbb2d6d | ||
![]() |
3b3fb93990 | ||
![]() |
960f3ca2c2 | ||
![]() |
f41015b793 | ||
![]() |
c0f69e2daa | ||
![]() |
f2ac130517 | ||
![]() |
7e02518394 | ||
![]() |
88d1645c4a | ||
![]() |
63c4ce5915 | ||
![]() |
5dd9842bdc | ||
![]() |
2642c14cb7 | ||
![]() |
6eddfb8b97 | ||
![]() |
55f467e8ba | ||
![]() |
d90edde68a | ||
![]() |
dd96b8b9f9 | ||
![]() |
03e731a836 | ||
![]() |
735ac0eb3d | ||
![]() |
f65dcdd66f | ||
![]() |
4eea995c89 | ||
![]() |
a1fb4636d2 | ||
![]() |
b54ae7ba25 | ||
![]() |
9483f2ce8e | ||
![]() |
e0897cce2e | ||
![]() |
6853f9a306 | ||
![]() |
c9448b1781 | ||
![]() |
7440cf1b74 | ||
![]() |
4fb173e364 | ||
![]() |
8cf3dd39f9 | ||
![]() |
1114af11ea | ||
![]() |
4cebbbd8a7 | ||
![]() |
c8b64281c2 | ||
![]() |
d37b473e10 | ||
![]() |
69b210a4b8 | ||
![]() |
03d074a0e8 | ||
![]() |
003f5331fa | ||
![]() |
ce2fa5cf80 | ||
![]() |
fe50205eca | ||
![]() |
b625fa4fca | ||
![]() |
6a01145ab8 | ||
![]() |
ada640c5f2 | ||
![]() |
3d72425874 | ||
![]() |
cdfe1566fa | ||
![]() |
46ebd0f276 | ||
![]() |
df2adf8d06 | ||
![]() |
ab891463f7 | ||
![]() |
8d403866ed | ||
![]() |
2e96a22d3d | ||
![]() |
98d2a1219b | ||
![]() |
5de0673df5 | ||
![]() |
b9aca242fd | ||
![]() |
1c362beafb | ||
![]() |
9aef0aad42 | ||
![]() |
6036b87a2a | ||
![]() |
46f5d7b9cd | ||
![]() |
7e5fa235e2 | ||
![]() |
57ae045d05 | ||
![]() |
2ef61b1c17 | ||
![]() |
3109dff3c2 | ||
![]() |
ab2c1d49b6 | ||
![]() |
5d768188bc | ||
![]() |
e4c2589e07 | ||
![]() |
1d0df6b5f0 | ||
![]() |
a6bf0f4b7f | ||
![]() |
11095cf8c9 | ||
![]() |
e34492a204 | ||
![]() |
b44529586b | ||
![]() |
85b8c9a0c2 | ||
![]() |
38075ae36d | ||
![]() |
eb4782c50b | ||
![]() |
d2f41cd571 | ||
![]() |
b78ad7ea4f | ||
![]() |
c00daa2462 | ||
![]() |
6942428b25 | ||
![]() |
bc9760b1eb | ||
![]() |
55d23298e0 | ||
![]() |
f91e4ac73f | ||
![]() |
375e47818c | ||
![]() |
22b5088f0b | ||
![]() |
1f101f4155 | ||
![]() |
91d7c46e83 | ||
![]() |
42ab9a757c | ||
![]() |
0b467f736f | ||
![]() |
dd8a3440d6 | ||
![]() |
6c81216bc2 | ||
![]() |
7f23cc9d51 | ||
![]() |
c750514919 | ||
![]() |
2d3aab24b6 | ||
![]() |
fdf4655068 | ||
![]() |
63e83c3b21 | ||
![]() |
f8bb714d25 | ||
![]() |
1aa454013c | ||
![]() |
95eb48f303 | ||
![]() |
3109340bdc | ||
![]() |
b056d31110 | ||
![]() |
47ea7267ed | ||
![]() |
100ba7088b | ||
![]() |
cee71a2cc4 | ||
![]() |
23ee10fbc8 | ||
![]() |
1d4c9bef5e | ||
![]() |
272a614ce4 | ||
![]() |
7e03c3d81a | ||
![]() |
4911e012e8 | ||
![]() |
7832732a25 | ||
![]() |
0ebcec416f | ||
![]() |
393694eab8 | ||
![]() |
0537e24a82 | ||
![]() |
852d88de84 | ||
![]() |
0ceb93da7a | ||
![]() |
5e1329fbd4 | ||
![]() |
9db4bcd24e | ||
![]() |
b640108c7e | ||
![]() |
8b7346aa37 | ||
![]() |
e8a0b151fb | ||
![]() |
74bb9f7f44 | ||
![]() |
9854d971b6 | ||
![]() |
44be338c3d | ||
![]() |
a31d2d3188 | ||
![]() |
27420f6c01 | ||
![]() |
3b7e031d76 | ||
![]() |
e49bd8b300 | ||
![]() |
a7d1aafecd | ||
![]() |
c7bdf6816b | ||
![]() |
dd14c8e86d | ||
![]() |
873910400d | ||
![]() |
198cc7357c | ||
![]() |
90ede44790 | ||
![]() |
828cc272ee | ||
![]() |
4bb2a0e0d0 | ||
![]() |
4bd8210ef1 | ||
![]() |
cf0ab74870 | ||
![]() |
9e410662ea | ||
![]() |
d32680a14f | ||
![]() |
78e8341206 | ||
![]() |
f2107219c2 | ||
![]() |
fe8635667c | ||
![]() |
75d8abcf55 | ||
![]() |
4c680ca7f3 | ||
![]() |
4407926ae8 | ||
![]() |
cb79503f5f | ||
![]() |
3203bd99d2 | ||
![]() |
160865cd86 | ||
![]() |
089dcecf1c | ||
![]() |
de44771755 | ||
![]() |
c0a13ad65f | ||
![]() |
a2c8c4678e | ||
![]() |
be87a31b84 | ||
![]() |
90822e9258 | ||
![]() |
132eeda4a0 | ||
![]() |
b4e23c2cbc | ||
![]() |
0c9c93e493 | ||
![]() |
9cbefca4bb | ||
![]() |
0f8c310c8d | ||
![]() |
110a004d59 | ||
![]() |
6d6d9bc7cd | ||
![]() |
860bc8ae97 | ||
![]() |
1dfe3d962f | ||
![]() |
27f212f3ec | ||
![]() |
6793780d4f | ||
![]() |
d415ceae15 | ||
![]() |
cba2e67a70 | ||
![]() |
42d31eb0e6 | ||
![]() |
2159b3d49b | ||
![]() |
cc8b9bb41d | ||
![]() |
35c5f063f1 | ||
![]() |
6dec9fb928 | ||
![]() |
884c8219fd | ||
![]() |
988f5dbfdc | ||
![]() |
b6939dcce4 | ||
![]() |
b3f8ac0a5b | ||
![]() |
0a96d17dd1 | ||
![]() |
c831cf77cc | ||
![]() |
f05d0e12a0 | ||
![]() |
30ed545981 | ||
![]() |
a69488b32f | ||
![]() |
1654443b00 | ||
![]() |
0f44fcb8fd | ||
![]() |
8d20135779 | ||
![]() |
2f06bf8b47 | ||
![]() |
68899ec476 | ||
![]() |
3fe1893336 | ||
![]() |
5bb5e3a96b | ||
![]() |
faecbd03a0 | ||
![]() |
2bb4ae6984 | ||
![]() |
ed5de47197 | ||
![]() |
e3400006a9 | ||
![]() |
de4198efaf | ||
![]() |
25b8a47371 | ||
![]() |
51ed0f9625 | ||
![]() |
3f87462994 | ||
![]() |
da8811eadf | ||
![]() |
9848816df4 | ||
![]() |
af5836cb09 | ||
![]() |
645f9fc5e3 | ||
![]() |
a09587c7e2 | ||
![]() |
7b773f94f9 | ||
![]() |
0a9941d93d | ||
![]() |
d0d287f668 | ||
![]() |
69ecaf023f | ||
![]() |
9b7c4c7b4a | ||
![]() |
ef40b5f3db | ||
![]() |
13e54ced07 | ||
![]() |
6f8b49c974 | ||
![]() |
d10a56ed44 | ||
![]() |
f3ef78d292 | ||
![]() |
efd4ebeadf | ||
![]() |
e827be1b5b | ||
![]() |
8a7feba9cc | ||
![]() |
11caf22dc6 | ||
![]() |
f9fee5beb0 | ||
![]() |
5f989739bb | ||
![]() |
dbd4c467f8 | ||
![]() |
592f0c65bf | ||
![]() |
7b4abe46c1 | ||
![]() |
3d749120a7 | ||
![]() |
13c90e6c4a | ||
![]() |
2c88703588 | ||
![]() |
853e281710 | ||
![]() |
1f607d3e6d | ||
![]() |
cba103af77 | ||
![]() |
b8b60d52bf | ||
![]() |
1fd85f40e7 | ||
![]() |
5bfc30651a | ||
![]() |
57af421561 | ||
![]() |
976d06ea1a | ||
![]() |
85c8e7035d | ||
![]() |
09cdb08fdb | ||
![]() |
73cedf858b | ||
![]() |
e87693d989 | ||
![]() |
d530a1cf56 | ||
![]() |
50cd629d7d | ||
![]() |
9e85911b5d | ||
![]() |
cb9290fb89 | ||
![]() |
8b7eba6698 | ||
![]() |
bd1db5412b | ||
![]() |
232ed2229b | ||
![]() |
6214b7f659 | ||
![]() |
45782355db | ||
![]() |
3213fd2ba1 | ||
![]() |
9cb8e0f5d5 | ||
![]() |
2fda08a33e | ||
![]() |
10171d9228 | ||
![]() |
74c93907c5 | ||
![]() |
16a0344ceb | ||
![]() |
c725b36ff8 | ||
![]() |
6821254aeb | ||
![]() |
9106fb1321 | ||
![]() |
e72ccd3012 | ||
![]() |
24c3ebbf57 | ||
![]() |
77bb9dbed8 | ||
![]() |
7ae6c38e9a | ||
![]() |
c43505befb | ||
![]() |
582eebd436 | ||
![]() |
f635df8502 | ||
![]() |
ab1ec07686 | ||
![]() |
1fcc1dae1c | ||
![]() |
b2e736bb55 | ||
![]() |
f9181b85f9 | ||
![]() |
b14faf5352 | ||
![]() |
d079d7770e | ||
![]() |
47352eddd0 | ||
![]() |
dc46bc2e84 | ||
![]() |
42e9f7b2dd | ||
![]() |
85c83045f3 | ||
![]() |
cbaa8676fc | ||
![]() |
22ad6260c7 | ||
![]() |
2740743320 | ||
![]() |
303de3d972 | ||
![]() |
04e318c9ca | ||
![]() |
9e939131b7 | ||
![]() |
ba3b90bdf6 | ||
![]() |
ee85533aa4 | ||
![]() |
9d59b094e6 | ||
![]() |
092bdc0578 | ||
![]() |
c125243915 | ||
![]() |
142db699a4 | ||
![]() |
e7c727f446 | ||
![]() |
67ceaec460 | ||
![]() |
a2b924837a | ||
![]() |
bfcd88ebb2 | ||
![]() |
752c7c7022 | ||
![]() |
9860008549 | ||
![]() |
f07497a39c | ||
![]() |
64c1d7fa04 | ||
![]() |
a92288b342 | ||
![]() |
138859ccd6 | ||
![]() |
e8ebb0069b | ||
![]() |
74e34ee695 | ||
![]() |
d250f4cafb | ||
![]() |
8c45250f78 | ||
![]() |
ef56f8db88 | ||
![]() |
0515948055 | ||
![]() |
888760e6ee | ||
![]() |
5c00c4463c | ||
![]() |
63761ead27 | ||
![]() |
625c306b2f | ||
![]() |
4331fa4047 | ||
![]() |
478ff88a82 | ||
![]() |
d8b4fc403d | ||
![]() |
1f41bbdc0d | ||
![]() |
8669fddd70 | ||
![]() |
ce8c928337 | ||
![]() |
c9cdc1a5dc | ||
![]() |
d04f26b6e4 | ||
![]() |
96014a7365 | ||
![]() |
4c75f6078d | ||
![]() |
2d14e3c1c4 | ||
![]() |
f0481ffa14 | ||
![]() |
4f40160c40 | ||
![]() |
487da9615f | ||
![]() |
05285d23b7 | ||
![]() |
0283962ed2 | ||
![]() |
e4527da122 | ||
![]() |
133cabeed8 | ||
![]() |
3f5b5ffc93 | ||
![]() |
968b35f324 | ||
![]() |
33e09cc344 | ||
![]() |
2182ff4c59 | ||
![]() |
780f77dab9 | ||
![]() |
997b4a4698 | ||
![]() |
fd7f942595 | ||
![]() |
fa929a4cf4 | ||
![]() |
b05080bbd9 | ||
![]() |
ca06edd797 | ||
![]() |
a82032d5d0 | ||
![]() |
0d864a866c | ||
![]() |
83c8d6d303 | ||
![]() |
f25dafe913 | ||
![]() |
a49ea5c3c3 | ||
![]() |
1b81267841 | ||
![]() |
2008708cd9 | ||
![]() |
99ebc84a91 | ||
![]() |
930c906460 | ||
![]() |
a6b1f21ce5 | ||
![]() |
d69021804d | ||
![]() |
eaf04dd78e | ||
![]() |
6bfd76caf2 | ||
![]() |
e53a56f7b2 | ||
![]() |
ed2de37207 | ||
![]() |
9d4bfe0f63 | ||
![]() |
1ea35f6977 | ||
![]() |
2868e3c4f1 | ||
![]() |
d672983a90 | ||
![]() |
d9ed0ffb0d | ||
![]() |
944fb46aa5 | ||
![]() |
7bff114602 | ||
![]() |
3c35927085 | ||
![]() |
ad8896b2e8 | ||
![]() |
0e3172b277 | ||
![]() |
98117b19c0 | ||
![]() |
e434f39839 | ||
![]() |
e48fb93c77 | ||
![]() |
e895d1f0d3 | ||
![]() |
14cd500121 | ||
![]() |
eab27d402b | ||
![]() |
816bd4b2e6 | ||
![]() |
79b1396e4a | ||
![]() |
ba54e8b798 | ||
![]() |
8d5c7abe89 | ||
![]() |
4e0808449d | ||
![]() |
052f411433 | ||
![]() |
ddb94cc831 | ||
![]() |
05f2a76bc4 | ||
![]() |
97f5391567 | ||
![]() |
a4fe44437a | ||
![]() |
1d47956bcf | ||
![]() |
e43355ce56 | ||
![]() |
939e3004b6 | ||
![]() |
be3148d0e7 | ||
![]() |
019c6019d7 | ||
![]() |
31dad85da6 | ||
![]() |
d6dcf6bf7b | ||
![]() |
d20ef02745 | ||
![]() |
56ff9d1353 | ||
![]() |
51a0bb26fa | ||
![]() |
9cafe80763 | ||
![]() |
0b160d2e04 | ||
![]() |
ba6d2e7793 | ||
![]() |
5ee8307703 | ||
![]() |
8bd6e56065 | ||
![]() |
853d7ed047 | ||
![]() |
2b8a882933 | ||
![]() |
e3140573ac | ||
![]() |
a777c89ef6 | ||
![]() |
94ace2e1af | ||
![]() |
20c3a50f3b | ||
![]() |
91696f5ccb | ||
![]() |
f1901d75ac | ||
![]() |
bdb71d828f | ||
![]() |
cbd8c977e2 | ||
![]() |
215dcc0dd9 | ||
![]() |
f3a9581d89 | ||
![]() |
a0bf49d1ea | ||
![]() |
b2bde89834 | ||
![]() |
b8261fcecf | ||
![]() |
60f02c0181 | ||
![]() |
5582e906c8 | ||
![]() |
7d48ad736c | ||
![]() |
e984ed82ef | ||
![]() |
cd0b5b80f9 | ||
![]() |
5d4bdf6139 | ||
![]() |
9a9bf92126 | ||
![]() |
80063eb786 | ||
![]() |
4fd914ac7a | ||
![]() |
1d263b2aee | ||
![]() |
46978e1086 | ||
![]() |
7ea294bb66 | ||
![]() |
50ac03e97d | ||
![]() |
7f3acb215e | ||
![]() |
040c900be5 | ||
![]() |
e9ce43832a | ||
![]() |
4935ba5c8d | ||
![]() |
92f7d5e197 | ||
![]() |
92f8678b3d | ||
![]() |
4565f58a75 | ||
![]() |
e1a8c42f9e | ||
![]() |
760eae5a7b | ||
![]() |
a5c65f7b30 | ||
![]() |
6ebd24e2bb | ||
![]() |
083ceca893 | ||
![]() |
b152c682fa | ||
![]() |
8c25134031 | ||
![]() |
0e5c039536 | ||
![]() |
8ddc6a656d | ||
![]() |
c57f7105e1 | ||
![]() |
4d11c97d51 | ||
![]() |
f3e08bdc68 | ||
![]() |
378c177490 | ||
![]() |
96906ec35a | ||
![]() |
5e0683df23 | ||
![]() |
07c19aef37 | ||
![]() |
49400a9ed9 | ||
![]() |
340cba3ebe | ||
![]() |
9180c9f5c0 | ||
![]() |
17b6ca83d7 | ||
![]() |
8f2c827280 | ||
![]() |
28ba6921d1 | ||
![]() |
0b7c5fb616 | ||
![]() |
04e8b591ab | ||
![]() |
85ae6f86b1 | ||
![]() |
cfff0ba0e8 | ||
![]() |
79943b9d4a | ||
![]() |
97f5697af7 | ||
![]() |
4b699d5376 | ||
![]() |
c9f6be61b5 | ||
![]() |
b7527e78e7 | ||
![]() |
f875c6902f | ||
![]() |
bcb3ac1fe0 | ||
![]() |
9da6893ac3 | ||
![]() |
f97b1851e6 | ||
![]() |
240bcce445 | ||
![]() |
4f4310c0ca | ||
![]() |
6ddd1571c7 | ||
![]() |
2dc689e2a1 | ||
![]() |
f0f0bd09d3 | ||
![]() |
cc43561bf2 | ||
![]() |
0a373de7a4 | ||
![]() |
a653e6392c | ||
![]() |
2a6b514232 | ||
![]() |
e930e03378 | ||
![]() |
c53df989b8 | ||
![]() |
c399a17369 | ||
![]() |
cc6b2736ce | ||
![]() |
d662c15fb8 | ||
![]() |
99a86c4080 | ||
![]() |
01fce7f94f | ||
![]() |
2910acc906 | ||
![]() |
43cc017c91 | ||
![]() |
675151b4ef | ||
![]() |
64d709c142 | ||
![]() |
4f2650cd47 | ||
![]() |
d7efc99bdb | ||
![]() |
3c6389c004 | ||
![]() |
a8dedd8ac2 | ||
![]() |
ef4c67530a | ||
![]() |
f236dc3d3e | ||
![]() |
53c4ba52c4 | ||
![]() |
4e4d3ec3f9 | ||
![]() |
0b1e2732c2 | ||
![]() |
c6a2b1174e | ||
![]() |
db3e8a62c4 | ||
![]() |
4bf5a37cf8 | ||
![]() |
618b1096f3 | ||
![]() |
a3af6b8b9f | ||
![]() |
186b2d0dc8 | ||
![]() |
3cc54ad2ff | ||
![]() |
6e5468daa1 | ||
![]() |
d9c2ea6fb6 | ||
![]() |
2b15a3c17b | ||
![]() |
22b072962e | ||
![]() |
30669f8058 | ||
![]() |
7c49f03f21 | ||
![]() |
e7912359eb | ||
![]() |
5e9931fa03 | ||
![]() |
8ae3ae3e80 | ||
![]() |
e343918ef9 | ||
![]() |
ff93f96959 | ||
![]() |
44127c563d | ||
![]() |
0100415e3e | ||
![]() |
a30ec646b2 | ||
![]() |
593aaf3894 | ||
![]() |
a965ab755a | ||
![]() |
515ca01d38 | ||
![]() |
2645b2bfd4 | ||
![]() |
a48deb9160 | ||
![]() |
0a71d6b45d | ||
![]() |
71704d5cbc | ||
![]() |
5be35d1d87 | ||
![]() |
a7c9be9b16 | ||
![]() |
f95bfc397d | ||
![]() |
7f3db513e5 | ||
![]() |
5bb3721b3a | ||
![]() |
41b0d0fa7e | ||
![]() |
7e0f4bbc0e | ||
![]() |
b0b285bc1f | ||
![]() |
8bb4d9012f | ||
![]() |
a345788dba | ||
![]() |
71605e98dc | ||
![]() |
a50efda113 | ||
![]() |
6452d2da96 | ||
![]() |
d94e8a4ad2 | ||
![]() |
03c5a224de | ||
![]() |
d6aa60b211 | ||
![]() |
a2fac20e6b | ||
![]() |
78b5f489ef | ||
![]() |
435fc27e31 | ||
![]() |
3fa5a51600 | ||
![]() |
4084cd5260 | ||
![]() |
3aa8bf4ea0 | ||
![]() |
93973131f7 | ||
![]() |
62f9c84252 | ||
![]() |
e92be5322d | ||
![]() |
4a8ff47fce | ||
![]() |
0d4c61df96 | ||
![]() |
4895e2d790 | ||
![]() |
f0c00c144c | ||
![]() |
a653b0156a | ||
![]() |
ee6daf356b | ||
![]() |
5f27b65a92 | ||
![]() |
ab7067f247 | ||
![]() |
3b962b7546 | ||
![]() |
2e6984bfbe | ||
![]() |
c6bc8b5e37 | ||
![]() |
f9bcf33d50 | ||
![]() |
d9f91da9a6 | ||
![]() |
2553b8520c | ||
![]() |
7c9492e6b4 | ||
![]() |
9795b31b69 | ||
![]() |
578a98250b | ||
![]() |
38ab8ac353 | ||
![]() |
29403c1d84 | ||
![]() |
d70fd02178 | ||
![]() |
02ed2293e6 | ||
![]() |
35f9900101 | ||
![]() |
0e16eae5bc | ||
![]() |
ea61e8f2d3 | ||
![]() |
c26c6b9e89 | ||
![]() |
f325e9d057 | ||
![]() |
68c6648da5 | ||
![]() |
38059ec324 | ||
![]() |
1b2678d6ed | ||
![]() |
38eafdeedd | ||
![]() |
97994c5e43 | ||
![]() |
1fd6e447af | ||
![]() |
4f7f7b2cb5 | ||
![]() |
b6415dadb4 | ||
![]() |
29c1aaf33c | ||
![]() |
767db8a598 | ||
![]() |
f298ed7156 | ||
![]() |
3aac399456 | ||
![]() |
fc631c8781 | ||
![]() |
b393befb70 | ||
![]() |
2befa22b2c | ||
![]() |
f48236deb9 | ||
![]() |
62a4d58756 | ||
![]() |
8e62d9ae7e | ||
![]() |
6e89a55879 | ||
![]() |
a7aa554f66 | ||
![]() |
51b2aeda1a | ||
![]() |
03b7f59dce | ||
![]() |
709bd9d699 | ||
![]() |
4775c17653 | ||
![]() |
c27d8f6287 | ||
![]() |
dbbad1c95c | ||
![]() |
f328a9de5e | ||
![]() |
9f7536a926 | ||
![]() |
198c9fbaff | ||
![]() |
8fd1bc39f3 | ||
![]() |
8a3001465f | ||
![]() |
3eab46d7f9 | ||
![]() |
9824e8df09 | ||
![]() |
5065c37e13 | ||
![]() |
c39e1cab6e | ||
![]() |
f5ce844803 | ||
![]() |
3df1327b9b | ||
![]() |
cbf59f9dd0 | ||
![]() |
fc52e3e1c0 | ||
![]() |
f41be4bbe4 | ||
![]() |
c586de61d2 | ||
![]() |
f8578b178e | ||
![]() |
f65824857c | ||
![]() |
886919adb7 | ||
![]() |
c5bb3adfdc | ||
![]() |
a936b5d070 | ||
![]() |
09444b0181 | ||
![]() |
70a93b01db | ||
![]() |
40925009bb | ||
![]() |
52c4554877 | ||
![]() |
ec44c1efe5 | ||
![]() |
647d63cd10 | ||
![]() |
80fffdac61 | ||
![]() |
38c25f0ecc | ||
![]() |
92430e163e | ||
![]() |
cb73e6352c | ||
![]() |
6f3ca8b9c4 | ||
![]() |
d7b7af76b7 | ||
![]() |
5e80b04266 | ||
![]() |
41b3eab1fd | ||
![]() |
7dff9a9499 | ||
![]() |
fba02e32b5 | ||
![]() |
45a3efa2b3 | ||
![]() |
92e4d99c1c | ||
![]() |
1203cacbe4 | ||
![]() |
96bffff8ac | ||
![]() |
239ccbf0f3 | ||
![]() |
c33cacedaf | ||
![]() |
da9396fff1 | ||
![]() |
3df03bbb1e | ||
![]() |
93339cbbfb | ||
![]() |
12b3a5af12 | ||
![]() |
962a2fb3d6 | ||
![]() |
0db225e07c | ||
![]() |
05332409be | ||
![]() |
96b7c35a2a | ||
![]() |
edb3564e29 | ||
![]() |
2b59644e02 | ||
![]() |
6703fa3652 | ||
![]() |
c1b0b854fc | ||
![]() |
bdc190a7d6 | ||
![]() |
29b78df530 | ||
![]() |
0702183210 | ||
![]() |
367387f967 | ||
![]() |
6e5912a633 | ||
![]() |
157b1e242f | ||
![]() |
1c5256a5d7 | ||
![]() |
4820ab3694 | ||
![]() |
f4d9191ed5 | ||
![]() |
91541748a2 | ||
![]() |
a32154cacb | ||
![]() |
281b2ce450 | ||
![]() |
ec67059128 | ||
![]() |
b57a122ac1 | ||
![]() |
315265fbb4 | ||
![]() |
c23d4e1988 | ||
![]() |
e543895940 | ||
![]() |
f79f95e25a | ||
![]() |
94096cdc95 | ||
![]() |
01e6db5720 | ||
![]() |
1d225912f4 | ||
![]() |
4a007698a6 | ||
![]() |
939efa5b17 | ||
![]() |
46d79f24fc | ||
![]() |
0f36372d38 | ||
![]() |
4970bb8ead | ||
![]() |
054ac8b3fa | ||
![]() |
784e2abe37 | ||
![]() |
45137f2f8e | ||
![]() |
e314c401e7 | ||
![]() |
bc98fd69b1 | ||
![]() |
869662152a | ||
![]() |
45ab80bb29 | ||
![]() |
7b4cee35c7 | ||
![]() |
26894fcb01 | ||
![]() |
c5ed7699d6 | ||
![]() |
49b954e63d | ||
![]() |
1b114b6bae | ||
![]() |
833db3b5ce | ||
![]() |
7267736166 | ||
![]() |
3ab6bd84f9 | ||
![]() |
11dc9f6460 | ||
![]() |
dcd425edc0 | ||
![]() |
fa5eac0b72 | ||
![]() |
e367265396 | ||
![]() |
14ae58690c | ||
![]() |
a3c78e4d02 | ||
![]() |
76a489c492 | ||
![]() |
7945c0dead | ||
![]() |
ab8a180141 | ||
![]() |
b7b42b77e9 | ||
![]() |
ec4b2bf331 | ||
![]() |
b84e8ef7d6 | ||
![]() |
cf4a1a436f | ||
![]() |
4431d25793 | ||
![]() |
4a5bb059de | ||
![]() |
6a87a372fb | ||
![]() |
72ff2b8805 | ||
![]() |
424839c793 | ||
![]() |
898f331bfd | ||
![]() |
249af82d7d | ||
![]() |
cdb8100819 | ||
![]() |
928d3e8c9a | ||
![]() |
3ed414cf96 | ||
![]() |
a2e1ce0fe2 | ||
![]() |
8737f4a9fc | ||
![]() |
250749a3cd | ||
![]() |
39c2aeb19a | ||
![]() |
5e7146d19a | ||
![]() |
e1a6e2386f | ||
![]() |
d6a8ef9e59 | ||
![]() |
33fd77e3b8 | ||
![]() |
9c6a652a02 | ||
![]() |
dccd0256d3 | ||
![]() |
280a5e7279 | ||
![]() |
0dd31c287c | ||
![]() |
6568f9fcbc | ||
![]() |
de3e34060c | ||
![]() |
f69985d8df | ||
![]() |
e36ed7f941 | ||
![]() |
1d16a6edee | ||
![]() |
801ec4e7fa | ||
![]() |
fae80fbb26 | ||
![]() |
2365d314c6 | ||
![]() |
13c9a14a50 | ||
![]() |
571e4a18e4 | ||
![]() |
3d203f628f | ||
![]() |
ccc9978540 | ||
![]() |
68ace5af24 | ||
![]() |
b1b406e2d0 | ||
![]() |
b0d0d66790 | ||
![]() |
f58f008a63 | ||
![]() |
0174b4ca00 | ||
![]() |
8b7a9f215c | ||
![]() |
7f929f96a1 | ||
![]() |
04a05e3c52 | ||
![]() |
15b0beba17 | ||
![]() |
3e9aca4137 | ||
![]() |
8f0488aa6a | ||
![]() |
082c9117a2 | ||
![]() |
1a114c6c45 | ||
![]() |
51a091fe44 | ||
![]() |
db19842ce0 | ||
![]() |
c52262a405 | ||
![]() |
14adb2a302 | ||
![]() |
b751594bfe | ||
![]() |
c92970a3ed | ||
![]() |
c54c905d2e | ||
![]() |
0f18a0979d | ||
![]() |
e3175530ed | ||
![]() |
a67fdfeb89 | ||
![]() |
0a11c4cd3e | ||
![]() |
3d55c0b45b | ||
![]() |
c7b16c6713 | ||
![]() |
2f80d37c24 | ||
![]() |
b18231c25d | ||
![]() |
dc86129cca | ||
![]() |
14a38f2f50 | ||
![]() |
81c44ef4ae | ||
![]() |
9261f60225 | ||
![]() |
b017f35010 | ||
![]() |
d6cac24fed | ||
![]() |
8dfeabcf18 | ||
![]() |
d70252a3f2 | ||
![]() |
822a75258c | ||
![]() |
d62a3c711d | ||
![]() |
9db556f4b7 | ||
![]() |
ead4f449d6 | ||
![]() |
0a515dfae1 | ||
![]() |
4fb12261e4 | ||
![]() |
1845e392d2 | ||
![]() |
2cd026b34b | ||
![]() |
9e82463e46 | ||
![]() |
05aa6a39fa | ||
![]() |
0506160f7b | ||
![]() |
ea559d0675 | ||
![]() |
bdbb6ce03a | ||
![]() |
1295158de2 | ||
![]() |
b0e4f59f43 | ||
![]() |
3eddb1402c | ||
![]() |
d9590743d9 | ||
![]() |
94eb8c7084 | ||
![]() |
5fef0d012f | ||
![]() |
2ce62aac65 | ||
![]() |
6159013b36 | ||
![]() |
2f2a3a7755 | ||
![]() |
274013ad68 | ||
![]() |
893e68b3da | ||
![]() |
ee287220b8 | ||
![]() |
0db2499666 | ||
![]() |
e3b73ff49a | ||
![]() |
a2be7d9013 | ||
![]() |
2a481f6889 | ||
![]() |
a16bc22c4a | ||
![]() |
9dfa18aead | ||
![]() |
da566b44ce | ||
![]() |
85300331e0 | ||
![]() |
f5ce318be7 | ||
![]() |
842ddb5e24 | ||
![]() |
20f17da913 | ||
![]() |
5036deb61e | ||
![]() |
c6705e2cb9 | ||
![]() |
23ed692436 | ||
![]() |
4711b50836 | ||
![]() |
7fb2de4bae | ||
![]() |
953d3ed8d7 | ||
![]() |
3db689b782 | ||
![]() |
990deab398 | ||
![]() |
f948112e42 | ||
![]() |
11205dcf72 | ||
![]() |
3db3a516eb | ||
![]() |
a7d8bbf26f | ||
![]() |
7569a58f97 | ||
![]() |
f1655496d8 | ||
![]() |
48c3e1f747 | ||
![]() |
73bef92f42 | ||
![]() |
eb6df43384 | ||
![]() |
c4f416396a | ||
![]() |
871f1f2d90 | ||
![]() |
246d1c5f32 | ||
![]() |
c81eb9ec69 | ||
![]() |
3784e58fc4 | ||
![]() |
ad1819c10e | ||
![]() |
6f91fb41f8 | ||
![]() |
93c5bd4a95 | ||
![]() |
4258ea1c40 | ||
![]() |
6deac28a8b | ||
![]() |
c64c1a19b7 | ||
![]() |
89673dfd2d | ||
![]() |
be2cb0cc43 | ||
![]() |
de3c224b02 | ||
![]() |
f8f77babe8 | ||
![]() |
474cc194ca | ||
![]() |
dd1853cab3 | ||
![]() |
67407e1095 | ||
![]() |
c0c80919f7 | ||
![]() |
713e33bf57 | ||
![]() |
0fde562b32 | ||
![]() |
db803bfb0c | ||
![]() |
60cd63c274 | ||
![]() |
ac820baf72 | ||
![]() |
e1df9c600c | ||
![]() |
82f4a975bb | ||
![]() |
fdfdb0935f | ||
![]() |
c1cdd5dce1 | ||
![]() |
bc1b057324 | ||
![]() |
e887379dc2 | ||
![]() |
d90ea02b50 | ||
![]() |
e6efffb981 | ||
![]() |
8239f12ff7 | ||
![]() |
53aa7ec849 | ||
![]() |
d22c714dbd | ||
![]() |
85d7752d0a | ||
![]() |
97b93aaa2b | ||
![]() |
76396f224f | ||
![]() |
cd667371e6 | ||
![]() |
ebacde9631 | ||
![]() |
bd4cd81f50 | ||
![]() |
c11fdd11d8 | ||
![]() |
f1084452d4 | ||
![]() |
0ced75d451 | ||
![]() |
bb775bde08 | ||
![]() |
61f9d3ac86 | ||
![]() |
cb9ef84027 | ||
![]() |
bc69c8d6ed | ||
![]() |
0736aef5fc | ||
![]() |
40308ce8cb | ||
![]() |
eaea0e2c23 | ||
![]() |
8e394563e4 | ||
![]() |
3abb426fc3 | ||
![]() |
c2eb9a9b67 | ||
![]() |
f77f9c5435 | ||
![]() |
8b03a0c196 | ||
![]() |
680a468b6c | ||
![]() |
06ae1bc044 | ||
![]() |
09029dd193 | ||
![]() |
f0a8780755 | ||
![]() |
cb3b2e0d8e | ||
![]() |
74b285a77f | ||
![]() |
d7c19c76c9 | ||
![]() |
eef1e0a406 | ||
![]() |
578084ff96 | ||
![]() |
cda80f5de6 | ||
![]() |
4e2ab4c048 | ||
![]() |
47e065442e | ||
![]() |
070054892e | ||
![]() |
bd6e2a3046 | ||
![]() |
6eff521459 | ||
![]() |
186c1fecea | ||
![]() |
89bedfda13 | ||
![]() |
ba544cf5f1 | ||
![]() |
0570c85133 | ||
![]() |
4a5d732c01 | ||
![]() |
116947082a | ||
![]() |
d88ad55504 | ||
![]() |
d338046329 | ||
![]() |
00195be469 | ||
![]() |
996f81ced2 | ||
![]() |
d76f7e13b3 | ||
![]() |
7bb9615355 | ||
![]() |
8c7a821e8d | ||
![]() |
27f5793fd8 | ||
![]() |
8ed63c7e7a | ||
![]() |
911b95dc87 | ||
![]() |
f062de9092 | ||
![]() |
d3a22d0876 | ||
![]() |
ccdb161de4 | ||
![]() |
aa38418c91 | ||
![]() |
eec57945c0 | ||
![]() |
2da1a5acd4 | ||
![]() |
2495251a22 | ||
![]() |
ada40cd746 | ||
![]() |
c09708a09e | ||
![]() |
da01be7986 | ||
![]() |
2da1e14374 | ||
![]() |
89575a4591 | ||
![]() |
e0aae7d797 | ||
![]() |
63a86df035 | ||
![]() |
6e26e20f66 | ||
![]() |
1895d2f909 | ||
![]() |
2fe91bc35e | ||
![]() |
08af7ee157 | ||
![]() |
afef5ea233 | ||
![]() |
37b3f1ff57 | ||
![]() |
7ff4b9b4cd | ||
![]() |
bfc35691b6 | ||
![]() |
85b7e77324 | ||
![]() |
e52c17f586 | ||
![]() |
467d96b9ee | ||
![]() |
2392507634 | ||
![]() |
d941ac5eb0 | ||
![]() |
1f2fe25cd4 | ||
![]() |
521218a45c | ||
![]() |
70b392996c | ||
![]() |
27525122b8 | ||
![]() |
7e7f0fe6f2 | ||
![]() |
2bd05dec38 | ||
![]() |
c6c472402d | ||
![]() |
d2ade5c69b | ||
![]() |
1405705fd5 | ||
![]() |
623748091d | ||
![]() |
bfc79f3a79 | ||
![]() |
55c1bd0572 | ||
![]() |
a03053e80a | ||
![]() |
bbca04c79b | ||
![]() |
2d44c35951 | ||
![]() |
abefe3d389 | ||
![]() |
bbe7475f75 | ||
![]() |
30385baf16 | ||
![]() |
37cf054d8d | ||
![]() |
e16ab9e74d | ||
![]() |
5a99249920 | ||
![]() |
bcba0e3f47 | ||
![]() |
b42de49cb4 | ||
![]() |
e6d784368f | ||
![]() |
0f5a49fdd4 | ||
![]() |
5dd4b8de1c | ||
![]() |
30bebd8361 | ||
![]() |
5b2d75c018 | ||
![]() |
5af7cac4b7 | ||
![]() |
69cc6ee3bb | ||
![]() |
ddc4181bba | ||
![]() |
d39ca46953 | ||
![]() |
e7e07bd03b | ||
![]() |
405a31a415 | ||
![]() |
c8a62a8e79 | ||
![]() |
3a7e68da9e | ||
![]() |
442651b6fe | ||
![]() |
f681eaac97 | ||
![]() |
ff87f0a77f | ||
![]() |
0a77a9d210 | ||
![]() |
c02194d894 | ||
![]() |
ce1ffb83cf | ||
![]() |
7d1bfd9ae0 | ||
![]() |
d00888e6a3 | ||
![]() |
851e13f485 | ||
![]() |
8b9e5e5e5a | ||
![]() |
a6b75ce822 | ||
![]() |
9f50a0003c | ||
![]() |
f538e0da8c | ||
![]() |
8a06aa1c9b | ||
![]() |
e085bcd54c | ||
![]() |
9d2a4eb999 | ||
![]() |
c7e9e16a6b | ||
![]() |
fa9690886b | ||
![]() |
06dd4c8dcb | ||
![]() |
f0dba2f6e7 | ||
![]() |
6d0207c32e | ||
![]() |
0a4725b962 | ||
![]() |
2d78fa802f | ||
![]() |
afad8fb85a | ||
![]() |
8de3842285 | ||
![]() |
9b108e36d8 | ||
![]() |
eb83736dce | ||
![]() |
1248bd0523 | ||
![]() |
216ec24680 | ||
![]() |
9ab4b55de1 | ||
![]() |
a845f66099 | ||
![]() |
c6811d83b2 | ||
![]() |
e6bce05bd2 | ||
![]() |
3e0df70c56 | ||
![]() |
9f0873a3af | ||
![]() |
c3af53a18d | ||
![]() |
aa0abaeb70 | ||
![]() |
8e3b064786 | ||
![]() |
6d1801c0fd | ||
![]() |
58db8a1f00 | ||
![]() |
92f71ca13a | ||
![]() |
de74d6457d | ||
![]() |
ddb9791e11 | ||
![]() |
b0586384f8 | ||
![]() |
4857ed2d45 | ||
![]() |
a299340db7 | ||
![]() |
bcb47c9490 | ||
![]() |
6d006e70b3 | ||
![]() |
6b27568ac4 | ||
![]() |
3eee4f115a | ||
![]() |
b841c4b4c9 | ||
![]() |
80205e94fb | ||
![]() |
2a22a8b6cb | ||
![]() |
e84e9c15e2 | ||
![]() |
87b82a6751 | ||
![]() |
6d807793c2 | ||
![]() |
ae9cb5b89b | ||
![]() |
b14bffa1c5 | ||
![]() |
2fd8982485 | ||
![]() |
93c9abb887 | ||
![]() |
10009dc1fe | ||
![]() |
4ba45ba66f | ||
![]() |
0a10e44778 | ||
![]() |
47d6bb095b | ||
![]() |
cf03ad3edb | ||
![]() |
669a7bc1cf | ||
![]() |
acd61b7a68 | ||
![]() |
8f1d4d3b6f | ||
![]() |
b655b54890 | ||
![]() |
09fc1c5545 | ||
![]() |
fdb710dfd1 | ||
![]() |
e4401ed22c | ||
![]() |
c330362cf0 | ||
![]() |
481cd91646 | ||
![]() |
4f06d0081e | ||
![]() |
69fd955360 | ||
![]() |
692a25abcb | ||
![]() |
c7145a63cb | ||
![]() |
008752ae74 | ||
![]() |
6b12a006fb | ||
![]() |
ea6482b94b | ||
![]() |
59f21c8561 | ||
![]() |
9959becc09 | ||
![]() |
04f7b4a5a9 | ||
![]() |
36ee32d82c | ||
![]() |
db21ad2d3c | ||
![]() |
3a3a480429 | ||
![]() |
71e11027d0 | ||
![]() |
785dd036d1 | ||
![]() |
cd0617d0f6 | ||
![]() |
f60f2c160a | ||
![]() |
78ed3cee96 | ||
![]() |
9384466c29 | ||
![]() |
c2350cbb73 | ||
![]() |
d8653d8345 | ||
![]() |
0429b3f7dd | ||
![]() |
c9b08ffdd7 | ||
![]() |
2b03fb49be | ||
![]() |
3e5f3d45a6 | ||
![]() |
77419183ec | ||
![]() |
b4d8ee08dc | ||
![]() |
9cfa079e46 | ||
![]() |
3fc53c21d2 | ||
![]() |
7f9a03822c | ||
![]() |
0d7e7e4009 | ||
![]() |
87c3cd456e | ||
![]() |
82fed66a8a | ||
![]() |
3a3ea52077 | ||
![]() |
08c089b25c | ||
![]() |
5ea37b0d12 | ||
![]() |
2b0b220e4d | ||
![]() |
4cddb8d6ea | ||
![]() |
916afe1627 | ||
![]() |
6b7df1e46e | ||
![]() |
0c1420ab2e | ||
![]() |
62056648ee | ||
![]() |
5cd6285ce0 | ||
![]() |
2f7ee04032 | ||
![]() |
05e99c666d | ||
![]() |
7b5e831b66 | ||
![]() |
b695bbc4b1 | ||
![]() |
249e38c32f | ||
![]() |
d2e32631d0 | ||
![]() |
9870fd13b6 | ||
![]() |
df3ba4c751 | ||
![]() |
ac83a5a138 | ||
![]() |
cf53f07e1f | ||
![]() |
4f7f87be0a | ||
![]() |
af3a6369cc | ||
![]() |
bc5cf590c7 | ||
![]() |
be3f7900e8 | ||
![]() |
14824daa21 | ||
![]() |
78558ca86e | ||
![]() |
aa877fd85a | ||
![]() |
602991767c | ||
![]() |
8fc125a1c5 | ||
![]() |
bbb91e8eca | ||
![]() |
182aea0f98 | ||
![]() |
13f52e1b50 | ||
![]() |
56933cdbd6 | ||
![]() |
a525bca1cf | ||
![]() |
92199f2f01 | ||
![]() |
c1e5e287b7 | ||
![]() |
0945661c9d | ||
![]() |
d41499d656 | ||
![]() |
7a08ab532f | ||
![]() |
83072bc2c4 | ||
![]() |
cba8f3b68f | ||
![]() |
b06792524e | ||
![]() |
3fef37a5f4 | ||
![]() |
8af69afba6 | ||
![]() |
96333c1cba | ||
![]() |
c6e86392ff | ||
![]() |
5ff0ef8de7 | ||
![]() |
eb841a88ca | ||
![]() |
4e52611d36 | ||
![]() |
72f238ac84 | ||
![]() |
216acf5ae8 | ||
![]() |
1d4581a78e | ||
![]() |
a030c25f4f | ||
![]() |
98a333b7fa | ||
![]() |
4c94203fd9 | ||
![]() |
d13a8c101a | ||
![]() |
b7f0195234 | ||
![]() |
ff5d2b571f | ||
![]() |
2f9c3b8056 | ||
![]() |
5dac28f882 | ||
![]() |
67b608e4e5 | ||
![]() |
ce42783583 | ||
![]() |
4929a8c15d | ||
![]() |
5d3372207a | ||
![]() |
5af5306311 | ||
![]() |
81cbef82b4 | ||
![]() |
208146882e | ||
![]() |
b82be525b9 | ||
![]() |
659b46fd4e | ||
![]() |
3050e3dbc5 | ||
![]() |
59b9fd6a8c | ||
![]() |
92421bac05 | ||
![]() |
58ff02a3f4 | ||
![]() |
728d1467b9 | ||
![]() |
95180512c7 | ||
![]() |
c1b5bf5279 | ||
![]() |
d19e77049a | ||
![]() |
b619716669 | ||
![]() |
910f9a68a6 | ||
![]() |
91e06b8496 | ||
![]() |
f51d192e13 | ||
![]() |
ba028408a1 | ||
![]() |
fdaa2ad106 | ||
![]() |
c4b059502c | ||
![]() |
2c594cae5d | ||
![]() |
ed630454a5 | ||
![]() |
a085515b3d | ||
![]() |
9ea7959dc1 | ||
![]() |
a08963f0e5 | ||
![]() |
bfb04648f8 | ||
![]() |
7351584697 | ||
![]() |
4cbf97b499 | ||
![]() |
09db3524cf | ||
![]() |
09fb7969b4 | ||
![]() |
0c039a6b5e | ||
![]() |
91fc0f5383 | ||
![]() |
b70986ef16 | ||
![]() |
d5488fc5f1 | ||
![]() |
b7aad893a3 | ||
![]() |
ab165a4d7b | ||
![]() |
36de588497 | ||
![]() |
568e82c07e | ||
![]() |
b7339e9423 | ||
![]() |
5f2efbb72a | ||
![]() |
22fb0337b9 | ||
![]() |
e41ae50812 | ||
![]() |
544b924ab9 | ||
![]() |
b0c27869b6 | ||
![]() |
0ee5332c01 | ||
![]() |
ed829fcfd6 | ||
![]() |
a688a029bf | ||
![]() |
b95c93de59 | ||
![]() |
12868902e9 | ||
![]() |
414c4e0c67 | ||
![]() |
b2c63c6dc7 | ||
![]() |
009c8a6be5 | ||
![]() |
2f7d6f6452 | ||
![]() |
d9e5b07054 | ||
![]() |
75586ec4e5 | ||
![]() |
1e1be1e73b | ||
![]() |
11ddf0478c | ||
![]() |
3ed2cde207 | ||
![]() |
0e0e042cd0 | ||
![]() |
91f2f1014e | ||
![]() |
084a048740 | ||
![]() |
19e722a9d7 | ||
![]() |
9588f30778 | ||
![]() |
d9f9b682d4 | ||
![]() |
a44ce0e4e7 | ||
![]() |
c4b0eb9b51 | ||
![]() |
2c80f1a758 | ||
![]() |
d850ae2307 | ||
![]() |
8b0f45f878 | ||
![]() |
e2c3811f4d | ||
![]() |
f841a179d1 | ||
![]() |
cbc28ea53a | ||
![]() |
2beee93e07 | ||
![]() |
08efdaf135 | ||
![]() |
8e4ef71168 | ||
![]() |
ed4b9fa1d2 | ||
![]() |
8c75c3694b | ||
![]() |
4779b753e6 | ||
![]() |
146046a1ff | ||
![]() |
0216de670c | ||
![]() |
e782730455 | ||
![]() |
8ebce69d5d | ||
![]() |
30ecc1c9f6 | ||
![]() |
f2994b0764 | ||
![]() |
b5f7c0a481 | ||
![]() |
248cefe1f3 | ||
![]() |
d25a395936 | ||
![]() |
7ca966be05 | ||
![]() |
54283b71fd | ||
![]() |
29048a0c60 | ||
![]() |
dd60f5cd3b | ||
![]() |
b7feebddf9 | ||
![]() |
0f98042520 | ||
![]() |
67c7c9b8b5 | ||
![]() |
319e1faf3b | ||
![]() |
4d44f3fce8 | ||
![]() |
9d6cabe737 | ||
![]() |
cdbbb9d580 | ||
![]() |
aa52516a41 | ||
![]() |
b3107eb6b2 | ||
![]() |
d68b959856 | ||
![]() |
a8f9483684 | ||
![]() |
c819d56f18 | ||
![]() |
8a6204205f | ||
![]() |
09cbd036fa | ||
![]() |
776ad3f540 | ||
![]() |
c7e1d4af7e | ||
![]() |
4f51970250 | ||
![]() |
3c9d7b67cc | ||
![]() |
acd3137cbc | ||
![]() |
1491f7318f | ||
![]() |
2bd686dfe8 | ||
![]() |
c65a25524c | ||
![]() |
63b0bbd120 | ||
![]() |
54ce7a07a6 | ||
![]() |
91ddc838a7 | ||
![]() |
aeae0bcec3 | ||
![]() |
e82690f324 | ||
![]() |
6710e9a4b9 | ||
![]() |
6419915d5a | ||
![]() |
df696ed81e | ||
![]() |
04577d213c | ||
![]() |
919f9c63b8 | ||
![]() |
d250ac727b | ||
![]() |
e1d9e0f3b1 | ||
![]() |
c48dcdc785 | ||
![]() |
0887da72ed | ||
![]() |
6a4b323d06 | ||
![]() |
96c3af5485 | ||
![]() |
295ef8f02a | ||
![]() |
0a8d08aac8 | ||
![]() |
efacc22c10 | ||
![]() |
107eab5eac | ||
![]() |
16399b760b | ||
![]() |
2256cd00e1 | ||
![]() |
7401366ac9 | ||
![]() |
8d3d325967 | ||
![]() |
914149aad2 | ||
![]() |
a6de6b2932 | ||
![]() |
5e02404d6a | ||
![]() |
60f5a1441c | ||
![]() |
584152d2fa | ||
![]() |
3c005006ce | ||
![]() |
47d4526f05 | ||
![]() |
0aadd51e71 | ||
![]() |
52c61ec1ca | ||
![]() |
15a3d74ada | ||
![]() |
06c974265e | ||
![]() |
2848b50f8b | ||
![]() |
2aa39311c2 | ||
![]() |
a58825d7ea | ||
![]() |
d8d8a45d88 | ||
![]() |
3352d83102 | ||
![]() |
b11232b607 | ||
![]() |
e35a6e05c7 | ||
![]() |
c1a6f545cf | ||
![]() |
d24f94476e | ||
![]() |
5d995aa8c8 | ||
![]() |
882e010052 | ||
![]() |
5f31703316 | ||
![]() |
08346b9b18 | ||
![]() |
678e2b09ea | ||
![]() |
94dc2d019f | ||
![]() |
17c42e4bd0 | ||
![]() |
a9c88bb5d7 | ||
![]() |
5050605f72 | ||
![]() |
2c2d8c868c | ||
![]() |
db0b106656 | ||
![]() |
a89e4143c0 | ||
![]() |
3caec653e5 | ||
![]() |
049cd56215 | ||
![]() |
1d1c682c2c | ||
![]() |
3701d7864a | ||
![]() |
f483630e02 | ||
![]() |
b8cffa6744 | ||
![]() |
c288fd5534 | ||
![]() |
c32fb3a6b7 | ||
![]() |
a9495cd907 | ||
![]() |
253c880595 | ||
![]() |
57435aa6b6 | ||
![]() |
615bcfb666 | ||
![]() |
8395472f8d | ||
![]() |
c0166a26f8 | ||
![]() |
d80eec031c | ||
![]() |
097be4396e | ||
![]() |
6ba99eef87 | ||
![]() |
fbf9d4b409 | ||
![]() |
826854592f | ||
![]() |
3bca6d3f70 | ||
![]() |
a0b39cd150 | ||
![]() |
663a95c7a9 | ||
![]() |
f4539b3803 | ||
![]() |
88285b0a6b | ||
![]() |
7a16bf4ce5 | ||
![]() |
55acd955b9 | ||
![]() |
af2515bbf6 | ||
![]() |
f374fbbeb0 | ||
![]() |
0e64736db4 | ||
![]() |
fb1e19e510 | ||
![]() |
033db01b9a | ||
![]() |
527f649fc2 | ||
![]() |
1018cad3b4 | ||
![]() |
4dc2c60863 | ||
![]() |
cce393de09 | ||
![]() |
a0970d0720 | ||
![]() |
649bfa10b7 | ||
![]() |
aa1b9e6696 | ||
![]() |
7d3723fe69 | ||
![]() |
625bdcebba | ||
![]() |
53d9a15d11 | ||
![]() |
d7203535e7 | ||
![]() |
b3ca915459 | ||
![]() |
588e22462b | ||
![]() |
2399f954ed | ||
![]() |
e80b1e03ac | ||
![]() |
fbc47c1428 | ||
![]() |
36b269cd19 | ||
![]() |
c4e7e617b2 | ||
![]() |
7ed69c93fc | ||
![]() |
8f08289822 | ||
![]() |
732fb87944 | ||
![]() |
7451f00534 | ||
![]() |
07b79c1e06 | ||
![]() |
cfcdfebe4e | ||
![]() |
4d1def468f | ||
![]() |
1f154adf42 | ||
![]() |
d287dca854 | ||
![]() |
3345a81077 | ||
![]() |
9bd33003a0 | ||
![]() |
82b188a2ac | ||
![]() |
71db322b6d | ||
![]() |
d573bac5b0 | ||
![]() |
e3e726e56c | ||
![]() |
4a55cdf938 | ||
![]() |
0db711c08d | ||
![]() |
52e0f373af | ||
![]() |
fe41133d2b | ||
![]() |
4b3b392c01 | ||
![]() |
c5a1fd9b1c | ||
![]() |
f93c8151fe | ||
![]() |
87775be76c | ||
![]() |
4e24050760 | ||
![]() |
655f2bf7e3 | ||
![]() |
57eaad7c1b | ||
![]() |
de085f7e02 | ||
![]() |
8c6ce67100 | ||
![]() |
60f2be678a | ||
![]() |
d813f1ec17 | ||
![]() |
bc6f3d5cd9 | ||
![]() |
4db53deb42 | ||
![]() |
8ed76f47ee | ||
![]() |
e7f76705c8 | ||
![]() |
f6707490f8 | ||
![]() |
7fd3ea236e | ||
![]() |
2cdb634865 | ||
![]() |
40d873551f | ||
![]() |
f04ac83ec9 | ||
![]() |
fe1806b016 | ||
![]() |
2a296c0302 | ||
![]() |
09ccf3d428 | ||
![]() |
87638363b3 | ||
![]() |
d54dc0b7bb | ||
![]() |
da329089fb | ||
![]() |
c1d0ec43c3 | ||
![]() |
407e7662e9 | ||
![]() |
a477f421cb | ||
![]() |
b731ab57b3 | ||
![]() |
ad9218d14c | ||
![]() |
c717d919af | ||
![]() |
34d7172ad6 | ||
![]() |
d16c7fc95a | ||
![]() |
dcf3a9dae8 | ||
![]() |
04ea93cce8 | ||
![]() |
c1daa99617 | ||
![]() |
793bd618ac | ||
![]() |
628d684ab9 | ||
![]() |
eded748ab8 | ||
![]() |
e7eb3120cf | ||
![]() |
8977ac6826 | ||
![]() |
f6cf7c9b0c | ||
![]() |
f0a40d8b6e | ||
![]() |
edd2de49c3 | ||
![]() |
232145eac1 | ||
![]() |
44312bdb6c | ||
![]() |
2ff0ae9550 | ||
![]() |
6b08ab0e97 | ||
![]() |
4f0cba0c84 | ||
![]() |
ce334e215d | ||
![]() |
5e0af1dc5b | ||
![]() |
468b6717ee | ||
![]() |
05d8e409c4 | ||
![]() |
abdd2fbb8e | ||
![]() |
8efa9d609a | ||
![]() |
ce3f210919 | ||
![]() |
3ebad383d6 | ||
![]() |
9703350f41 | ||
![]() |
bfdfb6d5ef | ||
![]() |
3bfd5cbf0d | ||
![]() |
db1adb327a | ||
![]() |
7f315720ab | ||
![]() |
53ac16fcee | ||
![]() |
19aa5eb7f7 | ||
![]() |
a6a133b885 | ||
![]() |
ca2fe21beb | ||
![]() |
361fbc83ba | ||
![]() |
9f2de0abd7 | ||
![]() |
2cd17a904e | ||
![]() |
a1a8c06565 | ||
![]() |
4f065492ef | ||
![]() |
fc4a96acd8 | ||
![]() |
9c9ecc5140 | ||
![]() |
0e47ac6900 | ||
![]() |
ded9b146a2 | ||
![]() |
ee38ff3eea | ||
![]() |
7dd216d212 | ||
![]() |
955087d523 | ||
![]() |
ddb1cfd659 | ||
![]() |
94300592d9 | ||
![]() |
fa8052611e | ||
![]() |
53a10aa44f | ||
![]() |
5e30d0d218 | ||
![]() |
7f652f8620 | ||
![]() |
aa85cddd84 | ||
![]() |
12a9a1cf29 | ||
![]() |
ca0ed2844e | ||
![]() |
58609a4f50 | ||
![]() |
05edd59b05 | ||
![]() |
3df221bc4a | ||
![]() |
a184ce7268 | ||
![]() |
6284811fec | ||
![]() |
2095229061 | ||
![]() |
8acb1b9a76 | ||
![]() |
0ec4e3756d | ||
![]() |
67e1020684 | ||
![]() |
7fa25d3209 | ||
![]() |
1f799d1ef1 | ||
![]() |
61bbbf442d | ||
![]() |
d55fc7cd69 | ||
![]() |
750da53970 | ||
![]() |
49a3c3370f | ||
![]() |
54d7849191 | ||
![]() |
be4db93da5 | ||
![]() |
6fa72607b8 | ||
![]() |
4454c0d261 | ||
![]() |
0f44c5edba | ||
![]() |
1d3c18423b | ||
![]() |
31fe8343ad | ||
![]() |
ef17cc3489 | ||
![]() |
9fd8da42a1 | ||
![]() |
81fcd44b66 | ||
![]() |
68f266d6be | ||
![]() |
91363bf753 | ||
![]() |
f7051a3f50 | ||
![]() |
bf040a73fa | ||
![]() |
d647bfb097 | ||
![]() |
aa9bfefd55 | ||
![]() |
7d985f137f | ||
![]() |
4004b6251f | ||
![]() |
7111f54c9b | ||
![]() |
141f133c7a | ||
![]() |
45d046b445 | ||
![]() |
87f2da7e2f | ||
![]() |
568224ba78 | ||
![]() |
955c2f5f6c | ||
![]() |
31aa3fcf30 | ||
![]() |
8b823cdf59 | ||
![]() |
f5c4018fee | ||
![]() |
57b2bc431e | ||
![]() |
2a03c51207 | ||
![]() |
5d0074d821 | ||
![]() |
0fba2c255e | ||
![]() |
66563d30d9 | ||
![]() |
a3024bc837 | ||
![]() |
16c34a95d3 | ||
![]() |
44bb904ab0 | ||
![]() |
93ea5ea9fb | ||
![]() |
28d5456e72 | ||
![]() |
2c9d8ab7fc | ||
![]() |
2dcd87cd41 | ||
![]() |
51a2159559 | ||
![]() |
3240faf7f2 | ||
![]() |
576208d3aa | ||
![]() |
27874b3a9e | ||
![]() |
dd3562c00f | ||
![]() |
888d28aed6 | ||
![]() |
301ac10515 | ||
![]() |
3a0d40d86d | ||
![]() |
5d828e2341 | ||
![]() |
8f506c0b0b | ||
![]() |
231fb3dd8a | ||
![]() |
c7a40e9fa6 | ||
![]() |
94fb0c8a02 | ||
![]() |
4d5fd98ae5 | ||
![]() |
3223e20e33 | ||
![]() |
8bb0e58e6c | ||
![]() |
e6ec5ee242 | ||
![]() |
dc1bdc2f3b | ||
![]() |
a4de616b7a | ||
![]() |
ab6166c36d | ||
![]() |
a8bf9f7614 | ||
![]() |
09f3e8b004 | ||
![]() |
334ba0b956 | ||
![]() |
226003d38d | ||
![]() |
5877af55ae | ||
![]() |
a797e43178 | ||
![]() |
4860e06af3 | ||
![]() |
4b4ee4b6db | ||
![]() |
68a719f48a | ||
![]() |
85f3232de0 | ||
![]() |
799be52224 | ||
![]() |
b222887745 | ||
![]() |
8473627370 | ||
![]() |
43f8ea0814 | ||
![]() |
41b5797307 | ||
![]() |
feec3b1c51 | ||
![]() |
901545d404 | ||
![]() |
4e26c77327 | ||
![]() |
6b2b759a16 | ||
![]() |
3a1b814603 | ||
![]() |
739ecfdea3 | ||
![]() |
6d111c6e7d | ||
![]() |
c0cd0d33bf | ||
![]() |
0c06b67f3d | ||
![]() |
b709f9e17a | ||
![]() |
1e9c6ccf2e | ||
![]() |
fffae79c24 | ||
![]() |
967174b549 | ||
![]() |
c99cf93c8b | ||
![]() |
cf1519f792 | ||
![]() |
51a02fe40f | ||
![]() |
97e3bac98e | ||
![]() |
f60ce3f56c | ||
![]() |
cdad2c66ed | ||
![]() |
9871ad0f01 | ||
![]() |
113ae202b7 | ||
![]() |
9c9c0a4819 | ||
![]() |
ceed2c31d7 | ||
![]() |
ba8de64686 | ||
![]() |
ea7f82ed1a | ||
![]() |
4489037619 | ||
![]() |
220b3d1441 | ||
![]() |
fc511fd94f | ||
![]() |
ae10cd0db4 | ||
![]() |
2f6a812fdc | ||
![]() |
a3b316c4a8 | ||
![]() |
4c58514152 | ||
![]() |
f35528cd78 | ||
![]() |
9bd236d66c | ||
![]() |
9b2e5c2426 | ||
![]() |
81359f929e | ||
![]() |
1083f57ec1 | ||
![]() |
7ca6db9555 | ||
![]() |
ef784dffa8 | ||
![]() |
e4d106a298 | ||
![]() |
9ec0a9060c | ||
![]() |
52dc6f8977 | ||
![]() |
6da00f84cb | ||
![]() |
34cdacc303 | ||
![]() |
5292acfef0 | ||
![]() |
0ff1257aef | ||
![]() |
f525af993f | ||
![]() |
9b6bb724d6 | ||
![]() |
292cc40bc4 | ||
![]() |
87c5ff56b8 | ||
![]() |
aaae885161 | ||
![]() |
da8e41249b | ||
![]() |
b3773a1561 | ||
![]() |
3b5509ff4b | ||
![]() |
2a8c570a00 | ||
![]() |
a01bcd6be2 | ||
![]() |
102ff2d0be | ||
![]() |
cb68daa7f9 | ||
![]() |
b8998d711a | ||
![]() |
d553b07af2 | ||
![]() |
c8016e9af2 | ||
![]() |
f098b9c6d8 | ||
![]() |
4a7f0ef9a2 | ||
![]() |
772004756e | ||
![]() |
04b5db1f4c | ||
![]() |
a02f985efa | ||
![]() |
7b7da9a110 | ||
![]() |
7c67db22f3 | ||
![]() |
126d96e4ba | ||
![]() |
510e8e1ba5 | ||
![]() |
50ee88fbc7 | ||
![]() |
6819d678a5 | ||
![]() |
93de06aded | ||
![]() |
bcb7c048b5 | ||
![]() |
ea9dec34b3 | ||
![]() |
1f38a84fa9 | ||
![]() |
f8893431fb | ||
![]() |
cad37b0d55 | ||
![]() |
cbb182c18a | ||
![]() |
10106bf6dd | ||
![]() |
62008e85c6 | ||
![]() |
526c978328 | ||
![]() |
47659b5cec | ||
![]() |
4cd73c12f0 | ||
![]() |
7e87f8b81f | ||
![]() |
fd998b7566 | ||
![]() |
077548eb72 | ||
![]() |
b734b210e3 | ||
![]() |
1f99c00899 | ||
![]() |
9500c987cc | ||
![]() |
305daf5a10 | ||
![]() |
d99e3edb52 | ||
![]() |
a6ed36808d | ||
![]() |
4a70097c64 | ||
![]() |
ca1a07677a | ||
![]() |
68067a81c9 | ||
![]() |
763b4d3ea0 | ||
![]() |
d8df407b02 | ||
![]() |
b58cb74612 | ||
![]() |
99808969d7 | ||
![]() |
27452ac31b | ||
![]() |
3567d9e113 | ||
![]() |
3ae9fd7b88 | ||
![]() |
a5c66a8c6e | ||
![]() |
7c472c8e18 | ||
![]() |
15c1b98d5c | ||
![]() |
6a49d9656c | ||
![]() |
b9628a6960 | ||
![]() |
9f4bfe015c | ||
![]() |
9b28fde89f | ||
![]() |
350d77aec6 | ||
![]() |
c0fad0fe26 | ||
![]() |
2d3c333b21 | ||
![]() |
1c988a0b5a | ||
![]() |
a666b53ace | ||
![]() |
30140d7a3b | ||
![]() |
254327f9a7 | ||
![]() |
9fd22c92ff | ||
![]() |
f8d235fb43 | ||
![]() |
5f22877e1c | ||
![]() |
5b4c40fe07 | ||
![]() |
7279dfacac | ||
![]() |
ff9620caf5 | ||
![]() |
dc5dd51462 | ||
![]() |
499c9e50c5 | ||
![]() |
3f19888b11 | ||
![]() |
fe0fe0e6a2 | ||
![]() |
3aed80d94b | ||
![]() |
ac739de345 | ||
![]() |
648f23e15a | ||
![]() |
03dc389f85 | ||
![]() |
91cf8c9472 | ||
![]() |
8411dd4f9c | ||
![]() |
fe41108ed6 | ||
![]() |
98d574afbf | ||
![]() |
c274752477 | ||
![]() |
f1c7846605 | ||
![]() |
61ba6d52d0 | ||
![]() |
5296bf1477 | ||
![]() |
943253084b | ||
![]() |
b4a51c4a91 | ||
![]() |
4edd90d7aa | ||
![]() |
abc3631b16 | ||
![]() |
837683217b | ||
![]() |
db01c9c476 | ||
![]() |
d3686de3cd | ||
![]() |
9709b4f51c | ||
![]() |
3d66f7f311 | ||
![]() |
a2fb86f518 | ||
![]() |
34211bb1b7 | ||
![]() |
72c2a6a942 | ||
![]() |
9e30d442e1 | ||
![]() |
e8337aad30 | ||
![]() |
c1ccdcfca8 | ||
![]() |
a6379a8006 | ||
![]() |
1bed5ff87c | ||
![]() |
5591104c21 | ||
![]() |
66cde1d88b | ||
![]() |
d80f2d58ad | ||
![]() |
b863802479 | ||
![]() |
70576c72be | ||
![]() |
783d6aa4d1 | ||
![]() |
3731ed8f23 | ||
![]() |
ffa2c962dc | ||
![]() |
c262a8dd52 | ||
![]() |
9b88a14566 | ||
![]() |
14d7ce38fd | ||
![]() |
c6e9002f2c | ||
![]() |
3b5e883397 | ||
![]() |
fa08a8cfa4 | ||
![]() |
21747dcf40 | ||
![]() |
463561d971 | ||
![]() |
bd2e379073 | ||
![]() |
e9abbfa743 | ||
![]() |
eeeace080f | ||
![]() |
066f67bcd4 | ||
![]() |
aff6a03fa2 | ||
![]() |
105390fa88 | ||
![]() |
4d94295864 | ||
![]() |
8a90687867 | ||
![]() |
5cde91ef23 | ||
![]() |
aee57c448b | ||
![]() |
50f68d34b9 | ||
![]() |
ab6f41ec68 | ||
![]() |
9c61ccebdb | ||
![]() |
fe7c77ad03 | ||
![]() |
535d343faf | ||
![]() |
2226d5a23a | ||
![]() |
b35bb9b1f5 | ||
![]() |
e8d3a7a4ef | ||
![]() |
f4e23bb2ed | ||
![]() |
cd00760cc4 | ||
![]() |
753bc43452 | ||
![]() |
771947536e | ||
![]() |
2b1c335ea9 | ||
![]() |
e302baa340 | ||
![]() |
214c920afd | ||
![]() |
219443b5a7 | ||
![]() |
b31d8cb55b | ||
![]() |
b4cee703f2 | ||
![]() |
56cb8ccdda | ||
![]() |
962521947e | ||
![]() |
de40bed7e1 | ||
![]() |
6c9a375b27 | ||
![]() |
abef4f36e0 | ||
![]() |
65981e3a25 | ||
![]() |
d1d88ea26b | ||
![]() |
177b2a170d | ||
![]() |
02bac89b17 | ||
![]() |
1b6a643b9c | ||
![]() |
1b83bf38ac | ||
![]() |
aaad028f3a | ||
![]() |
994610c516 | ||
![]() |
ea0eae6b11 | ||
![]() |
d1d1201a6e | ||
![]() |
1e4fe9fa2c | ||
![]() |
a189ff6e4e | ||
![]() |
67ec0f748c | ||
![]() |
bc74d97195 | ||
![]() |
e437898e33 | ||
![]() |
5ef87602ca | ||
![]() |
fa02eb5733 | ||
![]() |
70fd272871 | ||
![]() |
47c0c2f6ab | ||
![]() |
d8e63240b6 | ||
![]() |
2db4f3f972 | ||
![]() |
dc7df0fc5f | ||
![]() |
c306fe9157 | ||
![]() |
03f2869281 | ||
![]() |
e3789631ba | ||
![]() |
f3b247dc81 | ||
![]() |
960e04d425 | ||
![]() |
5c722ff07b | ||
![]() |
0df1426c8a | ||
![]() |
7f09ff0c09 | ||
![]() |
2651b81792 | ||
![]() |
71be2d8e82 | ||
![]() |
e070883934 | ||
![]() |
744d46161e | ||
![]() |
6bbb939e46 | ||
![]() |
135ec1f0ff | ||
![]() |
8306007f84 | ||
![]() |
42d21bcbe9 | ||
![]() |
2ee5226d62 | ||
![]() |
a0a2fe4cf0 | ||
![]() |
ccbfa2744e | ||
![]() |
00a0f4399f | ||
![]() |
10fa5108da | ||
![]() |
9522edda74 | ||
![]() |
d042ed7508 | ||
![]() |
f4fa7db8c8 | ||
![]() |
8d44f6e444 | ||
![]() |
9384bc093b | ||
![]() |
23dcfadefa | ||
![]() |
901112b29d | ||
![]() |
41e0919c6f | ||
![]() |
4fdc23569b | ||
![]() |
82af6fc60e | ||
![]() |
93fec74728 | ||
![]() |
4d3b7967c4 | ||
![]() |
831a1b40fa | ||
![]() |
708518883a | ||
![]() |
1ad693931f | ||
![]() |
f9d2fbb0ae | ||
![]() |
c7b076e5ea | ||
![]() |
711a1418b6 | ||
![]() |
e063636d61 | ||
![]() |
ffc1f27e33 | ||
![]() |
a3ac298ae4 | ||
![]() |
7dfe9eac24 | ||
![]() |
78caf63093 | ||
![]() |
62ddbe5ce1 | ||
![]() |
2e21f8abba | ||
![]() |
d21788569f | ||
![]() |
7dc56e6009 | ||
![]() |
46bcfcf015 | ||
![]() |
95f932f79f | ||
![]() |
ee21a2724c | ||
![]() |
9b485bece3 | ||
![]() |
fd28883429 | ||
![]() |
240f03d372 | ||
![]() |
c0eecabcca | ||
![]() |
fbd7a0c8e7 | ||
![]() |
95af56a118 | ||
![]() |
6a6c2287d4 | ||
![]() |
36da418660 | ||
![]() |
14c525e7ff | ||
![]() |
f5ce7d8588 | ||
![]() |
3301a57a01 | ||
![]() |
6928bd1244 | ||
![]() |
d687b7341e | ||
![]() |
89b15b534f | ||
![]() |
1b8ea8f3d5 | ||
![]() |
5786d014dc | ||
![]() |
4f717c663b | ||
![]() |
5626adc574 | ||
![]() |
a2b939d1fc | ||
![]() |
0aeec1d2bb | ||
![]() |
afb5762a71 | ||
![]() |
2ec7cb3e8d | ||
![]() |
f7b3e0b81c | ||
![]() |
fbdda9dc77 | ||
![]() |
44c1dc2c44 | ||
![]() |
2efba7f932 | ||
![]() |
7d5179b38a | ||
![]() |
9b5c2656ee | ||
![]() |
080bab482d | ||
![]() |
36cba44c07 | ||
![]() |
dc18bab86f | ||
![]() |
ddfbcff0cb | ||
![]() |
03acf2002a | ||
![]() |
13b3944ae7 | ||
![]() |
32cf4375ad | ||
![]() |
5fa4dde379 | ||
![]() |
87bb85bdf1 | ||
![]() |
1b948f2489 | ||
![]() |
c11fffb8f2 | ||
![]() |
6dcdbcbf1d | ||
![]() |
ed7736e310 | ||
![]() |
2f8cd8e202 | ||
![]() |
34884a4e30 | ||
![]() |
9734328488 | ||
![]() |
3853a21387 | ||
![]() |
2e86b7753b | ||
![]() |
3cedecca0c | ||
![]() |
fd529d347b | ||
![]() |
3c645ee49b | ||
![]() |
a6507bcc8c | ||
![]() |
03adcc86c3 | ||
![]() |
171680e50f | ||
![]() |
85a653c013 | ||
![]() |
8db488bbae | ||
![]() |
a76a465cf1 | ||
![]() |
5d57711fcb | ||
![]() |
ed49eced45 | ||
![]() |
6a62b7979e | ||
![]() |
533c16af0a | ||
![]() |
154b0bda45 | ||
![]() |
dc88ba57e2 | ||
![]() |
651f40ea25 | ||
![]() |
85b633750e | ||
![]() |
327de12f1a | ||
![]() |
be5dbbfc55 | ||
![]() |
c7ace4ff8e | ||
![]() |
8e54bb95c5 | ||
![]() |
b3f0034140 | ||
![]() |
bf225d870b | ||
![]() |
17b33108c5 | ||
![]() |
f7531fcdb5 | ||
![]() |
1346d370cb | ||
![]() |
8f7d09f41a | ||
![]() |
e2d5b619be | ||
![]() |
9f1df4d937 | ||
![]() |
bb425ca072 | ||
![]() |
4091477dd8 | ||
![]() |
2483e5289e | ||
![]() |
6facb9ae98 | ||
![]() |
d6f0fbca98 | ||
![]() |
f16234e084 | ||
![]() |
1cc01b6c73 | ||
![]() |
e1ad61a974 | ||
![]() |
86af7638e8 | ||
![]() |
991b91dfdc | ||
![]() |
804496b50b | ||
![]() |
4fb720e271 | ||
![]() |
19712433bc | ||
![]() |
38b16f3f8f | ||
![]() |
067fa54736 | ||
![]() |
fd8f7c6e74 | ||
![]() |
8669460088 | ||
![]() |
2ae83e8785 | ||
![]() |
808c4cefc3 | ||
![]() |
2528df2f10 | ||
![]() |
aa3c1c4f33 | ||
![]() |
e0f27edaff | ||
![]() |
19ce9c625e | ||
![]() |
2707f49c43 | ||
![]() |
e6ef9889d0 | ||
![]() |
bae65cb49a | ||
![]() |
6ada30118e | ||
![]() |
ef389b90e8 | ||
![]() |
18a08fb611 | ||
![]() |
ae95ef1b74 | ||
![]() |
31afbe0423 | ||
![]() |
36ecacc021 | ||
![]() |
9c7316eea9 | ||
![]() |
1d319009c4 | ||
![]() |
f205b67cd5 | ||
![]() |
5977f10ee6 | ||
![]() |
f2500bec15 | ||
![]() |
1e5cda97c3 | ||
![]() |
9ea8eca7d5 | ||
![]() |
9bf73529a7 | ||
![]() |
c9159b8bff | ||
![]() |
a7e58c154c | ||
![]() |
e3789c18fb | ||
![]() |
3c4a58b8a3 | ||
![]() |
0624189905 | ||
![]() |
eabf9cd654 | ||
![]() |
5609b88250 | ||
![]() |
6e622e59ab | ||
![]() |
1011c8fadc | ||
![]() |
78ba64c2ec | ||
![]() |
458f163f7e | ||
![]() |
a6128a3e47 | ||
![]() |
96bcee1066 | ||
![]() |
430cb42d14 | ||
![]() |
2650508999 | ||
![]() |
a876291f12 | ||
![]() |
f84e0e4fd6 | ||
![]() |
6a90f94b17 | ||
![]() |
953512cac9 | ||
![]() |
5aa2710362 | ||
![]() |
82f87f677e | ||
![]() |
082802cbba | ||
![]() |
fa1b53a81c | ||
![]() |
60dcf7f12c | ||
![]() |
78e2e1bcc4 | ||
![]() |
40904ab75f | ||
![]() |
da5877c9c4 | ||
![]() |
17302321b8 | ||
![]() |
32a106ad1d | ||
![]() |
5e0dfcc631 | ||
![]() |
5a2a68644b | ||
![]() |
dc7d3a1f1f | ||
![]() |
534400c80c | ||
![]() |
0c12c0248e | ||
![]() |
d6d48eb749 | ||
![]() |
d7621e4af0 | ||
![]() |
489bf1425c | ||
![]() |
3f9ed656de | ||
![]() |
017811fb92 | ||
![]() |
0d6ec3a001 | ||
![]() |
4427eb2d44 | ||
![]() |
1161bf65d1 | ||
![]() |
f801ed3b23 | ||
![]() |
100d1a144b | ||
![]() |
85cd9abd4c | ||
![]() |
8f334fefeb | ||
![]() |
e890f54d49 | ||
![]() |
40f4482bd6 | ||
![]() |
3ea503f527 | ||
![]() |
862d7b1183 | ||
![]() |
f6b83871d3 | ||
![]() |
38d7fcdd4a | ||
![]() |
20aa6b8d64 | ||
![]() |
3741c0c62b | ||
![]() |
c2159456e2 | ||
![]() |
abde150ce0 | ||
![]() |
b14b2a9611 | ||
![]() |
6651fe43e6 | ||
![]() |
a9a10667fa | ||
![]() |
4109bb270d | ||
![]() |
19583c2b75 | ||
![]() |
eb1d21f827 | ||
![]() |
aadd89be1b | ||
![]() |
bfcfd163a3 | ||
![]() |
ec74cbed48 | ||
![]() |
9b9f1a0cce | ||
![]() |
116c161ab7 | ||
![]() |
d4a7247629 | ||
![]() |
cb83f6991d | ||
![]() |
32ae91eef5 | ||
![]() |
50f0856f67 | ||
![]() |
d6a66017e9 | ||
![]() |
afe102e90a | ||
![]() |
560b7392cf | ||
![]() |
6cc1880b17 | ||
![]() |
cf120d27e4 | ||
![]() |
80de44ecc6 | ||
![]() |
e37bf801f4 | ||
![]() |
4427970ffc | ||
![]() |
cd0234b4e8 | ||
![]() |
cf0576a717 | ||
![]() |
769e439148 | ||
![]() |
cb7544cbec | ||
![]() |
66e958a2ba | ||
![]() |
2ad2b86e88 | ||
![]() |
ce9fa45034 | ||
![]() |
bab3a81cce | ||
![]() |
f83f5d00da | ||
![]() |
61bffdac71 | ||
![]() |
de3fcaab2d | ||
![]() |
17d4413835 | ||
![]() |
1bad4cc5f7 | ||
![]() |
3dbb5758d5 | ||
![]() |
6efb3e4927 | ||
![]() |
9886f59d17 | ||
![]() |
f44b420473 | ||
![]() |
8325d2256a | ||
![]() |
e04a9f9682 | ||
![]() |
e0bdc3c338 | ||
![]() |
7611a26f0a | ||
![]() |
57a8bf5f8b | ||
![]() |
5542f246f5 | ||
![]() |
a2e99c05a6 | ||
![]() |
6544ebed94 | ||
![]() |
aa164dd090 | ||
![]() |
eead91385e | ||
![]() |
9d6c88a8bf | ||
![]() |
49dddf42a4 | ||
![]() |
f1f1abf095 | ||
![]() |
af2a9ecfb6 | ||
![]() |
bc77804eee | ||
![]() |
47575b5c56 | ||
![]() |
cb2bf7945b | ||
![]() |
1d72c31557 | ||
![]() |
a5492cac35 | ||
![]() |
c032cae0ff | ||
![]() |
03ca3e0e05 | ||
![]() |
0c1b854b69 | ||
![]() |
4de47bd810 | ||
![]() |
36ff25a95c | ||
![]() |
38f2cadb80 | ||
![]() |
65244e6784 | ||
![]() |
5761f32935 | ||
![]() |
38c0ae1972 | ||
![]() |
ff0f3a27f9 | ||
![]() |
e44f8b67a7 | ||
![]() |
632f18c4a2 | ||
![]() |
3ea7226330 | ||
![]() |
6bfcb338d4 | ||
![]() |
f3ee1201e4 | ||
![]() |
25968d181d | ||
![]() |
930cf7d613 | ||
![]() |
4b32f92777 | ||
![]() |
96f358d94c | ||
![]() |
07a161c0a9 | ||
![]() |
b9d5e8f701 | ||
![]() |
c545139927 | ||
![]() |
c8fa5303c4 | ||
![]() |
d2e2b492fb | ||
![]() |
44824fa6cf | ||
![]() |
86464608d7 | ||
![]() |
d315521c7d | ||
![]() |
577a4250b6 | ||
![]() |
d22acac8a5 | ||
![]() |
a17a5edff2 | ||
![]() |
bc5379d305 | ||
![]() |
de82ab4e1d | ||
![]() |
35d30e148e | ||
![]() |
9941e543ed | ||
![]() |
63e37a879f | ||
![]() |
77449ac165 | ||
![]() |
89f8630f12 | ||
![]() |
234b39acb7 | ||
![]() |
1764ceb55a | ||
![]() |
79b789d2c0 | ||
![]() |
c78ff5df30 | ||
![]() |
a46b8e8663 | ||
![]() |
d01afa4cb6 | ||
![]() |
2b4147ed9f | ||
![]() |
efb37e459f | ||
![]() |
194cb24d2c | ||
![]() |
78e7810c14 | ||
![]() |
c1d023de0f | ||
![]() |
9d51cd72ac | ||
![]() |
ed54b028a9 | ||
![]() |
91aa4610a1 | ||
![]() |
80437abcc0 | ||
![]() |
3caa411390 | ||
![]() |
8eda160a30 | ||
![]() |
2d11283db5 | ||
![]() |
5230da8d68 | ||
![]() |
8762efb86b | ||
![]() |
09d21740c8 | ||
![]() |
0c259673d8 | ||
![]() |
74ca3c2aa5 | ||
![]() |
4d0c4babe6 | ||
![]() |
b05e38d761 | ||
![]() |
66d16bf875 | ||
![]() |
3ce8cbf5bd | ||
![]() |
f5785c3de0 | ||
![]() |
4add713ab8 | ||
![]() |
686403d978 | ||
![]() |
948dfa55ec | ||
![]() |
eaa94bdf83 | ||
![]() |
9c4aa9127f | ||
![]() |
d39047bfea | ||
![]() |
419ea5510d | ||
![]() |
93326a90d1 | ||
![]() |
15cf724074 | ||
![]() |
71e6c294e4 | ||
![]() |
5a461589a5 | ||
![]() |
0d099a0126 | ||
![]() |
c7db510dad | ||
![]() |
0cec8f9bd5 | ||
![]() |
a1116608e7 | ||
![]() |
4f7858ad68 | ||
![]() |
e1ef7491c4 | ||
![]() |
55389ce735 | ||
![]() |
5442aaa4dd | ||
![]() |
bd906c2f68 | ||
![]() |
048eca0457 | ||
![]() |
4a61c20e92 | ||
![]() |
232815cce8 | ||
![]() |
9ba01d16bf | ||
![]() |
ddaf4b65cd | ||
![]() |
7605ec219f | ||
![]() |
bab1e6536c | ||
![]() |
21eb159330 | ||
![]() |
7ef7489c79 | ||
![]() |
fd1b0cbb59 | ||
![]() |
1caa7510b6 | ||
![]() |
9ec94e8e88 | ||
![]() |
3f0d8b2577 | ||
![]() |
f5c6c4c0fe | ||
![]() |
d6f643cb4d | ||
![]() |
020cac971f | ||
![]() |
068734c275 | ||
![]() |
a0e34e8971 | ||
![]() |
bb4d3e7f2a | ||
![]() |
44bcaf465a | ||
![]() |
0ca4de23bf | ||
![]() |
56295726bd | ||
![]() |
925c7c08d1 | ||
![]() |
780e0e3d54 | ||
![]() |
18ae6e6da2 | ||
![]() |
6f3c5b4e85 | ||
![]() |
2b7585c1cf | ||
![]() |
82ec7fe45c | ||
![]() |
50ffaa4ea9 | ||
![]() |
2834a8966e | ||
![]() |
4e855b8132 | ||
![]() |
0f16b71688 | ||
![]() |
cb8bf870b1 | ||
![]() |
ee0de7b27e | ||
![]() |
2a1b28821b | ||
![]() |
3b84f110ac | ||
![]() |
a17d09e0a2 | ||
![]() |
b2d6e79d30 | ||
![]() |
2a89c45ef3 | ||
![]() |
433a3b02d6 | ||
![]() |
604a4c0a88 | ||
![]() |
97a33d3972 | ||
![]() |
f5dc1ddc96 | ||
![]() |
460335fce9 | ||
![]() |
24c2b9731c | ||
![]() |
f4a7aa7ced | ||
![]() |
21c56fe260 | ||
![]() |
0f28843163 | ||
![]() |
445d6ca667 | ||
![]() |
387b372606 | ||
![]() |
b77c6380b3 | ||
![]() |
6556745618 | ||
![]() |
18de950fae | ||
![]() |
071a1c28de | ||
![]() |
dbb084accd | ||
![]() |
bf3d3c6d69 | ||
![]() |
308b776f43 | ||
![]() |
5cf8cc1d55 | ||
![]() |
0efdc9df00 | ||
![]() |
26e7875c02 | ||
![]() |
7904db43ea | ||
![]() |
62bc8c99c2 | ||
![]() |
43f1c6d8a6 | ||
![]() |
01edc26914 | ||
![]() |
48cef5d4fa | ||
![]() |
0ad4796e1e | ||
![]() |
4740204231 | ||
![]() |
bc791f2338 | ||
![]() |
8c41f18a1b | ||
![]() |
7849182f98 | ||
![]() |
688142c83e | ||
![]() |
56de1eda6b | ||
![]() |
d5a52dcf46 | ||
![]() |
9928b9408f | ||
![]() |
edc97f0f26 | ||
![]() |
1038aa5d99 | ||
![]() |
fa6777ae9d | ||
![]() |
6c64bdcb16 | ||
![]() |
a63a7cae01 | ||
![]() |
e49de99203 | ||
![]() |
3ba5c17937 | ||
![]() |
3296e0da57 | ||
![]() |
1c01024733 | ||
![]() |
01114da06d | ||
![]() |
2fb2b1bf03 | ||
![]() |
49706c12d1 | ||
![]() |
78f6027176 | ||
![]() |
299596ae58 | ||
![]() |
5ed815a348 | ||
![]() |
bbd9223ced | ||
![]() |
010989f05b | ||
![]() |
0680899a94 | ||
![]() |
8401d53dc0 | ||
![]() |
841ec9d289 | ||
![]() |
4bcf4bf60d | ||
![]() |
68edafc48d | ||
![]() |
d641ad8539 | ||
![]() |
34a2534cc8 | ||
![]() |
04f899d72f | ||
![]() |
f4abb6389c | ||
![]() |
716b1c802b | ||
![]() |
8dd337f345 | ||
![]() |
f6f7085b73 | ||
![]() |
a2843aa7be | ||
![]() |
0cb5d25b0c | ||
![]() |
addf0bcb36 | ||
![]() |
3011c08bb2 | ||
![]() |
f500aad71e | ||
![]() |
33fea6e316 | ||
![]() |
152d0963db | ||
![]() |
e8263e742d | ||
![]() |
ca296ef5eb | ||
![]() |
32d89db8c6 | ||
![]() |
c354280a62 | ||
![]() |
69cd579eda | ||
![]() |
2e665e2771 | ||
![]() |
ff4ba92376 | ||
![]() |
39929028e7 | ||
![]() |
4eda47df4b | ||
![]() |
d2c8f0de68 | ||
![]() |
89facb24c6 | ||
![]() |
fdf69cabcc | ||
![]() |
8679a1241f | ||
![]() |
7a83557772 | ||
![]() |
cdac0e4632 | ||
![]() |
d02dd290ca | ||
![]() |
1c869ee00c | ||
![]() |
eb6ffcbc9b | ||
![]() |
3908ed16db | ||
![]() |
f2109e4e0b | ||
![]() |
9643f69a82 | ||
![]() |
36b815095e | ||
![]() |
55400a906d | ||
![]() |
dc5c24cda9 | ||
![]() |
04e2966067 | ||
![]() |
54fb62e186 | ||
![]() |
b71fcab896 | ||
![]() |
249e7319ec | ||
![]() |
81bfec549d | ||
![]() |
8d578feaf3 | ||
![]() |
57d3ccc124 | ||
![]() |
39a3640c20 | ||
![]() |
5640225247 | ||
![]() |
8702367e13 | ||
![]() |
9b35942c17 | ||
![]() |
bdf165d80c | ||
![]() |
c2cd6a9c66 | ||
![]() |
51a67efdf1 | ||
![]() |
e8eac5f2ca | ||
![]() |
a53fb0ed5b | ||
![]() |
c4e31a5fb1 | ||
![]() |
2c31350488 | ||
![]() |
d07db77103 | ||
![]() |
5ab0acf4c0 | ||
![]() |
a456a9b8d9 | ||
![]() |
1a9f86dc9d | ||
![]() |
b98f59fb94 | ||
![]() |
5a5a632778 | ||
![]() |
8993391b56 |
@ -1,8 +0,0 @@
|
||||
--gettext locale/
|
||||
--groups strings array
|
||||
|
||||
--ignore about_sitec about_mailc
|
||||
--ignore repo_add_http
|
||||
--ignore /updateIntervalValues.*/
|
||||
--ignore /dbSyncModeValues.*/
|
||||
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.gpg binary
|
54
.gitignore
vendored
54
.gitignore
vendored
@ -1,9 +1,49 @@
|
||||
# Built application files
|
||||
*.apk
|
||||
*.ap_
|
||||
|
||||
# Files for the Dalvik VM
|
||||
*.dex
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# Generated files
|
||||
bin/
|
||||
gen/
|
||||
build.xml
|
||||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
build.properties
|
||||
project.properties
|
||||
.classpath
|
||||
bin/*
|
||||
gen/*
|
||||
proguard.cfg
|
||||
proguard-project.txt
|
||||
|
||||
# Proguard folder generated by Eclipse
|
||||
proguard/
|
||||
|
||||
# Log Files
|
||||
*.log
|
||||
|
||||
# Editor swap/save files
|
||||
*~
|
||||
*.swp
|
||||
|
||||
# More IDE stuff
|
||||
.idea/
|
||||
*.iml
|
||||
out
|
||||
.settings/
|
||||
|
||||
# Imported libs
|
||||
extern/*/libs/
|
||||
extern/*/*/libs/
|
||||
|
||||
# Tests
|
||||
junit-report.xml
|
||||
|
||||
# Screen dumps from Android Studio/DDMS
|
||||
captures/
|
||||
|
||||
/fdroid/
|
||||
|
142
.gitlab-ci.yml
Normal file
142
.gitlab-ci.yml
Normal file
@ -0,0 +1,142 @@
|
||||
stages:
|
||||
- test
|
||||
- deploy
|
||||
|
||||
.base:
|
||||
image: registry.gitlab.com/fdroid/ci-images-client:latest
|
||||
before_script:
|
||||
- export GRADLE_USER_HOME=$PWD/.gradle
|
||||
- export ANDROID_COMPILE_SDK=`sed -n 's,.*compileSdkVersion\s*\([0-9][0-9]*\).*,\1,p' app/build.gradle`
|
||||
- alias sdkmanager="sdkmanager --no_https"
|
||||
- echo y | sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" > /dev/null
|
||||
# limit RAM usage for all gradle runs
|
||||
- export maxmem=$(expr $(sed -n 's,^MemAvailable:[^0-9]*\([0-9][0-9]*\)[^0-9]*$,\1,p' /proc/meminfo) / 1024 / 2 / 1024 \* 1024)
|
||||
- printf "\norg.gradle.jvmargs=-Xmx${maxmem}m -XX:MaxPermSize=${maxmem}m\norg.gradle.daemon=false\norg.gradle.parallel=false\n" >> gradle.properties
|
||||
after_script:
|
||||
# this file changes every time but should not be cached
|
||||
- rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock
|
||||
- rm -fr $GRADLE_USER_HOME/caches/*/plugin-resolution/
|
||||
cache:
|
||||
paths:
|
||||
- .gradle/wrapper
|
||||
- .gradle/caches
|
||||
|
||||
.test-template: &test-template
|
||||
extends: .base
|
||||
stage: test
|
||||
artifacts:
|
||||
name: "${CI_PROJECT_PATH}_${CI_JOB_STAGE}_${CI_COMMIT_REF_NAME}_${CI_COMMIT_SHA}"
|
||||
paths:
|
||||
- kernel.log
|
||||
- logcat.txt
|
||||
- app/core*
|
||||
- app/*.log
|
||||
- app/build/reports
|
||||
- app/build/outputs/*ml
|
||||
- app/build/outputs/apk
|
||||
expire_in: 1 week
|
||||
when: on_failure
|
||||
after_script:
|
||||
- echo "Download debug artifacts from https://gitlab.com/${CI_PROJECT_PATH}/-/jobs"
|
||||
|
||||
# Run the most important first. Then we can decide whether to ignore
|
||||
# the style tests if the rest of the more meaningful tests pass.
|
||||
test_lint_pmd_checkstyle:
|
||||
<<: *test-template
|
||||
script:
|
||||
- export EXITVALUE=0
|
||||
- function set_error() { export EXITVALUE=1; printf "\x1b[31mERROR `history|tail -2|head -1|cut -b 6-500`\x1b[0m\n"; }
|
||||
- ./gradlew assemble
|
||||
# always report on lint errors to the build log
|
||||
- sed -i -e 's,textReport .*,textReport true,' app/build.gradle
|
||||
- ./gradlew testFullDebugUnitTest || set_error
|
||||
- ./gradlew lint || set_error
|
||||
- ./gradlew pmd || set_error
|
||||
- ./gradlew checkstyle || set_error
|
||||
- ./tools/check-format-strings.py || set_error
|
||||
- ./tools/check-fastlane-whitespace.py || set_error
|
||||
- ./tools/remove-unused-and-blank-translations.py || set_error
|
||||
- echo "These are unused or blank translations that should be removed:"
|
||||
- git --no-pager diff --ignore-all-space --name-only --exit-code app/src/*/res/values*/strings.xml || set_error
|
||||
- exit $EXITVALUE
|
||||
|
||||
errorprone:
|
||||
extends: .base
|
||||
stage: test
|
||||
script:
|
||||
- apt-get update
|
||||
- apt-get install -t stretch-backports openjdk-11-jdk-headless
|
||||
- update-java-alternatives --set java-1.11.0-openjdk-amd64
|
||||
- export JAVA_HOME=/usr/lib/jvm/java-1.11.0-openjdk-amd64
|
||||
- cat config/errorprone.gradle >> app/build.gradle
|
||||
- ./gradlew -Dorg.gradle.dependency.verification=lenient assembleDebug
|
||||
|
||||
# Run the tests in the emulator. Each step is broken out to run on
|
||||
# its own since the CI runner can have limited RAM, and the emulator
|
||||
# can take a while to start.
|
||||
#
|
||||
# once these prove stable, the task should be switched to
|
||||
# connectedCheck to test all the build flavors
|
||||
.connected-template: &connected-template
|
||||
extends: .base
|
||||
script:
|
||||
- ./gradlew assembleFullDebug
|
||||
- export AVD_SDK=`echo $CI_JOB_NAME | awk '{print $2}'`
|
||||
- export AVD_TAG=`echo $CI_JOB_NAME | awk '{print $3}'`
|
||||
- export AVD_ARCH=`echo $CI_JOB_NAME | awk '{print $4}'`
|
||||
- export AVD_PACKAGE="system-images;android-${AVD_SDK};${AVD_TAG};${AVD_ARCH}"
|
||||
- echo $AVD_PACKAGE
|
||||
|
||||
- alias sdkmanager
|
||||
- ls -l ~/.android
|
||||
|
||||
- adb start-server
|
||||
- start-emulator
|
||||
- wait-for-emulator
|
||||
- adb devices
|
||||
- adb shell input keyevent 82 &
|
||||
- ./gradlew installFullDebug
|
||||
- adb shell am start -n org.fdroid.fdroid.debug/org.fdroid.fdroid.views.main.MainActivity
|
||||
- if [ $AVD_SDK -lt 25 ] || ! emulator -accel-check; then
|
||||
export FLAG=-Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.LargeTest;
|
||||
fi
|
||||
- ./gradlew connectedFullDebugAndroidTest $FLAG
|
||||
|
||||
no-accel 22 default x86:
|
||||
<<: *test-template
|
||||
<<: *connected-template
|
||||
|
||||
.kvm-template: &kvm-template
|
||||
tags:
|
||||
- fdroid
|
||||
- kvm
|
||||
only:
|
||||
variables:
|
||||
- $RUN_KVM_JOBS
|
||||
<<: *test-template
|
||||
<<: *connected-template
|
||||
|
||||
kvm 29 microg x86_64:
|
||||
<<: *kvm-template
|
||||
|
||||
deploy_nightly:
|
||||
extends: .base
|
||||
stage: deploy
|
||||
only:
|
||||
- master
|
||||
script:
|
||||
- test -z "$DEBUG_KEYSTORE" && exit 0
|
||||
- sed -i
|
||||
's,<string name="app_name">.*</string>,<string name="app_name">F-Nightly</string>,'
|
||||
app/src/main/res/values*/strings.xml
|
||||
# add this nightly repo as a enabled repo
|
||||
- sed -i -e '/<\/string-array>/d' -e '/<\/resources>/d' app/src/main/res/values/default_repos.xml
|
||||
- echo "<item>${CI_PROJECT_PATH}-nightly</item>" >> app/src/main/res/values/default_repos.xml
|
||||
- echo "<item>${CI_PROJECT_URL}-nightly/raw/master/fdroid/repo</item>" >> app/src/main/res/values/default_repos.xml
|
||||
- cat config/nightly-repo/repo.xml >> app/src/main/res/values/default_repos.xml
|
||||
- export DB=`sed -n 's,.*DB_VERSION *= *\([0-9][0-9]*\).*,\1,p' app/src/main/java/org/fdroid/fdroid/data/DBHelper.java`
|
||||
- export versionCode=`printf '%d%05d' $DB $(date '+%s'| cut -b4-8)`
|
||||
- sed -i "s,^\(\s*versionCode\) *[0-9].*,\1 $versionCode," app/build.gradle
|
||||
# build the APKs!
|
||||
- ./gradlew assembleDebug
|
||||
- fdroid nightly -v
|
33
.project
33
.project
@ -1,33 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>fdroid</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
3
.weblate
Normal file
3
.weblate
Normal file
@ -0,0 +1,3 @@
|
||||
[weblate]
|
||||
url = https://hosted.weblate.org/api/
|
||||
translation = f-droid/f-droid
|
26
Android.mk
26
Android.mk
@ -1,8 +1,26 @@
|
||||
LOCAL_PATH := $(call my-dir)
|
||||
LOCAL_PATH:= $(call my-dir)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
|
||||
LOCAL_PACKAGE_NAME := FDroid
|
||||
LOCAL_SRC_FILES := $(call all-java-files-under,src)
|
||||
LOCAL_MODULE := F-Droid
|
||||
LOCAL_MODULE_TAGS := optional
|
||||
LOCAL_PACKAGE_NAME := F-Droid
|
||||
|
||||
include $(BUILD_PACKAGE)
|
||||
fdroid_root := $(LOCAL_PATH)
|
||||
fdroid_dir := app
|
||||
fdroid_out := $(PWD)/$(OUT_DIR)/target/common/obj/APPS/$(LOCAL_MODULE)_intermediates
|
||||
fdroid_build := $(fdroid_root)/$(fdroid_dir)/build
|
||||
fdroid_apk := build/outputs/apk/full/release/$(fdroid_dir)-full-release-unsigned.apk
|
||||
|
||||
$(fdroid_root)/$(fdroid_dir)/$(fdroid_apk):
|
||||
rm -Rf $(fdroid_build)
|
||||
mkdir -p $(fdroid_out)
|
||||
ln -sf $(fdroid_out) $(fdroid_build)
|
||||
cd $(fdroid_root)/$(fdroid_dir) && gradle assembleRelease
|
||||
|
||||
LOCAL_CERTIFICATE := platform
|
||||
LOCAL_SRC_FILES := $(fdroid_dir)/$(fdroid_apk)
|
||||
LOCAL_MODULE_CLASS := APPS
|
||||
LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX)
|
||||
|
||||
include $(BUILD_PREBUILT)
|
||||
|
@ -1,83 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.fdroid.fdroid"
|
||||
android:installLocation="auto"
|
||||
android:versionCode="39"
|
||||
android:versionName="0.39-test" >
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="3"
|
||||
android:targetSdkVersion="15" />
|
||||
|
||||
<supports-screens
|
||||
android:anyDensity="true"
|
||||
android:largeScreens="true"
|
||||
android:normalScreens="true"
|
||||
android:resizeable="true"
|
||||
android:smallScreens="true"
|
||||
android:xlargeScreens="true" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:name="FDroidApp"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name" >
|
||||
<activity
|
||||
android:name="FDroid"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.default_searchable"
|
||||
android:value=".SearchResults" />
|
||||
</activity>
|
||||
<activity android:name="ManageRepo" />
|
||||
<activity android:name="Settings" />
|
||||
<activity
|
||||
android:name="AppDetails"
|
||||
android:exported="true" >
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<data android:scheme="fdroid.app" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="Preferences" />
|
||||
<activity
|
||||
android:name="SearchResults"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.searchable"
|
||||
android:resource="@xml/searchable" />
|
||||
</activity>
|
||||
|
||||
<receiver android:name="StartupReceiver" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
|
||||
<category android:name="android.intent.category.HOME" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service android:name="UpdateService" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
1081
CHANGELOG.md
Normal file
1081
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
81
CONTRIBUTING.md
Normal file
81
CONTRIBUTING.md
Normal file
@ -0,0 +1,81 @@
|
||||
# Contributing
|
||||
|
||||
## Reporting issues
|
||||
|
||||
If you find an issue in the client, you can use our [Issue
|
||||
Tracker](https://gitlab.com/fdroid/fdroidclient/issues). Make sure that it
|
||||
hasn't yet been reported by searching first.
|
||||
|
||||
Remember to include the following information:
|
||||
|
||||
* Android version
|
||||
* Device model
|
||||
* F-Droid version
|
||||
* Steps to reproduce the issue
|
||||
* Logcat - see [instructions](https://f-droid.org/wiki/page/Getting_logcat_messages_after_crash)
|
||||
|
||||
## Translating
|
||||
|
||||
The strings are translated using [Weblate](https://weblate.org/en/). Follow
|
||||
[these instructions](https://hosted.weblate.org/engage/f-droid/) if you would
|
||||
like to contribute.
|
||||
|
||||
Please *do not* send merge requests or patches modifying the translations. Use
|
||||
Weblate instead - it applies a series of fixes and suggestions, plus it keeps
|
||||
track of modifications and fuzzy translations. Applying translations manually
|
||||
skips all of the fixes and checks, and overrides the fuzzy state of strings.
|
||||
|
||||
Note that you cannot change the English strings on Weblate. If you have any
|
||||
suggestions on how to improve them, open an issue or merge request like you
|
||||
would if you were making code changes. This way the changes can be reviewed
|
||||
before the source strings on Weblate are changed.
|
||||
|
||||
|
||||
## Code Style
|
||||
|
||||
We follow the default Android Studio code formatter (e.g. `Ctrl-Alt-L`). This
|
||||
should be more or less the same as [Android Java
|
||||
style](https://source.android.com/source/code-style.html). Some key points:
|
||||
|
||||
* Four space indentation
|
||||
* UTF-8 source files
|
||||
* Exactly one top-level class per file
|
||||
* No wildcard imports
|
||||
* One statement per line
|
||||
* K&R spacings with braces and parenthesis
|
||||
* Commented fallthroughs
|
||||
* Braces are always used after if, for and while
|
||||
|
||||
The current code base doesn't follow it entirely, but new code should follow
|
||||
it. We enforce some of these, but not all, via `./gradlew checkstyle`.
|
||||
|
||||
|
||||
## Running the test suite
|
||||
|
||||
Before pushing commits to a merge request, make sure this passes:
|
||||
|
||||
./gradlew checkstyle pmd lint
|
||||
|
||||
In order to run the F-Droid test suite, you will need to have either a real device
|
||||
connected via `adb`, or an emulator running. Then, execute the following from the
|
||||
command line:
|
||||
|
||||
./gradlew check
|
||||
|
||||
Many important tests require a device or emulator, but do not work in GitLab CI.
|
||||
That mean they need to be run locally, and that is usually easiest in Android
|
||||
Studio rather than the command line.
|
||||
|
||||
For a quick way to run a specific JUnit/Robolectric test:
|
||||
|
||||
./gradlew testFullDebugUnitTest --tests *LocaleSelectionTest*
|
||||
|
||||
For a quick way to run a specific emulator test:
|
||||
|
||||
./gradlew connectedFullDebugAndroidTest \
|
||||
-Pandroid.testInstrumentationRunnerArguments.class=org.fdroid.fdroid.MainActivityExpressoTest
|
||||
|
||||
|
||||
## Making releases
|
||||
|
||||
See https://gitlab.com/fdroid/wiki/-/wikis/Internal/Release-Process#fdroidclient
|
339
COPYING
339
COPYING
@ -1,339 +0,0 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is 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 2 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.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
11
FUNDING.yml
Normal file
11
FUNDING.yml
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
liberapay: F-Droid-Data
|
||||
open_collective: F-Droid
|
||||
github:
|
||||
- f-droid
|
||||
- eighthave
|
||||
custom:
|
||||
- https://f-droid.org/donate/
|
||||
- https://www.hellotux.com/f-droid
|
||||
- https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=E2FCXCT6837GL
|
||||
- https://blockchain.info/address/15u8aAPK4jJ5N8wpWJ5gutAyyeHtKX5i18
|
674
LICENSE
Normal file
674
LICENSE
Normal file
@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is 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.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
67
README.md
Normal file
67
README.md
Normal file
@ -0,0 +1,67 @@
|
||||
# F-Droid Client
|
||||
|
||||
[](https://gitlab.com/fdroid/fdroidclient/-/jobs)
|
||||
[](https://hosted.weblate.org/engage/f-droid/)
|
||||
|
||||
Client for [F-Droid](https://f-droid.org), the Free Software repository system
|
||||
for Android.
|
||||
|
||||
## Building with Gradle
|
||||
|
||||
./gradlew assembleRelease
|
||||
|
||||
## Direct download
|
||||
|
||||
You can [download the application](https://f-droid.org/FDroid.apk) directly
|
||||
from our site or [browse it in the repo](https://f-droid.org/app/org.fdroid.fdroid).
|
||||
|
||||
## Contributing
|
||||
|
||||
See our [Contributing doc](CONTRIBUTING.md) for information on how to report
|
||||
issues, translate the app into your language or help with development.
|
||||
|
||||
## IRC
|
||||
|
||||
We are on `#fdroid` and `#fdroid-dev` on Freenode. We hold weekly dev meetings
|
||||
on `#fdroid-dev` on Thursdays at 11:30h UTC, which usually last half an hour.
|
||||
|
||||
## FAQ
|
||||
|
||||
* Why does F-Droid require "Unknown Sources" to install apps by default?
|
||||
|
||||
Because a regular Android app cannot act as a package manager on its
|
||||
own. To do so, it would require system privileges (see below), similar
|
||||
to what Google Play does.
|
||||
|
||||
* Can I avoid enabling "Unknown Sources" by installing F-Droid as a
|
||||
privileged system app?
|
||||
|
||||
This used to be the case, but no longer is. Now the [Privileged
|
||||
Extension](https://gitlab.com/fdroid/privileged-extension) is the one that should be placed in
|
||||
the system. It can be bundled with a ROM or installed via a zip.
|
||||
## License
|
||||
|
||||
This program is Free Software: You can use, study share and improve it at your
|
||||
will. Specifically you can redistribute and/or modify it under the terms of the
|
||||
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) as
|
||||
published by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Some icons are made by [Picol](http://www.flaticon.com/authors/picol),
|
||||
[Icomoon](http://www.flaticon.com/authors/icomoon) or
|
||||
[Dave Gandy](http://www.flaticon.com/authors/dave-gandy) from
|
||||
[Flaticon](http://www.flaticon.com) or by Google and are licensed by
|
||||
[Creative Commons BY 3.0](https://creativecommons.org/licenses/by/3.0/).
|
||||
|
||||
Other icons are from the
|
||||
[Material Design Icon set](https://github.com/google/material-design-icons)
|
||||
released under an
|
||||
[Attribution 4.0 International license](https://creativecommons.org/licenses/by/4.0/).
|
||||
|
||||
|
||||
## Translation
|
||||
|
||||
Everything can be translated. See
|
||||
[Translation and Localization](https://f-droid.org/docs/Translation_and_Localization)
|
||||
for more info.
|
||||
[](https://hosted.weblate.org/engage/f-droid/?utm_source=widget)
|
@ -1,18 +0,0 @@
|
||||
# This file is used to override default values used by the Ant build system.
|
||||
#
|
||||
# This file must be checked into Version Control Systems, as it is
|
||||
# integral to the build system of your project.
|
||||
|
||||
# This file is only used by the Ant script.
|
||||
|
||||
# You can use this to override default values such as
|
||||
# 'source.dir' for the location of your java source folder and
|
||||
# 'out.dir' for the location of your output folder.
|
||||
|
||||
# You can also use it define how the release builds are signed by declaring
|
||||
# the following properties:
|
||||
# 'key.store' for the location of your keystore and
|
||||
# 'key.alias' for the name of the key to use.
|
||||
# The password will be asked during the build when you use the 'release' target.
|
||||
|
||||
application.package=org.fdroid.fdroid
|
230
app/build.gradle
Normal file
230
app/build.gradle
Normal file
@ -0,0 +1,230 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'checkstyle'
|
||||
apply plugin: 'pmd'
|
||||
|
||||
/* gets the version name from the latest Git tag */
|
||||
def getVersionName = { ->
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine 'git', 'describe', '--tags', '--always'
|
||||
standardOutput = stdout
|
||||
}
|
||||
return stdout.toString().trim()
|
||||
}
|
||||
|
||||
def isCi = "true" == System.getenv("CI")
|
||||
def preDexEnabled = "true" == System.getProperty("pre-dex", "true")
|
||||
|
||||
def fullApplicationId = "org.fdroid.fdroid"
|
||||
def basicApplicationId = "org.fdroid.basic"
|
||||
// yes, this actually needs both quotes https://stackoverflow.com/a/41391841
|
||||
def privilegedExtensionApplicationId = '"org.fdroid.fdroid.privileged"'
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
|
||||
defaultConfig {
|
||||
versionCode 1013001
|
||||
versionName getVersionName()
|
||||
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
minSdkVersion 24
|
||||
//noinspection ExpiredTargetSdkVersion
|
||||
targetSdkVersion 28
|
||||
/*
|
||||
The Android Testing Support Library collects analytics to continuously improve the testing
|
||||
experience. More specifically, it uploads a hash of the package name of the application
|
||||
under test for each invocation. If you do not wish to upload this data, you can opt-out by
|
||||
passing the following argument to the test runner: disableAnalytics "true".
|
||||
*/
|
||||
testInstrumentationRunnerArguments disableAnalytics: 'true'
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
// use proguard on debug too since we have unknowingly broken
|
||||
// release builds before.
|
||||
all {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
buildConfigField "String", "PRIVILEGED_EXTENSION_PACKAGE_NAME", privilegedExtensionApplicationId
|
||||
buildConfigField "String", "ACRA_REPORT_EMAIL", '"reports@f-droid.org"' // String needs both quotes
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
testProguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro', 'src/androidTest/proguard-rules.pro'
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
resValue "string", "applicationId", fullApplicationId + applicationIdSuffix
|
||||
versionNameSuffix "-debug"
|
||||
println 'buildTypes.debug defaultConfig.versionCode ' + defaultConfig.versionCode
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions "base"
|
||||
productFlavors {
|
||||
full {
|
||||
dimension "base"
|
||||
applicationId fullApplicationId
|
||||
resValue "string", "applicationId", fullApplicationId
|
||||
}
|
||||
basic {
|
||||
dimension "base"
|
||||
applicationId basicApplicationId
|
||||
resValue "string", "applicationId", basicApplicationId
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
compileOptions.encoding = "UTF-8"
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
aaptOptions {
|
||||
cruncherEnabled = false
|
||||
}
|
||||
|
||||
dexOptions {
|
||||
// Improve build server performance by allowing disabling of pre-dexing
|
||||
// see http://tools.android.com/tech-docs/new-build-system/tips#TOC-Improving-Build-Server-performance
|
||||
// Skip pre-dexing when running on CI or when disabled via -Dpre-dex=false.
|
||||
preDexLibraries = preDexEnabled && !isCi
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
// prevent tests from dying on android.util.Log calls
|
||||
returnDefaultValues = true
|
||||
all {
|
||||
// All the usual Gradle options.
|
||||
testLogging {
|
||||
events "skipped", "failed", "standardOut", "standardError"
|
||||
showStandardStreams = true
|
||||
}
|
||||
systemProperty 'robolectric.dependency.repo.url', 'https://repo1.maven.org/maven2'
|
||||
|
||||
// hack to avoid memory leak crashes
|
||||
forkEvery = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
test {
|
||||
java.srcDirs += "$projectDir/src/testShared/java"
|
||||
}
|
||||
|
||||
androidTest {
|
||||
java.srcDirs += "$projectDir/src/testShared/java"
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
checkReleaseBuilds false
|
||||
abortOnError true
|
||||
|
||||
htmlReport true
|
||||
xmlReport false
|
||||
textReport false
|
||||
|
||||
lintConfig file("lint.xml")
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'META-INF/LICENSE'
|
||||
exclude 'META-INF/LICENSE.txt'
|
||||
exclude 'META-INF/NOTICE'
|
||||
exclude 'META-INF/NOTICE.txt'
|
||||
exclude 'META-INF/INDEX.LIST'
|
||||
exclude '.readme'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.vectordrawable:vectordrawable:1.1.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.palette:palette:1.0.0'
|
||||
implementation 'androidx.work:work-runtime:2.4.0'
|
||||
|
||||
implementation 'com.google.android.material:material:1.3.0'
|
||||
|
||||
implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
|
||||
implementation 'com.google.zxing:core:3.3.3'
|
||||
implementation 'info.guardianproject.netcipher:netcipher:2.2.0-alpha'
|
||||
implementation 'info.guardianproject.panic:panic:1.0'
|
||||
implementation 'commons-io:commons-io:2.6'
|
||||
implementation 'commons-net:commons-net:3.6'
|
||||
implementation 'ch.acra:acra:4.9.1'
|
||||
implementation 'com.hannesdorfmann:adapterdelegates3:3.0.1'
|
||||
|
||||
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
|
||||
implementation 'io.reactivex.rxjava3:rxjava:3.0.9'
|
||||
|
||||
implementation 'com.fasterxml.jackson.core:jackson-core:2.11.1'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.11.1'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.1'
|
||||
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.65'
|
||||
fullImplementation 'org.bouncycastle:bcpkix-jdk15on:1.65'
|
||||
fullImplementation 'cc.mvdan.accesspoint:library:0.2.0'
|
||||
fullImplementation 'org.jmdns:jmdns:3.5.5'
|
||||
fullImplementation 'org.nanohttpd:nanohttpd:2.3.1'
|
||||
|
||||
testImplementation 'androidx.test:core:1.3.0'
|
||||
testImplementation 'junit:junit:4.13.1'
|
||||
testImplementation 'org.robolectric:robolectric:4.3'
|
||||
testImplementation 'org.mockito:mockito-core:3.3.3'
|
||||
testImplementation 'org.hamcrest:hamcrest:2.2'
|
||||
testImplementation 'org.bouncycastle:bcprov-jdk15on:1.65'
|
||||
|
||||
androidTestImplementation 'androidx.arch.core:core-testing:2.1.0'
|
||||
androidTestImplementation 'androidx.test:core:1.3.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||
androidTestImplementation 'androidx.test:monitor:1.3.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
|
||||
androidTestImplementation 'androidx.work:work-testing:2.4.0'
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
toolVersion = '7.2'
|
||||
}
|
||||
|
||||
task checkstyle(type: Checkstyle) {
|
||||
configFile file("${project.rootDir}/config/checkstyle/checkstyle.xml")
|
||||
source 'src/main/java', 'src/test/java', 'src/androidTest/java'
|
||||
include '**/*.java'
|
||||
|
||||
classpath = files()
|
||||
}
|
||||
|
||||
pmd {
|
||||
toolVersion = '6.20.0'
|
||||
consoleOutput = true
|
||||
}
|
||||
|
||||
task pmdMain(type: Pmd) {
|
||||
dependsOn 'assembleDebug'
|
||||
ruleSetFiles = files("${project.rootDir}/config/pmd/rules.xml", "${project.rootDir}/config/pmd/rules-main.xml")
|
||||
ruleSets = [] // otherwise defaults clash with the list in rules.xml
|
||||
source 'src/main/java'
|
||||
include '**/*.java'
|
||||
}
|
||||
|
||||
task pmdTest(type: Pmd) {
|
||||
dependsOn 'assembleDebug'
|
||||
ruleSetFiles = files("${project.rootDir}/config/pmd/rules.xml", "${project.rootDir}/config/pmd/rules-test.xml")
|
||||
ruleSets = [] // otherwise defaults clash with the list in rules.xml
|
||||
source 'src/test/java', 'src/androidTest/java'
|
||||
include '**/*.java'
|
||||
}
|
||||
|
||||
task pmd(dependsOn: [pmdMain, pmdTest]) {}
|
67
app/lint.xml
Normal file
67
app/lint.xml
Normal file
@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lint>
|
||||
<!-- Our translations are crowd-sourced -->
|
||||
<issue id="MissingTranslation" severity="ignore"/>
|
||||
|
||||
<!-- to make CI fail on errors until this is fixed
|
||||
https://github.com/rtyley/spongycastle/issues/7 -->
|
||||
<issue id="InvalidPackage" severity="warning"/>
|
||||
|
||||
<issue id="ImpliedQuantity" severity="error"/>
|
||||
<issue id="DefaultLocale" severity="error"/>
|
||||
<issue id="SimpleDateFormat" severity="error"/>
|
||||
<issue id="NewApi" severity="error"/>
|
||||
<issue id="InlinedApi" severity="error"/>
|
||||
|
||||
<!-- These are important to us, so promote from warning to error -->
|
||||
<issue id="UnusedResources" severity="error">
|
||||
<ignore path="src/main/res/drawable/category_**.png" />
|
||||
<ignore path="src/main/res/values/dimens.xml"/>
|
||||
<ignore path="src/main/res/values/styles.xml"/>
|
||||
<ignore path="src/full/res/values/styles.xml"/>
|
||||
<!-- keep a single strings.xml for all build flavors -->
|
||||
<ignore path="src/main/res/values**/strings.xml"/>
|
||||
</issue>
|
||||
<issue id="AppCompatMethod" severity="error"/>
|
||||
<issue id="NestedScrolling" severity="error"/>
|
||||
<issue id="Typos" severity="error"/>
|
||||
<issue id="StringFormatCount" severity="error"/>
|
||||
<issue id="UnsafeProtectedBroadcastReceiver" severity="error"/>
|
||||
<issue id="GetInstance" severity="error"/>
|
||||
<issue id="PackageManagerGetSignatures" severity="error"/>
|
||||
<issue id="HardwareIds" severity="error"/>
|
||||
<issue id="TrustAllX509TrustManager" severity="error">
|
||||
<!-- these come from included libraries -->
|
||||
<ignore path="org/apache/commons/net/ftp/FTPSTrustManager.class"/>
|
||||
<ignore path="org/bouncycastle/est/jcajce/JcaJceUtils$1.class"/>
|
||||
<ignore path="org/bouncycastle/est/jcajce/JcaJceUtils$2.class"/>
|
||||
<ignore path="org/apache/commons/net/util/TrustManagerUtils$TrustManager.class"/>
|
||||
</issue>
|
||||
|
||||
<issue id="PluralsCandidate" severity="error"/>
|
||||
<issue id="HardcodedText" severity="error"/>
|
||||
<issue id="RtlCompat" severity="error"/>
|
||||
<issue id="RtlEnabled" severity="error"/>
|
||||
|
||||
<!-- both the correct and deprecated locales need to be present for
|
||||
them to be recognized on all devices -->
|
||||
<issue id="LocaleFolder" severity="error">
|
||||
<ignore path="src/main/res/values-he"/>
|
||||
<ignore path="src/main/res/values-id"/>
|
||||
</issue>
|
||||
|
||||
<issue id="SetWorldReadable" severity="error">
|
||||
<ignore path="src/main/java/org/fdroid/fdroid/installer/ApkFileProvider.java"/>
|
||||
</issue>
|
||||
|
||||
<issue id="ProtectedPermissions" severity="error">
|
||||
<ignore path="src/debug/AndroidManifest.xml"/>
|
||||
<ignore path="src/full/AndroidManifest.xml"/>
|
||||
</issue>
|
||||
|
||||
<!-- these should be fixed, but it'll be a chunk of work -->
|
||||
<issue id="SetTextI18n" severity="error">
|
||||
<ignore path="src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java"/>
|
||||
<ignore path="src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java"/>
|
||||
</issue>
|
||||
</lint>
|
47
app/proguard-rules.pro
vendored
Normal file
47
app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
-dontobfuscate
|
||||
-dontoptimize
|
||||
-keepattributes SourceFile,LineNumberTable,Exceptions
|
||||
-keep class org.fdroid.fdroid.** {*;}
|
||||
-dontskipnonpubliclibraryclassmembers
|
||||
-dontwarn android.test.**
|
||||
|
||||
-dontwarn javax.naming.**
|
||||
-dontwarn org.slf4j.**
|
||||
-dontnote org.apache.http.**
|
||||
-dontnote android.net.http.**
|
||||
-dontnote **ILicensingService
|
||||
|
||||
# Needed for espresso https://stackoverflow.com/a/21706087
|
||||
-dontwarn org.xmlpull.v1.**
|
||||
|
||||
# StrongHttpsClient and its support classes are totally unused, so the
|
||||
# ch.boye.httpclientandroidlib.** classes are also unneeded
|
||||
-dontwarn info.guardianproject.netcipher.client.**
|
||||
|
||||
# These libraries are known to break if minification is enabled on them. They
|
||||
# use reflection to instantiate classes, for example. If the keep flags are
|
||||
# removed, proguard will strip classes which are required, which may result in
|
||||
# crashes.
|
||||
-keep class kellinwood.security.zipsigner.** {*;}
|
||||
-keep class org.bouncycastle.** {*;}
|
||||
|
||||
# This keeps class members used for SystemInstaller IPC.
|
||||
# Reference: https://gitlab.com/fdroid/fdroidclient/issues/79
|
||||
-keepclassmembers class * implements android.os.IInterface {
|
||||
public *;
|
||||
}
|
||||
|
||||
-keepattributes *Annotation*,EnclosingMethod,Signature
|
||||
-keepnames class com.fasterxml.jackson.** { *; }
|
||||
-dontwarn com.fasterxml.jackson.databind.ext.**
|
||||
-keep class org.codehaus.** { *; }
|
||||
-keepclassmembers public final enum org.codehaus.jackson.annotate.JsonAutoDetect$Visibility {
|
||||
public static final org.codehaus.jackson.annotate.JsonAutoDetect$Visibility *; }
|
||||
-keep public class your.class.** {
|
||||
*;
|
||||
}
|
||||
|
||||
# This is necessary so that RemoteWorkManager can be initialized (also marked with @Keep)
|
||||
-keep class androidx.work.multiprocess.RemoteWorkManagerClient {
|
||||
public <init>(...);
|
||||
}
|
24
app/src/androidTest/AndroidManifest.xml
Normal file
24
app/src/androidTest/AndroidManifest.xml
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- package name must be unique so suffix with "tests" so package loader doesn't ignore us -->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.fdroid.fdroid.tests"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0">
|
||||
|
||||
<uses-sdk tools:overrideLibrary="android_libs.ub_uiautomator" />
|
||||
|
||||
<!-- We add an application tag here just so that we can indicate that
|
||||
this package needs to link against the android.test library,
|
||||
which is needed when building test cases. -->
|
||||
<application>
|
||||
<uses-library
|
||||
android:name="android.test.runner"
|
||||
android:required="false" />
|
||||
</application>
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
</manifest>
|
125
app/src/androidTest/assets/extendedPerms.xml
Normal file
125
app/src/androidTest/assets/extendedPerms.xml
Normal file
@ -0,0 +1,125 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<fdroid>
|
||||
<repo name="F-Droid" icon="fdroid-icon.png" maxage="14"
|
||||
pubkey="3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef"
|
||||
timestamp="1467169032" url="http://f-droid.org/repo" version="16">
|
||||
<description>
|
||||
This is just a test of the extended permissions attributes.
|
||||
</description>
|
||||
</repo>
|
||||
|
||||
<application id="urzip.at.or.at.urzip">
|
||||
<id>at.bitfire.davdroid</id>
|
||||
<added>2013-10-13</added>
|
||||
<lastupdated>2016-06-26</lastupdated>
|
||||
<name>DAVdroid</name>
|
||||
<summary>Contacts and Calendar sync</summary>
|
||||
<icon>at.bitfire.davdroid.107.png</icon>
|
||||
<desc>apk generated from urzip to test extended permissions</desc>
|
||||
<license>GPLv3</license>
|
||||
<categories>Internet</categories>
|
||||
<category>Internet</category>
|
||||
<web>https://davdroid.bitfire.at/</web>
|
||||
<source>https://davdroid.bitfire.at/source/</source>
|
||||
<tracker>https://davdroid.bitfire.at/forums/</tracker>
|
||||
<changelog>https://gitlab.com/bitfireAT/davdroid/tags</changelog>
|
||||
<donate>https://davdroid.bitfire.at/donate/</donate>
|
||||
<bitcoin>1KSCy7RHztKuhW9fLLaUYqdwdC2iwbejZU</bitcoin>
|
||||
<flattr>2100160</flattr>
|
||||
<marketversion>1.1.1.2</marketversion>
|
||||
<marketvercode>107</marketvercode>
|
||||
<package>
|
||||
<version>1.3.2-FAKE</version>
|
||||
<versioncode>117</versioncode>
|
||||
<apkname>org.fdroid.extendedpermissionstest.apk</apkname>
|
||||
<srcname>at.bitfire.davdroid_116_src.tar.gz</srcname>
|
||||
<hash type="sha256">f1aa02257e99c167d2ea9b0e9525c3ce7c181fe2e7f4dd00b65dd81ed2e27a62
|
||||
</hash>
|
||||
<sig>03542175324d067b4c36582242f8aecc</sig>
|
||||
<size>3298864</size>
|
||||
<sdkver>14</sdkver>
|
||||
<targetSdkVersion>23</targetSdkVersion>
|
||||
<added>2016-09-22</added>
|
||||
<permissions>
|
||||
READ_EXTERNAL_STORAGE,WRITE_SYNC_SETTINGS,ACCESS_NETWORK_STATE,WRITE_EXTERNAL_STORAGE,WRITE_CONTACTS,ACCESS_WIFI_STATE,REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,WRITE_CALENDAR,READ_CONTACTS,READ_SYNC_SETTINGS,MANAGE_ACCOUNTS,INTERNET,AUTHENTICATE_ACCOUNTS,GET_ACCOUNTS,READ_CALENDAR,READ_SYNC_STATS
|
||||
</permissions>
|
||||
<uses-permission name="android.permission.GET_ACCOUNTS" maxSdkVersion="22" />
|
||||
<uses-permission name="android.permission.READ_EXTERNAL_STORAGE" maxSdkVersion="18" />
|
||||
<uses-permission name="android.permission.WRITE_EXTERNAL_STORAGE" maxSdkVersion="18" />
|
||||
<uses-permission name="android.permission.AUTHENTICATE_ACCOUNTS" maxSdkVersion="22" />
|
||||
<uses-permission name="android.permission.MANAGE_ACCOUNTS" maxSdkVersion="22" />
|
||||
<uses-permission-sdk-23 name="android.permission.CAMERA" />
|
||||
<uses-permission-sdk-23 name="android.permission.CALL_PHONE" maxSdkVersion="23" />
|
||||
</package>
|
||||
<package>
|
||||
<version>1.3.1-ose</version>
|
||||
<versioncode>116</versioncode>
|
||||
<apkname>at.bitfire.davdroid_116.apk</apkname>
|
||||
<srcname>at.bitfire.davdroid_116_src.tar.gz</srcname>
|
||||
<hash type="sha256">f1aa02257e99c167d2ea9b0e9525c3ce7c181fe2e7f4dd00b65dd81ed2e27a62
|
||||
</hash>
|
||||
<sig>03542175324d067b4c36582242f8aecc</sig>
|
||||
<size>3298864</size>
|
||||
<sdkver>14</sdkver>
|
||||
<targetSdkVersion>24</targetSdkVersion>
|
||||
<added>2016-09-21</added>
|
||||
<permissions>
|
||||
READ_EXTERNAL_STORAGE,WRITE_SYNC_SETTINGS,ACCESS_NETWORK_STATE,WRITE_EXTERNAL_STORAGE,org.dmfs.permission.READ_TASKS,WRITE_CONTACTS,ACCESS_WIFI_STATE,REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,WRITE_CALENDAR,READ_CONTACTS,READ_SYNC_SETTINGS,MANAGE_ACCOUNTS,INTERNET,AUTHENTICATE_ACCOUNTS,GET_ACCOUNTS,READ_CALENDAR,org.dmfs.permission.WRITE_TASKS
|
||||
</permissions>
|
||||
<uses-permission name="android.permission.GET_ACCOUNTS" maxSdkVersion="22" />
|
||||
<uses-permission name="android.permission.READ_EXTERNAL_STORAGE" maxSdkVersion="18" />
|
||||
<uses-permission name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission name="android.permission.AUTHENTICATE_ACCOUNTS" maxSdkVersion="22" />
|
||||
<uses-permission name="android.permission.MANAGE_ACCOUNTS" maxSdkVersion="22" />
|
||||
</package>
|
||||
<package>
|
||||
<version>1.1.1.2</version>
|
||||
<versioncode>107</versioncode>
|
||||
<apkname>at.bitfire.davdroid_107.apk</apkname>
|
||||
<srcname>at.bitfire.davdroid_107_src.tar.gz</srcname>
|
||||
<hash type="sha256">9a616f2e97bf8cf012baf896f95667dea4e3ce3252b31c5715073638a9fcc3d4
|
||||
</hash>
|
||||
<sig>03542175324d067b4c36582242f8aecc</sig>
|
||||
<size>3134363</size>
|
||||
<sdkver>14</sdkver>
|
||||
<targetSdkVersion>23</targetSdkVersion>
|
||||
<added>2016-06-26</added>
|
||||
<permissions>
|
||||
org.dmfs.permission.READ_TASKS,WRITE_CONTACTS,GET_ACCOUNTS,AUTHENTICATE_ACCOUNTS,WRITE_EXTERNAL_STORAGE,READ_CALENDAR,ACCESS_WIFI_STATE,org.dmfs.permission.WRITE_TASKS,ACCESS_NETWORK_STATE,WRITE_CALENDAR,READ_CONTACTS,READ_SYNC_SETTINGS,INTERNET,MANAGE_ACCOUNTS,WRITE_SYNC_SETTINGS
|
||||
</permissions>
|
||||
</package>
|
||||
<package>
|
||||
<version>1.1.1.1</version>
|
||||
<versioncode>105</versioncode>
|
||||
<apkname>at.bitfire.davdroid_105.apk</apkname>
|
||||
<srcname>at.bitfire.davdroid_105_src.tar.gz</srcname>
|
||||
<hash type="sha256">4a0408c61536a1cc1028cea4273adbde2e57dfa2b12d93c3b52f4c3d095e2849
|
||||
</hash>
|
||||
<sig>03542175324d067b4c36582242f8aecc</sig>
|
||||
<size>3131567</size>
|
||||
<sdkver>14</sdkver>
|
||||
<targetSdkVersion>23</targetSdkVersion>
|
||||
<added>2016-06-24</added>
|
||||
<permissions>
|
||||
org.dmfs.permission.READ_TASKS,READ_EXTERNAL_STORAGE,WRITE_CONTACTS,GET_ACCOUNTS,AUTHENTICATE_ACCOUNTS,WRITE_EXTERNAL_STORAGE,READ_CALENDAR,ACCESS_WIFI_STATE,org.dmfs.permission.WRITE_TASKS,ACCESS_NETWORK_STATE,WRITE_CALENDAR,READ_CONTACTS,READ_SYNC_SETTINGS,INTERNET,MANAGE_ACCOUNTS,WRITE_SYNC_SETTINGS
|
||||
</permissions>
|
||||
</package>
|
||||
<package>
|
||||
<version>1.1.1</version>
|
||||
<versioncode>104</versioncode>
|
||||
<apkname>at.bitfire.davdroid_104.apk</apkname>
|
||||
<srcname>at.bitfire.davdroid_104_src.tar.gz</srcname>
|
||||
<hash type="sha256">09ba34996177efe8b1498a93fe6521ab84efab3bccb0f42449116e80b59e5b56
|
||||
</hash>
|
||||
<sig>03542175324d067b4c36582242f8aecc</sig>
|
||||
<size>3131367</size>
|
||||
<sdkver>14</sdkver>
|
||||
<targetSdkVersion>23</targetSdkVersion>
|
||||
<added>2016-06-22</added>
|
||||
<permissions>
|
||||
org.dmfs.permission.READ_TASKS,READ_EXTERNAL_STORAGE,WRITE_CONTACTS,GET_ACCOUNTS,AUTHENTICATE_ACCOUNTS,WRITE_EXTERNAL_STORAGE,READ_CALENDAR,ACCESS_WIFI_STATE,org.dmfs.permission.WRITE_TASKS,ACCESS_NETWORK_STATE,WRITE_CALENDAR,READ_CONTACTS,READ_SYNC_SETTINGS,INTERNET,MANAGE_ACCOUNTS,WRITE_SYNC_SETTINGS
|
||||
</permissions>
|
||||
</package>
|
||||
</application>
|
||||
|
||||
</fdroid>
|
Binary file not shown.
BIN
app/src/androidTest/assets/org.fdroid.permissions.minmax.apk
Normal file
BIN
app/src/androidTest/assets/org.fdroid.permissions.minmax.apk
Normal file
Binary file not shown.
BIN
app/src/androidTest/assets/org.fdroid.permissions.minmax.zip
Normal file
BIN
app/src/androidTest/assets/org.fdroid.permissions.minmax.zip
Normal file
Binary file not shown.
BIN
app/src/androidTest/assets/org.fdroid.permissions.sdk14.apk
Normal file
BIN
app/src/androidTest/assets/org.fdroid.permissions.sdk14.apk
Normal file
Binary file not shown.
BIN
app/src/androidTest/assets/org.fdroid.permissions.sdk14.zip
Normal file
BIN
app/src/androidTest/assets/org.fdroid.permissions.sdk14.zip
Normal file
Binary file not shown.
0
app/src/androidTest/assets/simpleIndex.jar
Normal file
0
app/src/androidTest/assets/simpleIndex.jar
Normal file
43
app/src/androidTest/java/org/fdroid/fdroid/AssetUtils.java
Normal file
43
app/src/androidTest/java/org/fdroid/fdroid/AssetUtils.java
Normal file
@ -0,0 +1,43 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class AssetUtils {
|
||||
|
||||
private static final String TAG = "Utils";
|
||||
|
||||
/**
|
||||
* This requires {@link Context} from {@link android.app.Instrumentation#getContext()}
|
||||
*/
|
||||
@Nullable
|
||||
public static File copyAssetToDir(Context context, String assetName, File directory) {
|
||||
File tempFile = null;
|
||||
InputStream input = null;
|
||||
OutputStream output = null;
|
||||
try {
|
||||
tempFile = File.createTempFile(assetName, ".testasset", directory);
|
||||
Log.i(TAG, "Copying asset file " + assetName + " to directory " + directory);
|
||||
input = context.getAssets().open(assetName);
|
||||
output = new FileOutputStream(tempFile);
|
||||
Utils.copy(input, output);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Check the context is from Instrumentation.getContext()");
|
||||
fail(e.getMessage());
|
||||
} finally {
|
||||
Utils.closeQuietly(output);
|
||||
Utils.closeQuietly(input);
|
||||
}
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
}
|
222
app/src/androidTest/java/org/fdroid/fdroid/LocalizationTest.java
Normal file
222
app/src/androidTest/java/org/fdroid/fdroid/LocalizationTest.java
Normal file
@ -0,0 +1,222 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import android.text.TextUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.IllegalFormatException;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Runs through all of the translated strings and tests them with the same format
|
||||
* values that the source strings expect. This is to ensure that the formats in
|
||||
* the translations are correct in number and in type (e.g. {@code s} or {@code s}.
|
||||
* It reads the source formats and then builds {@code formats} to represent the
|
||||
* position and type of the formats. Then it runs through all of the translations
|
||||
* with formats of the correct number and type.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class LocalizationTest {
|
||||
public static final String TAG = "LocalizationTest";
|
||||
|
||||
private final Pattern androidFormat = Pattern.compile("(%[a-z0-9]\\$?[a-z]?)");
|
||||
private final Locale[] locales = Locale.getAvailableLocales();
|
||||
private final HashSet<String> localeNames = new HashSet<>(locales.length);
|
||||
|
||||
private AssetManager assets;
|
||||
private Configuration config;
|
||||
private Resources resources;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
for (Locale locale : Languages.LOCALES_TO_TEST) {
|
||||
localeNames.add(locale.toString());
|
||||
}
|
||||
for (Locale locale : locales) {
|
||||
localeNames.add(locale.toString());
|
||||
}
|
||||
|
||||
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
|
||||
Context context = instrumentation.getTargetContext();
|
||||
assets = context.getAssets();
|
||||
config = context.getResources().getConfiguration();
|
||||
config.locale = Locale.ENGLISH;
|
||||
// Resources() requires DisplayMetrics, but they are only needed for drawables
|
||||
resources = new Resources(assets, new DisplayMetrics(), config);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadAllPlural() throws IllegalAccessException {
|
||||
Field[] fields = R.plurals.class.getDeclaredFields();
|
||||
|
||||
HashMap<String, String> haveFormats = new HashMap<>();
|
||||
for (Field field : fields) {
|
||||
//Log.i(TAG, field.getName());
|
||||
int resId = field.getInt(int.class);
|
||||
CharSequence string = resources.getQuantityText(resId, 4);
|
||||
//Log.i(TAG, field.getName() + ": '" + string + "'");
|
||||
Matcher matcher = androidFormat.matcher(string);
|
||||
int matches = 0;
|
||||
char[] formats = new char[5];
|
||||
while (matcher.find()) {
|
||||
String match = matcher.group(0);
|
||||
char formatType = match.charAt(match.length() - 1);
|
||||
switch (match.length()) {
|
||||
case 2:
|
||||
formats[matches] = formatType;
|
||||
matches++;
|
||||
break;
|
||||
case 4:
|
||||
formats[Integer.parseInt(match.substring(1, 2)) - 1] = formatType;
|
||||
break;
|
||||
case 5:
|
||||
formats[Integer.parseInt(match.substring(1, 3)) - 1] = formatType;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException(field.getName() + " has bad format: " + match);
|
||||
}
|
||||
}
|
||||
haveFormats.put(field.getName(), new String(formats).trim());
|
||||
}
|
||||
|
||||
for (Locale locale : locales) {
|
||||
config.locale = locale;
|
||||
// Resources() requires DisplayMetrics, but they are only needed for drawables
|
||||
resources = new Resources(assets, new DisplayMetrics(), config);
|
||||
for (Field field : fields) {
|
||||
String formats = null;
|
||||
try {
|
||||
int resId = field.getInt(int.class);
|
||||
for (int quantity = 0; quantity < 567; quantity++) {
|
||||
resources.getQuantityString(resId, quantity);
|
||||
}
|
||||
|
||||
formats = haveFormats.get(field.getName());
|
||||
switch (formats) {
|
||||
case "d":
|
||||
resources.getQuantityString(resId, 1, 1);
|
||||
break;
|
||||
case "s":
|
||||
resources.getQuantityString(resId, 1, "ONE");
|
||||
break;
|
||||
case "ds":
|
||||
resources.getQuantityString(resId, 2, 1, "TWO");
|
||||
break;
|
||||
default:
|
||||
if (!TextUtils.isEmpty(formats)) {
|
||||
throw new IllegalStateException("Pattern not included in tests: " + formats);
|
||||
}
|
||||
}
|
||||
} catch (IllegalFormatException | Resources.NotFoundException e) {
|
||||
Log.i(TAG, locale + " " + field.getName());
|
||||
throw new IllegalArgumentException("Bad '" + formats + "' format in " + locale + " "
|
||||
+ field.getName() + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadAllStrings() throws IllegalAccessException {
|
||||
Field[] fields = R.string.class.getDeclaredFields();
|
||||
|
||||
HashMap<String, String> haveFormats = new HashMap<>();
|
||||
for (Field field : fields) {
|
||||
String string = resources.getString(field.getInt(int.class));
|
||||
Matcher matcher = androidFormat.matcher(string);
|
||||
int matches = 0;
|
||||
char[] formats = new char[5];
|
||||
while (matcher.find()) {
|
||||
String match = matcher.group(0);
|
||||
char formatType = match.charAt(match.length() - 1);
|
||||
switch (match.length()) {
|
||||
case 2:
|
||||
formats[matches] = formatType;
|
||||
matches++;
|
||||
break;
|
||||
case 4:
|
||||
formats[Integer.parseInt(match.substring(1, 2)) - 1] = formatType;
|
||||
break;
|
||||
case 5:
|
||||
formats[Integer.parseInt(match.substring(1, 3)) - 1] = formatType;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException(field.getName() + " has bad format: " + match);
|
||||
}
|
||||
}
|
||||
haveFormats.put(field.getName(), new String(formats).trim());
|
||||
}
|
||||
|
||||
for (Locale locale : locales) {
|
||||
config.locale = locale;
|
||||
// Resources() requires DisplayMetrics, but they are only needed for drawables
|
||||
resources = new Resources(assets, new DisplayMetrics(), config);
|
||||
for (Field field : fields) {
|
||||
int resId = field.getInt(int.class);
|
||||
resources.getString(resId);
|
||||
|
||||
String formats = haveFormats.get(field.getName());
|
||||
try {
|
||||
switch (formats) {
|
||||
case "d":
|
||||
resources.getString(resId, 1);
|
||||
break;
|
||||
case "dd":
|
||||
resources.getString(resId, 1, 2);
|
||||
break;
|
||||
case "ds":
|
||||
resources.getString(resId, 1, "TWO");
|
||||
break;
|
||||
case "dds":
|
||||
resources.getString(resId, 1, 2, "THREE");
|
||||
break;
|
||||
case "sds":
|
||||
resources.getString(resId, "ONE", 2, "THREE");
|
||||
break;
|
||||
case "s":
|
||||
resources.getString(resId, "ONE");
|
||||
break;
|
||||
case "ss":
|
||||
resources.getString(resId, "ONE", "TWO");
|
||||
break;
|
||||
case "sss":
|
||||
resources.getString(resId, "ONE", "TWO", "THREE");
|
||||
break;
|
||||
case "ssss":
|
||||
resources.getString(resId, "ONE", "TWO", "THREE", "FOUR");
|
||||
break;
|
||||
case "ssd":
|
||||
resources.getString(resId, "ONE", "TWO", 3);
|
||||
break;
|
||||
case "sssd":
|
||||
resources.getString(resId, "ONE", "TWO", "THREE", 4);
|
||||
break;
|
||||
default:
|
||||
if (!TextUtils.isEmpty(formats)) {
|
||||
throw new IllegalStateException("Pattern not included in tests: " + formats);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.i(TAG, locale + " " + field.getName());
|
||||
throw new IllegalArgumentException("Bad format in '" + locale + "' '" + field.getName() + "': "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,303 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.espresso.IdlingPolicies;
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.uiautomator.UiDevice;
|
||||
import androidx.test.uiautomator.UiObject;
|
||||
import androidx.test.uiautomator.UiObjectNotFoundException;
|
||||
import androidx.test.uiautomator.UiSelector;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import org.fdroid.fdroid.views.StatusBanner;
|
||||
import org.fdroid.fdroid.views.main.MainActivity;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.After;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.swipeDown;
|
||||
import static androidx.test.espresso.action.ViewActions.swipeLeft;
|
||||
import static androidx.test.espresso.action.ViewActions.swipeRight;
|
||||
import static androidx.test.espresso.action.ViewActions.swipeUp;
|
||||
import static androidx.test.espresso.action.ViewActions.typeText;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assume.assumeTrue;
|
||||
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class MainActivityEspressoTest {
|
||||
public static final String TAG = "MainActivityEspressoTest";
|
||||
|
||||
/**
|
||||
* Emulators older than {@code android-25} seem to fail at running Espresso tests.
|
||||
* <p>
|
||||
* ARM emulators are too slow to run these tests in a useful way. The sad
|
||||
* thing is that it would probably work if Android didn't put up the ANR
|
||||
* "Process system isn't responding" on boot each time. There seems to be no
|
||||
* way to increase the ANR timeout.
|
||||
*/
|
||||
private static boolean canRunEspresso() {
|
||||
if (Build.VERSION.SDK_INT < 25
|
||||
|| (Build.SUPPORTED_ABIS[0].startsWith("arm") && isEmulator())) {
|
||||
Log.e(TAG, "SKIPPING TEST: ARM emulators are too slow to run these tests in a useful way");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
public static void classSetUp() {
|
||||
IdlingPolicies.setIdlingResourceTimeout(10, TimeUnit.MINUTES);
|
||||
IdlingPolicies.setMasterPolicyTimeout(10, TimeUnit.MINUTES);
|
||||
if (!canRunEspresso()) {
|
||||
return;
|
||||
}
|
||||
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
|
||||
try {
|
||||
UiDevice.getInstance(instrumentation)
|
||||
.executeShellCommand("pm grant "
|
||||
+ instrumentation.getTargetContext().getPackageName()
|
||||
+ " android.permission.SET_ANIMATION_SCALE");
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
SystemAnimations.disableAll(ApplicationProvider.getApplicationContext());
|
||||
|
||||
// dismiss the ANR or any other system dialogs that might be there
|
||||
UiObject button = new UiObject(new UiSelector().text("Wait").enabled(true));
|
||||
try {
|
||||
button.click();
|
||||
} catch (UiObjectNotFoundException e) {
|
||||
Log.d(TAG, e.getLocalizedMessage());
|
||||
}
|
||||
new UiWatchers().registerAnrAndCrashWatchers();
|
||||
|
||||
Context context = instrumentation.getTargetContext();
|
||||
ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo();
|
||||
ActivityManager activityManager = ContextCompat.getSystemService(context, ActivityManager.class);
|
||||
activityManager.getMemoryInfo(mi);
|
||||
long percentAvail = mi.availMem / mi.totalMem;
|
||||
Log.i(TAG, "RAM: " + mi.availMem + " / " + mi.totalMem + " = " + percentAvail);
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void classTearDown() {
|
||||
SystemAnimations.enableAll(ApplicationProvider.getApplicationContext());
|
||||
}
|
||||
|
||||
public static boolean isEmulator() {
|
||||
return Build.FINGERPRINT.startsWith("generic")
|
||||
|| Build.FINGERPRINT.startsWith("unknown")
|
||||
|| Build.MODEL.contains("google_sdk")
|
||||
|| Build.MODEL.contains("Emulator")
|
||||
|| Build.MODEL.contains("Android SDK built for x86")
|
||||
|| Build.MANUFACTURER.contains("Genymotion")
|
||||
|| (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
|
||||
|| "google_sdk".equals(Build.PRODUCT);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
assumeTrue(canRunEspresso());
|
||||
}
|
||||
|
||||
/**
|
||||
* Placate {@link android.os.StrictMode}
|
||||
*
|
||||
* @see <a href="https://github.com/aosp-mirror/platform_frameworks_base/commit/6f3a38f3afd79ed6dddcef5c83cb442d6749e2ff"> Run finalizers before counting for StrictMode</a>
|
||||
*/
|
||||
@After
|
||||
public void tearDown() {
|
||||
System.gc();
|
||||
System.runFinalization();
|
||||
System.gc();
|
||||
}
|
||||
|
||||
@Rule
|
||||
public ActivityTestRule<MainActivity> activityTestRule =
|
||||
new ActivityTestRule<>(MainActivity.class);
|
||||
|
||||
@Rule
|
||||
public GrantPermissionRule accessCoarseLocationPermissionRule = GrantPermissionRule.grant(
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION);
|
||||
|
||||
@Rule
|
||||
public GrantPermissionRule writeExternalStoragePermissionRule = GrantPermissionRule.grant(
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
|
||||
@Test
|
||||
public void bottomNavFlavorCheck() {
|
||||
onView(withText(R.string.main_menu__updates)).check(matches(isDisplayed()));
|
||||
onView(withText(R.string.menu_settings)).check(matches(isDisplayed()));
|
||||
onView(withText("THIS SHOULD NOT SHOW UP ANYWHERE!!!")).check(doesNotExist());
|
||||
|
||||
assertTrue(BuildConfig.FLAVOR.startsWith("full") || BuildConfig.FLAVOR.startsWith("basic"));
|
||||
|
||||
if (BuildConfig.FLAVOR.startsWith("basic")) {
|
||||
onView(withText(R.string.main_menu__latest_apps)).check(matches(isDisplayed()));
|
||||
onView(withText(R.string.main_menu__categories)).check(doesNotExist());
|
||||
onView(withText(R.string.main_menu__swap_nearby)).check(doesNotExist());
|
||||
}
|
||||
|
||||
if (BuildConfig.FLAVOR.startsWith("full")) {
|
||||
onView(withText(R.string.main_menu__latest_apps)).check(matches(isDisplayed()));
|
||||
onView(withText(R.string.main_menu__categories)).check(matches(isDisplayed()));
|
||||
onView(withText(R.string.main_menu__swap_nearby)).check(matches(isDisplayed()));
|
||||
}
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void showSettings() {
|
||||
ViewInteraction settingsBottonNavButton = onView(
|
||||
allOf(withText(R.string.menu_settings), isDisplayed()));
|
||||
settingsBottonNavButton.perform(click());
|
||||
onView(withText(R.string.preference_manage_installed_apps)).check(matches(isDisplayed()));
|
||||
if (BuildConfig.FLAVOR.startsWith("basic") && BuildConfig.APPLICATION_ID.endsWith(".debug")) {
|
||||
// TODO fix me by sorting out the flavor applicationId for debug builds in app/build.gradle
|
||||
Log.i(TAG, "Skipping the remainder of showSettings test because it just crashes on basic .debug builds");
|
||||
return;
|
||||
}
|
||||
ViewInteraction manageInstalledAppsButton = onView(
|
||||
allOf(withText(R.string.preference_manage_installed_apps), isDisplayed()));
|
||||
manageInstalledAppsButton.perform(click());
|
||||
onView(withText(R.string.installed_apps__activity_title)).check(matches(isDisplayed()));
|
||||
onView(withContentDescription(R.string.abc_action_bar_up_description)).perform(click());
|
||||
|
||||
onView(withText(R.string.menu_manage)).perform(click());
|
||||
onView(withContentDescription(R.string.abc_action_bar_up_description)).perform(click());
|
||||
|
||||
manageInstalledAppsButton.perform(click());
|
||||
onView(withText(R.string.installed_apps__activity_title)).check(matches(isDisplayed()));
|
||||
onView(withContentDescription(R.string.abc_action_bar_up_description)).perform(click());
|
||||
|
||||
onView(withText(R.string.menu_manage)).perform(click());
|
||||
onView(withContentDescription(R.string.abc_action_bar_up_description)).perform(click());
|
||||
|
||||
onView(withText(R.string.about_title)).perform(click());
|
||||
onView(withId(R.id.version)).check(matches(isDisplayed()));
|
||||
onView(withId(R.id.ok_button)).perform(click());
|
||||
|
||||
onView(withId(android.R.id.list_container)).perform(swipeUp()).perform(swipeUp()).perform(swipeUp());
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void showUpdates() {
|
||||
ViewInteraction updatesBottonNavButton = onView(allOf(withText(R.string.main_menu__updates), isDisplayed()));
|
||||
updatesBottonNavButton.perform(click());
|
||||
onView(withText(R.string.main_menu__updates)).check(matches(isDisplayed()));
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void startSwap() {
|
||||
if (!BuildConfig.FLAVOR.startsWith("full")) {
|
||||
return;
|
||||
}
|
||||
ViewInteraction nearbyBottonNavButton = onView(
|
||||
allOf(withText(R.string.main_menu__swap_nearby), isDisplayed()));
|
||||
nearbyBottonNavButton.perform(click());
|
||||
ViewInteraction findPeopleButton = onView(
|
||||
allOf(withId(R.id.find_people_button), withText(R.string.nearby_splash__find_people_button),
|
||||
isDisplayed()));
|
||||
findPeopleButton.perform(click());
|
||||
onView(withText(R.string.swap_send_fdroid)).check(matches(isDisplayed()));
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void showCategories() {
|
||||
if (!BuildConfig.FLAVOR.startsWith("full")) {
|
||||
return;
|
||||
}
|
||||
onView(allOf(withText(R.string.menu_settings), isDisplayed())).perform(click());
|
||||
onView(allOf(withText(R.string.main_menu__categories), isDisplayed())).perform(click());
|
||||
onView(allOf(withId(R.id.swipe_to_refresh), isDisplayed()))
|
||||
.perform(swipeDown())
|
||||
.perform(swipeUp())
|
||||
.perform(swipeUp())
|
||||
.perform(swipeUp())
|
||||
.perform(swipeUp())
|
||||
.perform(swipeUp())
|
||||
.perform(swipeUp())
|
||||
.perform(swipeDown())
|
||||
.perform(swipeDown())
|
||||
.perform(swipeRight())
|
||||
.perform(swipeLeft())
|
||||
.perform(swipeLeft())
|
||||
.perform(swipeLeft())
|
||||
.perform(swipeLeft())
|
||||
.perform(click());
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void showLatest() {
|
||||
if (!BuildConfig.FLAVOR.startsWith("full")) {
|
||||
return;
|
||||
}
|
||||
onView(Matchers.<View>instanceOf(StatusBanner.class)).check(matches(not(isDisplayed())));
|
||||
onView(allOf(withText(R.string.menu_settings), isDisplayed())).perform(click());
|
||||
onView(allOf(withText(R.string.main_menu__latest_apps), isDisplayed())).perform(click());
|
||||
onView(allOf(withId(R.id.swipe_to_refresh), isDisplayed()))
|
||||
.perform(swipeDown())
|
||||
.perform(swipeUp())
|
||||
.perform(swipeUp())
|
||||
.perform(swipeUp())
|
||||
.perform(swipeDown())
|
||||
.perform(swipeUp())
|
||||
.perform(swipeDown())
|
||||
.perform(swipeDown())
|
||||
.perform(swipeDown())
|
||||
.perform(swipeDown())
|
||||
.perform(click());
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void showSearch() {
|
||||
onView(allOf(withText(R.string.menu_settings), isDisplayed())).perform(click());
|
||||
onView(withId(R.id.fab_search)).check(doesNotExist());
|
||||
if (!BuildConfig.FLAVOR.startsWith("full")) {
|
||||
return;
|
||||
}
|
||||
onView(allOf(withText(R.string.main_menu__latest_apps), isDisplayed())).perform(click());
|
||||
onView(allOf(withId(R.id.fab_search), isDisplayed())).perform(click());
|
||||
onView(withId(R.id.sort)).check(matches(isDisplayed()));
|
||||
onView(allOf(withId(R.id.search), isDisplayed()))
|
||||
.perform(click())
|
||||
.perform(typeText("test"));
|
||||
onView(allOf(withId(R.id.sort), isDisplayed())).perform(click());
|
||||
}
|
||||
}
|
372
app/src/androidTest/java/org/fdroid/fdroid/Netstat.java
Normal file
372
app/src/androidTest/java/org/fdroid/fdroid/Netstat.java
Normal file
@ -0,0 +1,372 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.FileReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Replacer for the netstat utility, by reading the /proc filesystem it can find out the
|
||||
* open connections of the system
|
||||
* From http://www.ussg.iu.edu/hypermail/linux/kernel/0409.1/2166.html :
|
||||
* It will first list all listening TCP sockets, and next list all established
|
||||
* TCP connections. A typical entry of /proc/net/tcp would look like this (split
|
||||
* up into 3 parts because of the length of the line):
|
||||
* <p>
|
||||
* 46: 010310AC:9C4C 030310AC:1770 01
|
||||
* | | | | | |--> connection state
|
||||
* | | | | |------> remote TCP port number
|
||||
* | | | |-------------> remote IPv4 address
|
||||
* | | |--------------------> local TCP port number
|
||||
* | |---------------------------> local IPv4 address
|
||||
* |----------------------------------> number of entry
|
||||
* <p>
|
||||
* 00000150:00000000 01:00000019 00000000
|
||||
* | | | | |--> number of unrecovered RTO timeouts
|
||||
* | | | |----------> number of jiffies until timer expires
|
||||
* | | |----------------> timer_active (see below)
|
||||
* | |----------------------> receive-queue
|
||||
* |-------------------------------> transmit-queue
|
||||
* <p>
|
||||
* 1000 0 54165785 4 cd1e6040 25 4 27 3 -1
|
||||
* | | | | | | | | | |--> slow start size threshold,
|
||||
* | | | | | | | | | or -1 if the treshold
|
||||
* | | | | | | | | | is >= 0xFFFF
|
||||
* | | | | | | | | |----> sending congestion window
|
||||
* | | | | | | | |-------> (ack.quick<<1)|ack.pingpong
|
||||
* | | | | | | |---------> Predicted tick of soft clock
|
||||
* | | | | | | (delayed ACK control data)
|
||||
* | | | | | |------------> retransmit timeout
|
||||
* | | | | |------------------> location of socket in memory
|
||||
* | | | |-----------------------> socket reference count
|
||||
* | | |-----------------------------> inode
|
||||
* | |----------------------------------> unanswered 0-window probes
|
||||
* |---------------------------------------------> uid
|
||||
*
|
||||
* @author Ciprian Dobre
|
||||
*/
|
||||
public class Netstat {
|
||||
|
||||
/**
|
||||
* Possible values for states in /proc/net/tcp
|
||||
*/
|
||||
private static final String[] STATES = {
|
||||
"ESTBLSH", "SYNSENT", "SYNRECV", "FWAIT1", "FWAIT2", "TMEWAIT",
|
||||
"CLOSED", "CLSWAIT", "LASTACK", "LISTEN", "CLOSING", "UNKNOWN",
|
||||
};
|
||||
/**
|
||||
* Pattern used when parsing through /proc/net/tcp
|
||||
*/
|
||||
private static final Pattern NET_PATTERN = Pattern.compile(
|
||||
"\\d+:\\s+([\\dA-F]+):([\\dA-F]+)\\s+([\\dA-F]+):([\\dA-F]+)\\s+([\\dA-F]+)\\s+" +
|
||||
"[\\dA-F]+:[\\dA-F]+\\s+[\\dA-F]+:[\\dA-F]+\\s+[\\dA-F]+\\s+([\\d]+)\\s+[\\d]+\\s+([\\d]+)");
|
||||
|
||||
/**
|
||||
* Utility method that converts an address from a hex representation as founded in /proc to String representation
|
||||
*/
|
||||
private static String getAddress(final String hexa) {
|
||||
try {
|
||||
// first let's convert the address to Integer
|
||||
final long v = Long.parseLong(hexa, 16);
|
||||
// in /proc the order is little endian and java uses big endian order we also need to invert the order
|
||||
final long adr = (v >>> 24) | (v << 24) |
|
||||
((v << 8) & 0x00FF0000) | ((v >> 8) & 0x0000FF00);
|
||||
// and now it's time to output the result
|
||||
return ((adr >> 24) & 0xff) + "." + ((adr >> 16) & 0xff) + "." + ((adr >> 8) & 0xff) + "." + (adr & 0xff);
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
return "0.0.0.0"; // NOPMD
|
||||
}
|
||||
}
|
||||
|
||||
private static int getInt16(final String hexa) {
|
||||
try {
|
||||
return Integer.parseInt(hexa, 16);
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
private static String getPName(final int pid) {
|
||||
final Pattern pattern = Pattern.compile("Name:\\s*(\\S+)");
|
||||
try {
|
||||
BufferedReader in = new BufferedReader(new FileReader("/proc/" + pid + "/status"));
|
||||
String line;
|
||||
while ((line = in.readLine()) != null) {
|
||||
final Matcher matcher = pattern.matcher(line);
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
}
|
||||
}
|
||||
in.close();
|
||||
} catch (Throwable t) {
|
||||
// ignored
|
||||
}
|
||||
return "UNKNOWN";
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Method used to question for the connections currently openned
|
||||
*
|
||||
* @return The list of connections (as Connection objects)
|
||||
*/
|
||||
public static List<Connection> getConnections() {
|
||||
|
||||
final ArrayList<Connection> net = new ArrayList<>();
|
||||
|
||||
// read from /proc/net/tcp the list of currently openned socket connections
|
||||
try {
|
||||
BufferedReader in = new BufferedReader(new FileReader("/proc/net/tcp"));
|
||||
String line;
|
||||
while ((line = in.readLine()) != null) { // NOPMD
|
||||
Matcher matcher = NET_PATTERN.matcher(line);
|
||||
if (matcher.find()) {
|
||||
final Connection c = new Connection();
|
||||
c.setProtocol(Connection.TCP_CONNECTION);
|
||||
net.add(c);
|
||||
final String localPortHexa = matcher.group(2);
|
||||
final String remoteAddressHexa = matcher.group(3);
|
||||
final String remotePortHexa = matcher.group(4);
|
||||
final String statusHexa = matcher.group(5);
|
||||
//final String uid = matcher.group(6);
|
||||
//final String inode = matcher.group(7);
|
||||
c.setLocalPort(getInt16(localPortHexa));
|
||||
c.setRemoteAddress(getAddress(remoteAddressHexa));
|
||||
c.setRemotePort(getInt16(remotePortHexa));
|
||||
try {
|
||||
c.setStatus(STATES[Integer.parseInt(statusHexa, 16) - 1]);
|
||||
} catch (Exception ex) {
|
||||
c.setStatus(STATES[11]); // unknown
|
||||
}
|
||||
c.setPID(-1); // unknown
|
||||
c.setPName("UNKNOWN");
|
||||
}
|
||||
}
|
||||
in.close();
|
||||
} catch (Throwable t) { // NOPMD
|
||||
// ignored
|
||||
}
|
||||
|
||||
// read from /proc/net/udp the list of currently openned socket connections
|
||||
try {
|
||||
BufferedReader in = new BufferedReader(new FileReader("/proc/net/udp"));
|
||||
String line;
|
||||
while ((line = in.readLine()) != null) { // NOPMD
|
||||
Matcher matcher = NET_PATTERN.matcher(line);
|
||||
if (matcher.find()) {
|
||||
final Connection c = new Connection();
|
||||
c.setProtocol(Connection.UDP_CONNECTION);
|
||||
net.add(c);
|
||||
final String localPortHexa = matcher.group(2);
|
||||
final String remoteAddressHexa = matcher.group(3);
|
||||
final String remotePortHexa = matcher.group(4);
|
||||
final String statusHexa = matcher.group(5);
|
||||
//final String uid = matcher.group(6);
|
||||
//final String inode = matcher.group(7);
|
||||
c.setLocalPort(getInt16(localPortHexa));
|
||||
c.setRemoteAddress(getAddress(remoteAddressHexa));
|
||||
c.setRemotePort(getInt16(remotePortHexa));
|
||||
try {
|
||||
c.setStatus(STATES[Integer.parseInt(statusHexa, 16) - 1]);
|
||||
} catch (Exception ex) {
|
||||
c.setStatus(STATES[11]); // unknown
|
||||
}
|
||||
c.setPID(-1); // unknown
|
||||
c.setPName("UNKNOWN");
|
||||
}
|
||||
}
|
||||
in.close();
|
||||
} catch (Throwable t) { // NOPMD
|
||||
// ignored
|
||||
}
|
||||
|
||||
// read from /proc/net/raw the list of currently openned socket connections
|
||||
try {
|
||||
BufferedReader in = new BufferedReader(new FileReader("/proc/net/raw"));
|
||||
String line;
|
||||
while ((line = in.readLine()) != null) { // NOPMD
|
||||
Matcher matcher = NET_PATTERN.matcher(line);
|
||||
if (matcher.find()) {
|
||||
final Connection c = new Connection();
|
||||
c.setProtocol(Connection.RAW_CONNECTION);
|
||||
net.add(c);
|
||||
//final String localAddressHexa = matcher.group(1);
|
||||
final String localPortHexa = matcher.group(2);
|
||||
final String remoteAddressHexa = matcher.group(3);
|
||||
final String remotePortHexa = matcher.group(4);
|
||||
final String statusHexa = matcher.group(5);
|
||||
//final String uid = matcher.group(6);
|
||||
//final String inode = matcher.group(7);
|
||||
c.setLocalPort(getInt16(localPortHexa));
|
||||
c.setRemoteAddress(getAddress(remoteAddressHexa));
|
||||
c.setRemotePort(getInt16(remotePortHexa));
|
||||
try {
|
||||
c.setStatus(STATES[Integer.parseInt(statusHexa, 16) - 1]);
|
||||
} catch (Exception ex) {
|
||||
c.setStatus(STATES[11]); // unknown
|
||||
}
|
||||
c.setPID(-1); // unknown
|
||||
c.setPName("UNKNOWN");
|
||||
}
|
||||
}
|
||||
in.close();
|
||||
} catch (Throwable t) { // NOPMD
|
||||
// ignored
|
||||
}
|
||||
return net;
|
||||
}
|
||||
|
||||
/**
|
||||
* Informations about a given connection
|
||||
*
|
||||
* @author Ciprian Dobre
|
||||
*/
|
||||
public static class Connection {
|
||||
|
||||
/**
|
||||
* Types of connection protocol
|
||||
***/
|
||||
public static final byte TCP_CONNECTION = 0;
|
||||
public static final byte UDP_CONNECTION = 1;
|
||||
public static final byte RAW_CONNECTION = 2;
|
||||
/**
|
||||
* <code>serialVersionUID</code>
|
||||
*/
|
||||
private static final long serialVersionUID = 1988671591829311032L;
|
||||
/**
|
||||
* The protocol of the connection (can be tcp, udp or raw)
|
||||
*/
|
||||
protected byte protocol;
|
||||
|
||||
/**
|
||||
* The owner of the connection (username)
|
||||
*/
|
||||
protected String powner;
|
||||
|
||||
/**
|
||||
* The pid of the owner process
|
||||
*/
|
||||
protected int pid;
|
||||
|
||||
/**
|
||||
* The name of the program owning the connection
|
||||
*/
|
||||
protected String pname;
|
||||
|
||||
/**
|
||||
* Local port
|
||||
*/
|
||||
protected int localPort;
|
||||
|
||||
/**
|
||||
* Remote address of the connection
|
||||
*/
|
||||
protected String remoteAddress;
|
||||
|
||||
/**
|
||||
* Remote port
|
||||
*/
|
||||
protected int remotePort;
|
||||
|
||||
/**
|
||||
* Status of the connection
|
||||
*/
|
||||
protected String status;
|
||||
|
||||
public final byte getProtocol() {
|
||||
return protocol;
|
||||
}
|
||||
|
||||
public final void setProtocol(final byte protocol) {
|
||||
this.protocol = protocol;
|
||||
}
|
||||
|
||||
public final String getProtocolAsString() {
|
||||
switch (protocol) {
|
||||
case TCP_CONNECTION:
|
||||
return "TCP";
|
||||
case UDP_CONNECTION:
|
||||
return "UDP";
|
||||
case RAW_CONNECTION:
|
||||
return "RAW";
|
||||
}
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
public final String getPOwner() {
|
||||
return powner;
|
||||
}
|
||||
|
||||
public final void setPOwner(final String owner) {
|
||||
this.powner = owner;
|
||||
}
|
||||
|
||||
public final int getPID() {
|
||||
return pid;
|
||||
}
|
||||
|
||||
public final void setPID(final int pid) {
|
||||
this.pid = pid;
|
||||
}
|
||||
|
||||
public final String getPName() {
|
||||
return pname;
|
||||
}
|
||||
|
||||
public final void setPName(final String pname) {
|
||||
this.pname = pname;
|
||||
}
|
||||
|
||||
public final int getLocalPort() {
|
||||
return localPort;
|
||||
}
|
||||
|
||||
public final void setLocalPort(final int localPort) {
|
||||
this.localPort = localPort;
|
||||
}
|
||||
|
||||
public final String getRemoteAddress() {
|
||||
return remoteAddress;
|
||||
}
|
||||
|
||||
public final void setRemoteAddress(final String remoteAddress) {
|
||||
this.remoteAddress = remoteAddress;
|
||||
}
|
||||
|
||||
public final int getRemotePort() {
|
||||
return remotePort;
|
||||
}
|
||||
|
||||
public final void setRemotePort(final int remotePort) {
|
||||
this.remotePort = remotePort;
|
||||
}
|
||||
|
||||
public final String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public final void setStatus(final String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
StringBuffer buf = new StringBuffer();
|
||||
buf.append("[Prot=").append(getProtocolAsString());
|
||||
buf.append(",POwner=").append(powner);
|
||||
buf.append(",PID=").append(pid);
|
||||
buf.append(",PName=").append(pname);
|
||||
buf.append(",LPort=").append(localPort);
|
||||
buf.append(",RAddress=").append(remoteAddress);
|
||||
buf.append(",RPort=").append(remotePort);
|
||||
buf.append(",Status=").append(status);
|
||||
buf.append("]");
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* @see <a href="https://artemzin.com/blog/easiest-way-to-give-set_animation_scale-permission-for-your-ui-tests-on-android/>EASIEST WAY TO GIVE SET_ANIMATION_SCALE PERMISSION FOR YOUR UI TESTS ON ANDROID</a>
|
||||
* @see <a href="https://gist.github.com/xrigau/11284124>Disable animations for Espresso tests</a>
|
||||
*/
|
||||
class SystemAnimations {
|
||||
public static final String TAG = "SystemAnimations";
|
||||
|
||||
private static final float DISABLED = 0.0f;
|
||||
private static final float DEFAULT = 1.0f;
|
||||
|
||||
static void disableAll(Context context) {
|
||||
int permStatus = context.checkCallingOrSelfPermission(Manifest.permission.SET_ANIMATION_SCALE);
|
||||
if (permStatus == PackageManager.PERMISSION_GRANTED) {
|
||||
Log.i(TAG, "Manifest.permission.SET_ANIMATION_SCALE PERMISSION_GRANTED");
|
||||
setSystemAnimationsScale(DISABLED);
|
||||
} else {
|
||||
Log.i(TAG, "Disabling Manifest.permission.SET_ANIMATION_SCALE failed: " + permStatus);
|
||||
}
|
||||
}
|
||||
|
||||
static void enableAll(Context context) {
|
||||
int permStatus = context.checkCallingOrSelfPermission(Manifest.permission.SET_ANIMATION_SCALE);
|
||||
if (permStatus == PackageManager.PERMISSION_GRANTED) {
|
||||
Log.i(TAG, "Manifest.permission.SET_ANIMATION_SCALE PERMISSION_GRANTED");
|
||||
setSystemAnimationsScale(DEFAULT);
|
||||
} else {
|
||||
Log.i(TAG, "Enabling Manifest.permission.SET_ANIMATION_SCALE failed: " + permStatus);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setSystemAnimationsScale(float animationScale) {
|
||||
try {
|
||||
Class<?> windowManagerStubClazz = Class.forName("android.view.IWindowManager$Stub");
|
||||
Method asInterface = windowManagerStubClazz.getDeclaredMethod("asInterface", IBinder.class);
|
||||
Class<?> serviceManagerClazz = Class.forName("android.os.ServiceManager");
|
||||
Method getService = serviceManagerClazz.getDeclaredMethod("getService", String.class);
|
||||
Class<?> windowManagerClazz = Class.forName("android.view.IWindowManager");
|
||||
Method setAnimationScales = windowManagerClazz.getDeclaredMethod("setAnimationScales", float[].class);
|
||||
Method getAnimationScales = windowManagerClazz.getDeclaredMethod("getAnimationScales");
|
||||
|
||||
IBinder windowManagerBinder = (IBinder) getService.invoke(null, "window");
|
||||
Object windowManagerObj = asInterface.invoke(null, windowManagerBinder);
|
||||
float[] currentScales = (float[]) getAnimationScales.invoke(windowManagerObj);
|
||||
for (int i = 0; i < currentScales.length; i++) {
|
||||
currentScales[i] = animationScale;
|
||||
}
|
||||
setAnimationScales.invoke(windowManagerObj, new Object[]{currentScales});
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Could not change animation scale to " + animationScale + " :'(");
|
||||
}
|
||||
}
|
||||
}
|
156
app/src/androidTest/java/org/fdroid/fdroid/UiWatchers.java
Normal file
156
app/src/androidTest/java/org/fdroid/fdroid/UiWatchers.java
Normal file
@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright (C) 2013 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import androidx.test.uiautomator.UiDevice;
|
||||
import androidx.test.uiautomator.UiObject;
|
||||
import androidx.test.uiautomator.UiObjectNotFoundException;
|
||||
import androidx.test.uiautomator.UiSelector;
|
||||
import androidx.test.uiautomator.UiWatcher;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@SuppressWarnings("MemberName")
|
||||
public class UiWatchers {
|
||||
private static final String LOG_TAG = UiWatchers.class.getSimpleName();
|
||||
private final List<String> mErrors = new ArrayList<String>();
|
||||
|
||||
/**
|
||||
* We can use the UiDevice registerWatcher to register a small script to be executed when the
|
||||
* framework is waiting for a control to appear. Waiting may be the cause of an unexpected
|
||||
* dialog on the screen and it is the time when the framework runs the registered watchers.
|
||||
* This is a sample watcher looking for ANR and crashes. it closes it and moves on. You should
|
||||
* create your own watchers and handle error logging properly for your type of tests.
|
||||
*/
|
||||
public void registerAnrAndCrashWatchers() {
|
||||
UiDevice.getInstance().registerWatcher("ANR", new UiWatcher() {
|
||||
@Override
|
||||
public boolean checkForCondition() {
|
||||
UiObject window = new UiObject(new UiSelector().className(
|
||||
"com.android.server.am.AppNotRespondingDialog"));
|
||||
String errorText = null;
|
||||
if (window.exists()) {
|
||||
try {
|
||||
errorText = window.getText();
|
||||
} catch (UiObjectNotFoundException e) {
|
||||
Log.e(LOG_TAG, "dialog gone?", e);
|
||||
}
|
||||
onAnrDetected(errorText);
|
||||
postHandler("Wait");
|
||||
return true; // triggered
|
||||
}
|
||||
return false; // no trigger
|
||||
}
|
||||
});
|
||||
// class names may have changed
|
||||
UiDevice.getInstance().registerWatcher("ANR2", new UiWatcher() {
|
||||
@Override
|
||||
public boolean checkForCondition() {
|
||||
UiObject window = new UiObject(new UiSelector().packageName("android")
|
||||
.textContains("isn't responding."));
|
||||
if (window.exists()) {
|
||||
String errorText = null;
|
||||
try {
|
||||
errorText = window.getText();
|
||||
} catch (UiObjectNotFoundException e) {
|
||||
Log.e(LOG_TAG, "dialog gone?", e);
|
||||
}
|
||||
onAnrDetected(errorText);
|
||||
postHandler("Wait");
|
||||
return true; // triggered
|
||||
}
|
||||
return false; // no trigger
|
||||
}
|
||||
});
|
||||
UiDevice.getInstance().registerWatcher("CRASH", new UiWatcher() {
|
||||
@Override
|
||||
public boolean checkForCondition() {
|
||||
UiObject window = new UiObject(new UiSelector().className(
|
||||
"com.android.server.am.AppErrorDialog"));
|
||||
if (window.exists()) {
|
||||
String errorText = null;
|
||||
try {
|
||||
errorText = window.getText();
|
||||
} catch (UiObjectNotFoundException e) {
|
||||
Log.e(LOG_TAG, "dialog gone?", e);
|
||||
}
|
||||
onCrashDetected(errorText);
|
||||
postHandler("OK");
|
||||
return true; // triggered
|
||||
}
|
||||
return false; // no trigger
|
||||
}
|
||||
});
|
||||
UiDevice.getInstance().registerWatcher("CRASH2", new UiWatcher() {
|
||||
@Override
|
||||
public boolean checkForCondition() {
|
||||
UiObject window = new UiObject(new UiSelector().packageName("android")
|
||||
.textContains("has stopped"));
|
||||
if (window.exists()) {
|
||||
String errorText = null;
|
||||
try {
|
||||
errorText = window.getText();
|
||||
} catch (UiObjectNotFoundException e) {
|
||||
Log.e(LOG_TAG, "dialog gone?", e);
|
||||
}
|
||||
onCrashDetected(errorText);
|
||||
postHandler("OK");
|
||||
return true; // triggered
|
||||
}
|
||||
return false; // no trigger
|
||||
}
|
||||
});
|
||||
Log.i(LOG_TAG, "Registered GUI Exception watchers");
|
||||
}
|
||||
|
||||
public void onAnrDetected(String errorText) {
|
||||
mErrors.add(errorText);
|
||||
}
|
||||
|
||||
public void onCrashDetected(String errorText) {
|
||||
mErrors.add(errorText);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
mErrors.clear();
|
||||
}
|
||||
|
||||
public List<String> getErrors() {
|
||||
return Collections.unmodifiableList(mErrors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Current implementation ignores the exception and continues.
|
||||
*/
|
||||
public void postHandler(String buttonText) {
|
||||
// TODO: Add custom error logging here
|
||||
String formatedOutput = String.format("UI Exception Message: %-20s\n", UiDevice
|
||||
.getInstance().getCurrentPackageName());
|
||||
Log.e(LOG_TAG, formatedOutput);
|
||||
UiObject buttonOK = new UiObject(new UiSelector().text(buttonText).enabled(true));
|
||||
// sometimes it takes a while for the OK button to become enabled
|
||||
buttonOK.waitForExists(5000);
|
||||
try {
|
||||
buttonOK.click();
|
||||
} catch (UiObjectNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
package org.fdroid.fdroid.compat;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import android.util.Log;
|
||||
|
||||
import org.fdroid.fdroid.AssetUtils;
|
||||
import org.fdroid.fdroid.data.SanitizedFile;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assume.assumeTrue;
|
||||
|
||||
|
||||
/**
|
||||
* This test needs to run on the emulator, even though it technically could
|
||||
* run as a plain JUnit test, because it is testing the specifics of
|
||||
* Android's symlink handling.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class FileCompatTest {
|
||||
|
||||
private static final String TAG = "FileCompatTest";
|
||||
|
||||
private SanitizedFile sourceFile;
|
||||
private SanitizedFile destFile;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
|
||||
File dir = getWriteableDir(instrumentation);
|
||||
sourceFile = SanitizedFile.knownSanitized(
|
||||
AssetUtils.copyAssetToDir(instrumentation.getContext(), "simpleIndex.jar", dir));
|
||||
destFile = new SanitizedFile(dir, "dest-" + UUID.randomUUID() + ".testproduct");
|
||||
assertFalse(destFile.exists());
|
||||
assertTrue(sourceFile.getAbsolutePath() + " should exist.", sourceFile.exists());
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
if (!sourceFile.delete()) {
|
||||
Log.w(TAG, "Can't delete " + sourceFile.getAbsolutePath() + ".");
|
||||
}
|
||||
|
||||
if (!destFile.delete()) {
|
||||
Log.w(TAG, "Can't delete " + destFile.getAbsolutePath() + ".");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSymlinkRuntime() {
|
||||
FileCompat.symlinkRuntime(sourceFile, destFile);
|
||||
assertTrue(destFile.getAbsolutePath() + " should exist after symlinking", destFile.exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSymlinkLibcore() {
|
||||
assumeTrue(Build.VERSION.SDK_INT >= 19);
|
||||
FileCompat.symlinkLibcore(sourceFile, destFile);
|
||||
assertTrue(destFile.getAbsolutePath() + " should exist after symlinking", destFile.exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSymlinkOs() {
|
||||
assumeTrue(Build.VERSION.SDK_INT >= 21);
|
||||
FileCompat.symlinkOs(sourceFile, destFile);
|
||||
assertTrue(destFile.getAbsolutePath() + " should exist after symlinking", destFile.exists());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Prefer internal over external storage, because external tends to be FAT filesystems,
|
||||
* which don't support symlinks (which we test using this method).
|
||||
*/
|
||||
public static File getWriteableDir(Instrumentation instrumentation) {
|
||||
Context context = instrumentation.getContext();
|
||||
Context targetContext = instrumentation.getTargetContext();
|
||||
|
||||
File[] dirsToTry = new File[]{
|
||||
context.getCacheDir(),
|
||||
context.getFilesDir(),
|
||||
targetContext.getCacheDir(),
|
||||
targetContext.getFilesDir(),
|
||||
context.getExternalCacheDir(),
|
||||
context.getExternalFilesDir(null),
|
||||
targetContext.getExternalCacheDir(),
|
||||
targetContext.getExternalFilesDir(null),
|
||||
Environment.getExternalStorageDirectory(),
|
||||
};
|
||||
|
||||
return getWriteableDir(dirsToTry);
|
||||
}
|
||||
|
||||
private static File getWriteableDir(File[] dirsToTry) {
|
||||
|
||||
for (File dir : dirsToTry) {
|
||||
if (dir != null && dir.canWrite()) {
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,454 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* This program is 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid.installer;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import org.fdroid.fdroid.AssetUtils;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.compat.FileCompatTest;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.Repo;
|
||||
import org.fdroid.fdroid.data.RepoXMLHandler;
|
||||
import org.fdroid.fdroid.mock.RepoDetails;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
/**
|
||||
* This test checks the ApkVerifier by parsing a repo from permissionsRepo.xml
|
||||
* and checking the listed permissions against the ones specified in apks' AndroidManifest,
|
||||
* which have been specifically generated for this test.
|
||||
* <p>
|
||||
* NOTE: This androidTest cannot run as a Robolectric test because the
|
||||
* required methods from PackageManger are not included in Robolectric's Android API.
|
||||
* java.lang.NoClassDefFoundError: java/util/jar/StrictJarFile
|
||||
* at android.content.pm.PackageManager.getPackageArchiveInfo(PackageManager.java:3545)
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ApkVerifierTest {
|
||||
public static final String TAG = "ApkVerifierTest";
|
||||
|
||||
Instrumentation instrumentation;
|
||||
|
||||
File sdk14Apk;
|
||||
File minMaxApk;
|
||||
private File extendedPermissionsApk;
|
||||
private File extendedPermsXml;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
instrumentation = InstrumentationRegistry.getInstrumentation();
|
||||
File dir = FileCompatTest.getWriteableDir(instrumentation);
|
||||
assertTrue(dir.isDirectory());
|
||||
assertTrue(dir.canWrite());
|
||||
sdk14Apk = AssetUtils.copyAssetToDir(instrumentation.getContext(),
|
||||
"org.fdroid.permissions.sdk14.apk",
|
||||
dir
|
||||
);
|
||||
minMaxApk = AssetUtils.copyAssetToDir(instrumentation.getContext(),
|
||||
"org.fdroid.permissions.minmax.apk",
|
||||
dir
|
||||
);
|
||||
extendedPermissionsApk = AssetUtils.copyAssetToDir(instrumentation.getContext(),
|
||||
"org.fdroid.extendedpermissionstest.apk",
|
||||
dir
|
||||
);
|
||||
extendedPermsXml = AssetUtils.copyAssetToDir(instrumentation.getContext(),
|
||||
"extendedPerms.xml",
|
||||
dir
|
||||
);
|
||||
assertTrue(sdk14Apk.exists());
|
||||
assertTrue(minMaxApk.exists());
|
||||
assertTrue(extendedPermissionsApk.exists());
|
||||
assertTrue(extendedPermsXml.exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNulls() {
|
||||
assertTrue(ApkVerifier.requestedPermissionsEqual(null, null));
|
||||
|
||||
String[] perms = new String[]{"Blah"};
|
||||
assertFalse(ApkVerifier.requestedPermissionsEqual(perms, null));
|
||||
assertFalse(ApkVerifier.requestedPermissionsEqual(null, perms));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithoutPrefix() {
|
||||
Apk apk = new Apk();
|
||||
apk.packageName = "org.fdroid.permissions.sdk14";
|
||||
apk.targetSdkVersion = 14;
|
||||
ArrayList<String> noPrefixPermissionsList = new ArrayList<>(Arrays.asList(
|
||||
"AUTHENTICATE_ACCOUNTS",
|
||||
"MANAGE_ACCOUNTS",
|
||||
"READ_PROFILE",
|
||||
"WRITE_PROFILE",
|
||||
"GET_ACCOUNTS",
|
||||
"READ_CONTACTS",
|
||||
"WRITE_CONTACTS",
|
||||
"WRITE_EXTERNAL_STORAGE",
|
||||
"READ_EXTERNAL_STORAGE",
|
||||
"INTERNET",
|
||||
"ACCESS_NETWORK_STATE",
|
||||
"NFC",
|
||||
"READ_SYNC_SETTINGS",
|
||||
"WRITE_SYNC_SETTINGS",
|
||||
"WRITE_CALL_LOG", // implied-permission!
|
||||
"READ_CALL_LOG" // implied-permission!
|
||||
));
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
noPrefixPermissionsList.add("android.permission.ACCESS_MEDIA_LOCATION");
|
||||
}
|
||||
String[] noPrefixPermissions = noPrefixPermissionsList.toArray(new String[0]);
|
||||
|
||||
for (int i = 0; i < noPrefixPermissions.length; i++) {
|
||||
noPrefixPermissions[i] = RepoXMLHandler.fdroidToAndroidPermission(noPrefixPermissions[i]);
|
||||
}
|
||||
apk.requestedPermissions = noPrefixPermissions;
|
||||
|
||||
Uri uri = Uri.fromFile(sdk14Apk);
|
||||
ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
|
||||
|
||||
try {
|
||||
apkVerifier.verifyApk();
|
||||
} catch (ApkVerifier.ApkVerificationException | ApkVerifier.ApkPermissionUnequalException e) {
|
||||
e.printStackTrace();
|
||||
fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = ApkVerifier.ApkPermissionUnequalException.class)
|
||||
public void testWithMinMax()
|
||||
throws ApkVerifier.ApkPermissionUnequalException, ApkVerifier.ApkVerificationException {
|
||||
Apk apk = new Apk();
|
||||
apk.packageName = "org.fdroid.permissions.minmax";
|
||||
apk.targetSdkVersion = 24;
|
||||
ArrayList<String> permissionsList = new ArrayList<>();
|
||||
permissionsList.add("android.permission.READ_CALENDAR");
|
||||
if (Build.VERSION.SDK_INT <= 18) {
|
||||
permissionsList.add("android.permission.WRITE_EXTERNAL_STORAGE");
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
permissionsList.add("android.permission.ACCESS_FINE_LOCATION");
|
||||
}
|
||||
apk.requestedPermissions = permissionsList.toArray(new String[permissionsList.size()]);
|
||||
|
||||
Uri uri = Uri.fromFile(minMaxApk);
|
||||
ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
|
||||
apkVerifier.verifyApk();
|
||||
|
||||
permissionsList.add("ADDITIONAL_PERMISSION");
|
||||
apk.requestedPermissions = permissionsList.toArray(new String[permissionsList.size()]);
|
||||
apkVerifier.verifyApk();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithPrefix() {
|
||||
Apk apk = new Apk();
|
||||
apk.packageName = "org.fdroid.permissions.sdk14";
|
||||
apk.targetSdkVersion = 14;
|
||||
TreeSet<String> expectedSet = new TreeSet<>(Arrays.asList(
|
||||
"android.permission.AUTHENTICATE_ACCOUNTS",
|
||||
"android.permission.MANAGE_ACCOUNTS",
|
||||
"android.permission.READ_PROFILE",
|
||||
"android.permission.WRITE_PROFILE",
|
||||
"android.permission.GET_ACCOUNTS",
|
||||
"android.permission.READ_CONTACTS",
|
||||
"android.permission.WRITE_CONTACTS",
|
||||
"android.permission.WRITE_EXTERNAL_STORAGE",
|
||||
"android.permission.READ_EXTERNAL_STORAGE",
|
||||
"android.permission.INTERNET",
|
||||
"android.permission.ACCESS_NETWORK_STATE",
|
||||
"android.permission.NFC",
|
||||
"android.permission.READ_SYNC_SETTINGS",
|
||||
"android.permission.WRITE_SYNC_SETTINGS",
|
||||
"android.permission.WRITE_CALL_LOG", // implied-permission!
|
||||
"android.permission.READ_CALL_LOG"// implied-permission!
|
||||
));
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
expectedSet.add("android.permission.ACCESS_MEDIA_LOCATION");
|
||||
}
|
||||
apk.requestedPermissions = expectedSet.toArray(new String[0]);
|
||||
|
||||
Uri uri = Uri.fromFile(sdk14Apk);
|
||||
|
||||
ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
|
||||
|
||||
try {
|
||||
apkVerifier.verifyApk();
|
||||
} catch (ApkVerifier.ApkVerificationException | ApkVerifier.ApkPermissionUnequalException e) {
|
||||
e.printStackTrace();
|
||||
fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional permissions are okay. The user is simply
|
||||
* warned about a permission that is not used inside the apk
|
||||
*/
|
||||
@Test(expected = ApkVerifier.ApkPermissionUnequalException.class)
|
||||
public void testAdditionalPermission()
|
||||
throws ApkVerifier.ApkPermissionUnequalException, ApkVerifier.ApkVerificationException {
|
||||
Apk apk = new Apk();
|
||||
apk.packageName = "org.fdroid.permissions.sdk14";
|
||||
apk.targetSdkVersion = 14;
|
||||
apk.requestedPermissions = new String[]{
|
||||
"android.permission.AUTHENTICATE_ACCOUNTS",
|
||||
"android.permission.MANAGE_ACCOUNTS",
|
||||
"android.permission.READ_PROFILE",
|
||||
"android.permission.WRITE_PROFILE",
|
||||
"android.permission.GET_ACCOUNTS",
|
||||
"android.permission.READ_CONTACTS",
|
||||
"android.permission.WRITE_CONTACTS",
|
||||
"android.permission.WRITE_EXTERNAL_STORAGE",
|
||||
"android.permission.READ_EXTERNAL_STORAGE",
|
||||
"android.permission.INTERNET",
|
||||
"android.permission.ACCESS_NETWORK_STATE",
|
||||
"android.permission.NFC",
|
||||
"android.permission.READ_SYNC_SETTINGS",
|
||||
"android.permission.WRITE_SYNC_SETTINGS",
|
||||
"android.permission.WRITE_CALL_LOG", // implied-permission!
|
||||
"android.permission.READ_CALL_LOG", // implied-permission!
|
||||
"android.permission.FAKE_NEW_PERMISSION",
|
||||
};
|
||||
|
||||
Uri uri = Uri.fromFile(sdk14Apk);
|
||||
ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
|
||||
apkVerifier.verifyApk();
|
||||
}
|
||||
|
||||
/**
|
||||
* Missing permissions are not okay!
|
||||
* The user is then not warned about a permission that the apk uses!
|
||||
*/
|
||||
@Test
|
||||
public void testMissingPermission() {
|
||||
Apk apk = new Apk();
|
||||
apk.packageName = "org.fdroid.permissions.sdk14";
|
||||
apk.targetSdkVersion = 14;
|
||||
apk.requestedPermissions = new String[]{
|
||||
//"android.permission.AUTHENTICATE_ACCOUNTS",
|
||||
"android.permission.MANAGE_ACCOUNTS",
|
||||
"android.permission.READ_PROFILE",
|
||||
"android.permission.WRITE_PROFILE",
|
||||
"android.permission.GET_ACCOUNTS",
|
||||
"android.permission.READ_CONTACTS",
|
||||
"android.permission.WRITE_CONTACTS",
|
||||
"android.permission.WRITE_EXTERNAL_STORAGE",
|
||||
"android.permission.READ_EXTERNAL_STORAGE",
|
||||
"android.permission.INTERNET",
|
||||
"android.permission.ACCESS_NETWORK_STATE",
|
||||
"android.permission.NFC",
|
||||
"android.permission.READ_SYNC_SETTINGS",
|
||||
"android.permission.WRITE_SYNC_SETTINGS",
|
||||
"android.permission.WRITE_CALL_LOG", // implied-permission!
|
||||
"android.permission.READ_CALL_LOG", // implied-permission!
|
||||
};
|
||||
|
||||
Uri uri = Uri.fromFile(sdk14Apk);
|
||||
|
||||
ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
|
||||
|
||||
try {
|
||||
apkVerifier.verifyApk();
|
||||
fail();
|
||||
} catch (ApkVerifier.ApkVerificationException e) {
|
||||
e.printStackTrace();
|
||||
fail(e.getMessage());
|
||||
} catch (ApkVerifier.ApkPermissionUnequalException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtendedPerms() throws IOException,
|
||||
ApkVerifier.ApkPermissionUnequalException, ApkVerifier.ApkVerificationException {
|
||||
RepoDetails actualDetails = getFromFile(extendedPermsXml);
|
||||
HashSet<String> expectedSet = new HashSet<>(Arrays.asList(
|
||||
"android.permission.ACCESS_NETWORK_STATE",
|
||||
"android.permission.ACCESS_WIFI_STATE",
|
||||
"android.permission.INTERNET",
|
||||
"android.permission.READ_SYNC_STATS",
|
||||
"android.permission.READ_SYNC_SETTINGS",
|
||||
"android.permission.WRITE_SYNC_SETTINGS",
|
||||
"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS",
|
||||
"android.permission.READ_CONTACTS",
|
||||
"android.permission.WRITE_CONTACTS",
|
||||
"android.permission.READ_CALENDAR",
|
||||
"android.permission.WRITE_CALENDAR"
|
||||
));
|
||||
if (Build.VERSION.SDK_INT <= 18) {
|
||||
expectedSet.add("android.permission.READ_EXTERNAL_STORAGE");
|
||||
expectedSet.add("android.permission.WRITE_EXTERNAL_STORAGE");
|
||||
}
|
||||
if (Build.VERSION.SDK_INT <= 22) {
|
||||
expectedSet.add("android.permission.GET_ACCOUNTS");
|
||||
expectedSet.add("android.permission.AUTHENTICATE_ACCOUNTS");
|
||||
expectedSet.add("android.permission.MANAGE_ACCOUNTS");
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
expectedSet.add("android.permission.CAMERA");
|
||||
if (Build.VERSION.SDK_INT <= 23) {
|
||||
expectedSet.add("android.permission.CALL_PHONE");
|
||||
}
|
||||
}
|
||||
Apk apk = actualDetails.apks.get(0);
|
||||
HashSet<String> actualSet = new HashSet<>(Arrays.asList(apk.requestedPermissions));
|
||||
for (String permission : expectedSet) {
|
||||
if (!actualSet.contains(permission)) {
|
||||
Log.i(TAG, permission + " in expected but not actual! (android-"
|
||||
+ Build.VERSION.SDK_INT + ")");
|
||||
}
|
||||
}
|
||||
for (String permission : actualSet) {
|
||||
if (!expectedSet.contains(permission)) {
|
||||
Log.i(TAG, permission + " in actual but not expected! (android-"
|
||||
+ Build.VERSION.SDK_INT + ")");
|
||||
}
|
||||
}
|
||||
String[] expectedPermissions = expectedSet.toArray(new String[expectedSet.size()]);
|
||||
assertTrue(ApkVerifier.requestedPermissionsEqual(expectedPermissions, apk.requestedPermissions));
|
||||
|
||||
String[] badPermissions = Arrays.copyOf(expectedPermissions, expectedPermissions.length + 1);
|
||||
assertFalse(ApkVerifier.requestedPermissionsEqual(badPermissions, apk.requestedPermissions));
|
||||
badPermissions[badPermissions.length - 1] = "notarealpermission";
|
||||
assertFalse(ApkVerifier.requestedPermissionsEqual(badPermissions, apk.requestedPermissions));
|
||||
|
||||
Uri uri = Uri.fromFile(extendedPermissionsApk);
|
||||
ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
|
||||
apkVerifier.verifyApk();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImpliedPerms() throws IOException {
|
||||
RepoDetails actualDetails = getFromFile(extendedPermsXml);
|
||||
TreeSet<String> expectedSet = new TreeSet<>(Arrays.asList(
|
||||
"android.permission.ACCESS_NETWORK_STATE",
|
||||
"android.permission.ACCESS_WIFI_STATE",
|
||||
"android.permission.INTERNET",
|
||||
"android.permission.READ_CALENDAR",
|
||||
"android.permission.READ_CONTACTS",
|
||||
"android.permission.READ_EXTERNAL_STORAGE",
|
||||
"android.permission.READ_SYNC_SETTINGS",
|
||||
"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS",
|
||||
"android.permission.WRITE_CALENDAR",
|
||||
"android.permission.WRITE_CONTACTS",
|
||||
"android.permission.WRITE_EXTERNAL_STORAGE",
|
||||
"android.permission.WRITE_SYNC_SETTINGS",
|
||||
"org.dmfs.permission.READ_TASKS",
|
||||
"org.dmfs.permission.WRITE_TASKS"
|
||||
));
|
||||
if (Build.VERSION.SDK_INT <= 22) { // maxSdkVersion="22"
|
||||
expectedSet.addAll(Arrays.asList(
|
||||
"android.permission.AUTHENTICATE_ACCOUNTS",
|
||||
"android.permission.GET_ACCOUNTS",
|
||||
"android.permission.MANAGE_ACCOUNTS"
|
||||
));
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
expectedSet.add("android.permission.ACCESS_MEDIA_LOCATION");
|
||||
}
|
||||
Apk apk = actualDetails.apks.get(1);
|
||||
Log.i(TAG, "APK: " + apk.apkName);
|
||||
HashSet<String> actualSet = new HashSet<>(Arrays.asList(apk.requestedPermissions));
|
||||
for (String permission : expectedSet) {
|
||||
if (!actualSet.contains(permission)) {
|
||||
Log.i(TAG, permission + " in expected but not actual! (android-"
|
||||
+ Build.VERSION.SDK_INT + ")");
|
||||
}
|
||||
}
|
||||
for (String permission : actualSet) {
|
||||
if (!expectedSet.contains(permission)) {
|
||||
Log.i(TAG, permission + " in actual but not expected! (android-"
|
||||
+ Build.VERSION.SDK_INT + ")");
|
||||
}
|
||||
}
|
||||
String[] expectedPermissions = expectedSet.toArray(new String[expectedSet.size()]);
|
||||
assertTrue(ApkVerifier.requestedPermissionsEqual(expectedPermissions, apk.requestedPermissions));
|
||||
|
||||
expectedSet = new TreeSet<>(Arrays.asList(
|
||||
"android.permission.ACCESS_NETWORK_STATE",
|
||||
"android.permission.ACCESS_WIFI_STATE",
|
||||
"android.permission.AUTHENTICATE_ACCOUNTS",
|
||||
"android.permission.GET_ACCOUNTS",
|
||||
"android.permission.INTERNET",
|
||||
"android.permission.MANAGE_ACCOUNTS",
|
||||
"android.permission.READ_CALENDAR",
|
||||
"android.permission.READ_CONTACTS",
|
||||
"android.permission.READ_EXTERNAL_STORAGE",
|
||||
"android.permission.READ_SYNC_SETTINGS",
|
||||
"android.permission.WRITE_CALENDAR",
|
||||
"android.permission.WRITE_CONTACTS",
|
||||
"android.permission.WRITE_EXTERNAL_STORAGE",
|
||||
"android.permission.WRITE_SYNC_SETTINGS",
|
||||
"org.dmfs.permission.READ_TASKS",
|
||||
"org.dmfs.permission.WRITE_TASKS"
|
||||
));
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
expectedSet.add("android.permission.ACCESS_MEDIA_LOCATION");
|
||||
}
|
||||
expectedPermissions = expectedSet.toArray(new String[expectedSet.size()]);
|
||||
apk = actualDetails.apks.get(2);
|
||||
Log.i(TAG, "APK: " + apk.apkName);
|
||||
actualSet = new HashSet<>(Arrays.asList(apk.requestedPermissions));
|
||||
for (String permission : expectedSet) {
|
||||
if (!actualSet.contains(permission)) {
|
||||
Log.i(TAG, permission + " in expected but not actual! (android-"
|
||||
+ Build.VERSION.SDK_INT + ")");
|
||||
}
|
||||
}
|
||||
for (String permission : actualSet) {
|
||||
if (!expectedSet.contains(permission)) {
|
||||
Log.i(TAG, permission + " in actual but not expected! (android-"
|
||||
+ Build.VERSION.SDK_INT + ")");
|
||||
}
|
||||
}
|
||||
assertTrue(ApkVerifier.requestedPermissionsEqual(expectedPermissions, apk.requestedPermissions));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private RepoDetails getFromFile(File indexFile) throws IOException {
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = new FileInputStream(indexFile);
|
||||
return RepoDetails.getFromFile(inputStream, Repo.PUSH_REQUEST_IGNORE);
|
||||
} finally {
|
||||
Utils.closeQuietly(inputStream);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
package org.fdroid.fdroid.nearby;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.jmdns.ServiceEvent;
|
||||
import javax.jmdns.ServiceListener;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class BonjourManagerTest {
|
||||
|
||||
private static final String NAME = "Robolectric-test";
|
||||
private static final String LOCALHOST = "localhost";
|
||||
private static final int PORT = 8888;
|
||||
|
||||
@Test
|
||||
public void testStartStop() throws InterruptedException {
|
||||
Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
FDroidApp.ipAddressString = LOCALHOST;
|
||||
FDroidApp.port = PORT;
|
||||
|
||||
final CountDownLatch addedLatch = new CountDownLatch(1);
|
||||
final CountDownLatch resolvedLatch = new CountDownLatch(1);
|
||||
final CountDownLatch removedLatch = new CountDownLatch(1);
|
||||
BonjourManager.start(context, NAME, false,
|
||||
new ServiceListener() {
|
||||
@Override
|
||||
public void serviceAdded(ServiceEvent serviceEvent) {
|
||||
System.out.println("Service added: " + serviceEvent.getInfo());
|
||||
if (NAME.equals(serviceEvent.getName())) {
|
||||
addedLatch.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceRemoved(ServiceEvent serviceEvent) {
|
||||
System.out.println("Service removed: " + serviceEvent.getInfo());
|
||||
removedLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceResolved(ServiceEvent serviceEvent) {
|
||||
System.out.println("Service resolved: " + serviceEvent.getInfo());
|
||||
if (NAME.equals(serviceEvent.getName())) {
|
||||
resolvedLatch.countDown();
|
||||
}
|
||||
}
|
||||
}, getBlankServiceListener());
|
||||
BonjourManager.setVisible(context, true);
|
||||
assertTrue(addedLatch.await(30, TimeUnit.SECONDS));
|
||||
assertTrue(resolvedLatch.await(30, TimeUnit.SECONDS));
|
||||
BonjourManager.setVisible(context, false);
|
||||
assertTrue(removedLatch.await(30, TimeUnit.SECONDS));
|
||||
BonjourManager.stop(context);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestart() throws InterruptedException {
|
||||
Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
FDroidApp.ipAddressString = LOCALHOST;
|
||||
FDroidApp.port = PORT;
|
||||
|
||||
BonjourManager.start(context, NAME, false, getBlankServiceListener(), getBlankServiceListener());
|
||||
|
||||
final CountDownLatch addedLatch = new CountDownLatch(1);
|
||||
final CountDownLatch resolvedLatch = new CountDownLatch(1);
|
||||
final CountDownLatch removedLatch = new CountDownLatch(1);
|
||||
BonjourManager.restart(context, NAME, false,
|
||||
new ServiceListener() {
|
||||
@Override
|
||||
public void serviceAdded(ServiceEvent serviceEvent) {
|
||||
System.out.println("Service added: " + serviceEvent.getInfo());
|
||||
if (NAME.equals(serviceEvent.getName())) {
|
||||
addedLatch.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceRemoved(ServiceEvent serviceEvent) {
|
||||
System.out.println("Service removed: " + serviceEvent.getInfo());
|
||||
removedLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceResolved(ServiceEvent serviceEvent) {
|
||||
System.out.println("Service resolved: " + serviceEvent.getInfo());
|
||||
if (NAME.equals(serviceEvent.getName())) {
|
||||
resolvedLatch.countDown();
|
||||
}
|
||||
}
|
||||
}, getBlankServiceListener());
|
||||
BonjourManager.setVisible(context, true);
|
||||
assertTrue(addedLatch.await(30, TimeUnit.SECONDS));
|
||||
assertTrue(resolvedLatch.await(30, TimeUnit.SECONDS));
|
||||
BonjourManager.setVisible(context, false);
|
||||
assertTrue(removedLatch.await(30, TimeUnit.SECONDS));
|
||||
BonjourManager.stop(context);
|
||||
}
|
||||
|
||||
private ServiceListener getBlankServiceListener() {
|
||||
return new ServiceListener() {
|
||||
@Override
|
||||
public void serviceAdded(ServiceEvent serviceEvent) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceRemoved(ServiceEvent serviceEvent) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceResolved(ServiceEvent serviceEvent) {
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,192 @@
|
||||
package org.fdroid.fdroid.nearby;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
import org.fdroid.fdroid.Netstat;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class LocalHTTPDManagerTest {
|
||||
private static final String TAG = "LocalHTTPDManagerTest";
|
||||
|
||||
private Context context;
|
||||
private LocalBroadcastManager lbm;
|
||||
|
||||
private static final String LOCALHOST = "localhost";
|
||||
private static final int PORT = 8888;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
context = ApplicationProvider.getApplicationContext();
|
||||
lbm = LocalBroadcastManager.getInstance(context);
|
||||
|
||||
FDroidApp.ipAddressString = LOCALHOST;
|
||||
FDroidApp.port = PORT;
|
||||
|
||||
for (Netstat.Connection connection : Netstat.getConnections()) { // NOPMD
|
||||
Log.i("LocalHTTPDManagerTest", "connection: " + connection.toString());
|
||||
}
|
||||
assertFalse(Utils.isServerSocketInUse(PORT));
|
||||
LocalHTTPDManager.stop(context);
|
||||
|
||||
for (Netstat.Connection connection : Netstat.getConnections()) { // NOPMD
|
||||
Log.i("LocalHTTPDManagerTest", "connection: " + connection.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
lbm.unregisterReceiver(startedReceiver);
|
||||
lbm.unregisterReceiver(stoppedReceiver);
|
||||
lbm.unregisterReceiver(errorReceiver);
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@Test
|
||||
public void testStartStop() throws InterruptedException {
|
||||
Log.i(TAG, "testStartStop");
|
||||
|
||||
final CountDownLatch startLatch = new CountDownLatch(1);
|
||||
BroadcastReceiver latchReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
startLatch.countDown();
|
||||
}
|
||||
};
|
||||
lbm.registerReceiver(latchReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STARTED));
|
||||
lbm.registerReceiver(stoppedReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STOPPED));
|
||||
lbm.registerReceiver(errorReceiver, new IntentFilter(LocalHTTPDManager.ACTION_ERROR));
|
||||
LocalHTTPDManager.start(context, false);
|
||||
assertTrue(startLatch.await(30, TimeUnit.SECONDS));
|
||||
assertTrue(Utils.isServerSocketInUse(PORT));
|
||||
assertTrue(Utils.canConnectToSocket(LOCALHOST, PORT));
|
||||
lbm.unregisterReceiver(latchReceiver);
|
||||
lbm.unregisterReceiver(stoppedReceiver);
|
||||
lbm.unregisterReceiver(errorReceiver);
|
||||
|
||||
final CountDownLatch stopLatch = new CountDownLatch(1);
|
||||
latchReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
stopLatch.countDown();
|
||||
}
|
||||
};
|
||||
lbm.registerReceiver(startedReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STARTED));
|
||||
lbm.registerReceiver(latchReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STOPPED));
|
||||
lbm.registerReceiver(errorReceiver, new IntentFilter(LocalHTTPDManager.ACTION_ERROR));
|
||||
LocalHTTPDManager.stop(context);
|
||||
assertTrue(stopLatch.await(30, TimeUnit.SECONDS));
|
||||
assertFalse(Utils.isServerSocketInUse(PORT));
|
||||
assertFalse(Utils.canConnectToSocket(LOCALHOST, PORT)); // if this is flaky, just remove it
|
||||
lbm.unregisterReceiver(latchReceiver);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testError() throws InterruptedException, IOException {
|
||||
Log.i("LocalHTTPDManagerTest", "testError");
|
||||
ServerSocket blockerSocket = new ServerSocket(PORT);
|
||||
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
BroadcastReceiver latchReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
latch.countDown();
|
||||
}
|
||||
};
|
||||
lbm.registerReceiver(startedReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STARTED));
|
||||
lbm.registerReceiver(stoppedReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STOPPED));
|
||||
lbm.registerReceiver(latchReceiver, new IntentFilter(LocalHTTPDManager.ACTION_ERROR));
|
||||
LocalHTTPDManager.start(context, false);
|
||||
assertTrue(latch.await(30, TimeUnit.SECONDS));
|
||||
assertTrue(Utils.isServerSocketInUse(PORT));
|
||||
assertNotEquals(PORT, FDroidApp.port);
|
||||
assertFalse(Utils.isServerSocketInUse(FDroidApp.port));
|
||||
lbm.unregisterReceiver(latchReceiver);
|
||||
blockerSocket.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestart() throws InterruptedException, IOException {
|
||||
Log.i("LocalHTTPDManagerTest", "testRestart");
|
||||
assertFalse(Utils.isServerSocketInUse(PORT));
|
||||
final CountDownLatch startLatch = new CountDownLatch(1);
|
||||
BroadcastReceiver latchReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
startLatch.countDown();
|
||||
}
|
||||
};
|
||||
lbm.registerReceiver(latchReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STARTED));
|
||||
lbm.registerReceiver(stoppedReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STOPPED));
|
||||
lbm.registerReceiver(errorReceiver, new IntentFilter(LocalHTTPDManager.ACTION_ERROR));
|
||||
LocalHTTPDManager.start(context, false);
|
||||
assertTrue(startLatch.await(30, TimeUnit.SECONDS));
|
||||
assertTrue(Utils.isServerSocketInUse(PORT));
|
||||
lbm.unregisterReceiver(latchReceiver);
|
||||
lbm.unregisterReceiver(stoppedReceiver);
|
||||
|
||||
final CountDownLatch restartLatch = new CountDownLatch(1);
|
||||
latchReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
restartLatch.countDown();
|
||||
}
|
||||
};
|
||||
lbm.registerReceiver(latchReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STARTED));
|
||||
LocalHTTPDManager.restart(context, false);
|
||||
assertTrue(restartLatch.await(30, TimeUnit.SECONDS));
|
||||
lbm.unregisterReceiver(latchReceiver);
|
||||
}
|
||||
|
||||
private final BroadcastReceiver startedReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String message = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
Log.i(TAG, "startedReceiver: " + message);
|
||||
fail();
|
||||
}
|
||||
};
|
||||
|
||||
private final BroadcastReceiver stoppedReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String message = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
Log.i(TAG, "stoppedReceiver: " + message);
|
||||
fail();
|
||||
}
|
||||
};
|
||||
|
||||
private final BroadcastReceiver errorReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String message = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
Log.i(TAG, "errorReceiver: " + message);
|
||||
fail();
|
||||
}
|
||||
};
|
||||
}
|
@ -0,0 +1,159 @@
|
||||
|
||||
package org.fdroid.fdroid.net;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.ProgressListener;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class HttpDownloaderTest {
|
||||
private static final String TAG = "HttpDownloaderTest";
|
||||
|
||||
static final String[] URLS;
|
||||
|
||||
// https://developer.android.com/reference/javax/net/ssl/SSLContext
|
||||
static {
|
||||
ArrayList<String> tempUrls = new ArrayList<>(Arrays.asList(
|
||||
"https://f-droid.org/repo/index-v1.jar",
|
||||
// sites that use SNI for HTTPS
|
||||
"https://mirrors.kernel.org/debian/dists/stable/Release",
|
||||
"https://fdroid.tetaneutral.net/fdroid/repo/index-v1.jar",
|
||||
"https://ftp.fau.de/fdroid/repo/index-v1.jar",
|
||||
//"https://microg.org/fdroid/repo/index-v1.jar",
|
||||
//"https://grobox.de/fdroid/repo/index.jar",
|
||||
"https://guardianproject.info/fdroid/repo/index-v1.jar"
|
||||
));
|
||||
if (Build.VERSION.SDK_INT >= 22) {
|
||||
tempUrls.addAll(Arrays.asList(
|
||||
"https://en.wikipedia.org/wiki/Index.html", // no SNI but weird ipv6 lookup issues
|
||||
"https://mirror.cyberbits.eu/fdroid/repo/index-v1.jar" // TLSv1.2 only and SNI
|
||||
));
|
||||
}
|
||||
URLS = tempUrls.toArray(new String[tempUrls.size()]);
|
||||
}
|
||||
|
||||
private boolean receivedProgress;
|
||||
|
||||
@Test
|
||||
public void downloadUninterruptedTest() throws IOException, InterruptedException {
|
||||
for (String urlString : URLS) {
|
||||
Log.i(TAG, "URL: " + urlString);
|
||||
Uri uri = Uri.parse(urlString);
|
||||
File destFile = File.createTempFile("dl-", "");
|
||||
HttpDownloader httpDownloader = new HttpDownloader(uri, destFile);
|
||||
httpDownloader.download();
|
||||
assertTrue(destFile.exists());
|
||||
assertTrue(destFile.canRead());
|
||||
destFile.deleteOnExit();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void downloadUninterruptedTestWithProgress() throws IOException, InterruptedException {
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
String urlString = "https://f-droid.org/repo/index.jar";
|
||||
receivedProgress = false;
|
||||
Uri uri = Uri.parse(urlString);
|
||||
File destFile = File.createTempFile("dl-", "");
|
||||
final HttpDownloader httpDownloader = new HttpDownloader(uri, destFile);
|
||||
httpDownloader.setListener(new ProgressListener() {
|
||||
@Override
|
||||
public void onProgress(long bytesRead, long totalBytes) {
|
||||
receivedProgress = true;
|
||||
}
|
||||
});
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
httpDownloader.download();
|
||||
latch.countDown();
|
||||
} catch (IOException | InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
fail();
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
latch.await(100, TimeUnit.SECONDS); // either 2 progress reports or 100 seconds
|
||||
assertTrue(destFile.exists());
|
||||
assertTrue(destFile.canRead());
|
||||
assertTrue(receivedProgress);
|
||||
destFile.deleteOnExit();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void downloadHttpBasicAuth() throws IOException, InterruptedException {
|
||||
Uri uri = Uri.parse("https://httpbin.org/basic-auth/myusername/supersecretpassword");
|
||||
File destFile = File.createTempFile("dl-", "");
|
||||
HttpDownloader httpDownloader = new HttpDownloader(uri, destFile, "myusername", "supersecretpassword");
|
||||
httpDownloader.download();
|
||||
assertTrue(destFile.exists());
|
||||
assertTrue(destFile.canRead());
|
||||
destFile.deleteOnExit();
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void downloadHttpBasicAuthWrongPassword() throws IOException, InterruptedException {
|
||||
Uri uri = Uri.parse("https://httpbin.org/basic-auth/myusername/supersecretpassword");
|
||||
File destFile = File.createTempFile("dl-", "");
|
||||
HttpDownloader httpDownloader = new HttpDownloader(uri, destFile, "myusername", "wrongpassword");
|
||||
httpDownloader.download();
|
||||
assertFalse(destFile.exists());
|
||||
destFile.deleteOnExit();
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void downloadHttpBasicAuthWrongUsername() throws IOException, InterruptedException {
|
||||
Uri uri = Uri.parse("https://httpbin.org/basic-auth/myusername/supersecretpassword");
|
||||
File destFile = File.createTempFile("dl-", "");
|
||||
HttpDownloader httpDownloader = new HttpDownloader(uri, destFile, "wrongusername", "supersecretpassword");
|
||||
httpDownloader.download();
|
||||
assertFalse(destFile.exists());
|
||||
destFile.deleteOnExit();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void downloadThenCancel() throws IOException, InterruptedException {
|
||||
final CountDownLatch latch = new CountDownLatch(2);
|
||||
Uri uri = Uri.parse("https://f-droid.org/repo/index.jar");
|
||||
File destFile = File.createTempFile("dl-", "");
|
||||
final HttpDownloader httpDownloader = new HttpDownloader(uri, destFile);
|
||||
httpDownloader.setListener(new ProgressListener() {
|
||||
@Override
|
||||
public void onProgress(long bytesRead, long totalBytes) {
|
||||
receivedProgress = true;
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
httpDownloader.download();
|
||||
fail();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
fail();
|
||||
} catch (InterruptedException e) {
|
||||
// success!
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
latch.await(100, TimeUnit.SECONDS); // either 2 progress reports or 100 seconds
|
||||
httpDownloader.cancelDownload();
|
||||
assertTrue(receivedProgress);
|
||||
destFile.deleteOnExit();
|
||||
}
|
||||
}
|
@ -0,0 +1,203 @@
|
||||
package org.fdroid.fdroid.updater;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.os.Looper;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.BuildConfig;
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
import org.fdroid.fdroid.Hasher;
|
||||
import org.fdroid.fdroid.IndexUpdater;
|
||||
import org.fdroid.fdroid.Preferences;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.ApkProvider;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.data.Repo;
|
||||
import org.fdroid.fdroid.data.RepoProvider;
|
||||
import org.fdroid.fdroid.data.Schema;
|
||||
import org.fdroid.fdroid.nearby.LocalHTTPD;
|
||||
import org.fdroid.fdroid.nearby.LocalRepoKeyStore;
|
||||
import org.fdroid.fdroid.nearby.LocalRepoManager;
|
||||
import org.fdroid.fdroid.nearby.LocalRepoService;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.security.cert.Certificate;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@LargeTest
|
||||
public class SwapRepoEmulatorTest {
|
||||
public static final String TAG = "SwapRepoEmulatorTest";
|
||||
|
||||
/**
|
||||
* @see org.fdroid.fdroid.nearby.WifiStateChangeService.WifiInfoThread#run()
|
||||
*/
|
||||
@Ignore
|
||||
@Test
|
||||
public void testSwap()
|
||||
throws IOException, LocalRepoKeyStore.InitException, IndexUpdater.UpdateException, InterruptedException {
|
||||
Looper.prepare();
|
||||
LocalHTTPD localHttpd = null;
|
||||
try {
|
||||
Log.i(TAG, "REPO: " + FDroidApp.repo);
|
||||
final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
Preferences.setupForTests(context);
|
||||
|
||||
FDroidApp.initWifiSettings();
|
||||
assertNull(FDroidApp.repo.address);
|
||||
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
while (FDroidApp.repo.address == null) {
|
||||
try {
|
||||
Log.i(TAG, "Waiting for IP address... " + FDroidApp.repo.address);
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
latch.countDown();
|
||||
}
|
||||
}.start();
|
||||
latch.await(10, TimeUnit.MINUTES);
|
||||
assertNotNull(FDroidApp.repo.address);
|
||||
|
||||
LocalRepoService.runProcess(context, new String[]{context.getPackageName()});
|
||||
Log.i(TAG, "REPO: " + FDroidApp.repo);
|
||||
File indexJarFile = LocalRepoManager.get(context).getIndexJar();
|
||||
assertTrue(indexJarFile.isFile());
|
||||
|
||||
localHttpd = new LocalHTTPD(
|
||||
context,
|
||||
null,
|
||||
FDroidApp.port,
|
||||
LocalRepoManager.get(context).getWebRoot(),
|
||||
false);
|
||||
localHttpd.start();
|
||||
Thread.sleep(100); // give the server some tine to start.
|
||||
assertTrue(localHttpd.isAlive());
|
||||
|
||||
LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context);
|
||||
Certificate localCert = localRepoKeyStore.getCertificate();
|
||||
String signingCert = Hasher.hex(localCert);
|
||||
assertFalse(TextUtils.isEmpty(signingCert));
|
||||
assertFalse(TextUtils.isEmpty(Utils.calcFingerprint(localCert)));
|
||||
|
||||
Repo repoToDelete = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.address);
|
||||
while (repoToDelete != null) {
|
||||
Log.d(TAG, "Removing old test swap repo matching this one: " + repoToDelete.address);
|
||||
RepoProvider.Helper.remove(context, repoToDelete.getId());
|
||||
repoToDelete = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.address);
|
||||
}
|
||||
|
||||
ContentValues values = new ContentValues(4);
|
||||
values.put(Schema.RepoTable.Cols.SIGNING_CERT, signingCert);
|
||||
values.put(Schema.RepoTable.Cols.ADDRESS, FDroidApp.repo.address);
|
||||
values.put(Schema.RepoTable.Cols.NAME, FDroidApp.repo.name);
|
||||
values.put(Schema.RepoTable.Cols.IS_SWAP, true);
|
||||
final String lastEtag = UUID.randomUUID().toString();
|
||||
values.put(Schema.RepoTable.Cols.LAST_ETAG, lastEtag);
|
||||
RepoProvider.Helper.insert(context, values);
|
||||
Repo repo = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.address);
|
||||
assertTrue(repo.isSwap);
|
||||
assertNotEquals(-1, repo.getId());
|
||||
assertTrue(repo.name.startsWith(FDroidApp.repo.name));
|
||||
assertEquals(lastEtag, repo.lastetag);
|
||||
assertNull(repo.lastUpdated);
|
||||
|
||||
assertTrue(isPortInUse(FDroidApp.ipAddressString, FDroidApp.port));
|
||||
Thread.sleep(100);
|
||||
IndexUpdater updater = new IndexUpdater(context, repo);
|
||||
updater.update();
|
||||
assertTrue(updater.hasChanged());
|
||||
|
||||
repo = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.address);
|
||||
final Date lastUpdated = repo.lastUpdated;
|
||||
assertTrue("repo lastUpdated should be updated", new Date(2019, 5, 13).compareTo(repo.lastUpdated) > 0);
|
||||
|
||||
App app = AppProvider.Helper.findSpecificApp(context.getContentResolver(),
|
||||
context.getPackageName(), repo.getId());
|
||||
assertEquals(context.getPackageName(), app.packageName);
|
||||
|
||||
List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL);
|
||||
assertEquals(1, apks.size());
|
||||
for (Apk apk : apks) {
|
||||
Log.i(TAG, "Apk: " + apk);
|
||||
assertEquals(context.getPackageName(), apk.packageName);
|
||||
assertEquals(BuildConfig.VERSION_NAME, apk.versionName);
|
||||
assertEquals(BuildConfig.VERSION_CODE, apk.versionCode);
|
||||
assertEquals(app.repoId, apk.repoId);
|
||||
}
|
||||
|
||||
Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
|
||||
mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||
List<ResolveInfo> resolveInfoList = context.getPackageManager().queryIntentActivities(mainIntent, 0);
|
||||
HashSet<String> packageNames = new HashSet<>();
|
||||
for (ResolveInfo resolveInfo : resolveInfoList) {
|
||||
if (!isSystemPackage(resolveInfo)) {
|
||||
Log.i(TAG, "resolveInfo: " + resolveInfo);
|
||||
packageNames.add(resolveInfo.activityInfo.packageName);
|
||||
}
|
||||
}
|
||||
LocalRepoService.runProcess(context, packageNames.toArray(new String[0]));
|
||||
|
||||
updater = new IndexUpdater(context, repo);
|
||||
updater.update();
|
||||
assertTrue(updater.hasChanged());
|
||||
assertTrue("repo lastUpdated should be updated", lastUpdated.compareTo(repo.lastUpdated) < 0);
|
||||
|
||||
for (String packageName : packageNames) {
|
||||
assertNotNull(ApkProvider.Helper.findByPackageName(context, packageName));
|
||||
}
|
||||
} finally {
|
||||
if (localHttpd != null) {
|
||||
localHttpd.stop();
|
||||
}
|
||||
}
|
||||
if (localHttpd != null) {
|
||||
assertFalse(localHttpd.isAlive());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPortInUse(String host, int port) {
|
||||
boolean result = false;
|
||||
|
||||
try {
|
||||
(new Socket(host, port)).close();
|
||||
result = true;
|
||||
} catch (IOException e) {
|
||||
// Could not connect.
|
||||
e.printStackTrace();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean isSystemPackage(ResolveInfo resolveInfo) {
|
||||
return (resolveInfo.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
package org.fdroid.fdroid.work;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkInfo;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.fdroid.fdroid.compat.FileCompatTest;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* This test cannot run on Robolectric unfortunately since it does not support
|
||||
* getting the timestamps from the files completely.
|
||||
* <p>
|
||||
* This is marked with {@link LargeTest} because it always fails on the emulator
|
||||
* tests on GitLab CI. That excludes it from the test run there.
|
||||
*/
|
||||
@LargeTest
|
||||
public class CleanCacheWorkerTest {
|
||||
public static final String TAG = "CleanCacheWorkerEmulatorTest";
|
||||
|
||||
@Rule
|
||||
public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
|
||||
|
||||
@Rule
|
||||
public WorkManagerTestRule workManagerTestRule = new WorkManagerTestRule();
|
||||
|
||||
@Test
|
||||
public void testWorkRequest() throws ExecutionException, InterruptedException {
|
||||
OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(CleanCacheWorker.class).build();
|
||||
workManagerTestRule.workManager.enqueue(request).getResult();
|
||||
ListenableFuture<WorkInfo> workInfo = workManagerTestRule.workManager.getWorkInfoById(request.getId());
|
||||
assertEquals(WorkInfo.State.SUCCEEDED, workInfo.get().getState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClearOldFiles() throws IOException, InterruptedException {
|
||||
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
|
||||
File tempDir = FileCompatTest.getWriteableDir(instrumentation);
|
||||
assertTrue(tempDir.isDirectory());
|
||||
assertTrue(tempDir.canWrite());
|
||||
|
||||
File dir = new File(tempDir, "F-Droid-test.clearOldFiles");
|
||||
FileUtils.deleteQuietly(dir);
|
||||
assertTrue(dir.mkdirs());
|
||||
assertTrue(dir.isDirectory());
|
||||
|
||||
File first = new File(dir, "first");
|
||||
first.deleteOnExit();
|
||||
|
||||
File second = new File(dir, "second");
|
||||
second.deleteOnExit();
|
||||
|
||||
assertFalse(first.exists());
|
||||
assertFalse(second.exists());
|
||||
|
||||
assertTrue(first.createNewFile());
|
||||
assertTrue(first.exists());
|
||||
|
||||
Thread.sleep(7000);
|
||||
assertTrue(second.createNewFile());
|
||||
assertTrue(second.exists());
|
||||
|
||||
CleanCacheWorker.clearOldFiles(dir, 3000); // check all in dir
|
||||
assertFalse(first.exists());
|
||||
assertTrue(second.exists());
|
||||
|
||||
Thread.sleep(7000);
|
||||
CleanCacheWorker.clearOldFiles(second, 3000); // check just second file
|
||||
assertFalse(first.exists());
|
||||
assertFalse(second.exists());
|
||||
|
||||
// make sure it doesn't freak out on a non-existent file
|
||||
File nonexistent = new File(tempDir, "nonexistent");
|
||||
CleanCacheWorker.clearOldFiles(nonexistent, 1);
|
||||
CleanCacheWorker.clearOldFiles(null, 1);
|
||||
}
|
||||
|
||||
/*
|
||||
// TODO enable this once getImageCacheDir() can be mocked or provide a writable dir in the test
|
||||
@Test
|
||||
public void testDeleteOldIcons() throws IOException {
|
||||
Context context = InstrumentationRegistry.getInstrumentation().getContext();
|
||||
File imageCacheDir = Utils.getImageCacheDir(context);
|
||||
imageCacheDir.mkdirs();
|
||||
assertTrue(imageCacheDir.isDirectory());
|
||||
File oldIcon = new File(imageCacheDir, "old.png");
|
||||
assertTrue(oldIcon.createNewFile());
|
||||
Assume.assumeTrue("test environment must be able to set LastModified time",
|
||||
oldIcon.setLastModified(System.currentTimeMillis() - (DateUtils.DAY_IN_MILLIS * 370)));
|
||||
File currentIcon = new File(imageCacheDir, "current.png");
|
||||
assertTrue(currentIcon.createNewFile());
|
||||
CleanCacheWorker.deleteOldIcons(context);
|
||||
assertTrue(currentIcon.exists());
|
||||
assertFalse(oldIcon.exists());
|
||||
}
|
||||
*/
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Hans-Christoph Steiner <hans@eds.org>
|
||||
*
|
||||
* This program is 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid.work;
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkInfo;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* This actually runs {@link FDroidMetricsWorker} on a device/emulator and
|
||||
* submits a report to https://metrics.cleaninsights.org
|
||||
* <p>
|
||||
* This is marked with {@link LargeTest} to exclude it from running on GitLab CI
|
||||
* because it always fails on the emulator tests there. Also, it actually submits
|
||||
* a report.
|
||||
*/
|
||||
@LargeTest
|
||||
public class FDroidMetricsWorkerTest {
|
||||
public static final String TAG = "FDroidMetricsWorkerTest";
|
||||
|
||||
@Rule
|
||||
public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
|
||||
|
||||
@Rule
|
||||
public WorkManagerTestRule workManagerTestRule = new WorkManagerTestRule();
|
||||
|
||||
/**
|
||||
* A test for easy manual testing.
|
||||
*/
|
||||
@Ignore
|
||||
@Test
|
||||
public void testGenerateReport() throws IOException {
|
||||
String json = FDroidMetricsWorker.generateReport(
|
||||
InstrumentationRegistry.getInstrumentation().getTargetContext());
|
||||
System.out.println(json);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWorkRequest() throws ExecutionException, InterruptedException {
|
||||
OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(FDroidMetricsWorker.class).build();
|
||||
workManagerTestRule.workManager.enqueue(request).getResult();
|
||||
ListenableFuture<WorkInfo> workInfo = workManagerTestRule.workManager.getWorkInfoById(request.getId());
|
||||
assertEquals(WorkInfo.State.SUCCEEDED, workInfo.get().getState());
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package org.fdroid.fdroid.work;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.work.Configuration;
|
||||
import androidx.work.WorkManager;
|
||||
import androidx.work.testing.SynchronousExecutor;
|
||||
import androidx.work.testing.WorkManagerTestInitHelper;
|
||||
import org.junit.rules.TestWatcher;
|
||||
import org.junit.runner.Description;
|
||||
|
||||
public class WorkManagerTestRule extends TestWatcher {
|
||||
Context targetContext;
|
||||
Context testContext;
|
||||
Configuration configuration;
|
||||
WorkManager workManager;
|
||||
|
||||
@Override
|
||||
protected void starting(Description description) {
|
||||
final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
|
||||
targetContext = instrumentation.getTargetContext();
|
||||
testContext = instrumentation.getContext();
|
||||
configuration = new Configuration.Builder()
|
||||
.setMinimumLoggingLevel(Log.DEBUG)
|
||||
.setExecutor(new SynchronousExecutor())
|
||||
.build();
|
||||
|
||||
WorkManagerTestInitHelper.initializeTestWorkManager(targetContext, configuration);
|
||||
workManager = WorkManager.getInstance(targetContext);
|
||||
}
|
||||
}
|
25
app/src/androidTest/proguard-rules.pro
vendored
Normal file
25
app/src/androidTest/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
-dontoptimize
|
||||
-dontwarn
|
||||
-dontobfuscate
|
||||
|
||||
-dontwarn android.test.**
|
||||
-dontwarn android.support.test.**
|
||||
-dontnote junit.framework.**
|
||||
-dontnote junit.runner.**
|
||||
|
||||
# Uncomment this if you use Mockito
|
||||
#-dontwarn org.mockito.**
|
||||
|
||||
-keep class org.hamcrest.** { *; }
|
||||
-dontwarn org.hamcrest.**
|
||||
|
||||
-keep class org.junit.** { *; }
|
||||
-dontwarn org.junit.**
|
||||
|
||||
-keep class junit.** { *; }
|
||||
-dontwarn junit.**
|
||||
|
||||
# This is necessary so that RemoteWorkManager can be initialized (also marked with @Keep)
|
||||
-keep class androidx.work.multiprocess.RemoteWorkManagerClient {
|
||||
public <init>(...);
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Senecto Limited
|
||||
*
|
||||
* This program is 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid.nearby;
|
||||
|
||||
/**
|
||||
* Dummy version for basic app flavor.
|
||||
*/
|
||||
|
||||
public class BluetoothClient {
|
||||
|
||||
public BluetoothClient(String ignored) {
|
||||
}
|
||||
|
||||
public BluetoothConnection openConnection() {
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package org.fdroid.fdroid.nearby;
|
||||
|
||||
public class LocalRepoManager {
|
||||
public static final String[] WEB_ROOT_ASSET_FILES = {};
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Hans-Christoph Steiner <hans@eds.org>
|
||||
*
|
||||
* This program is 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid.nearby;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* Dummy version for basic app flavor.
|
||||
*/
|
||||
public class SDCardScannerService {
|
||||
public static void scan(Context context) {
|
||||
}
|
||||
}
|
30
app/src/basic/java/org/fdroid/fdroid/nearby/SwapService.java
Normal file
30
app/src/basic/java/org/fdroid/fdroid/nearby/SwapService.java
Normal file
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Hans-Christoph Steiner <hans@eds.org>
|
||||
*
|
||||
* This program is 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid.nearby;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* Dummy version for basic app flavor.
|
||||
*/
|
||||
public class SwapService {
|
||||
public static void start(Context context) {
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Senecto Limited
|
||||
*
|
||||
* This program is 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid.nearby;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
/**
|
||||
* Dummy version for basic app flavor.
|
||||
*/
|
||||
public class SwapWorkflowActivity {
|
||||
|
||||
public static final String EXTRA_PREVENT_FURTHER_SWAP_REQUESTS = "preventFurtherSwap";
|
||||
|
||||
public static void requestSwap(Context context, Uri uri) {
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Senecto Limited
|
||||
*
|
||||
* This program is 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid.nearby;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import android.content.Intent;
|
||||
|
||||
/**
|
||||
* Dummy version for basic app flavor.
|
||||
*/
|
||||
public class TreeUriScannerIntentService {
|
||||
public static void onActivityResult(AppCompatActivity activity, Intent intent) {
|
||||
throw new IllegalStateException("unimplemented");
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Senecto Limited
|
||||
*
|
||||
* This program is 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid.nearby;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Dummy version for basic app flavor.
|
||||
*/
|
||||
public class WifiStateChangeService {
|
||||
public static void start(Context context, @Nullable Intent intent) {
|
||||
}
|
||||
|
||||
public class WifiInfoThread extends Thread {
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Senecto Limited
|
||||
*
|
||||
* This program is 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid.nearby.peers;
|
||||
|
||||
import org.fdroid.fdroid.data.NewRepoConfig;
|
||||
|
||||
/**
|
||||
* Dummy version for basic app flavor.
|
||||
*/
|
||||
public class WifiPeer {
|
||||
public WifiPeer(NewRepoConfig config) {
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Senecto Limited
|
||||
*
|
||||
* This program is 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid.panic;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* Dummy version for basic app flavor.
|
||||
*/
|
||||
public class HidingManager {
|
||||
|
||||
public static boolean isHidden(Context context) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void showHideDialog(final Context context) {
|
||||
throw new IllegalStateException("unimplemented");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Senecto Limited
|
||||
*
|
||||
* This program is 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid.views.main;
|
||||
|
||||
import android.widget.FrameLayout;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.views.PreferencesFragment;
|
||||
import org.fdroid.fdroid.views.updates.UpdatesViewBinder;
|
||||
|
||||
/**
|
||||
* Decides which view on the main screen to attach to a given {@link FrameLayout}. This class
|
||||
* doesn't know which view it will be rendering at the time it is constructed. Rather, at some
|
||||
* point in the future the {@link MainViewAdapter} will have information about which view we
|
||||
* are required to render, and will invoke the relevant "bind*()" method on this class.
|
||||
*/
|
||||
class MainViewController extends RecyclerView.ViewHolder {
|
||||
|
||||
private final AppCompatActivity activity;
|
||||
private final FrameLayout frame;
|
||||
|
||||
@Nullable
|
||||
private UpdatesViewBinder updatesView = null;
|
||||
|
||||
MainViewController(AppCompatActivity activity, FrameLayout frame) {
|
||||
super(frame);
|
||||
this.activity = activity;
|
||||
this.frame = frame;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see LatestViewBinder
|
||||
*/
|
||||
public void bindLatestView() {
|
||||
new LatestViewBinder(activity, frame);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see UpdatesViewBinder
|
||||
*/
|
||||
public void bindUpdates() {
|
||||
if (updatesView == null) {
|
||||
updatesView = new UpdatesViewBinder(activity, frame);
|
||||
}
|
||||
|
||||
updatesView.bind();
|
||||
}
|
||||
|
||||
public void unbindUpdates() {
|
||||
if (updatesView != null) {
|
||||
updatesView.unbind();
|
||||
}
|
||||
}
|
||||
|
||||
public void bindCategoriesView() {
|
||||
throw new IllegalStateException("unimplemented");
|
||||
}
|
||||
|
||||
public void bindSwapView() {
|
||||
throw new IllegalStateException("unimplemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches a {@link PreferencesFragment} to the view. Everything else is managed by the
|
||||
* fragment itself, so no further work needs to be done by this view binder.
|
||||
* <p>
|
||||
* Note: It is tricky to attach a {@link Fragment} to a view from this view holder. This is due
|
||||
* to the way in which the {@link RecyclerView} will reuse existing views and ask us to
|
||||
* put a settings fragment in there at arbitrary times. Usually it wont be the same view we
|
||||
* attached the fragment to last time, which causes weirdness. The solution is to use code from
|
||||
* the com.lsjwzh.widget.recyclerviewpager.FragmentStatePagerAdapter which manages this.
|
||||
* The code has been ported to {@link SettingsView}.
|
||||
*
|
||||
* @see SettingsView
|
||||
*/
|
||||
public void bindSettingsView() {
|
||||
activity.getLayoutInflater().inflate(R.layout.main_tab_settings, frame, true);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package org.fdroid.fdroid.views.main;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
class NearbyViewBinder {
|
||||
public static void updateUsbOtg(Context context) {
|
||||
throw new IllegalStateException("unimplemented");
|
||||
}
|
||||
}
|
BIN
app/src/basic/res/drawable-hdpi/ic_launcher.png
Normal file
BIN
app/src/basic/res/drawable-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
app/src/basic/res/drawable-mdpi/ic_launcher.png
Normal file
BIN
app/src/basic/res/drawable-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.5 KiB |
BIN
app/src/basic/res/drawable-xhdpi/ic_launcher.png
Normal file
BIN
app/src/basic/res/drawable-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
BIN
app/src/basic/res/drawable-xxhdpi/ic_launcher.png
Normal file
BIN
app/src/basic/res/drawable-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
BIN
app/src/basic/res/drawable-xxxhdpi/ic_launcher.png
Normal file
BIN
app/src/basic/res/drawable-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 65 KiB |
16
app/src/basic/res/menu/main_activity_screens.xml
Normal file
16
app/src/basic/res/menu/main_activity_screens.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:ignore="MenuTitle">
|
||||
<!-- android:title and android:icon are set dynamically in MainActivity -->
|
||||
<item
|
||||
app:showAsAction="ifRoom|withText"
|
||||
android:id="@+id/latest"/>
|
||||
<item
|
||||
app:showAsAction="ifRoom|withText"
|
||||
android:id="@+id/updates"/>
|
||||
<item
|
||||
app:showAsAction="ifRoom|withText"
|
||||
android:id="@+id/settings"/>
|
||||
</menu>
|
5
app/src/basic/res/values/strings.xml
Normal file
5
app/src/basic/res/values/strings.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">F-Droid Basic</string>
|
||||
<string name="about_title">About F-Droid Basic</string>
|
||||
</resources>
|
183
app/src/basic/res/xml/preferences.xml
Normal file
183
app/src/basic/res/xml/preferences.xml
Normal file
@ -0,0 +1,183 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<PreferenceScreen android:title="@string/about_title"
|
||||
android:key="pref_about" />
|
||||
|
||||
<PreferenceCategory android:title="@string/preference_category__my_apps">
|
||||
<PreferenceScreen android:title="@string/preference_manage_installed_apps">
|
||||
<intent
|
||||
android:action="android.intent.action.MAIN"
|
||||
android:targetPackage="@string/applicationId"
|
||||
android:targetClass="org.fdroid.fdroid.views.installed.InstalledAppsActivity"/>
|
||||
</PreferenceScreen>
|
||||
<PreferenceScreen
|
||||
android:title="@string/menu_manage"
|
||||
android:summary="@string/repositories_summary">
|
||||
<intent
|
||||
android:action="android.intent.action.MAIN"
|
||||
android:targetPackage="@string/applicationId"
|
||||
android:targetClass="org.fdroid.fdroid.views.ManageReposActivity"/>
|
||||
</PreferenceScreen>
|
||||
<PreferenceScreen
|
||||
android:key="installHistory"
|
||||
android:visible="false"
|
||||
android:title="@string/install_history"
|
||||
android:summary="@string/install_history_summary">
|
||||
<intent
|
||||
android:action="android.intent.action.MAIN"
|
||||
android:targetPackage="@string/applicationId"
|
||||
android:targetClass="org.fdroid.fdroid.views.InstallHistoryActivity"/>
|
||||
</PreferenceScreen>
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/updates">
|
||||
<org.fdroid.fdroid.views.LiveSeekBarPreference
|
||||
android:key="overWifi"
|
||||
android:title="@string/over_wifi"
|
||||
android:defaultValue="@integer/defaultOverWifi"
|
||||
android:layout="@layout/preference_seekbar"/>
|
||||
<org.fdroid.fdroid.views.LiveSeekBarPreference
|
||||
android:key="overData"
|
||||
android:title="@string/over_data"
|
||||
android:defaultValue="@integer/defaultOverData"
|
||||
android:layout="@layout/preference_seekbar"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:title="@string/update_auto_download"
|
||||
android:summary="@string/update_auto_download_summary"
|
||||
android:key="updateAutoDownload"/>
|
||||
<org.fdroid.fdroid.views.LiveSeekBarPreference
|
||||
android:key="updateIntervalSeekBarPosition"
|
||||
android:title="@string/update_interval"
|
||||
android:defaultValue="@integer/defaultUpdateInterval"
|
||||
android:layout="@layout/preference_seekbar"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:title="@string/notify"
|
||||
android:defaultValue="true"
|
||||
android:key="updateNotify"/>
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/display"
|
||||
android:key="pref_category_display">
|
||||
<ListPreference
|
||||
android:title="@string/pref_language"
|
||||
android:key="language"/>
|
||||
<ListPreference
|
||||
android:title="@string/theme"
|
||||
android:key="theme"
|
||||
android:defaultValue="light"
|
||||
android:entries="@array/themeNames"
|
||||
android:entryValues="@array/themeValues"/>
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/appcompatibility"
|
||||
android:key="pref_category_appcompatibility">
|
||||
<SwitchPreferenceCompat
|
||||
android:title="@string/show_incompat_versions"
|
||||
android:defaultValue="false"
|
||||
android:key="incompatibleVersions"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:title="@string/show_anti_feature_apps"
|
||||
android:defaultValue="false"
|
||||
android:key="showAntiFeatureApps"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:title="@string/force_touch_apps"
|
||||
android:defaultValue="false"
|
||||
android:key="ignoreTouchscreen"/>
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/proxy">
|
||||
<SwitchPreferenceCompat
|
||||
android:key="useTor"
|
||||
android:summary="@string/useTorSummary"
|
||||
android:title="@string/useTor"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="enableProxy"
|
||||
android:title="@string/enable_proxy_title"
|
||||
android:summary="@string/enable_proxy_summary"/>
|
||||
<EditTextPreference
|
||||
android:key="proxyHost"
|
||||
android:title="@string/proxy_host"
|
||||
android:summary="@string/proxy_host_summary"
|
||||
android:dependency="enableProxy"/>
|
||||
<EditTextPreference
|
||||
android:key="proxyPort"
|
||||
android:title="@string/proxy_port"
|
||||
android:summary="@string/proxy_port_summary"
|
||||
android:dependency="enableProxy"/>
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="pref_category_privacy"
|
||||
android:title="@string/privacy">
|
||||
<SwitchPreferenceCompat
|
||||
android:key="promptToSendCrashReports"
|
||||
android:title="@string/prompt_to_send_crash_reports"
|
||||
android:summary="@string/prompt_to_send_crash_reports_summary"
|
||||
android:defaultValue="true"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="preventScreenshots"
|
||||
android:summary="@string/preventScreenshots_summary"
|
||||
android:title="@string/preventScreenshots_title"/>
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/other"
|
||||
android:key="pref_category_other">
|
||||
<ListPreference
|
||||
android:title="@string/cache_downloaded"
|
||||
android:key="keepCacheFor"
|
||||
android:defaultValue="86400000"
|
||||
android:entries="@array/keepCacheNames"
|
||||
android:entryValues="@array/keepCacheValues"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:title="@string/expert"
|
||||
android:defaultValue="false"
|
||||
android:key="expert"/>
|
||||
<CheckBoxPreference
|
||||
android:key="unstableUpdates"
|
||||
android:title="@string/unstable_updates"
|
||||
android:summary="@string/unstable_updates_summary"
|
||||
android:defaultValue="false"
|
||||
android:dependency="expert"/>
|
||||
<CheckBoxPreference
|
||||
android:key="keepInstallHistory"
|
||||
android:title="@string/keep_install_history"
|
||||
android:summary="@string/keep_install_history_summary"
|
||||
android:defaultValue="false"
|
||||
android:dependency="expert"/>
|
||||
<CheckBoxPreference
|
||||
android:key="sendToFdroidMetrics"
|
||||
android:title="@string/send_to_fdroid_metrics"
|
||||
android:summary="@string/send_to_fdroid_metrics_summary"
|
||||
android:defaultValue="false"
|
||||
android:dependency="expert"/>
|
||||
<CheckBoxPreference
|
||||
android:key="hideAllNotifications"
|
||||
android:title="@string/hide_all_notifications"
|
||||
android:summary="@string/hide_all_notifications_summary"
|
||||
android:defaultValue="false"
|
||||
android:dependency="expert"/>
|
||||
<CheckBoxPreference
|
||||
android:key="sendVersionAndUUIDToServers"
|
||||
android:title="@string/send_version_and_uuid"
|
||||
android:summary="@string/send_version_and_uuid_summary"
|
||||
android:defaultValue="false"
|
||||
android:dependency="expert"/>
|
||||
<CheckBoxPreference
|
||||
android:key="forceOldIndex"
|
||||
android:title="@string/force_old_index"
|
||||
android:summary="@string/force_old_index_summary"
|
||||
android:defaultValue="false"
|
||||
android:dependency="expert"/>
|
||||
<CheckBoxPreference
|
||||
android:title="@string/system_installer"
|
||||
android:defaultValue="false"
|
||||
android:key="privilegedInstaller"
|
||||
android:persistent="false"
|
||||
android:dependency="expert"/>
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
8
app/src/debug/AndroidManifest.xml
Normal file
8
app/src/debug/AndroidManifest.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- This file should be outside of release manifest (in this case app/src/mock/Manifest.xml -->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!--required to enable/disable system animations from the app itself during Espresso test runs-->
|
||||
<uses-permission android:name="android.permission.SET_ANIMATION_SCALE" />
|
||||
</manifest>
|
179
app/src/full/AndroidManifest.xml
Normal file
179
app/src/full/AndroidManifest.xml
Normal file
@ -0,0 +1,179 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
* Copyright (C) 2010-2012 Ciaran Gultnieks
|
||||
* Copyright (C) 2013-2017 Peter Serwylo
|
||||
* Copyright (C) 2014-2015 Daniel Martí
|
||||
* Copyright (C) 2014-2018 Hans-Christoph Steiner
|
||||
* Copyright (C) 2016 Dominik Schürmann
|
||||
* Copyright (C) 2018 Torsten Grote
|
||||
* Copyright (C) 2018 Senecto Limited
|
||||
*
|
||||
* This program is 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.fdroid.fdroid"
|
||||
android:installLocation="auto">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.nfc"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.bluetooth"
|
||||
android:required="false" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.usb.host"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
<uses-permission
|
||||
android:name="android.permission.USB_PERMISSION"
|
||||
android:maxSdkVersion="22" /><!-- maybe unnecessary -->
|
||||
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
|
||||
<application>
|
||||
|
||||
<activity
|
||||
android:name=".nearby.SwapWorkflowActivity"
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
android:label="@string/swap"
|
||||
android:launchMode="singleTask"
|
||||
android:parentActivityName=".views.main.MainActivity"
|
||||
android:screenOrientation="portrait">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".views.main.MainActivity" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".panic.PanicPreferencesActivity"
|
||||
android:label="@string/panic_settings"
|
||||
android:parentActivityName=".views.main.MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="info.guardianproject.panic.action.CONNECT" />
|
||||
<action android:name="info.guardianproject.panic.action.DISCONNECT" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".views.main.MainActivity" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".panic.SelectInstalledAppsActivity"
|
||||
android:parentActivityName=".panic.PanicPreferencesActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".panic.PanicResponderActivity"
|
||||
android:noHistory="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
|
||||
<!-- this can never have launchMode singleTask or singleInstance! -->
|
||||
<intent-filter>
|
||||
<action android:name="info.guardianproject.panic.action.TRIGGER" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".panic.ExitActivity"
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
|
||||
<activity
|
||||
android:name=".panic.CalculatorActivity"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/ic_calculator_launcher"
|
||||
android:label="@string/hiding_calculator">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<receiver android:name=".nearby.WifiStateChangeReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.wifi.STATE_CHANGE" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name=".receiver.DeviceStorageReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.DEVICE_STORAGE_LOW" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".nearby.UsbDeviceAttachedReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
|
||||
android:resource="@xml/device_filter" />
|
||||
</receiver>
|
||||
<receiver android:name=".nearby.UsbDeviceDetachedReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.hardware.usb.action.USB_DEVICE_DETACHED" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.hardware.usb.action.USB_DEVICE_DETACHED"
|
||||
android:resource="@xml/device_filter" />
|
||||
</receiver>
|
||||
<receiver android:name=".nearby.UsbDeviceMediaMountedReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_EJECT" />
|
||||
<action android:name="android.intent.action.MEDIA_REMOVED" />
|
||||
<action android:name="android.intent.action.MEDIA_MOUNTED" />
|
||||
<action android:name="android.intent.action.MEDIA_BAD_REMOVAL" />
|
||||
|
||||
<data android:scheme="content" />
|
||||
<data android:scheme="file" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".nearby.WifiStateChangeService"
|
||||
android:exported="false" />
|
||||
<service android:name=".nearby.SwapService" />
|
||||
|
||||
<service
|
||||
android:name=".nearby.LocalRepoService"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".nearby.TreeUriScannerIntentService"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".nearby.SDCardScannerService"
|
||||
android:exported="false" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
123
app/src/full/java/javax/jmdns/impl/FDroidServiceInfo.java
Normal file
123
app/src/full/java/javax/jmdns/impl/FDroidServiceInfo.java
Normal file
@ -0,0 +1,123 @@
|
||||
package javax.jmdns.impl;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import javax.jmdns.ServiceInfo;
|
||||
import javax.jmdns.impl.util.ByteWrangler;
|
||||
|
||||
/**
|
||||
* The ServiceInfo class needs to be serialized in order to be sent as an Android broadcast.
|
||||
* In order to make it Parcelable (or Serializable for that matter), there are some package-scope
|
||||
* methods which needed to be used. Thus, this class is in the javax.jmdns.impl package so that
|
||||
* it can access those methods. This is as an alternative to modifying the source code of JmDNS.
|
||||
*/
|
||||
public class FDroidServiceInfo extends ServiceInfoImpl implements Parcelable {
|
||||
|
||||
public FDroidServiceInfo(ServiceInfo info) {
|
||||
super(info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the fingerprint of the signing key, or {@code null} if it is not set.
|
||||
*/
|
||||
public String getFingerprint() {
|
||||
// getPropertyString() will return "true" if the value is a zero-length byte array
|
||||
// so we just do a custom version using getPropertyBytes()
|
||||
byte[] data = getPropertyBytes("fingerprint");
|
||||
if (data == null || data.length == 0) {
|
||||
return null;
|
||||
}
|
||||
String fingerprint = ByteWrangler.readUTF(data, 0, data.length);
|
||||
if (TextUtils.isEmpty(fingerprint)) {
|
||||
return null;
|
||||
}
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
public String getRepoAddress() {
|
||||
return getURL(); // Automatically appends the "path" property if present, so no need to do it ourselves.
|
||||
}
|
||||
|
||||
private static byte[] readBytes(Parcel in) {
|
||||
byte[] bytes = new byte[in.readInt()];
|
||||
in.readByteArray(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public FDroidServiceInfo(Parcel in) {
|
||||
super(
|
||||
in.readString(),
|
||||
in.readString(),
|
||||
in.readString(),
|
||||
in.readInt(),
|
||||
in.readInt(),
|
||||
in.readInt(),
|
||||
in.readByte() != 0,
|
||||
readBytes(in)
|
||||
);
|
||||
|
||||
int addressCount = in.readInt();
|
||||
for (int i = 0; i < addressCount; i++) {
|
||||
try {
|
||||
addAddress((Inet4Address) Inet4Address.getByAddress(readBytes(in)));
|
||||
} catch (UnknownHostException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
addressCount = in.readInt();
|
||||
for (int i = 0; i < addressCount; i++) {
|
||||
try {
|
||||
addAddress((Inet6Address) Inet6Address.getByAddress(readBytes(in)));
|
||||
} catch (UnknownHostException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(getType());
|
||||
dest.writeString(getName());
|
||||
dest.writeString(getSubtype());
|
||||
dest.writeInt(getPort());
|
||||
dest.writeInt(getWeight());
|
||||
dest.writeInt(getPriority());
|
||||
dest.writeByte(isPersistent() ? (byte) 1 : (byte) 0);
|
||||
dest.writeInt(getTextBytes().length);
|
||||
dest.writeByteArray(getTextBytes());
|
||||
dest.writeInt(getInet4Addresses().length);
|
||||
for (int i = 0; i < getInet4Addresses().length; i++) {
|
||||
Inet4Address address = getInet4Addresses()[i];
|
||||
dest.writeInt(address.getAddress().length);
|
||||
dest.writeByteArray(address.getAddress());
|
||||
}
|
||||
dest.writeInt(getInet6Addresses().length);
|
||||
for (int i = 0; i < getInet6Addresses().length; i++) {
|
||||
Inet6Address address = getInet6Addresses()[i];
|
||||
dest.writeInt(address.getAddress().length);
|
||||
dest.writeByteArray(address.getAddress());
|
||||
}
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<FDroidServiceInfo> CREATOR = new Parcelable.Creator<FDroidServiceInfo>() {
|
||||
public FDroidServiceInfo createFromParcel(Parcel source) {
|
||||
return new FDroidServiceInfo(source);
|
||||
}
|
||||
|
||||
public FDroidServiceInfo[] newArray(int size) {
|
||||
return new FDroidServiceInfo[size];
|
||||
}
|
||||
};
|
||||
}
|
94
app/src/full/java/kellinwood/logging/AbstractLogger.java
Normal file
94
app/src/full/java/kellinwood/logging/AbstractLogger.java
Normal file
@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package kellinwood.logging;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
public abstract class AbstractLogger implements LoggerInterface {
|
||||
|
||||
protected String category;
|
||||
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss", Locale.ENGLISH);
|
||||
|
||||
public AbstractLogger(String category) {
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
protected String format(String level, String message) {
|
||||
return String.format("%s %s %s: %s\n", dateFormat.format(new Date()), level, category, message);
|
||||
}
|
||||
|
||||
protected abstract void write(String level, String message, Throwable t);
|
||||
|
||||
protected void writeFixNullMessage(String level, String message, Throwable t) {
|
||||
if (message == null) {
|
||||
if (t != null) message = t.getClass().getName();
|
||||
else message = "null";
|
||||
}
|
||||
write(level, message, t);
|
||||
}
|
||||
|
||||
public void debug(String message, Throwable t) {
|
||||
writeFixNullMessage(DEBUG, message, t);
|
||||
}
|
||||
|
||||
public void debug(String message) {
|
||||
writeFixNullMessage(DEBUG, message, null);
|
||||
}
|
||||
|
||||
public void error(String message, Throwable t) {
|
||||
writeFixNullMessage(ERROR, message, t);
|
||||
}
|
||||
|
||||
public void error(String message) {
|
||||
writeFixNullMessage(ERROR, message, null);
|
||||
}
|
||||
|
||||
public void info(String message, Throwable t) {
|
||||
writeFixNullMessage(INFO, message, t);
|
||||
}
|
||||
|
||||
public void info(String message) {
|
||||
writeFixNullMessage(INFO, message, null);
|
||||
}
|
||||
|
||||
public void warning(String message, Throwable t) {
|
||||
writeFixNullMessage(WARNING, message, t);
|
||||
}
|
||||
|
||||
public void warning(String message) {
|
||||
writeFixNullMessage(WARNING, message, null);
|
||||
}
|
||||
|
||||
public boolean isDebugEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isErrorEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isInfoEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isWarningEnabled() {
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package kellinwood.logging;
|
||||
|
||||
public class ConsoleLoggerFactory implements LoggerFactory {
|
||||
|
||||
public LoggerInterface getLogger(String category) {
|
||||
return new StreamLogger(category, System.out);
|
||||
}
|
||||
}
|
22
app/src/full/java/kellinwood/logging/LoggerFactory.java
Normal file
22
app/src/full/java/kellinwood/logging/LoggerFactory.java
Normal file
@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package kellinwood.logging;
|
||||
|
||||
public interface LoggerFactory {
|
||||
|
||||
public LoggerInterface getLogger(String category);
|
||||
}
|
52
app/src/full/java/kellinwood/logging/LoggerInterface.java
Normal file
52
app/src/full/java/kellinwood/logging/LoggerInterface.java
Normal file
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package kellinwood.logging;
|
||||
|
||||
public interface LoggerInterface {
|
||||
|
||||
public static final String ERROR = "ERROR";
|
||||
public static final String WARNING = "WARNING";
|
||||
public static final String INFO = "INFO";
|
||||
public static final String DEBUG = "DEBUG";
|
||||
|
||||
public boolean isErrorEnabled();
|
||||
|
||||
public void error(String message);
|
||||
|
||||
public void error(String message, Throwable t);
|
||||
|
||||
|
||||
public boolean isWarningEnabled();
|
||||
|
||||
public void warning(String message);
|
||||
|
||||
public void warning(String message, Throwable t);
|
||||
|
||||
public boolean isInfoEnabled();
|
||||
|
||||
public void info(String message);
|
||||
|
||||
public void info(String message, Throwable t);
|
||||
|
||||
public boolean isDebugEnabled();
|
||||
|
||||
public void debug(String message);
|
||||
|
||||
public void debug(String message, Throwable t);
|
||||
|
||||
|
||||
}
|
41
app/src/full/java/kellinwood/logging/LoggerManager.java
Normal file
41
app/src/full/java/kellinwood/logging/LoggerManager.java
Normal file
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package kellinwood.logging;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
public class LoggerManager {
|
||||
|
||||
static LoggerFactory factory = new NullLoggerFactory();
|
||||
|
||||
static Map<String, LoggerInterface> loggers = new TreeMap<String, LoggerInterface>();
|
||||
|
||||
public static void setLoggerFactory(LoggerFactory f) {
|
||||
factory = f;
|
||||
}
|
||||
|
||||
public static LoggerInterface getLogger(String category) {
|
||||
|
||||
LoggerInterface logger = loggers.get(category);
|
||||
if (logger == null) {
|
||||
logger = factory.getLogger(category);
|
||||
loggers.put(category, logger);
|
||||
}
|
||||
return logger;
|
||||
}
|
||||
}
|
70
app/src/full/java/kellinwood/logging/NullLoggerFactory.java
Normal file
70
app/src/full/java/kellinwood/logging/NullLoggerFactory.java
Normal file
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package kellinwood.logging;
|
||||
|
||||
public class NullLoggerFactory implements LoggerFactory {
|
||||
|
||||
static LoggerInterface logger = new LoggerInterface() {
|
||||
|
||||
public void debug(String message) {
|
||||
}
|
||||
|
||||
public void debug(String message, Throwable t) {
|
||||
}
|
||||
|
||||
public void error(String message) {
|
||||
}
|
||||
|
||||
public void error(String message, Throwable t) {
|
||||
}
|
||||
|
||||
public void info(String message) {
|
||||
}
|
||||
|
||||
public void info(String message, Throwable t) {
|
||||
}
|
||||
|
||||
public boolean isDebugEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isErrorEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isInfoEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isWarningEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void warning(String message) {
|
||||
}
|
||||
|
||||
public void warning(String message, Throwable t) {
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
public LoggerInterface getLogger(String category) {
|
||||
return logger;
|
||||
}
|
||||
|
||||
}
|
36
app/src/full/java/kellinwood/logging/StreamLogger.java
Normal file
36
app/src/full/java/kellinwood/logging/StreamLogger.java
Normal file
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package kellinwood.logging;
|
||||
|
||||
import java.io.PrintStream;
|
||||
|
||||
public class StreamLogger extends AbstractLogger {
|
||||
|
||||
PrintStream out;
|
||||
|
||||
public StreamLogger(String category, PrintStream out) {
|
||||
super(category);
|
||||
this.out = out;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void write(String level, String message, Throwable t) {
|
||||
out.print(format(level, message));
|
||||
if (t != null) t.printStackTrace(out);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
|
||||
package kellinwood.security.zipsigner;
|
||||
|
||||
public class AutoKeyException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public AutoKeyException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AutoKeyException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
130
app/src/full/java/kellinwood/security/zipsigner/Base64.java
Normal file
130
app/src/full/java/kellinwood/security/zipsigner/Base64.java
Normal file
@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
package kellinwood.security.zipsigner;
|
||||
|
||||
import kellinwood.logging.LoggerInterface;
|
||||
import kellinwood.logging.LoggerManager;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/*
|
||||
* This class provides Base64 encoding services using one of several possible
|
||||
* implementations available elsewhere in the classpath. Supported implementations
|
||||
* are android.util.Base64 and org.bouncycastle.util.encoders.Base64Encoder.
|
||||
* These APIs are accessed via reflection, and as long as at least one is available
|
||||
* Base64 encoding is possible. This technique provides compatibility across different
|
||||
* Android OS versions, and also allows zipsigner-lib to operate in desktop environments
|
||||
* as long as the BouncyCastle provider jar is in the classpath.
|
||||
*
|
||||
* android.util.Base64 was added in API level 8 (Android 2.2, Froyo)
|
||||
* org.bouncycastle.util.encoders.Base64Encoder was removed in API level 11 (Android 3.0, Honeycomb)
|
||||
*
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public class Base64 {
|
||||
|
||||
static Method aEncodeMethod = null; // Reference to the android.util.Base64.encode() method, if available
|
||||
static Method aDecodeMethod = null; // Reference to the android.util.Base64.decode() method, if available
|
||||
|
||||
static Object bEncoder = null; // Reference to an org.bouncycastle.util.encoders.Base64Encoder instance, if available
|
||||
static Method bEncodeMethod = null; // Reference to the bEncoder.encode() method, if available
|
||||
|
||||
static Object bDecoder = null; // Reference to an org.bouncycastle.util.encoders.Base64Encoder instance, if available
|
||||
static Method bDecodeMethod = null; // Reference to the bEncoder.encode() method, if available
|
||||
|
||||
static LoggerInterface logger = null;
|
||||
|
||||
static {
|
||||
|
||||
Class<Object> clazz;
|
||||
|
||||
logger = LoggerManager.getLogger(Base64.class.getName());
|
||||
|
||||
try {
|
||||
clazz = (Class<Object>) Class.forName("android.util.Base64");
|
||||
// Looking for encode( byte[] input, int flags)
|
||||
aEncodeMethod = clazz.getMethod("encode", byte[].class, Integer.TYPE);
|
||||
aDecodeMethod = clazz.getMethod("decode", byte[].class, Integer.TYPE);
|
||||
logger.info(clazz.getName() + " is available.");
|
||||
} catch (ClassNotFoundException x) {
|
||||
} // Ignore
|
||||
catch (Exception x) {
|
||||
logger.error("Failed to initialize use of android.util.Base64", x);
|
||||
}
|
||||
|
||||
try {
|
||||
clazz = (Class<Object>) Class.forName("org.bouncycastle.util.encoders.Base64Encoder");
|
||||
bEncoder = clazz.newInstance();
|
||||
// Looking for encode( byte[] input, int offset, int length, OutputStream output)
|
||||
bEncodeMethod = clazz.getMethod("encode", byte[].class, Integer.TYPE, Integer.TYPE, OutputStream.class);
|
||||
logger.info(clazz.getName() + " is available.");
|
||||
// Looking for decode( byte[] input, int offset, int length, OutputStream output)
|
||||
bDecodeMethod = clazz.getMethod("decode", byte[].class, Integer.TYPE, Integer.TYPE, OutputStream.class);
|
||||
|
||||
} catch (ClassNotFoundException x) {
|
||||
} // Ignore
|
||||
catch (Exception x) {
|
||||
logger.error("Failed to initialize use of org.bouncycastle.util.encoders.Base64Encoder", x);
|
||||
}
|
||||
|
||||
if (aEncodeMethod == null && bEncodeMethod == null)
|
||||
throw new IllegalStateException("No base64 encoder implementation is available.");
|
||||
}
|
||||
|
||||
|
||||
public static String encode(byte[] data) {
|
||||
try {
|
||||
if (aEncodeMethod != null) {
|
||||
// Invoking a static method call, using null for the instance value
|
||||
byte[] encodedBytes = (byte[]) aEncodeMethod.invoke(null, data, 2);
|
||||
return new String(encodedBytes);
|
||||
} else if (bEncodeMethod != null) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
bEncodeMethod.invoke(bEncoder, data, 0, data.length, baos);
|
||||
return new String(baos.toByteArray());
|
||||
}
|
||||
} catch (Exception x) {
|
||||
throw new IllegalStateException(x.getClass().getName() + ": " + x.getMessage());
|
||||
}
|
||||
|
||||
|
||||
throw new IllegalStateException("No base64 encoder implementation is available.");
|
||||
}
|
||||
|
||||
public static byte[] decode(byte[] data) {
|
||||
try {
|
||||
if (aDecodeMethod != null) {
|
||||
// Invoking a static method call, using null for the instance value
|
||||
byte[] decodedBytes = (byte[]) aDecodeMethod.invoke(null, data, 2);
|
||||
return decodedBytes;
|
||||
} else if (bDecodeMethod != null) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
bDecodeMethod.invoke(bEncoder, data, 0, data.length, baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
} catch (Exception x) {
|
||||
throw new IllegalStateException(x.getClass().getName() + ": " + x.getMessage());
|
||||
}
|
||||
|
||||
|
||||
throw new IllegalStateException("No base64 encoder implementation is available.");
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
|
||||
package kellinwood.security.zipsigner;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Default resource adapter.
|
||||
*/
|
||||
public class DefaultResourceAdapter implements ResourceAdapter {
|
||||
|
||||
@Override
|
||||
public String getString(Item item, Object... args) {
|
||||
|
||||
switch (item) {
|
||||
case INPUT_SAME_AS_OUTPUT_ERROR:
|
||||
return "Input and output files are the same. Specify a different name for the output.";
|
||||
case AUTO_KEY_SELECTION_ERROR:
|
||||
return "Unable to auto-select key for signing " + args[0];
|
||||
case LOADING_CERTIFICATE_AND_KEY:
|
||||
return "Loading certificate and private key";
|
||||
case PARSING_CENTRAL_DIRECTORY:
|
||||
return "Parsing the input's central directory";
|
||||
case GENERATING_MANIFEST:
|
||||
return "Generating manifest";
|
||||
case GENERATING_SIGNATURE_FILE:
|
||||
return "Generating signature file";
|
||||
case GENERATING_SIGNATURE_BLOCK:
|
||||
return "Generating signature block file";
|
||||
case COPYING_ZIP_ENTRY:
|
||||
return String.format(Locale.ENGLISH, "Copying zip entry %d of %d", args[0], args[1]);
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown item " + item);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
package kellinwood.security.zipsigner;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Produces the classic hex dump with an address column, hex data
|
||||
* section (16 bytes per row) and right-column printable character dislpay.
|
||||
*/
|
||||
public class HexDumpEncoder {
|
||||
|
||||
static HexEncoder encoder = new HexEncoder();
|
||||
|
||||
public static String encode(byte[] data) {
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
encoder.encode(data, 0, data.length, baos);
|
||||
byte[] hex = baos.toByteArray();
|
||||
|
||||
StringBuilder hexDumpOut = new StringBuilder();
|
||||
for (int i = 0; i < hex.length; i += 32) {
|
||||
|
||||
int max = Math.min(i + 32, hex.length);
|
||||
|
||||
StringBuilder hexOut = new StringBuilder();
|
||||
StringBuilder chrOut = new StringBuilder();
|
||||
|
||||
hexOut.append(String.format("%08x: ", (i / 2)));
|
||||
|
||||
for (int j = i; j < max; j += 2) {
|
||||
hexOut.append(Character.valueOf((char) hex[j]));
|
||||
hexOut.append(Character.valueOf((char) hex[j + 1]));
|
||||
if ((j + 2) % 4 == 0) hexOut.append(' ');
|
||||
|
||||
int dataChar = data[j / 2];
|
||||
if (dataChar >= 32 && dataChar < 127) chrOut.append(Character.valueOf((char) dataChar));
|
||||
else chrOut.append('.');
|
||||
|
||||
}
|
||||
|
||||
hexDumpOut.append(hexOut.toString());
|
||||
for (int k = hexOut.length(); k < 50; k++) hexDumpOut.append(' ');
|
||||
hexDumpOut.append(" ");
|
||||
hexDumpOut.append(chrOut);
|
||||
hexDumpOut.append("\n");
|
||||
}
|
||||
|
||||
return hexDumpOut.toString();
|
||||
} catch (IOException x) {
|
||||
throw new IllegalStateException(x.getClass().getName() + ": " + x.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
182
app/src/full/java/kellinwood/security/zipsigner/HexEncoder.java
Normal file
182
app/src/full/java/kellinwood/security/zipsigner/HexEncoder.java
Normal file
@ -0,0 +1,182 @@
|
||||
|
||||
package kellinwood.security.zipsigner;
|
||||
|
||||
/*
|
||||
This file is a copy of org.bouncycastle.util.encoders.HexEncoder.
|
||||
|
||||
Please note: our license is an adaptation of the MIT X11 License and should be read as such.
|
||||
License
|
||||
|
||||
Copyright (c) 2000 - 2011 The Legion Of The Bouncy Castle (http://www.bouncycastle.org)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class HexEncoder {
|
||||
protected final byte[] encodingTable =
|
||||
{
|
||||
(byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', (byte) '6', (byte) '7',
|
||||
(byte) '8', (byte) '9', (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f'
|
||||
};
|
||||
|
||||
/*
|
||||
* set up the decoding table.
|
||||
*/
|
||||
protected final byte[] decodingTable = new byte[128];
|
||||
|
||||
protected void initialiseDecodingTable() {
|
||||
for (int i = 0; i < encodingTable.length; i++) {
|
||||
decodingTable[encodingTable[i]] = (byte) i;
|
||||
}
|
||||
|
||||
decodingTable['A'] = decodingTable['a'];
|
||||
decodingTable['B'] = decodingTable['b'];
|
||||
decodingTable['C'] = decodingTable['c'];
|
||||
decodingTable['D'] = decodingTable['d'];
|
||||
decodingTable['E'] = decodingTable['e'];
|
||||
decodingTable['F'] = decodingTable['f'];
|
||||
}
|
||||
|
||||
public HexEncoder() {
|
||||
initialiseDecodingTable();
|
||||
}
|
||||
|
||||
/**
|
||||
* encode the input data producing a Hex output stream.
|
||||
*
|
||||
* @return the number of bytes produced.
|
||||
*/
|
||||
public int encode(
|
||||
byte[] data,
|
||||
int off,
|
||||
int length,
|
||||
OutputStream out)
|
||||
throws IOException {
|
||||
for (int i = off; i < (off + length); i++) {
|
||||
int v = data[i] & 0xff;
|
||||
|
||||
out.write(encodingTable[(v >>> 4)]);
|
||||
out.write(encodingTable[v & 0xf]);
|
||||
}
|
||||
|
||||
return length * 2;
|
||||
}
|
||||
|
||||
private boolean ignore(
|
||||
char c) {
|
||||
return (c == '\n' || c == '\r' || c == '\t' || c == ' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* decode the Hex encoded byte data writing it to the given output stream,
|
||||
* whitespace characters will be ignored.
|
||||
*
|
||||
* @return the number of bytes produced.
|
||||
*/
|
||||
public int decode(
|
||||
byte[] data,
|
||||
int off,
|
||||
int length,
|
||||
OutputStream out)
|
||||
throws IOException {
|
||||
byte b1, b2;
|
||||
int outLen = 0;
|
||||
|
||||
int end = off + length;
|
||||
|
||||
while (end > off) {
|
||||
if (!ignore((char) data[end - 1])) {
|
||||
break;
|
||||
}
|
||||
|
||||
end--;
|
||||
}
|
||||
|
||||
int i = off;
|
||||
while (i < end) {
|
||||
while (i < end && ignore((char) data[i])) {
|
||||
i++;
|
||||
}
|
||||
|
||||
b1 = decodingTable[data[i++]];
|
||||
|
||||
while (i < end && ignore((char) data[i])) {
|
||||
i++;
|
||||
}
|
||||
|
||||
b2 = decodingTable[data[i++]];
|
||||
|
||||
out.write((b1 << 4) | b2);
|
||||
|
||||
outLen++;
|
||||
}
|
||||
|
||||
return outLen;
|
||||
}
|
||||
|
||||
/**
|
||||
* decode the Hex encoded String data writing it to the given output stream,
|
||||
* whitespace characters will be ignored.
|
||||
*
|
||||
* @return the number of bytes produced.
|
||||
*/
|
||||
public int decode(
|
||||
String data,
|
||||
OutputStream out)
|
||||
throws IOException {
|
||||
byte b1, b2;
|
||||
int length = 0;
|
||||
|
||||
int end = data.length();
|
||||
|
||||
while (end > 0) {
|
||||
if (!ignore(data.charAt(end - 1))) {
|
||||
break;
|
||||
}
|
||||
|
||||
end--;
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
while (i < end) {
|
||||
while (i < end && ignore(data.charAt(i))) {
|
||||
i++;
|
||||
}
|
||||
|
||||
b1 = decodingTable[data.charAt(i++)];
|
||||
|
||||
while (i < end && ignore(data.charAt(i))) {
|
||||
i++;
|
||||
}
|
||||
|
||||
b2 = decodingTable[data.charAt(i++)];
|
||||
|
||||
out.write((b1 << 4) | b2);
|
||||
|
||||
length++;
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
}
|
95
app/src/full/java/kellinwood/security/zipsigner/KeySet.java
Normal file
95
app/src/full/java/kellinwood/security/zipsigner/KeySet.java
Normal file
@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package kellinwood.security.zipsigner;
|
||||
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
public class KeySet {
|
||||
|
||||
String name;
|
||||
|
||||
// certificate
|
||||
X509Certificate publicKey = null;
|
||||
|
||||
// private key
|
||||
PrivateKey privateKey = null;
|
||||
|
||||
// signature block template
|
||||
byte[] sigBlockTemplate = null;
|
||||
|
||||
String signatureAlgorithm = "SHA1withRSA";
|
||||
|
||||
public KeySet() {
|
||||
}
|
||||
|
||||
public KeySet(String name, X509Certificate publicKey, PrivateKey privateKey, byte[] sigBlockTemplate) {
|
||||
this.name = name;
|
||||
this.publicKey = publicKey;
|
||||
this.privateKey = privateKey;
|
||||
this.sigBlockTemplate = sigBlockTemplate;
|
||||
}
|
||||
|
||||
public KeySet(String name, X509Certificate publicKey, PrivateKey privateKey, String signatureAlgorithm, byte[] sigBlockTemplate) {
|
||||
this.name = name;
|
||||
this.publicKey = publicKey;
|
||||
this.privateKey = privateKey;
|
||||
if (signatureAlgorithm != null) this.signatureAlgorithm = signatureAlgorithm;
|
||||
this.sigBlockTemplate = sigBlockTemplate;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public X509Certificate getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public void setPublicKey(X509Certificate publicKey) {
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
public PrivateKey getPrivateKey() {
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
public void setPrivateKey(PrivateKey privateKey) {
|
||||
this.privateKey = privateKey;
|
||||
}
|
||||
|
||||
public byte[] getSigBlockTemplate() {
|
||||
return sigBlockTemplate;
|
||||
}
|
||||
|
||||
public void setSigBlockTemplate(byte[] sigBlockTemplate) {
|
||||
this.sigBlockTemplate = sigBlockTemplate;
|
||||
}
|
||||
|
||||
public String getSignatureAlgorithm() {
|
||||
return signatureAlgorithm;
|
||||
}
|
||||
|
||||
public void setSignatureAlgorithm(String signatureAlgorithm) {
|
||||
if (signatureAlgorithm == null) signatureAlgorithm = "SHA1withRSA";
|
||||
else this.signatureAlgorithm = signatureAlgorithm;
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package kellinwood.security.zipsigner;
|
||||
|
||||
public class ProgressEvent {
|
||||
|
||||
public static final int PRORITY_NORMAL = 0;
|
||||
public static final int PRORITY_IMPORTANT = 1;
|
||||
|
||||
private String message;
|
||||
private int percentDone;
|
||||
private int priority;
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public int getPercentDone() {
|
||||
return percentDone;
|
||||
}
|
||||
|
||||
public void setPercentDone(int percentDone) {
|
||||
this.percentDone = percentDone;
|
||||
}
|
||||
|
||||
public int getPriority() {
|
||||
return priority;
|
||||
}
|
||||
|
||||
public void setPriority(int priority) {
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package kellinwood.security.zipsigner;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class ProgressHelper {
|
||||
|
||||
private int progressTotalItems = 0;
|
||||
private int progressCurrentItem = 0;
|
||||
private ProgressEvent progressEvent = new ProgressEvent();
|
||||
|
||||
public void initProgress() {
|
||||
progressTotalItems = 10000;
|
||||
progressCurrentItem = 0;
|
||||
}
|
||||
|
||||
public int getProgressTotalItems() {
|
||||
return progressTotalItems;
|
||||
}
|
||||
|
||||
public void setProgressTotalItems(int progressTotalItems) {
|
||||
this.progressTotalItems = progressTotalItems;
|
||||
}
|
||||
|
||||
public int getProgressCurrentItem() {
|
||||
return progressCurrentItem;
|
||||
}
|
||||
|
||||
public void setProgressCurrentItem(int progressCurrentItem) {
|
||||
this.progressCurrentItem = progressCurrentItem;
|
||||
}
|
||||
|
||||
public void progress(int priority, String message) {
|
||||
|
||||
progressCurrentItem += 1;
|
||||
|
||||
int percentDone;
|
||||
if (progressTotalItems == 0) percentDone = 0;
|
||||
else percentDone = (100 * progressCurrentItem) / progressTotalItems;
|
||||
|
||||
// Notify listeners here
|
||||
for (ProgressListener listener : listeners) {
|
||||
progressEvent.setMessage(message);
|
||||
progressEvent.setPercentDone(percentDone);
|
||||
progressEvent.setPriority(priority);
|
||||
listener.onProgress(progressEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private ArrayList<ProgressListener> listeners = new ArrayList<ProgressListener>();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public synchronized void addProgressListener(ProgressListener l) {
|
||||
ArrayList<ProgressListener> list = (ArrayList<ProgressListener>) listeners.clone();
|
||||
list.add(l);
|
||||
listeners = list;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public synchronized void removeProgressListener(ProgressListener l) {
|
||||
ArrayList<ProgressListener> list = (ArrayList<ProgressListener>) listeners.clone();
|
||||
list.remove(l);
|
||||
listeners = list;
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package kellinwood.security.zipsigner;
|
||||
|
||||
public interface ProgressListener {
|
||||
|
||||
/**
|
||||
* Called to notify the listener that progress has been made during
|
||||
* the zip signing operation.
|
||||
*/
|
||||
public void onProgress(ProgressEvent event);
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
|
||||
package kellinwood.security.zipsigner;
|
||||
|
||||
/**
|
||||
* Interface to obtain internationalized strings for the progress events.
|
||||
*/
|
||||
public interface ResourceAdapter {
|
||||
|
||||
public enum Item {
|
||||
INPUT_SAME_AS_OUTPUT_ERROR,
|
||||
AUTO_KEY_SELECTION_ERROR,
|
||||
LOADING_CERTIFICATE_AND_KEY,
|
||||
PARSING_CENTRAL_DIRECTORY,
|
||||
GENERATING_MANIFEST,
|
||||
GENERATING_SIGNATURE_FILE,
|
||||
GENERATING_SIGNATURE_BLOCK,
|
||||
COPYING_ZIP_ENTRY
|
||||
}
|
||||
|
||||
;
|
||||
|
||||
public String getString(Item item, Object... args);
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package kellinwood.security.zipsigner;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.PrivateKey;
|
||||
|
||||
@SuppressWarnings("restriction")
|
||||
public class ZipSignature {
|
||||
|
||||
byte[] beforeAlgorithmIdBytes = {0x30, 0x21};
|
||||
|
||||
// byte[] algorithmIdBytes;
|
||||
// algorithmIdBytes = sun.security.x509.AlgorithmId.get("SHA1").encode();
|
||||
byte[] algorithmIdBytes = {0x30, 0x09, 0x06, 0x05, 0x2B, 0x0E, 0x03, 0x02, 0x1A, 0x05, 0x00};
|
||||
|
||||
byte[] afterAlgorithmIdBytes = {0x04, 0x14};
|
||||
|
||||
Cipher cipher;
|
||||
|
||||
MessageDigest md;
|
||||
|
||||
|
||||
public ZipSignature() throws IOException, GeneralSecurityException {
|
||||
md = MessageDigest.getInstance("SHA1");
|
||||
cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
|
||||
}
|
||||
|
||||
public void initSign(PrivateKey privateKey) throws InvalidKeyException {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
|
||||
}
|
||||
|
||||
public void update(byte[] data) {
|
||||
md.update(data);
|
||||
}
|
||||
|
||||
public void update(byte[] data, int offset, int count) {
|
||||
md.update(data, offset, count);
|
||||
}
|
||||
|
||||
public byte[] sign() throws BadPaddingException, IllegalBlockSizeException {
|
||||
cipher.update(beforeAlgorithmIdBytes);
|
||||
cipher.update(algorithmIdBytes);
|
||||
cipher.update(afterAlgorithmIdBytes);
|
||||
cipher.update(md.digest());
|
||||
return cipher.doFinal();
|
||||
}
|
||||
}
|
796
app/src/full/java/kellinwood/security/zipsigner/ZipSigner.java
Normal file
796
app/src/full/java/kellinwood/security/zipsigner/ZipSigner.java
Normal file
@ -0,0 +1,796 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood
|
||||
* Copyright (C) 2008 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* This file is a heavily modified version of com.android.signapk.SignApk.java.
|
||||
* The changes include:
|
||||
* - addition of the signZip() convenience methods
|
||||
* - addition of a progress listener interface
|
||||
* - removal of main()
|
||||
* - switch to a signature generation method that verifies
|
||||
* in Android recovery
|
||||
* - eliminated dependency on sun.security and sun.misc APIs by
|
||||
* using signature block template files.
|
||||
*/
|
||||
|
||||
|
||||
package kellinwood.security.zipsigner;
|
||||
|
||||
import kellinwood.logging.LoggerInterface;
|
||||
import kellinwood.logging.LoggerManager;
|
||||
import kellinwood.zipio.ZioEntry;
|
||||
import kellinwood.zipio.ZipInput;
|
||||
import kellinwood.zipio.ZipOutput;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.EncryptedPrivateKeyInfo;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URL;
|
||||
import java.security.DigestOutputStream;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.Key;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyStore;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.Provider;
|
||||
import java.security.Security;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.KeySpec;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Observable;
|
||||
import java.util.Observer;
|
||||
import java.util.TreeMap;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* This is a modified copy of com.android.signapk.SignApk.java. It provides an
|
||||
* API to sign JAR files (including APKs and Zip/OTA updates) in
|
||||
* a way compatible with the mincrypt verifier, using SHA1 and RSA keys.
|
||||
* <p>
|
||||
* Please see the README.txt file in the root of this project for usage instructions.
|
||||
*/
|
||||
public class ZipSigner {
|
||||
|
||||
private boolean canceled = false;
|
||||
|
||||
private ProgressHelper progressHelper = new ProgressHelper();
|
||||
private ResourceAdapter resourceAdapter = new DefaultResourceAdapter();
|
||||
|
||||
static LoggerInterface log = null;
|
||||
|
||||
private static final String CERT_SF_NAME = "META-INF/CERT.SF";
|
||||
private static final String CERT_RSA_NAME = "META-INF/CERT.RSA";
|
||||
|
||||
// Files matching this pattern are not copied to the output.
|
||||
private static Pattern stripPattern =
|
||||
Pattern.compile("^META-INF/(.*)[.](SF|RSA|DSA)$");
|
||||
|
||||
Map<String, KeySet> loadedKeys = new HashMap<String, KeySet>();
|
||||
KeySet keySet = null;
|
||||
|
||||
public static LoggerInterface getLogger() {
|
||||
if (log == null) log = LoggerManager.getLogger(ZipSigner.class.getName());
|
||||
return log;
|
||||
}
|
||||
|
||||
public static final String MODE_AUTO_TESTKEY = "auto-testkey";
|
||||
public static final String MODE_AUTO_NONE = "auto-none";
|
||||
public static final String MODE_AUTO = "auto";
|
||||
public static final String KEY_NONE = "none";
|
||||
public static final String KEY_TESTKEY = "testkey";
|
||||
|
||||
// Allowable key modes.
|
||||
public static final String[] SUPPORTED_KEY_MODES =
|
||||
new String[]{MODE_AUTO_TESTKEY, MODE_AUTO, MODE_AUTO_NONE, "media", "platform", "shared", KEY_TESTKEY, KEY_NONE};
|
||||
|
||||
String keymode = KEY_TESTKEY; // backwards compatible with versions that only signed with this key
|
||||
|
||||
Map<String, String> autoKeyDetect = new HashMap<String, String>();
|
||||
|
||||
AutoKeyObservable autoKeyObservable = new AutoKeyObservable();
|
||||
|
||||
public ZipSigner() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
|
||||
// MD5 of the first 1458 bytes of the signature block generated by the key, mapped to the key name
|
||||
autoKeyDetect.put("aa9852bc5a53272ac8031d49b65e4b0e", "media");
|
||||
autoKeyDetect.put("e60418c4b638f20d0721e115674ca11f", "platform");
|
||||
autoKeyDetect.put("3e24e49741b60c215c010dc6048fca7d", "shared");
|
||||
autoKeyDetect.put("dab2cead827ef5313f28e22b6fa8479f", "testkey");
|
||||
|
||||
}
|
||||
|
||||
public ResourceAdapter getResourceAdapter() {
|
||||
return resourceAdapter;
|
||||
}
|
||||
|
||||
public void setResourceAdapter(ResourceAdapter resourceAdapter) {
|
||||
this.resourceAdapter = resourceAdapter;
|
||||
}
|
||||
|
||||
// when the key mode is automatic, the observers are called when the key is determined
|
||||
public void addAutoKeyObserver(Observer o) {
|
||||
autoKeyObservable.addObserver(o);
|
||||
}
|
||||
|
||||
public String getKeymode() {
|
||||
return keymode;
|
||||
}
|
||||
|
||||
public void setKeymode(String km) throws IOException, GeneralSecurityException {
|
||||
if (getLogger().isDebugEnabled()) getLogger().debug("setKeymode: " + km);
|
||||
keymode = km;
|
||||
if (keymode.startsWith(MODE_AUTO)) {
|
||||
keySet = null;
|
||||
} else {
|
||||
progressHelper.initProgress();
|
||||
loadKeys(keymode);
|
||||
}
|
||||
}
|
||||
|
||||
public static String[] getSupportedKeyModes() {
|
||||
return SUPPORTED_KEY_MODES;
|
||||
}
|
||||
|
||||
|
||||
protected String autoDetectKey(String mode, Map<String, ZioEntry> zioEntries)
|
||||
throws NoSuchAlgorithmException, IOException {
|
||||
boolean debug = getLogger().isDebugEnabled();
|
||||
|
||||
if (!mode.startsWith(MODE_AUTO)) return mode;
|
||||
|
||||
|
||||
// Auto-determine which keys to use
|
||||
String keyName = null;
|
||||
// Start by finding the signature block file in the input.
|
||||
for (Map.Entry<String, ZioEntry> entry : zioEntries.entrySet()) {
|
||||
String entryName = entry.getKey();
|
||||
if (entryName.startsWith("META-INF/") && entryName.endsWith(".RSA")) {
|
||||
|
||||
// Compute MD5 of the first 1458 bytes, which is the size of our signature block templates --
|
||||
// e.g., the portion of the sig block file that is the same for a given certificate.
|
||||
MessageDigest md5 = MessageDigest.getInstance("MD5");
|
||||
byte[] entryData = entry.getValue().getData();
|
||||
if (entryData.length < 1458) break; // sig block too short to be a supported key
|
||||
md5.update(entryData, 0, 1458);
|
||||
byte[] rawDigest = md5.digest();
|
||||
|
||||
// Create the hex representation of the digest value
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (byte b : rawDigest) {
|
||||
builder.append(String.format("%02x", b));
|
||||
}
|
||||
|
||||
String md5String = builder.toString();
|
||||
// Lookup the key name
|
||||
keyName = autoKeyDetect.get(md5String);
|
||||
|
||||
|
||||
if (debug) {
|
||||
if (keyName != null) {
|
||||
getLogger().debug(String.format("Auto-determined key=%s using md5=%s", keyName, md5String));
|
||||
} else {
|
||||
getLogger().debug(String.format("Auto key determination failed for md5=%s", md5String));
|
||||
}
|
||||
}
|
||||
if (keyName != null) return keyName;
|
||||
}
|
||||
}
|
||||
|
||||
if (mode.equals(MODE_AUTO_TESTKEY)) {
|
||||
// in auto-testkey mode, fallback to the testkey if it couldn't be determined
|
||||
if (debug) getLogger().debug("Falling back to key=" + keyName);
|
||||
return KEY_TESTKEY;
|
||||
|
||||
} else if (mode.equals(MODE_AUTO_NONE)) {
|
||||
// in auto-node mode, simply copy the input to the output when the key can't be determined.
|
||||
if (debug) getLogger().debug("Unable to determine key, returning: " + KEY_NONE);
|
||||
return KEY_NONE;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void issueLoadingCertAndKeysProgressEvent() {
|
||||
progressHelper.progress(ProgressEvent.PRORITY_IMPORTANT, resourceAdapter.getString(ResourceAdapter.Item.LOADING_CERTIFICATE_AND_KEY));
|
||||
}
|
||||
|
||||
// Loads one of the built-in keys (media, platform, shared, testkey)
|
||||
public void loadKeys(String name)
|
||||
throws IOException, GeneralSecurityException {
|
||||
|
||||
keySet = loadedKeys.get(name);
|
||||
if (keySet != null) return;
|
||||
|
||||
keySet = new KeySet();
|
||||
keySet.setName(name);
|
||||
loadedKeys.put(name, keySet);
|
||||
|
||||
if (KEY_NONE.equals(name)) return;
|
||||
|
||||
issueLoadingCertAndKeysProgressEvent();
|
||||
|
||||
// load the private key
|
||||
URL privateKeyUrl = getClass().getResource("/keys/" + name + ".pk8");
|
||||
keySet.setPrivateKey(readPrivateKey(privateKeyUrl, null));
|
||||
|
||||
// load the certificate
|
||||
URL publicKeyUrl = getClass().getResource("/keys/" + name + ".x509.pem");
|
||||
keySet.setPublicKey(readPublicKey(publicKeyUrl));
|
||||
|
||||
// load the signature block template
|
||||
URL sigBlockTemplateUrl = getClass().getResource("/keys/" + name + ".sbt");
|
||||
if (sigBlockTemplateUrl != null) {
|
||||
keySet.setSigBlockTemplate(readContentAsBytes(sigBlockTemplateUrl));
|
||||
}
|
||||
}
|
||||
|
||||
public void setKeys(String name, X509Certificate publicKey, PrivateKey privateKey, byte[] signatureBlockTemplate) {
|
||||
keySet = new KeySet(name, publicKey, privateKey, signatureBlockTemplate);
|
||||
}
|
||||
|
||||
public void setKeys(String name, X509Certificate publicKey, PrivateKey privateKey, String signatureAlgorithm, byte[] signatureBlockTemplate) {
|
||||
keySet = new KeySet(name, publicKey, privateKey, signatureAlgorithm, signatureBlockTemplate);
|
||||
}
|
||||
|
||||
public KeySet getKeySet() {
|
||||
return keySet;
|
||||
}
|
||||
|
||||
// Allow the operation to be canceled.
|
||||
public void cancel() {
|
||||
canceled = true;
|
||||
}
|
||||
|
||||
// Allow the instance to sign again if previously canceled.
|
||||
public void resetCanceled() {
|
||||
canceled = false;
|
||||
}
|
||||
|
||||
public boolean isCanceled() {
|
||||
return canceled;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void loadProvider(String providerClassName)
|
||||
throws ClassNotFoundException, IllegalAccessException, InstantiationException {
|
||||
Class providerClass = Class.forName(providerClassName);
|
||||
Provider provider = (Provider) providerClass.newInstance();
|
||||
Security.insertProviderAt(provider, 1);
|
||||
}
|
||||
|
||||
|
||||
public X509Certificate readPublicKey(URL publicKeyUrl)
|
||||
throws IOException, GeneralSecurityException {
|
||||
InputStream input = publicKeyUrl.openStream();
|
||||
try {
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
return (X509Certificate) cf.generateCertificate(input);
|
||||
} finally {
|
||||
input.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt an encrypted PKCS 8 format private key.
|
||||
* <p>
|
||||
* Based on ghstark's post on Aug 6, 2006 at
|
||||
* http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
|
||||
*
|
||||
* @param encryptedPrivateKey The raw data of the private key
|
||||
* @param keyPassword the key password
|
||||
*/
|
||||
private KeySpec decryptPrivateKey(byte[] encryptedPrivateKey, String keyPassword)
|
||||
throws GeneralSecurityException {
|
||||
EncryptedPrivateKeyInfo epkInfo;
|
||||
try {
|
||||
epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
|
||||
} catch (IOException ex) {
|
||||
// Probably not an encrypted key.
|
||||
return null;
|
||||
}
|
||||
|
||||
char[] keyPasswd = keyPassword.toCharArray();
|
||||
|
||||
SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
|
||||
Key key = skFactory.generateSecret(new PBEKeySpec(keyPasswd));
|
||||
|
||||
Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());
|
||||
|
||||
try {
|
||||
return epkInfo.getKeySpec(cipher);
|
||||
} catch (InvalidKeySpecException ex) {
|
||||
getLogger().error("signapk: Password for private key may be bad.");
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the content at the specified URL and return it as a byte array.
|
||||
*/
|
||||
public byte[] readContentAsBytes(URL contentUrl) throws IOException {
|
||||
return readContentAsBytes(contentUrl.openStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the content from the given stream and return it as a byte array.
|
||||
*/
|
||||
public byte[] readContentAsBytes(InputStream input) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
byte[] buffer = new byte[2048];
|
||||
|
||||
int numRead = input.read(buffer);
|
||||
while (numRead != -1) {
|
||||
baos.write(buffer, 0, numRead);
|
||||
numRead = input.read(buffer);
|
||||
}
|
||||
|
||||
byte[] bytes = baos.toByteArray();
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a PKCS 8 format private key.
|
||||
*/
|
||||
public PrivateKey readPrivateKey(URL privateKeyUrl, String keyPassword)
|
||||
throws IOException, GeneralSecurityException {
|
||||
DataInputStream input = new DataInputStream(privateKeyUrl.openStream());
|
||||
try {
|
||||
byte[] bytes = readContentAsBytes(input);
|
||||
|
||||
KeySpec spec = decryptPrivateKey(bytes, keyPassword);
|
||||
if (spec == null) {
|
||||
spec = new PKCS8EncodedKeySpec(bytes);
|
||||
}
|
||||
|
||||
try {
|
||||
return KeyFactory.getInstance("RSA").generatePrivate(spec);
|
||||
} catch (InvalidKeySpecException ex) {
|
||||
return KeyFactory.getInstance("DSA").generatePrivate(spec);
|
||||
}
|
||||
} finally {
|
||||
input.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the SHA1 of every file to the manifest, creating it if necessary.
|
||||
*/
|
||||
private Manifest addDigestsToManifest(Map<String, ZioEntry> entries)
|
||||
throws IOException, GeneralSecurityException {
|
||||
Manifest input = null;
|
||||
ZioEntry manifestEntry = entries.get(JarFile.MANIFEST_NAME);
|
||||
if (manifestEntry != null) {
|
||||
input = new Manifest();
|
||||
input.read(manifestEntry.getInputStream());
|
||||
}
|
||||
Manifest output = new Manifest();
|
||||
Attributes main = output.getMainAttributes();
|
||||
if (input != null) {
|
||||
main.putAll(input.getMainAttributes());
|
||||
} else {
|
||||
main.putValue("Manifest-Version", "1.0");
|
||||
main.putValue("Created-By", "1.0 (Android SignApk)");
|
||||
}
|
||||
|
||||
// BASE64Encoder base64 = new BASE64Encoder();
|
||||
MessageDigest md = MessageDigest.getInstance("SHA1");
|
||||
byte[] buffer = new byte[512];
|
||||
int num;
|
||||
|
||||
// We sort the input entries by name, and add them to the
|
||||
// output manifest in sorted order. We expect that the output
|
||||
// map will be deterministic.
|
||||
|
||||
TreeMap<String, ZioEntry> byName = new TreeMap<String, ZioEntry>();
|
||||
byName.putAll(entries);
|
||||
|
||||
boolean debug = getLogger().isDebugEnabled();
|
||||
if (debug) getLogger().debug("Manifest entries:");
|
||||
for (ZioEntry entry : byName.values()) {
|
||||
if (canceled) break;
|
||||
String name = entry.getName();
|
||||
if (debug) getLogger().debug(name);
|
||||
if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) &&
|
||||
!name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) &&
|
||||
(stripPattern == null ||
|
||||
!stripPattern.matcher(name).matches())) {
|
||||
|
||||
progressHelper.progress(ProgressEvent.PRORITY_NORMAL, resourceAdapter.getString(ResourceAdapter.Item.GENERATING_MANIFEST));
|
||||
InputStream data = entry.getInputStream();
|
||||
while ((num = data.read(buffer)) > 0) {
|
||||
md.update(buffer, 0, num);
|
||||
}
|
||||
|
||||
Attributes attr = null;
|
||||
if (input != null) {
|
||||
java.util.jar.Attributes inAttr = input.getAttributes(name);
|
||||
if (inAttr != null) attr = new Attributes(inAttr);
|
||||
}
|
||||
if (attr == null) attr = new Attributes();
|
||||
attr.putValue("SHA1-Digest", Base64.encode(md.digest()));
|
||||
output.getEntries().put(name, attr);
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Write the signature file to the given output stream.
|
||||
*/
|
||||
private void generateSignatureFile(Manifest manifest, OutputStream out)
|
||||
throws IOException, GeneralSecurityException {
|
||||
out.write(("Signature-Version: 1.0\r\n").getBytes());
|
||||
out.write(("Created-By: 1.0 (Android SignApk)\r\n").getBytes());
|
||||
|
||||
|
||||
// BASE64Encoder base64 = new BASE64Encoder();
|
||||
MessageDigest md = MessageDigest.getInstance("SHA1");
|
||||
PrintStream print = new PrintStream(
|
||||
new DigestOutputStream(new ByteArrayOutputStream(), md),
|
||||
true, "UTF-8");
|
||||
|
||||
// Digest of the entire manifest
|
||||
manifest.write(print);
|
||||
print.flush();
|
||||
|
||||
out.write(("SHA1-Digest-Manifest: " + Base64.encode(md.digest()) + "\r\n\r\n").getBytes());
|
||||
|
||||
Map<String, Attributes> entries = manifest.getEntries();
|
||||
for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
|
||||
if (canceled) break;
|
||||
progressHelper.progress(ProgressEvent.PRORITY_NORMAL, resourceAdapter.getString(ResourceAdapter.Item.GENERATING_SIGNATURE_FILE));
|
||||
// Digest of the manifest stanza for this entry.
|
||||
String nameEntry = "Name: " + entry.getKey() + "\r\n";
|
||||
print.print(nameEntry);
|
||||
for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
|
||||
print.print(att.getKey() + ": " + att.getValue() + "\r\n");
|
||||
}
|
||||
print.print("\r\n");
|
||||
print.flush();
|
||||
|
||||
out.write(nameEntry.getBytes());
|
||||
out.write(("SHA1-Digest: " + Base64.encode(md.digest()) + "\r\n\r\n").getBytes());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a .RSA file with a digital signature.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void writeSignatureBlock(KeySet keySet, byte[] signatureFileBytes, OutputStream out)
|
||||
throws IOException, GeneralSecurityException {
|
||||
if (keySet.getSigBlockTemplate() != null) {
|
||||
|
||||
// Can't use default Signature on Android. Although it generates a signature that can be verified by jarsigner,
|
||||
// the recovery program appears to require a specific algorithm/mode/padding. So we use the custom ZipSignature instead.
|
||||
// Signature signature = Signature.getInstance("SHA1withRSA");
|
||||
ZipSignature signature = new ZipSignature();
|
||||
signature.initSign(keySet.getPrivateKey());
|
||||
signature.update(signatureFileBytes);
|
||||
byte[] signatureBytes = signature.sign();
|
||||
|
||||
out.write(keySet.getSigBlockTemplate());
|
||||
out.write(signatureBytes);
|
||||
|
||||
if (getLogger().isDebugEnabled()) {
|
||||
|
||||
MessageDigest md = MessageDigest.getInstance("SHA1");
|
||||
md.update(signatureFileBytes);
|
||||
byte[] sfDigest = md.digest();
|
||||
getLogger().debug("Sig File SHA1: \n" + HexDumpEncoder.encode(sfDigest));
|
||||
|
||||
getLogger().debug("Signature: \n" + HexDumpEncoder.encode(signatureBytes));
|
||||
|
||||
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySet.getPublicKey());
|
||||
|
||||
byte[] tmpData = cipher.doFinal(signatureBytes);
|
||||
getLogger().debug("Signature Decrypted: \n" + HexDumpEncoder.encode(tmpData));
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
byte[] sigBlock = null;
|
||||
// Use reflection to call the optional generator.
|
||||
Class generatorClass = Class.forName("kellinwood.security.zipsigner.optional.SignatureBlockGenerator");
|
||||
Method generatorMethod = generatorClass.getMethod("generate", KeySet.class, (new byte[1]).getClass());
|
||||
sigBlock = (byte[]) generatorMethod.invoke(null, keySet, signatureFileBytes);
|
||||
out.write(sigBlock);
|
||||
} catch (Exception x) {
|
||||
throw new RuntimeException(x.getMessage(), x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy all the files in a manifest from input to output. We set
|
||||
* the modification times in the output to a fixed time, so as to
|
||||
* reduce variation in the output file and make incremental OTAs
|
||||
* more efficient.
|
||||
*/
|
||||
private void copyFiles(Manifest manifest, Map<String, ZioEntry> input, ZipOutput output, long timestamp)
|
||||
throws IOException {
|
||||
Map<String, Attributes> entries = manifest.getEntries();
|
||||
List<String> names = new ArrayList<String>(entries.keySet());
|
||||
Collections.sort(names);
|
||||
int i = 1;
|
||||
for (String name : names) {
|
||||
if (canceled) break;
|
||||
progressHelper.progress(ProgressEvent.PRORITY_NORMAL, resourceAdapter.getString(ResourceAdapter.Item.COPYING_ZIP_ENTRY, i, names.size()));
|
||||
i += 1;
|
||||
ZioEntry inEntry = input.get(name);
|
||||
inEntry.setTime(timestamp);
|
||||
output.write(inEntry);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy all the files from input to output.
|
||||
*/
|
||||
private void copyFiles(Map<String, ZioEntry> input, ZipOutput output)
|
||||
throws IOException {
|
||||
int i = 1;
|
||||
for (ZioEntry inEntry : input.values()) {
|
||||
if (canceled) break;
|
||||
progressHelper.progress(ProgressEvent.PRORITY_NORMAL, resourceAdapter.getString(ResourceAdapter.Item.COPYING_ZIP_ENTRY, i, input.size()));
|
||||
i += 1;
|
||||
output.write(inEntry);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated - use the version that takes the passwords as char[]
|
||||
*/
|
||||
public void signZip(URL keystoreURL,
|
||||
String keystoreType,
|
||||
String keystorePw,
|
||||
String certAlias,
|
||||
String certPw,
|
||||
String inputZipFilename,
|
||||
String outputZipFilename)
|
||||
throws ClassNotFoundException, IllegalAccessException, InstantiationException,
|
||||
IOException, GeneralSecurityException {
|
||||
signZip(keystoreURL, keystoreType, keystorePw.toCharArray(), certAlias, certPw.toCharArray(), "SHA1withRSA", inputZipFilename, outputZipFilename);
|
||||
}
|
||||
|
||||
public void signZip(URL keystoreURL,
|
||||
String keystoreType,
|
||||
char[] keystorePw,
|
||||
String certAlias,
|
||||
char[] certPw,
|
||||
String signatureAlgorithm,
|
||||
String inputZipFilename,
|
||||
String outputZipFilename)
|
||||
throws ClassNotFoundException, IllegalAccessException, InstantiationException,
|
||||
IOException, GeneralSecurityException {
|
||||
InputStream keystoreStream = null;
|
||||
|
||||
|
||||
try {
|
||||
KeyStore keystore = null;
|
||||
if (keystoreType == null) keystoreType = KeyStore.getDefaultType();
|
||||
keystore = KeyStore.getInstance(keystoreType);
|
||||
|
||||
keystoreStream = keystoreURL.openStream();
|
||||
keystore.load(keystoreStream, keystorePw);
|
||||
Certificate cert = keystore.getCertificate(certAlias);
|
||||
X509Certificate publicKey = (X509Certificate) cert;
|
||||
Key key = keystore.getKey(certAlias, certPw);
|
||||
PrivateKey privateKey = (PrivateKey) key;
|
||||
|
||||
setKeys("custom", publicKey, privateKey, signatureAlgorithm, null);
|
||||
|
||||
signZip(inputZipFilename, outputZipFilename);
|
||||
} finally {
|
||||
if (keystoreStream != null) keystoreStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sign the input with the default test key and certificate.
|
||||
* Save result to output file.
|
||||
*/
|
||||
public void signZip(Map<String, ZioEntry> zioEntries, String outputZipFilename)
|
||||
throws IOException, GeneralSecurityException {
|
||||
progressHelper.initProgress();
|
||||
signZip(zioEntries, new FileOutputStream(outputZipFilename), outputZipFilename);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sign the file using the given public key cert, private key,
|
||||
* and signature block template. The signature block template
|
||||
* parameter may be null, but if so
|
||||
* android-sun-jarsign-support.jar must be in the classpath.
|
||||
*/
|
||||
public void signZip(String inputZipFilename, String outputZipFilename)
|
||||
throws IOException, GeneralSecurityException {
|
||||
File inFile = new File(inputZipFilename).getCanonicalFile();
|
||||
File outFile = new File(outputZipFilename).getCanonicalFile();
|
||||
|
||||
if (inFile.equals(outFile)) {
|
||||
throw new IllegalArgumentException(resourceAdapter.getString(ResourceAdapter.Item.INPUT_SAME_AS_OUTPUT_ERROR));
|
||||
}
|
||||
|
||||
progressHelper.initProgress();
|
||||
progressHelper.progress(ProgressEvent.PRORITY_IMPORTANT, resourceAdapter.getString(ResourceAdapter.Item.PARSING_CENTRAL_DIRECTORY));
|
||||
|
||||
ZipInput input = null;
|
||||
OutputStream outStream = null;
|
||||
try {
|
||||
input = ZipInput.read(inputZipFilename);
|
||||
outStream = new FileOutputStream(outputZipFilename);
|
||||
signZip(input.getEntries(), outStream, outputZipFilename);
|
||||
} finally {
|
||||
if (input != null) input.close();
|
||||
if (outStream != null) outStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign the
|
||||
* and signature block template. The signature block template
|
||||
* parameter may be null, but if so
|
||||
* android-sun-jarsign-support.jar must be in the classpath.
|
||||
*/
|
||||
public void signZip(Map<String, ZioEntry> zioEntries, OutputStream outputStream, String outputZipFilename)
|
||||
throws IOException, GeneralSecurityException {
|
||||
boolean debug = getLogger().isDebugEnabled();
|
||||
|
||||
progressHelper.initProgress();
|
||||
if (keySet == null) {
|
||||
if (!keymode.startsWith(MODE_AUTO))
|
||||
throw new IllegalStateException("No keys configured for signing the file!");
|
||||
|
||||
// Auto-determine which keys to use
|
||||
String keyName = this.autoDetectKey(keymode, zioEntries);
|
||||
if (keyName == null)
|
||||
throw new AutoKeyException(resourceAdapter.getString(ResourceAdapter.Item.AUTO_KEY_SELECTION_ERROR, new File(outputZipFilename).getName()));
|
||||
|
||||
autoKeyObservable.notifyObservers(keyName);
|
||||
|
||||
loadKeys(keyName);
|
||||
|
||||
}
|
||||
|
||||
|
||||
ZipOutput zipOutput = null;
|
||||
|
||||
try {
|
||||
|
||||
|
||||
zipOutput = new ZipOutput(outputStream);
|
||||
|
||||
if (KEY_NONE.equals(keySet.getName())) {
|
||||
progressHelper.setProgressTotalItems(zioEntries.size());
|
||||
progressHelper.setProgressCurrentItem(0);
|
||||
copyFiles(zioEntries, zipOutput);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate total steps to complete for accurate progress percentages.
|
||||
int progressTotalItems = 0;
|
||||
for (ZioEntry entry : zioEntries.values()) {
|
||||
String name = entry.getName();
|
||||
if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) &&
|
||||
!name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) &&
|
||||
(stripPattern == null ||
|
||||
!stripPattern.matcher(name).matches())) {
|
||||
progressTotalItems += 3; // digest for manifest, digest in sig file, copy data
|
||||
}
|
||||
}
|
||||
progressTotalItems += 1; // CERT.RSA generation
|
||||
progressHelper.setProgressTotalItems(progressTotalItems);
|
||||
progressHelper.setProgressCurrentItem(0);
|
||||
|
||||
// Assume the certificate is valid for at least an hour.
|
||||
long timestamp = keySet.getPublicKey().getNotBefore().getTime() + 3600L * 1000;
|
||||
|
||||
// MANIFEST.MF
|
||||
// progress(ProgressEvent.PRORITY_NORMAL, JarFile.MANIFEST_NAME);
|
||||
Manifest manifest = addDigestsToManifest(zioEntries);
|
||||
if (canceled) return;
|
||||
ZioEntry ze = new ZioEntry(JarFile.MANIFEST_NAME);
|
||||
ze.setTime(timestamp);
|
||||
manifest.write(ze.getOutputStream());
|
||||
zipOutput.write(ze);
|
||||
|
||||
|
||||
// CERT.SF
|
||||
ze = new ZioEntry(CERT_SF_NAME);
|
||||
ze.setTime(timestamp);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
generateSignatureFile(manifest, out);
|
||||
if (canceled) return;
|
||||
byte[] sfBytes = out.toByteArray();
|
||||
if (debug) {
|
||||
getLogger().debug("Signature File: \n" + new String(sfBytes) + "\n" +
|
||||
HexDumpEncoder.encode(sfBytes));
|
||||
}
|
||||
ze.getOutputStream().write(sfBytes);
|
||||
zipOutput.write(ze);
|
||||
|
||||
// CERT.RSA
|
||||
progressHelper.progress(ProgressEvent.PRORITY_NORMAL, resourceAdapter.getString(ResourceAdapter.Item.GENERATING_SIGNATURE_BLOCK));
|
||||
ze = new ZioEntry(CERT_RSA_NAME);
|
||||
ze.setTime(timestamp);
|
||||
writeSignatureBlock(keySet, sfBytes, ze.getOutputStream());
|
||||
zipOutput.write(ze);
|
||||
if (canceled) return;
|
||||
|
||||
// Everything else
|
||||
copyFiles(manifest, zioEntries, zipOutput, timestamp);
|
||||
if (canceled) return;
|
||||
|
||||
} finally {
|
||||
if (zipOutput != null) zipOutput.close();
|
||||
if (canceled) {
|
||||
try {
|
||||
if (outputZipFilename != null) new File(outputZipFilename).delete();
|
||||
} catch (Throwable t) {
|
||||
getLogger().warning(t.getClass().getName() + ":" + t.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void addProgressListener(ProgressListener l) {
|
||||
progressHelper.addProgressListener(l);
|
||||
}
|
||||
|
||||
public synchronized void removeProgressListener(ProgressListener l) {
|
||||
progressHelper.removeProgressListener(l);
|
||||
}
|
||||
|
||||
|
||||
public static class AutoKeyObservable extends Observable {
|
||||
@Override
|
||||
public void notifyObservers(Object arg) {
|
||||
super.setChanged();
|
||||
super.notifyObservers(arg);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
|
||||
package kellinwood.security.zipsigner.optional;
|
||||
|
||||
import kellinwood.security.zipsigner.KeySet;
|
||||
import org.bouncycastle.jce.X509Principal;
|
||||
import org.bouncycastle.x509.X509V3CertificateGenerator;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.KeyStore;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Date;
|
||||
|
||||
|
||||
/**
|
||||
* All methods create self-signed certificates.
|
||||
*/
|
||||
public class CertCreator {
|
||||
|
||||
/**
|
||||
* Creates a new keystore and self-signed key. The key will have the same password as the key, and will be
|
||||
* RSA 2048, with the cert signed using SHA1withRSA. The certificate will have a validity of
|
||||
* 30 years).
|
||||
*
|
||||
* @param storePath - pathname of the new keystore file
|
||||
* @param password - keystore and key password
|
||||
* @param keyName - the new key will have this as its alias within the keystore
|
||||
* @param distinguishedNameValues - contains Country, State, Locality,...,Common Name, etc.
|
||||
*/
|
||||
public static void createKeystoreAndKey(String storePath, char[] password,
|
||||
String keyName, DistinguishedNameValues distinguishedNameValues) {
|
||||
createKeystoreAndKey(storePath, password, "RSA", 2048, keyName, password, "SHA1withRSA", 30,
|
||||
distinguishedNameValues);
|
||||
}
|
||||
|
||||
|
||||
public static KeySet createKeystoreAndKey(String storePath, char[] storePass,
|
||||
String keyAlgorithm, int keySize, String keyName, char[] keyPass,
|
||||
String certSignatureAlgorithm, int certValidityYears, DistinguishedNameValues distinguishedNameValues) {
|
||||
try {
|
||||
|
||||
KeySet keySet = createKey(keyAlgorithm, keySize, keyName, certSignatureAlgorithm, certValidityYears,
|
||||
distinguishedNameValues);
|
||||
|
||||
|
||||
KeyStore privateKS = KeyStoreFileManager.createKeyStore(storePath, storePass);
|
||||
|
||||
privateKS.setKeyEntry(keyName, keySet.getPrivateKey(),
|
||||
keyPass,
|
||||
new java.security.cert.Certificate[]{keySet.getPublicKey()});
|
||||
|
||||
File sfile = new File(storePath);
|
||||
if (sfile.exists()) {
|
||||
throw new IOException("File already exists: " + storePath);
|
||||
}
|
||||
KeyStoreFileManager.writeKeyStore(privateKS, storePath, storePass);
|
||||
|
||||
return keySet;
|
||||
} catch (RuntimeException x) {
|
||||
throw x;
|
||||
} catch (Exception x) {
|
||||
throw new RuntimeException(x.getMessage(), x);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new key and store it in an existing keystore.
|
||||
*/
|
||||
public static KeySet createKey(String storePath, char[] storePass,
|
||||
String keyAlgorithm, int keySize, String keyName, char[] keyPass,
|
||||
String certSignatureAlgorithm, int certValidityYears,
|
||||
DistinguishedNameValues distinguishedNameValues) {
|
||||
try {
|
||||
|
||||
KeySet keySet = createKey(keyAlgorithm, keySize, keyName, certSignatureAlgorithm, certValidityYears,
|
||||
distinguishedNameValues);
|
||||
|
||||
KeyStore privateKS = KeyStoreFileManager.loadKeyStore(storePath, storePass);
|
||||
|
||||
privateKS.setKeyEntry(keyName, keySet.getPrivateKey(),
|
||||
keyPass,
|
||||
new java.security.cert.Certificate[]{keySet.getPublicKey()});
|
||||
|
||||
KeyStoreFileManager.writeKeyStore(privateKS, storePath, storePass);
|
||||
|
||||
return keySet;
|
||||
|
||||
} catch (RuntimeException x) {
|
||||
throw x;
|
||||
} catch (Exception x) {
|
||||
throw new RuntimeException(x.getMessage(), x);
|
||||
}
|
||||
}
|
||||
|
||||
public static KeySet createKey(String keyAlgorithm, int keySize, String keyName,
|
||||
String certSignatureAlgorithm, int certValidityYears, DistinguishedNameValues distinguishedNameValues) {
|
||||
try {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(keyAlgorithm);
|
||||
keyPairGenerator.initialize(keySize);
|
||||
KeyPair KPair = keyPairGenerator.generateKeyPair();
|
||||
|
||||
X509V3CertificateGenerator v3CertGen = new X509V3CertificateGenerator();
|
||||
|
||||
X509Principal principal = distinguishedNameValues.getPrincipal();
|
||||
|
||||
// generate a postitive serial number
|
||||
BigInteger serialNumber = BigInteger.valueOf(new SecureRandom().nextInt());
|
||||
while (serialNumber.compareTo(BigInteger.ZERO) < 0) {
|
||||
serialNumber = BigInteger.valueOf(new SecureRandom().nextInt());
|
||||
}
|
||||
v3CertGen.setSerialNumber(serialNumber);
|
||||
v3CertGen.setIssuerDN(principal);
|
||||
v3CertGen.setNotBefore(new Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L));
|
||||
v3CertGen.setNotAfter(new Date(System.currentTimeMillis() + (1000L * 60L * 60L * 24L * 366L * (long) certValidityYears)));
|
||||
v3CertGen.setSubjectDN(principal);
|
||||
|
||||
v3CertGen.setPublicKey(KPair.getPublic());
|
||||
v3CertGen.setSignatureAlgorithm(certSignatureAlgorithm);
|
||||
|
||||
X509Certificate PKCertificate = v3CertGen.generate(KPair.getPrivate(), "BC");
|
||||
|
||||
KeySet keySet = new KeySet();
|
||||
keySet.setName(keyName);
|
||||
keySet.setPrivateKey(KPair.getPrivate());
|
||||
keySet.setPublicKey(PKCertificate);
|
||||
return keySet;
|
||||
} catch (Exception x) {
|
||||
throw new RuntimeException(x.getMessage(), x);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
|
||||
package kellinwood.security.zipsigner.optional;
|
||||
|
||||
import kellinwood.security.zipsigner.ZipSigner;
|
||||
|
||||
import java.security.Key;
|
||||
import java.security.KeyStore;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
/**
|
||||
*/
|
||||
public class CustomKeySigner {
|
||||
|
||||
/**
|
||||
* KeyStore-type agnostic. This method will sign the zip file, automatically handling JKS or BKS keystores.
|
||||
*/
|
||||
public static void signZip(ZipSigner zipSigner,
|
||||
String keystorePath,
|
||||
char[] keystorePw,
|
||||
String certAlias,
|
||||
char[] certPw,
|
||||
String signatureAlgorithm,
|
||||
String inputZipFilename,
|
||||
String outputZipFilename)
|
||||
throws Exception {
|
||||
zipSigner.issueLoadingCertAndKeysProgressEvent();
|
||||
KeyStore keystore = KeyStoreFileManager.loadKeyStore(keystorePath, keystorePw);
|
||||
Certificate cert = keystore.getCertificate(certAlias);
|
||||
X509Certificate publicKey = (X509Certificate) cert;
|
||||
Key key = keystore.getKey(certAlias, certPw);
|
||||
PrivateKey privateKey = (PrivateKey) key;
|
||||
|
||||
zipSigner.setKeys("custom", publicKey, privateKey, signatureAlgorithm, null);
|
||||
zipSigner.signZip(inputZipFilename, outputZipFilename);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
|
||||
package kellinwood.security.zipsigner.optional;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||
import org.bouncycastle.asn1.x500.style.BCStyle;
|
||||
import org.bouncycastle.jce.X509Principal;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Vector;
|
||||
|
||||
|
||||
/**
|
||||
* Helper class for dealing with the distinguished name RDNs.
|
||||
*/
|
||||
public class DistinguishedNameValues extends LinkedHashMap<ASN1ObjectIdentifier, String> {
|
||||
|
||||
public DistinguishedNameValues() {
|
||||
put(BCStyle.C, null);
|
||||
put(BCStyle.ST, null);
|
||||
put(BCStyle.L, null);
|
||||
put(BCStyle.STREET, null);
|
||||
put(BCStyle.O, null);
|
||||
put(BCStyle.OU, null);
|
||||
put(BCStyle.CN, null);
|
||||
}
|
||||
|
||||
public String put(ASN1ObjectIdentifier oid, String value) {
|
||||
if (value != null && value.equals("")) value = null;
|
||||
if (containsKey(oid)) super.put(oid, value); // preserve original ordering
|
||||
else {
|
||||
super.put(oid, value);
|
||||
// String cn = remove(BCStyle.CN); // CN will always be last.
|
||||
// put(BCStyle.CN,cn);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setCountry(String country) {
|
||||
put(BCStyle.C, country);
|
||||
}
|
||||
|
||||
public void setState(String state) {
|
||||
put(BCStyle.ST, state);
|
||||
}
|
||||
|
||||
public void setLocality(String locality) {
|
||||
put(BCStyle.L, locality);
|
||||
}
|
||||
|
||||
public void setStreet(String street) {
|
||||
put(BCStyle.STREET, street);
|
||||
}
|
||||
|
||||
public void setOrganization(String organization) {
|
||||
put(BCStyle.O, organization);
|
||||
}
|
||||
|
||||
public void setOrganizationalUnit(String organizationalUnit) {
|
||||
put(BCStyle.OU, organizationalUnit);
|
||||
}
|
||||
|
||||
public void setCommonName(String commonName) {
|
||||
put(BCStyle.CN, commonName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
int result = 0;
|
||||
for (String value : values()) {
|
||||
if (value != null) result += 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public X509Principal getPrincipal() {
|
||||
Vector<ASN1ObjectIdentifier> oids = new Vector<ASN1ObjectIdentifier>();
|
||||
Vector<String> values = new Vector<String>();
|
||||
|
||||
for (Map.Entry<ASN1ObjectIdentifier, String> entry : entrySet()) {
|
||||
if (entry.getValue() != null && !entry.getValue().equals("")) {
|
||||
oids.add(entry.getKey());
|
||||
values.add(entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
return new X509Principal(oids, values);
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
|
||||
package kellinwood.security.zipsigner.optional;
|
||||
|
||||
import kellinwood.logging.LoggerInterface;
|
||||
import kellinwood.logging.LoggerManager;
|
||||
import kellinwood.security.zipsigner.Base64;
|
||||
import org.bouncycastle.util.encoders.HexTranslator;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* User: ken
|
||||
* Date: 1/17/13
|
||||
*/
|
||||
public class Fingerprint {
|
||||
|
||||
static LoggerInterface logger = LoggerManager.getLogger(Fingerprint.class.getName());
|
||||
|
||||
static byte[] calcDigest(String algorithm, byte[] encodedCert) {
|
||||
byte[] result = null;
|
||||
try {
|
||||
MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
|
||||
messageDigest.update(encodedCert);
|
||||
result = messageDigest.digest();
|
||||
} catch (Exception x) {
|
||||
logger.error(x.getMessage(), x);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static String hexFingerprint(String algorithm, byte[] encodedCert) {
|
||||
try {
|
||||
byte[] digest = calcDigest(algorithm, encodedCert);
|
||||
if (digest == null) return null;
|
||||
HexTranslator hexTranslator = new HexTranslator();
|
||||
byte[] hex = new byte[digest.length * 2];
|
||||
hexTranslator.encode(digest, 0, digest.length, hex, 0);
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < hex.length; i += 2) {
|
||||
builder.append((char) hex[i]);
|
||||
builder.append((char) hex[i + 1]);
|
||||
if (i != (hex.length - 2)) builder.append(':');
|
||||
}
|
||||
return builder.toString().toUpperCase(Locale.ENGLISH);
|
||||
} catch (Exception x) {
|
||||
logger.error(x.getMessage(), x);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// public static void main(String[] args) {
|
||||
// byte[] data = "The Silence of the Lambs is a really good movie.".getBytes();
|
||||
// System.out.println(hexFingerprint("MD5", data));
|
||||
// System.out.println(hexFingerprint("SHA1", data));
|
||||
// System.out.println(base64Fingerprint("SHA1", data));
|
||||
//
|
||||
// }
|
||||
|
||||
public static String base64Fingerprint(String algorithm, byte[] encodedCert) {
|
||||
String result = null;
|
||||
try {
|
||||
byte[] digest = calcDigest(algorithm, encodedCert);
|
||||
if (digest == null) return result;
|
||||
return Base64.encode(digest);
|
||||
} catch (Exception x) {
|
||||
logger.error(x.getMessage(), x);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,500 @@
|
||||
/* JKS.java -- implementation of the "JKS" key store.
|
||||
Copyright (C) 2003 Casey Marshall <rsdio@metastatic.org>
|
||||
|
||||
Permission to use, copy, modify, distribute, and sell this software and
|
||||
its documentation for any purpose is hereby granted without fee,
|
||||
provided that the above copyright notice appear in all copies and that
|
||||
both that copyright notice and this permission notice appear in
|
||||
supporting documentation. No representations are made about the
|
||||
suitability of this software for any purpose. It is provided "as is"
|
||||
without express or implied warranty.
|
||||
|
||||
This program was derived by reverse-engineering Sun's own
|
||||
implementation, using only the public API that is available in the 1.4.1
|
||||
JDK. Hence nothing in this program is, or is derived from, anything
|
||||
copyrighted by Sun Microsystems. While the "Binary Evaluation License
|
||||
Agreement" that the JDK is licensed under contains blanket statements
|
||||
that forbid reverse-engineering (among other things), it is my position
|
||||
that US copyright law does not and cannot forbid reverse-engineering of
|
||||
software to produce a compatible implementation. There are, in fact,
|
||||
numerous clauses in copyright law that specifically allow
|
||||
reverse-engineering, and therefore I believe it is outside of Sun's
|
||||
power to enforce restrictions on reverse-engineering of their software,
|
||||
and it is irresponsible for them to claim they can. */
|
||||
|
||||
|
||||
package kellinwood.security.zipsigner.optional;
|
||||
|
||||
import javax.crypto.EncryptedPrivateKeyInfo;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.DigestInputStream;
|
||||
import java.security.DigestOutputStream;
|
||||
import java.security.Key;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.KeyStoreSpi;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Date;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Locale;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* This is an implementation of Sun's proprietary key store
|
||||
* algorithm, called "JKS" for "Java Key Store". This implementation was
|
||||
* created entirely through reverse-engineering.
|
||||
* <p>
|
||||
* <p>The format of JKS files is, from the start of the file:
|
||||
* <p>
|
||||
* <ol>
|
||||
* <li>Magic bytes. This is a four-byte integer, in big-endian byte
|
||||
* order, equal to <code>0xFEEDFEED</code>.</li>
|
||||
* <li>The version number (probably), as a four-byte integer (all
|
||||
* multibyte integral types are in big-endian byte order). The current
|
||||
* version number (in modern distributions of the JDK) is 2.</li>
|
||||
* <li>The number of entrires in this keystore, as a four-byte
|
||||
* integer. Call this value <i>n</i></li>
|
||||
* <li>Then, <i>n</i> times:
|
||||
* <ol>
|
||||
* <li>The entry type, a four-byte int. The value 1 denotes a private
|
||||
* key entry, and 2 denotes a trusted certificate.</li>
|
||||
* <li>The entry's alias, formatted as strings such as those written
|
||||
* by <a
|
||||
* href="http://java.sun.com/j2se/1.4.1/docs/api/java/io/DataOutput.html#writeUTF(java.lang.String)">DataOutput.writeUTF(String)</a>.</li>
|
||||
* <li>An eight-byte integer, representing the entry's creation date,
|
||||
* in milliseconds since the epoch.
|
||||
* <p>
|
||||
* <p>Then, if the entry is a private key entry:
|
||||
* <ol>
|
||||
* <li>The size of the encoded key as a four-byte int, then that
|
||||
* number of bytes. The encoded key is the DER encoded bytes of the
|
||||
* <a
|
||||
* href="http://java.sun.com/j2se/1.4.1/docs/api/javax/crypto/EncryptedPrivateKeyInfo.html">EncryptedPrivateKeyInfo</a> structure (the
|
||||
* encryption algorithm is discussed later).</li>
|
||||
* <li>A four-byte integer, followed by that many encoded
|
||||
* certificates, encoded as described in the trusted certificates
|
||||
* section.</li>
|
||||
* </ol>
|
||||
* <p>
|
||||
* <p>Otherwise, the entry is a trusted certificate, which is encoded
|
||||
* as the name of the encoding algorithm (e.g. X.509), encoded the same
|
||||
* way as alias names. Then, a four-byte integer representing the size
|
||||
* of the encoded certificate, then that many bytes representing the
|
||||
* encoded certificate (e.g. the DER bytes in the case of X.509).
|
||||
* </li>
|
||||
* </ol>
|
||||
* </li>
|
||||
* <li>Then, the signature.</li>
|
||||
* </ol>
|
||||
* </ol>
|
||||
* </li>
|
||||
* </ol>
|
||||
* <p>
|
||||
* <p>(See <a href="genkey.java">this file</a> for some idea of how I
|
||||
* was able to figure out these algorithms)</p>
|
||||
* <p>
|
||||
* <p>Decrypting the key works as follows:
|
||||
* <p>
|
||||
* <ol>
|
||||
* <li>The key length is the length of the ciphertext minus 40. The
|
||||
* encrypted key, <code>ekey</code>, is the middle bytes of the
|
||||
* ciphertext.</li>
|
||||
* <li>Take the first 20 bytes of the encrypted key as a seed value,
|
||||
* <code>K[0]</code>.</li>
|
||||
* <li>Compute <code>K[1] ... K[n]</code>, where
|
||||
* <code>|K[i]| = 20</code>, <code>n = ceil(|ekey| / 20)</code>, and
|
||||
* <code>K[i] = SHA-1(UTF-16BE(password) + K[i-1])</code>.</li>
|
||||
* <li><code>key = ekey ^ (K[1] + ... + K[n])</code>.</li>
|
||||
* <li>The last 20 bytes are the checksum, computed as <code>H =
|
||||
* SHA-1(UTF-16BE(password) + key)</code>. If this value does not match
|
||||
* the last 20 bytes of the ciphertext, output <code>FAIL</code>. Otherwise,
|
||||
* output <code>key</code>.</li>
|
||||
* </ol>
|
||||
* <p>
|
||||
* <p>The signature is defined as <code>SHA-1(UTF-16BE(password) +
|
||||
* US_ASCII("Mighty Aphrodite") + encoded_keystore)</code> (yup, Sun
|
||||
* engineers are just that clever).
|
||||
* <p>
|
||||
* <p>(Above, SHA-1 denotes the secure hash algorithm, UTF-16BE the
|
||||
* big-endian byte representation of a UTF-16 string, and US_ASCII the
|
||||
* ASCII byte representation of the string.)
|
||||
* <p>
|
||||
* <p>The source code of this class should be available in the file <a
|
||||
* href="http://metastatic.org/source/JKS.java">JKS.java</a>.
|
||||
*
|
||||
* @author Casey Marshall (rsdio@metastatic.org)
|
||||
* <p>
|
||||
* Changes by Ken Ellinwood:
|
||||
* ** Fixed a NullPointerException in engineLoad(). This method must return gracefully if the keystore input stream is null.
|
||||
* ** engineGetCertificateEntry() was updated to return the first cert in the chain for private key entries.
|
||||
* ** Lowercase the alias names, otherwise keytool chokes on the file created by this code.
|
||||
* ** Fixed the integrity check in engineLoad(), previously the exception was never thrown regardless of password value.
|
||||
*/
|
||||
public class JKS extends KeyStoreSpi {
|
||||
|
||||
// Constants and fields.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Ah, Sun. So goddamned clever with those magic bytes.
|
||||
*/
|
||||
private static final int MAGIC = 0xFEEDFEED;
|
||||
|
||||
private static final int PRIVATE_KEY = 1;
|
||||
private static final int TRUSTED_CERT = 2;
|
||||
|
||||
private final Vector aliases;
|
||||
private final HashMap trustedCerts;
|
||||
private final HashMap privateKeys;
|
||||
private final HashMap certChains;
|
||||
private final HashMap dates;
|
||||
|
||||
// Constructor.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
public JKS() {
|
||||
super();
|
||||
aliases = new Vector();
|
||||
trustedCerts = new HashMap();
|
||||
privateKeys = new HashMap();
|
||||
certChains = new HashMap();
|
||||
dates = new HashMap();
|
||||
|
||||
}
|
||||
|
||||
// Instance methods.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
public Key engineGetKey(String alias, char[] password)
|
||||
throws NoSuchAlgorithmException, UnrecoverableKeyException {
|
||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
||||
|
||||
if (!privateKeys.containsKey(alias))
|
||||
return null;
|
||||
byte[] key = decryptKey((byte[]) privateKeys.get(alias),
|
||||
charsToBytes(password));
|
||||
Certificate[] chain = engineGetCertificateChain(alias);
|
||||
if (chain.length > 0) {
|
||||
try {
|
||||
// Private and public keys MUST have the same algorithm.
|
||||
KeyFactory fact = KeyFactory.getInstance(
|
||||
chain[0].getPublicKey().getAlgorithm());
|
||||
return fact.generatePrivate(new PKCS8EncodedKeySpec(key));
|
||||
} catch (InvalidKeySpecException x) {
|
||||
throw new UnrecoverableKeyException(x.getMessage());
|
||||
}
|
||||
} else
|
||||
return new SecretKeySpec(key, alias);
|
||||
}
|
||||
|
||||
public Certificate[] engineGetCertificateChain(String alias) {
|
||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
||||
return (Certificate[]) certChains.get(alias);
|
||||
}
|
||||
|
||||
public Certificate engineGetCertificate(String alias) {
|
||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
||||
if (engineIsKeyEntry(alias)) {
|
||||
Certificate[] certChain = (Certificate[]) certChains.get(alias);
|
||||
if (certChain != null && certChain.length > 0) return certChain[0];
|
||||
}
|
||||
return (Certificate) trustedCerts.get(alias);
|
||||
}
|
||||
|
||||
public Date engineGetCreationDate(String alias) {
|
||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
||||
return (Date) dates.get(alias);
|
||||
}
|
||||
|
||||
// XXX implement writing methods.
|
||||
|
||||
public void engineSetKeyEntry(String alias, Key key, char[] passwd, Certificate[] certChain)
|
||||
throws KeyStoreException {
|
||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
||||
if (trustedCerts.containsKey(alias))
|
||||
throw new KeyStoreException("\"" + alias + " is a trusted certificate entry");
|
||||
privateKeys.put(alias, encryptKey(key, charsToBytes(passwd)));
|
||||
if (certChain != null)
|
||||
certChains.put(alias, certChain);
|
||||
else
|
||||
certChains.put(alias, new Certificate[0]);
|
||||
if (!aliases.contains(alias)) {
|
||||
dates.put(alias, new Date());
|
||||
aliases.add(alias);
|
||||
}
|
||||
}
|
||||
|
||||
public void engineSetKeyEntry(String alias, byte[] encodedKey, Certificate[] certChain)
|
||||
throws KeyStoreException {
|
||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
||||
if (trustedCerts.containsKey(alias))
|
||||
throw new KeyStoreException("\"" + alias + "\" is a trusted certificate entry");
|
||||
try {
|
||||
new EncryptedPrivateKeyInfo(encodedKey);
|
||||
} catch (IOException ioe) {
|
||||
throw new KeyStoreException("encoded key is not an EncryptedPrivateKeyInfo");
|
||||
}
|
||||
privateKeys.put(alias, encodedKey);
|
||||
if (certChain != null)
|
||||
certChains.put(alias, certChain);
|
||||
else
|
||||
certChains.put(alias, new Certificate[0]);
|
||||
if (!aliases.contains(alias)) {
|
||||
dates.put(alias, new Date());
|
||||
aliases.add(alias);
|
||||
}
|
||||
}
|
||||
|
||||
public void engineSetCertificateEntry(String alias, Certificate cert)
|
||||
throws KeyStoreException {
|
||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
||||
if (privateKeys.containsKey(alias))
|
||||
throw new KeyStoreException("\"" + alias + "\" is a private key entry");
|
||||
if (cert == null)
|
||||
throw new NullPointerException();
|
||||
trustedCerts.put(alias, cert);
|
||||
if (!aliases.contains(alias)) {
|
||||
dates.put(alias, new Date());
|
||||
aliases.add(alias);
|
||||
}
|
||||
}
|
||||
|
||||
public void engineDeleteEntry(String alias) throws KeyStoreException {
|
||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
||||
aliases.remove(alias);
|
||||
}
|
||||
|
||||
public Enumeration engineAliases() {
|
||||
return aliases.elements();
|
||||
}
|
||||
|
||||
public boolean engineContainsAlias(String alias) {
|
||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
||||
return aliases.contains(alias);
|
||||
}
|
||||
|
||||
public int engineSize() {
|
||||
return aliases.size();
|
||||
}
|
||||
|
||||
public boolean engineIsKeyEntry(String alias) {
|
||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
||||
return privateKeys.containsKey(alias);
|
||||
}
|
||||
|
||||
public boolean engineIsCertificateEntry(String alias) {
|
||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
||||
return trustedCerts.containsKey(alias);
|
||||
}
|
||||
|
||||
public String engineGetCertificateAlias(Certificate cert) {
|
||||
for (Iterator keys = trustedCerts.keySet().iterator(); keys.hasNext(); ) {
|
||||
String alias = (String) keys.next();
|
||||
if (cert.equals(trustedCerts.get(alias)))
|
||||
return alias;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void engineStore(OutputStream out, char[] passwd)
|
||||
throws IOException, NoSuchAlgorithmException, CertificateException {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA1");
|
||||
md.update(charsToBytes(passwd));
|
||||
md.update("Mighty Aphrodite".getBytes("UTF-8"));
|
||||
DataOutputStream dout = new DataOutputStream(new DigestOutputStream(out, md));
|
||||
dout.writeInt(MAGIC);
|
||||
dout.writeInt(2);
|
||||
dout.writeInt(aliases.size());
|
||||
for (Enumeration e = aliases.elements(); e.hasMoreElements(); ) {
|
||||
String alias = (String) e.nextElement();
|
||||
if (trustedCerts.containsKey(alias)) {
|
||||
dout.writeInt(TRUSTED_CERT);
|
||||
dout.writeUTF(alias);
|
||||
dout.writeLong(((Date) dates.get(alias)).getTime());
|
||||
writeCert(dout, (Certificate) trustedCerts.get(alias));
|
||||
} else {
|
||||
dout.writeInt(PRIVATE_KEY);
|
||||
dout.writeUTF(alias);
|
||||
dout.writeLong(((Date) dates.get(alias)).getTime());
|
||||
byte[] key = (byte[]) privateKeys.get(alias);
|
||||
dout.writeInt(key.length);
|
||||
dout.write(key);
|
||||
Certificate[] chain = (Certificate[]) certChains.get(alias);
|
||||
dout.writeInt(chain.length);
|
||||
for (int i = 0; i < chain.length; i++)
|
||||
writeCert(dout, chain[i]);
|
||||
}
|
||||
}
|
||||
byte[] digest = md.digest();
|
||||
dout.write(digest);
|
||||
}
|
||||
|
||||
public void engineLoad(InputStream in, char[] passwd)
|
||||
throws IOException, NoSuchAlgorithmException, CertificateException {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA");
|
||||
if (passwd != null) md.update(charsToBytes(passwd));
|
||||
md.update("Mighty Aphrodite".getBytes("UTF-8")); // HAR HAR
|
||||
aliases.clear();
|
||||
trustedCerts.clear();
|
||||
privateKeys.clear();
|
||||
certChains.clear();
|
||||
dates.clear();
|
||||
if (in == null) return;
|
||||
DataInputStream din = new DataInputStream(new DigestInputStream(in, md));
|
||||
if (din.readInt() != MAGIC)
|
||||
throw new IOException("not a JavaKeyStore");
|
||||
din.readInt(); // version no.
|
||||
final int n = din.readInt();
|
||||
aliases.ensureCapacity(n);
|
||||
if (n < 0)
|
||||
throw new LoadKeystoreException("Malformed key store");
|
||||
for (int i = 0; i < n; i++) {
|
||||
int type = din.readInt();
|
||||
String alias = din.readUTF();
|
||||
aliases.add(alias);
|
||||
dates.put(alias, new Date(din.readLong()));
|
||||
switch (type) {
|
||||
case PRIVATE_KEY:
|
||||
int len = din.readInt();
|
||||
byte[] encoded = new byte[len];
|
||||
din.read(encoded);
|
||||
privateKeys.put(alias, encoded);
|
||||
int count = din.readInt();
|
||||
Certificate[] chain = new Certificate[count];
|
||||
for (int j = 0; j < count; j++)
|
||||
chain[j] = readCert(din);
|
||||
certChains.put(alias, chain);
|
||||
break;
|
||||
|
||||
case TRUSTED_CERT:
|
||||
trustedCerts.put(alias, readCert(din));
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new LoadKeystoreException("Malformed key store");
|
||||
}
|
||||
}
|
||||
|
||||
if (passwd != null) {
|
||||
byte[] computedHash = md.digest();
|
||||
byte[] storedHash = new byte[20];
|
||||
din.read(storedHash);
|
||||
if (!MessageDigest.isEqual(storedHash, computedHash)) {
|
||||
throw new LoadKeystoreException("Incorrect password, or integrity check failed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Own methods.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
private static Certificate readCert(DataInputStream in)
|
||||
throws IOException, CertificateException, NoSuchAlgorithmException {
|
||||
String type = in.readUTF();
|
||||
int len = in.readInt();
|
||||
byte[] encoded = new byte[len];
|
||||
in.read(encoded);
|
||||
CertificateFactory factory = CertificateFactory.getInstance(type);
|
||||
return factory.generateCertificate(new ByteArrayInputStream(encoded));
|
||||
}
|
||||
|
||||
private static void writeCert(DataOutputStream dout, Certificate cert)
|
||||
throws IOException, CertificateException {
|
||||
dout.writeUTF(cert.getType());
|
||||
byte[] b = cert.getEncoded();
|
||||
dout.writeInt(b.length);
|
||||
dout.write(b);
|
||||
}
|
||||
|
||||
private static byte[] decryptKey(byte[] encryptedPKI, byte[] passwd)
|
||||
throws UnrecoverableKeyException {
|
||||
try {
|
||||
EncryptedPrivateKeyInfo epki =
|
||||
new EncryptedPrivateKeyInfo(encryptedPKI);
|
||||
byte[] encr = epki.getEncryptedData();
|
||||
byte[] keystream = new byte[20];
|
||||
System.arraycopy(encr, 0, keystream, 0, 20);
|
||||
byte[] check = new byte[20];
|
||||
System.arraycopy(encr, encr.length - 20, check, 0, 20);
|
||||
byte[] key = new byte[encr.length - 40];
|
||||
MessageDigest sha = MessageDigest.getInstance("SHA1");
|
||||
int count = 0;
|
||||
while (count < key.length) {
|
||||
sha.reset();
|
||||
sha.update(passwd);
|
||||
sha.update(keystream);
|
||||
sha.digest(keystream, 0, keystream.length);
|
||||
for (int i = 0; i < keystream.length && count < key.length; i++) {
|
||||
key[count] = (byte) (keystream[i] ^ encr[count + 20]);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
sha.reset();
|
||||
sha.update(passwd);
|
||||
sha.update(key);
|
||||
if (!MessageDigest.isEqual(check, sha.digest()))
|
||||
throw new UnrecoverableKeyException("checksum mismatch");
|
||||
return key;
|
||||
} catch (Exception x) {
|
||||
throw new UnrecoverableKeyException(x.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] encryptKey(Key key, byte[] passwd)
|
||||
throws KeyStoreException {
|
||||
try {
|
||||
MessageDigest sha = MessageDigest.getInstance("SHA1");
|
||||
SecureRandom rand = SecureRandom.getInstance("SHA1PRNG");
|
||||
byte[] k = key.getEncoded();
|
||||
byte[] encrypted = new byte[k.length + 40];
|
||||
byte[] keystream = rand.getSeed(20);
|
||||
System.arraycopy(keystream, 0, encrypted, 0, 20);
|
||||
int count = 0;
|
||||
while (count < k.length) {
|
||||
sha.reset();
|
||||
sha.update(passwd);
|
||||
sha.update(keystream);
|
||||
sha.digest(keystream, 0, keystream.length);
|
||||
for (int i = 0; i < keystream.length && count < k.length; i++) {
|
||||
encrypted[count + 20] = (byte) (keystream[i] ^ k[count]);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
sha.reset();
|
||||
sha.update(passwd);
|
||||
sha.update(k);
|
||||
sha.digest(encrypted, encrypted.length - 20, 20);
|
||||
// 1.3.6.1.4.1.42.2.17.1.1 is Sun's private OID for this
|
||||
// encryption algorithm.
|
||||
return new EncryptedPrivateKeyInfo("1.3.6.1.4.1.42.2.17.1.1",
|
||||
encrypted).getEncoded();
|
||||
} catch (Exception x) {
|
||||
throw new KeyStoreException(x.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] charsToBytes(char[] passwd) {
|
||||
byte[] buf = new byte[passwd.length * 2];
|
||||
for (int i = 0, j = 0; i < passwd.length; i++) {
|
||||
buf[j++] = (byte) (passwd[i] >>> 8);
|
||||
buf[j++] = (byte) passwd[i];
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
|
||||
package kellinwood.security.zipsigner.optional;
|
||||
|
||||
|
||||
import java.security.KeyStore;
|
||||
|
||||
public class JksKeyStore extends KeyStore {
|
||||
|
||||
public JksKeyStore() {
|
||||
super(new JKS(), KeyStoreFileManager.getProvider(), "jks");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
|
||||
package kellinwood.security.zipsigner.optional;
|
||||
|
||||
public class KeyNameConflictException extends Exception {
|
||||
}
|
@ -0,0 +1,285 @@
|
||||
|
||||
package kellinwood.security.zipsigner.optional;
|
||||
|
||||
|
||||
import kellinwood.logging.LoggerInterface;
|
||||
import kellinwood.logging.LoggerManager;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.security.Key;
|
||||
import java.security.KeyStore;
|
||||
import java.security.Provider;
|
||||
import java.security.Security;
|
||||
import java.security.cert.Certificate;
|
||||
import java.util.Locale;
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
public class KeyStoreFileManager {
|
||||
|
||||
static Provider provider = new BouncyCastleProvider();
|
||||
|
||||
public static Provider getProvider() {
|
||||
return provider;
|
||||
}
|
||||
|
||||
public static void setProvider(Provider provider) {
|
||||
if (KeyStoreFileManager.provider != null) Security.removeProvider(KeyStoreFileManager.provider.getName());
|
||||
KeyStoreFileManager.provider = provider;
|
||||
Security.addProvider(provider);
|
||||
}
|
||||
|
||||
static LoggerInterface logger = LoggerManager.getLogger(KeyStoreFileManager.class.getName());
|
||||
|
||||
static {
|
||||
// Add the bouncycastle version of the BC provider so that the implementation classes returned
|
||||
// from the keystore are all from the bouncycastle libs.
|
||||
Security.addProvider(getProvider());
|
||||
}
|
||||
|
||||
|
||||
public static KeyStore loadKeyStore(String keystorePath, String encodedPassword)
|
||||
throws Exception {
|
||||
char password[] = null;
|
||||
try {
|
||||
if (encodedPassword != null) {
|
||||
password = PasswordObfuscator.getInstance().decodeKeystorePassword(keystorePath, encodedPassword);
|
||||
}
|
||||
return loadKeyStore(keystorePath, password);
|
||||
} finally {
|
||||
if (password != null) PasswordObfuscator.flush(password);
|
||||
}
|
||||
}
|
||||
|
||||
public static KeyStore createKeyStore(String keystorePath, char[] password)
|
||||
throws Exception {
|
||||
KeyStore ks = null;
|
||||
if (keystorePath.toLowerCase(Locale.ENGLISH).endsWith(".bks")) {
|
||||
ks = KeyStore.getInstance("bks", new BouncyCastleProvider());
|
||||
} else ks = new JksKeyStore();
|
||||
ks.load(null, password);
|
||||
|
||||
return ks;
|
||||
}
|
||||
|
||||
public static KeyStore loadKeyStore(String keystorePath, char[] password)
|
||||
throws Exception {
|
||||
KeyStore ks = null;
|
||||
try {
|
||||
ks = new JksKeyStore();
|
||||
FileInputStream fis = new FileInputStream(keystorePath);
|
||||
ks.load(fis, password);
|
||||
fis.close();
|
||||
return ks;
|
||||
} catch (LoadKeystoreException x) {
|
||||
// This type of exception is thrown when the keystore is a JKS keystore, but the file is malformed
|
||||
// or the validity/password check failed. In this case don't bother to attempt loading it as a BKS keystore.
|
||||
throw x;
|
||||
} catch (Exception x) {
|
||||
// logger.warning( x.getMessage(), x);
|
||||
try {
|
||||
ks = KeyStore.getInstance("bks", getProvider());
|
||||
FileInputStream fis = new FileInputStream(keystorePath);
|
||||
ks.load(fis, password);
|
||||
fis.close();
|
||||
return ks;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to load keystore: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeKeyStore(KeyStore ks, String keystorePath, String encodedPassword)
|
||||
throws Exception {
|
||||
char password[] = null;
|
||||
try {
|
||||
password = PasswordObfuscator.getInstance().decodeKeystorePassword(keystorePath, encodedPassword);
|
||||
writeKeyStore(ks, keystorePath, password);
|
||||
} finally {
|
||||
if (password != null) PasswordObfuscator.flush(password);
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeKeyStore(KeyStore ks, String keystorePath, char[] password)
|
||||
throws Exception {
|
||||
|
||||
File keystoreFile = new File(keystorePath);
|
||||
try {
|
||||
if (keystoreFile.exists()) {
|
||||
// I've had some trouble saving new versions of the keystore file in which the file becomes empty/corrupt.
|
||||
// Saving the new version to a new file and creating a backup of the old version.
|
||||
File tmpFile = File.createTempFile(keystoreFile.getName(), null, keystoreFile.getParentFile());
|
||||
FileOutputStream fos = new FileOutputStream(tmpFile);
|
||||
ks.store(fos, password);
|
||||
fos.flush();
|
||||
fos.close();
|
||||
/* create a backup of the previous version
|
||||
int i = 1;
|
||||
File backup = new File( keystorePath + "." + i + ".bak");
|
||||
while (backup.exists()) {
|
||||
i += 1;
|
||||
backup = new File( keystorePath + "." + i + ".bak");
|
||||
}
|
||||
renameTo(keystoreFile, backup);
|
||||
*/
|
||||
renameTo(tmpFile, keystoreFile);
|
||||
} else {
|
||||
FileOutputStream fos = new FileOutputStream(keystorePath);
|
||||
ks.store(fos, password);
|
||||
fos.close();
|
||||
}
|
||||
} catch (Exception x) {
|
||||
try {
|
||||
File logfile = File.createTempFile("zipsigner-error", ".log", keystoreFile.getParentFile());
|
||||
PrintWriter pw = new PrintWriter(new FileWriter(logfile));
|
||||
x.printStackTrace(pw);
|
||||
pw.flush();
|
||||
pw.close();
|
||||
} catch (Exception y) {
|
||||
}
|
||||
throw x;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void copyFile(File srcFile, File destFile, boolean preserveFileDate) throws IOException {
|
||||
if (destFile.exists() && destFile.isDirectory()) {
|
||||
throw new IOException("Destination '" + destFile + "' exists but is a directory");
|
||||
}
|
||||
|
||||
FileInputStream input = new FileInputStream(srcFile);
|
||||
try {
|
||||
FileOutputStream output = new FileOutputStream(destFile);
|
||||
try {
|
||||
byte[] buffer = new byte[4096];
|
||||
long count = 0;
|
||||
int n = 0;
|
||||
while (-1 != (n = input.read(buffer))) {
|
||||
output.write(buffer, 0, n);
|
||||
count += n;
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
output.close();
|
||||
} catch (IOException x) {
|
||||
} // Ignore
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
input.close();
|
||||
} catch (IOException x) {
|
||||
}
|
||||
}
|
||||
|
||||
if (srcFile.length() != destFile.length()) {
|
||||
throw new IOException("Failed to copy full contents from '" +
|
||||
srcFile + "' to '" + destFile + "'");
|
||||
}
|
||||
if (preserveFileDate) {
|
||||
destFile.setLastModified(srcFile.lastModified());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void renameTo(File fromFile, File toFile)
|
||||
throws IOException {
|
||||
copyFile(fromFile, toFile, true);
|
||||
if (!fromFile.delete()) throw new IOException("Failed to delete " + fromFile);
|
||||
}
|
||||
|
||||
public static void deleteKey(String storePath, String storePass, String keyName)
|
||||
throws Exception {
|
||||
KeyStore ks = loadKeyStore(storePath, storePass);
|
||||
ks.deleteEntry(keyName);
|
||||
writeKeyStore(ks, storePath, storePass);
|
||||
}
|
||||
|
||||
public static String renameKey(String keystorePath, String storePass, String oldKeyName, String newKeyName, String keyPass)
|
||||
throws Exception {
|
||||
char[] keyPw = null;
|
||||
|
||||
try {
|
||||
KeyStore ks = loadKeyStore(keystorePath, storePass);
|
||||
if (ks instanceof JksKeyStore) newKeyName = newKeyName.toLowerCase(Locale.ENGLISH);
|
||||
|
||||
if (ks.containsAlias(newKeyName)) throw new KeyNameConflictException();
|
||||
|
||||
keyPw = PasswordObfuscator.getInstance().decodeAliasPassword(keystorePath, oldKeyName, keyPass);
|
||||
Key key = ks.getKey(oldKeyName, keyPw);
|
||||
Certificate cert = ks.getCertificate(oldKeyName);
|
||||
|
||||
ks.setKeyEntry(newKeyName, key, keyPw, new Certificate[]{cert});
|
||||
ks.deleteEntry(oldKeyName);
|
||||
|
||||
writeKeyStore(ks, keystorePath, storePass);
|
||||
return newKeyName;
|
||||
} finally {
|
||||
PasswordObfuscator.flush(keyPw);
|
||||
}
|
||||
}
|
||||
|
||||
public static KeyStore.Entry getKeyEntry(String keystorePath, String storePass, String keyName, String keyPass)
|
||||
throws Exception {
|
||||
char[] keyPw = null;
|
||||
KeyStore.PasswordProtection passwordProtection = null;
|
||||
|
||||
try {
|
||||
KeyStore ks = loadKeyStore(keystorePath, storePass);
|
||||
keyPw = PasswordObfuscator.getInstance().decodeAliasPassword(keystorePath, keyName, keyPass);
|
||||
passwordProtection = new KeyStore.PasswordProtection(keyPw);
|
||||
return ks.getEntry(keyName, passwordProtection);
|
||||
} finally {
|
||||
if (keyPw != null) PasswordObfuscator.flush(keyPw);
|
||||
if (passwordProtection != null) passwordProtection.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean containsKey(String keystorePath, String storePass, String keyName)
|
||||
throws Exception {
|
||||
KeyStore ks = loadKeyStore(keystorePath, storePass);
|
||||
return ks.containsAlias(keyName);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param keystorePath
|
||||
* @param encodedPassword
|
||||
* @throws Exception if the password is invalid
|
||||
*/
|
||||
public static void validateKeystorePassword(String keystorePath, String encodedPassword)
|
||||
throws Exception {
|
||||
char[] password = null;
|
||||
try {
|
||||
KeyStore ks = KeyStoreFileManager.loadKeyStore(keystorePath, encodedPassword);
|
||||
} finally {
|
||||
if (password != null) PasswordObfuscator.flush(password);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param keystorePath
|
||||
* @param keyName
|
||||
* @param encodedPassword
|
||||
* @throws java.security.UnrecoverableKeyException if the password is invalid
|
||||
*/
|
||||
public static void validateKeyPassword(String keystorePath, String keyName, String encodedPassword)
|
||||
throws Exception {
|
||||
char[] password = null;
|
||||
try {
|
||||
KeyStore ks = KeyStoreFileManager.loadKeyStore(keystorePath, (char[]) null);
|
||||
password = PasswordObfuscator.getInstance().decodeAliasPassword(keystorePath, keyName, encodedPassword);
|
||||
ks.getKey(keyName, password);
|
||||
} finally {
|
||||
if (password != null) PasswordObfuscator.flush(password);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
|
||||
package kellinwood.security.zipsigner.optional;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Thrown by JKS.engineLoad() for errors that occur after determining the keystore is actually a JKS keystore.
|
||||
*/
|
||||
public class LoadKeystoreException extends IOException {
|
||||
|
||||
public LoadKeystoreException() {
|
||||
}
|
||||
|
||||
public LoadKeystoreException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public LoadKeystoreException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public LoadKeystoreException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
|
||||
package kellinwood.security.zipsigner.optional;
|
||||
|
||||
import kellinwood.logging.LoggerInterface;
|
||||
import kellinwood.logging.LoggerManager;
|
||||
import kellinwood.security.zipsigner.Base64;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
|
||||
public class PasswordObfuscator {
|
||||
|
||||
private static PasswordObfuscator instance = null;
|
||||
|
||||
static final String x = "harold-and-maude";
|
||||
|
||||
LoggerInterface logger;
|
||||
SecretKeySpec skeySpec;
|
||||
|
||||
private PasswordObfuscator() {
|
||||
logger = LoggerManager.getLogger(PasswordObfuscator.class.getName());
|
||||
skeySpec = new SecretKeySpec(x.getBytes(), "AES");
|
||||
}
|
||||
|
||||
public static PasswordObfuscator getInstance() {
|
||||
if (instance == null) instance = new PasswordObfuscator();
|
||||
return instance;
|
||||
}
|
||||
|
||||
public String encodeKeystorePassword(String keystorePath, String password) {
|
||||
return encode(keystorePath, password);
|
||||
}
|
||||
|
||||
public String encodeKeystorePassword(String keystorePath, char[] password) {
|
||||
return encode(keystorePath, password);
|
||||
}
|
||||
|
||||
public String encodeAliasPassword(String keystorePath, String aliasName, String password) {
|
||||
return encode(keystorePath + aliasName, password);
|
||||
}
|
||||
|
||||
public String encodeAliasPassword(String keystorePath, String aliasName, char[] password) {
|
||||
return encode(keystorePath + aliasName, password);
|
||||
}
|
||||
|
||||
public char[] decodeKeystorePassword(String keystorePath, String password) {
|
||||
return decode(keystorePath, password);
|
||||
}
|
||||
|
||||
public char[] decodeAliasPassword(String keystorePath, String aliasName, String password) {
|
||||
return decode(keystorePath + aliasName, password);
|
||||
}
|
||||
|
||||
public String encode(String junk, String password) {
|
||||
if (password == null) return null;
|
||||
char[] c = password.toCharArray();
|
||||
String result = encode(junk, c);
|
||||
flush(c);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>This uses the AES-ECB cipher which is known to be insecure</b>
|
||||
*
|
||||
* @see <a href="https://blog.filippo.io/the-ecb-penguin/">The ECB Penguin</a>
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("GetInstance")
|
||||
public String encode(String junk, char[] password) {
|
||||
if (password == null) return null;
|
||||
try {
|
||||
// Instantiate the cipher
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
Writer w = new OutputStreamWriter(baos);
|
||||
w.write(junk);
|
||||
w.write(password);
|
||||
w.flush();
|
||||
byte[] encoded = cipher.doFinal(baos.toByteArray());
|
||||
return Base64.encode(encoded);
|
||||
} catch (Exception x) {
|
||||
logger.error("Failed to obfuscate password", x);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>This uses the AES-ECB cipher which is known to be insecure</b>
|
||||
*
|
||||
* @see <a href="https://blog.filippo.io/the-ecb-penguin/">The ECB Penguin</a>
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("GetInstance")
|
||||
public char[] decode(String junk, String password) {
|
||||
if (password == null) return null;
|
||||
try {
|
||||
// Instantiate the cipher
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
|
||||
SecretKeySpec skeySpec = new SecretKeySpec(x.getBytes(), "AES");
|
||||
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
|
||||
byte[] bytes = cipher.doFinal(Base64.decode(password.getBytes()));
|
||||
BufferedReader r = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(bytes)));
|
||||
char[] cb = new char[128];
|
||||
int length = 0;
|
||||
int numRead;
|
||||
while ((numRead = r.read(cb, length, 128 - length)) != -1) {
|
||||
length += numRead;
|
||||
}
|
||||
|
||||
if (length <= junk.length()) return null;
|
||||
|
||||
char[] result = new char[length - junk.length()];
|
||||
int j = 0;
|
||||
for (int i = junk.length(); i < length; i++) {
|
||||
result[j] = cb[i];
|
||||
j += 1;
|
||||
}
|
||||
flush(cb);
|
||||
return result;
|
||||
|
||||
} catch (Exception x) {
|
||||
logger.error("Failed to decode password", x);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void flush(char[] charArray) {
|
||||
if (charArray == null) return;
|
||||
for (int i = 0; i < charArray.length; i++) {
|
||||
charArray[i] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
public static void flush(byte[] charArray) {
|
||||
if (charArray == null) return;
|
||||
for (int i = 0; i < charArray.length; i++) {
|
||||
charArray[i] = 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
|
||||
package kellinwood.security.zipsigner.optional;
|
||||
|
||||
import kellinwood.security.zipsigner.KeySet;
|
||||
import org.bouncycastle.cert.jcajce.JcaCertStore;
|
||||
import org.bouncycastle.cms.CMSProcessableByteArray;
|
||||
import org.bouncycastle.cms.CMSSignedData;
|
||||
import org.bouncycastle.cms.CMSSignedDataGenerator;
|
||||
import org.bouncycastle.cms.CMSTypedData;
|
||||
import org.bouncycastle.cms.SignerInfoGenerator;
|
||||
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
|
||||
import org.bouncycastle.operator.ContentSigner;
|
||||
import org.bouncycastle.operator.DigestCalculatorProvider;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
|
||||
import org.bouncycastle.util.Store;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class SignatureBlockGenerator {
|
||||
|
||||
/**
|
||||
* Sign the given content using the private and public keys from the keySet, and return the encoded CMS (PKCS#7) data.
|
||||
* Use of direct signature and DER encoding produces a block that is verifiable by Android recovery programs.
|
||||
*/
|
||||
public static byte[] generate(KeySet keySet, byte[] content) {
|
||||
try {
|
||||
List certList = new ArrayList();
|
||||
CMSTypedData msg = new CMSProcessableByteArray(content);
|
||||
|
||||
certList.add(keySet.getPublicKey());
|
||||
|
||||
Store certs = new JcaCertStore(certList);
|
||||
|
||||
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
|
||||
|
||||
JcaContentSignerBuilder jcaContentSignerBuilder = new JcaContentSignerBuilder(keySet.getSignatureAlgorithm()).setProvider("BC");
|
||||
ContentSigner sha1Signer = jcaContentSignerBuilder.build(keySet.getPrivateKey());
|
||||
|
||||
JcaDigestCalculatorProviderBuilder jcaDigestCalculatorProviderBuilder = new JcaDigestCalculatorProviderBuilder().setProvider("BC");
|
||||
DigestCalculatorProvider digestCalculatorProvider = jcaDigestCalculatorProviderBuilder.build();
|
||||
|
||||
JcaSignerInfoGeneratorBuilder jcaSignerInfoGeneratorBuilder = new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider);
|
||||
jcaSignerInfoGeneratorBuilder.setDirectSignature(true);
|
||||
SignerInfoGenerator signerInfoGenerator = jcaSignerInfoGeneratorBuilder.build(sha1Signer, keySet.getPublicKey());
|
||||
|
||||
gen.addSignerInfoGenerator(signerInfoGenerator);
|
||||
|
||||
gen.addCertificates(certs);
|
||||
|
||||
CMSSignedData sigData = gen.generate(msg, false);
|
||||
return sigData.toASN1Structure().getEncoded("DER");
|
||||
|
||||
} catch (Exception x) {
|
||||
throw new RuntimeException(x.getMessage(), x);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
105
app/src/full/java/kellinwood/zipio/CentralEnd.java
Normal file
105
app/src/full/java/kellinwood/zipio/CentralEnd.java
Normal file
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package kellinwood.zipio;
|
||||
|
||||
import kellinwood.logging.LoggerInterface;
|
||||
import kellinwood.logging.LoggerManager;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class CentralEnd {
|
||||
public int signature = 0x06054b50; // end of central dir signature 4 bytes
|
||||
public short numberThisDisk = 0; // number of this disk 2 bytes
|
||||
public short centralStartDisk = 0; // number of the disk with the start of the central directory 2 bytes
|
||||
public short numCentralEntries; // total number of entries in the central directory on this disk 2 bytes
|
||||
public short totalCentralEntries; // total number of entries in the central directory 2 bytes
|
||||
|
||||
public int centralDirectorySize; // size of the central directory 4 bytes
|
||||
public int centralStartOffset; // offset of start of central directory with respect to the starting disk number 4 bytes
|
||||
public String fileComment; // .ZIP file comment (variable size)
|
||||
|
||||
private static LoggerInterface log;
|
||||
|
||||
public static CentralEnd read(ZipInput input) throws IOException {
|
||||
|
||||
int signature = input.readInt();
|
||||
if (signature != 0x06054b50) {
|
||||
// back up to the signature
|
||||
input.seek(input.getFilePointer() - 4);
|
||||
return null;
|
||||
}
|
||||
|
||||
CentralEnd entry = new CentralEnd();
|
||||
|
||||
entry.doRead(input);
|
||||
return entry;
|
||||
}
|
||||
|
||||
public static LoggerInterface getLogger() {
|
||||
if (log == null) log = LoggerManager.getLogger(CentralEnd.class.getName());
|
||||
return log;
|
||||
}
|
||||
|
||||
|
||||
private void doRead(ZipInput input) throws IOException {
|
||||
|
||||
boolean debug = getLogger().isDebugEnabled();
|
||||
|
||||
numberThisDisk = input.readShort();
|
||||
if (debug) log.debug(String.format("This disk number: 0x%04x", numberThisDisk));
|
||||
|
||||
centralStartDisk = input.readShort();
|
||||
if (debug) log.debug(String.format("Central dir start disk number: 0x%04x", centralStartDisk));
|
||||
|
||||
numCentralEntries = input.readShort();
|
||||
if (debug) log.debug(String.format("Central entries on this disk: 0x%04x", numCentralEntries));
|
||||
|
||||
totalCentralEntries = input.readShort();
|
||||
if (debug) log.debug(String.format("Total number of central entries: 0x%04x", totalCentralEntries));
|
||||
|
||||
centralDirectorySize = input.readInt();
|
||||
if (debug) log.debug(String.format("Central directory size: 0x%08x", centralDirectorySize));
|
||||
|
||||
centralStartOffset = input.readInt();
|
||||
if (debug) log.debug(String.format("Central directory offset: 0x%08x", centralStartOffset));
|
||||
|
||||
short zipFileCommentLen = input.readShort();
|
||||
fileComment = input.readString(zipFileCommentLen);
|
||||
if (debug) log.debug(".ZIP file comment: " + fileComment);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
public void write(ZipOutput output) throws IOException {
|
||||
|
||||
boolean debug = getLogger().isDebugEnabled();
|
||||
|
||||
output.writeInt(signature);
|
||||
output.writeShort(numberThisDisk);
|
||||
output.writeShort(centralStartDisk);
|
||||
output.writeShort(numCentralEntries);
|
||||
output.writeShort(totalCentralEntries);
|
||||
output.writeInt(centralDirectorySize);
|
||||
output.writeInt(centralStartOffset);
|
||||
output.writeShort((short) fileComment.length());
|
||||
output.writeString(fileComment);
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
632
app/src/full/java/kellinwood/zipio/ZioEntry.java
Normal file
632
app/src/full/java/kellinwood/zipio/ZioEntry.java
Normal file
@ -0,0 +1,632 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package kellinwood.zipio;
|
||||
|
||||
import kellinwood.logging.LoggerInterface;
|
||||
import kellinwood.logging.LoggerManager;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.SequenceInputStream;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.zip.CRC32;
|
||||
import java.util.zip.Inflater;
|
||||
import java.util.zip.InflaterInputStream;
|
||||
|
||||
public class ZioEntry implements Cloneable {
|
||||
|
||||
private ZipInput zipInput;
|
||||
|
||||
// public int signature = 0x02014b50;
|
||||
private short versionMadeBy;
|
||||
private short versionRequired;
|
||||
private short generalPurposeBits;
|
||||
private short compression;
|
||||
private short modificationTime;
|
||||
private short modificationDate;
|
||||
private int crc32;
|
||||
private int compressedSize;
|
||||
private int size;
|
||||
private String filename;
|
||||
private byte[] extraData;
|
||||
private short numAlignBytes = 0;
|
||||
private String fileComment;
|
||||
private short diskNumberStart;
|
||||
private short internalAttributes;
|
||||
private int externalAttributes;
|
||||
|
||||
private int localHeaderOffset;
|
||||
private long dataPosition = -1;
|
||||
private byte[] data = null;
|
||||
private ZioEntryOutputStream entryOut = null;
|
||||
|
||||
|
||||
private static byte[] alignBytes = new byte[4];
|
||||
|
||||
private static LoggerInterface log;
|
||||
|
||||
public ZioEntry(ZipInput input) {
|
||||
zipInput = input;
|
||||
}
|
||||
|
||||
public static LoggerInterface getLogger() {
|
||||
if (log == null) log = LoggerManager.getLogger(ZioEntry.class.getName());
|
||||
return log;
|
||||
}
|
||||
|
||||
public ZioEntry(String name) {
|
||||
filename = name;
|
||||
fileComment = "";
|
||||
compression = 8;
|
||||
extraData = new byte[0];
|
||||
setTime(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
|
||||
public ZioEntry(String name, String sourceDataFile)
|
||||
throws IOException {
|
||||
zipInput = new ZipInput(sourceDataFile);
|
||||
filename = name;
|
||||
fileComment = "";
|
||||
this.compression = 0;
|
||||
this.size = (int) zipInput.getFileLength();
|
||||
this.compressedSize = this.size;
|
||||
|
||||
if (getLogger().isDebugEnabled())
|
||||
getLogger().debug(String.format(Locale.ENGLISH, "Computing CRC for %s, size=%d", sourceDataFile, size));
|
||||
|
||||
// compute CRC
|
||||
CRC32 crc = new CRC32();
|
||||
|
||||
byte[] buffer = new byte[8096];
|
||||
|
||||
int numRead = 0;
|
||||
while (numRead != size) {
|
||||
int count = zipInput.read(buffer, 0, Math.min(buffer.length, (this.size - numRead)));
|
||||
if (count > 0) {
|
||||
crc.update(buffer, 0, count);
|
||||
numRead += count;
|
||||
}
|
||||
}
|
||||
|
||||
this.crc32 = (int) crc.getValue();
|
||||
|
||||
zipInput.seek(0);
|
||||
this.dataPosition = 0;
|
||||
extraData = new byte[0];
|
||||
setTime(new File(sourceDataFile).lastModified());
|
||||
}
|
||||
|
||||
|
||||
public ZioEntry(String name, String sourceDataFile, short compression, int crc32, int compressedSize, int size)
|
||||
throws IOException {
|
||||
zipInput = new ZipInput(sourceDataFile);
|
||||
filename = name;
|
||||
fileComment = "";
|
||||
this.compression = compression;
|
||||
this.crc32 = crc32;
|
||||
this.compressedSize = compressedSize;
|
||||
this.size = size;
|
||||
this.dataPosition = 0;
|
||||
extraData = new byte[0];
|
||||
setTime(new File(sourceDataFile).lastModified());
|
||||
}
|
||||
|
||||
// Return a copy with a new name
|
||||
public ZioEntry getClonedEntry(String newName) {
|
||||
|
||||
ZioEntry clone;
|
||||
try {
|
||||
clone = (ZioEntry) this.clone();
|
||||
} catch (CloneNotSupportedException e) {
|
||||
throw new IllegalStateException("clone() failed!");
|
||||
}
|
||||
clone.setName(newName);
|
||||
return clone;
|
||||
}
|
||||
|
||||
public void readLocalHeader() throws IOException {
|
||||
ZipInput input = zipInput;
|
||||
int tmp;
|
||||
boolean debug = getLogger().isDebugEnabled();
|
||||
|
||||
input.seek(localHeaderOffset);
|
||||
|
||||
if (debug) getLogger().debug(String.format("FILE POSITION: 0x%08x", input.getFilePointer()));
|
||||
|
||||
// 0 4 Local file header signature = 0x04034b50
|
||||
int signature = input.readInt();
|
||||
if (signature != 0x04034b50) {
|
||||
throw new IllegalStateException(String.format("Local header not found at pos=0x%08x, file=%s", input.getFilePointer(), filename));
|
||||
}
|
||||
|
||||
// This method is usually called just before the data read, so
|
||||
// its only purpose currently is to position the file pointer
|
||||
// for the data read. The entry's attributes might also have
|
||||
// been changed since the central dir entry was read (e.g.,
|
||||
// filename), so throw away the values here.
|
||||
|
||||
int tmpInt;
|
||||
short tmpShort;
|
||||
|
||||
// 4 2 Version needed to extract (minimum)
|
||||
/* versionRequired */
|
||||
tmpShort = input.readShort();
|
||||
if (debug) log.debug(String.format("Version required: 0x%04x", tmpShort /*versionRequired*/));
|
||||
|
||||
// 6 2 General purpose bit flag
|
||||
/* generalPurposeBits */
|
||||
tmpShort = input.readShort();
|
||||
if (debug) log.debug(String.format("General purpose bits: 0x%04x", tmpShort /* generalPurposeBits */));
|
||||
|
||||
// 8 2 Compression method
|
||||
/* compression */
|
||||
tmpShort = input.readShort();
|
||||
if (debug) log.debug(String.format("Compression: 0x%04x", tmpShort /* compression */));
|
||||
|
||||
// 10 2 File last modification time
|
||||
/* modificationTime */
|
||||
tmpShort = input.readShort();
|
||||
if (debug) log.debug(String.format("Modification time: 0x%04x", tmpShort /* modificationTime */));
|
||||
|
||||
// 12 2 File last modification date
|
||||
/* modificationDate */
|
||||
tmpShort = input.readShort();
|
||||
if (debug) log.debug(String.format("Modification date: 0x%04x", tmpShort /* modificationDate */));
|
||||
|
||||
// 14 4 CRC-32
|
||||
/* crc32 */
|
||||
tmpInt = input.readInt();
|
||||
if (debug) log.debug(String.format("CRC-32: 0x%04x", tmpInt /*crc32*/));
|
||||
|
||||
// 18 4 Compressed size
|
||||
/* compressedSize*/
|
||||
tmpInt = input.readInt();
|
||||
if (debug) log.debug(String.format("Compressed size: 0x%04x", tmpInt /*compressedSize*/));
|
||||
|
||||
// 22 4 Uncompressed size
|
||||
/* size */
|
||||
tmpInt = input.readInt();
|
||||
if (debug) log.debug(String.format("Size: 0x%04x", tmpInt /*size*/));
|
||||
|
||||
// 26 2 File name length (n)
|
||||
short fileNameLen = input.readShort();
|
||||
if (debug) log.debug(String.format("File name length: 0x%04x", fileNameLen));
|
||||
|
||||
// 28 2 Extra field length (m)
|
||||
short extraLen = input.readShort();
|
||||
if (debug) log.debug(String.format("Extra length: 0x%04x", extraLen));
|
||||
|
||||
// 30 n File name
|
||||
String filename = input.readString(fileNameLen);
|
||||
if (debug) log.debug("Filename: " + filename);
|
||||
|
||||
// Extra data
|
||||
byte[] extra = input.readBytes(extraLen);
|
||||
|
||||
// Record the file position of this entry's data.
|
||||
dataPosition = input.getFilePointer();
|
||||
if (debug) log.debug(String.format("Data position: 0x%08x", dataPosition));
|
||||
|
||||
}
|
||||
|
||||
public void writeLocalEntry(ZipOutput output) throws IOException {
|
||||
if (data == null && dataPosition < 0 && zipInput != null) {
|
||||
readLocalHeader();
|
||||
}
|
||||
|
||||
localHeaderOffset = (int) output.getFilePointer();
|
||||
|
||||
boolean debug = getLogger().isDebugEnabled();
|
||||
|
||||
if (debug) {
|
||||
getLogger().debug(String.format("Writing local header at 0x%08x - %s", localHeaderOffset, filename));
|
||||
}
|
||||
|
||||
if (entryOut != null) {
|
||||
entryOut.close();
|
||||
size = entryOut.getSize();
|
||||
data = ((ByteArrayOutputStream) entryOut.getWrappedStream()).toByteArray();
|
||||
compressedSize = data.length;
|
||||
crc32 = entryOut.getCRC();
|
||||
}
|
||||
|
||||
output.writeInt(0x04034b50);
|
||||
output.writeShort(versionRequired);
|
||||
output.writeShort(generalPurposeBits);
|
||||
output.writeShort(compression);
|
||||
output.writeShort(modificationTime);
|
||||
output.writeShort(modificationDate);
|
||||
output.writeInt(crc32);
|
||||
output.writeInt(compressedSize);
|
||||
output.writeInt(size);
|
||||
output.writeShort((short) filename.length());
|
||||
|
||||
numAlignBytes = 0;
|
||||
|
||||
// Zipalign if the file is uncompressed, i.e., "Stored", and file size is not zero.
|
||||
if (compression == 0) {
|
||||
|
||||
long dataPos = output.getFilePointer() + // current position
|
||||
2 + // plus size of extra data length
|
||||
filename.length() + // plus filename
|
||||
extraData.length; // plus extra data
|
||||
|
||||
short dataPosMod4 = (short) (dataPos % 4);
|
||||
|
||||
if (dataPosMod4 > 0) {
|
||||
numAlignBytes = (short) (4 - dataPosMod4);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 28 2 Extra field length (m)
|
||||
output.writeShort((short) (extraData.length + numAlignBytes));
|
||||
|
||||
// 30 n File name
|
||||
output.writeString(filename);
|
||||
|
||||
// Extra data
|
||||
output.writeBytes(extraData);
|
||||
|
||||
// Zipalign bytes
|
||||
if (numAlignBytes > 0) {
|
||||
output.writeBytes(alignBytes, 0, numAlignBytes);
|
||||
}
|
||||
|
||||
if (debug) getLogger().debug(String.format(Locale.ENGLISH, "Data position 0x%08x", output.getFilePointer()));
|
||||
if (data != null) {
|
||||
output.writeBytes(data);
|
||||
if (debug) getLogger().debug(String.format(Locale.ENGLISH, "Wrote %d bytes", data.length));
|
||||
} else {
|
||||
|
||||
if (debug) getLogger().debug(String.format("Seeking to position 0x%08x", dataPosition));
|
||||
zipInput.seek(dataPosition);
|
||||
|
||||
int bufferSize = Math.min(compressedSize, 8096);
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
long totalCount = 0;
|
||||
|
||||
while (totalCount != compressedSize) {
|
||||
int numRead = zipInput.in.read(buffer, 0, (int) Math.min(compressedSize - totalCount, bufferSize));
|
||||
if (numRead > 0) {
|
||||
output.writeBytes(buffer, 0, numRead);
|
||||
if (debug) getLogger().debug(String.format(Locale.ENGLISH, "Wrote %d bytes", numRead));
|
||||
totalCount += numRead;
|
||||
} else
|
||||
throw new IllegalStateException(String.format(Locale.ENGLISH, "EOF reached while copying %s with %d bytes left to go", filename, compressedSize - totalCount));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static ZioEntry read(ZipInput input) throws IOException {
|
||||
|
||||
// 0 4 Central directory header signature = 0x02014b50
|
||||
int signature = input.readInt();
|
||||
if (signature != 0x02014b50) {
|
||||
// back up to the signature
|
||||
input.seek(input.getFilePointer() - 4);
|
||||
return null;
|
||||
}
|
||||
|
||||
ZioEntry entry = new ZioEntry(input);
|
||||
|
||||
entry.doRead(input);
|
||||
return entry;
|
||||
}
|
||||
|
||||
private void doRead(ZipInput input) throws IOException {
|
||||
|
||||
boolean debug = getLogger().isDebugEnabled();
|
||||
|
||||
// 4 2 Version needed to extract (minimum)
|
||||
versionMadeBy = input.readShort();
|
||||
if (debug) log.debug(String.format("Version made by: 0x%04x", versionMadeBy));
|
||||
|
||||
// 4 2 Version required
|
||||
versionRequired = input.readShort();
|
||||
if (debug) log.debug(String.format("Version required: 0x%04x", versionRequired));
|
||||
|
||||
// 6 2 General purpose bit flag
|
||||
generalPurposeBits = input.readShort();
|
||||
if (debug) log.debug(String.format("General purpose bits: 0x%04x", generalPurposeBits));
|
||||
// Bits 1, 2, 3, and 11 are allowed to be set (first bit is bit zero). Any others are a problem.
|
||||
if ((generalPurposeBits & 0xF7F1) != 0x0000) {
|
||||
throw new IllegalStateException("Can't handle general purpose bits == " + String.format("0x%04x", generalPurposeBits));
|
||||
}
|
||||
|
||||
// 8 2 Compression method
|
||||
compression = input.readShort();
|
||||
if (debug) log.debug(String.format("Compression: 0x%04x", compression));
|
||||
|
||||
// 10 2 File last modification time
|
||||
modificationTime = input.readShort();
|
||||
if (debug) log.debug(String.format("Modification time: 0x%04x", modificationTime));
|
||||
|
||||
// 12 2 File last modification date
|
||||
modificationDate = input.readShort();
|
||||
if (debug) log.debug(String.format("Modification date: 0x%04x", modificationDate));
|
||||
|
||||
// 14 4 CRC-32
|
||||
crc32 = input.readInt();
|
||||
if (debug) log.debug(String.format("CRC-32: 0x%04x", crc32));
|
||||
|
||||
// 18 4 Compressed size
|
||||
compressedSize = input.readInt();
|
||||
if (debug) log.debug(String.format("Compressed size: 0x%04x", compressedSize));
|
||||
|
||||
// 22 4 Uncompressed size
|
||||
size = input.readInt();
|
||||
if (debug) log.debug(String.format("Size: 0x%04x", size));
|
||||
|
||||
// 26 2 File name length (n)
|
||||
short fileNameLen = input.readShort();
|
||||
if (debug) log.debug(String.format("File name length: 0x%04x", fileNameLen));
|
||||
|
||||
// 28 2 Extra field length (m)
|
||||
short extraLen = input.readShort();
|
||||
if (debug) log.debug(String.format("Extra length: 0x%04x", extraLen));
|
||||
|
||||
short fileCommentLen = input.readShort();
|
||||
if (debug) log.debug(String.format("File comment length: 0x%04x", fileCommentLen));
|
||||
|
||||
diskNumberStart = input.readShort();
|
||||
if (debug) log.debug(String.format("Disk number start: 0x%04x", diskNumberStart));
|
||||
|
||||
internalAttributes = input.readShort();
|
||||
if (debug) log.debug(String.format("Internal attributes: 0x%04x", internalAttributes));
|
||||
|
||||
externalAttributes = input.readInt();
|
||||
if (debug) log.debug(String.format("External attributes: 0x%08x", externalAttributes));
|
||||
|
||||
localHeaderOffset = input.readInt();
|
||||
if (debug) log.debug(String.format("Local header offset: 0x%08x", localHeaderOffset));
|
||||
|
||||
// 30 n File name
|
||||
filename = input.readString(fileNameLen);
|
||||
if (debug) log.debug("Filename: " + filename);
|
||||
|
||||
extraData = input.readBytes(extraLen);
|
||||
|
||||
fileComment = input.readString(fileCommentLen);
|
||||
if (debug) log.debug("File comment: " + fileComment);
|
||||
|
||||
generalPurposeBits = (short) (generalPurposeBits & 0x0800); // Don't write a data descriptor, preserve UTF-8 encoded filename bit
|
||||
|
||||
// Don't write zero-length entries with compression.
|
||||
if (size == 0) {
|
||||
compressedSize = 0;
|
||||
compression = 0;
|
||||
crc32 = 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the entry's data.
|
||||
*/
|
||||
public byte[] getData() throws IOException {
|
||||
if (data != null) return data;
|
||||
|
||||
byte[] tmpdata = new byte[size];
|
||||
|
||||
InputStream din = getInputStream();
|
||||
int count = 0;
|
||||
|
||||
while (count != size) {
|
||||
int numRead = din.read(tmpdata, count, size - count);
|
||||
if (numRead < 0)
|
||||
throw new IllegalStateException(String.format(Locale.ENGLISH, "Read failed, expecting %d bytes, got %d instead", size, count));
|
||||
count += numRead;
|
||||
}
|
||||
return tmpdata;
|
||||
}
|
||||
|
||||
// Returns an input stream for reading the entry's data.
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return getInputStream(null);
|
||||
}
|
||||
|
||||
// Returns an input stream for reading the entry's data.
|
||||
public InputStream getInputStream(OutputStream monitorStream) throws IOException {
|
||||
|
||||
if (entryOut != null) {
|
||||
entryOut.close();
|
||||
size = entryOut.getSize();
|
||||
data = ((ByteArrayOutputStream) entryOut.getWrappedStream()).toByteArray();
|
||||
compressedSize = data.length;
|
||||
crc32 = entryOut.getCRC();
|
||||
entryOut = null;
|
||||
InputStream rawis = new ByteArrayInputStream(data);
|
||||
if (compression == 0) return rawis;
|
||||
else {
|
||||
// Hacky, inflate using a sequence of input streams that returns 1 byte more than the actual length of the data.
|
||||
// This extra dummy byte is required by InflaterInputStream when the data doesn't have the header and crc fields (as it is in zip files).
|
||||
return new InflaterInputStream(new SequenceInputStream(rawis, new ByteArrayInputStream(new byte[1])), new Inflater(true));
|
||||
}
|
||||
}
|
||||
|
||||
ZioEntryInputStream dataStream;
|
||||
dataStream = new ZioEntryInputStream(this);
|
||||
if (monitorStream != null) dataStream.setMonitorStream(monitorStream);
|
||||
if (compression != 0) {
|
||||
// Note: When using nowrap=true with Inflater it is also necessary to provide
|
||||
// an extra "dummy" byte as input. This is required by the ZLIB native library
|
||||
// in order to support certain optimizations.
|
||||
dataStream.setReturnDummyByte(true);
|
||||
return new InflaterInputStream(dataStream, new Inflater(true));
|
||||
} else return dataStream;
|
||||
}
|
||||
|
||||
// Returns an output stream for writing an entry's data.
|
||||
public OutputStream getOutputStream() {
|
||||
entryOut = new ZioEntryOutputStream(compression, new ByteArrayOutputStream());
|
||||
return entryOut;
|
||||
}
|
||||
|
||||
|
||||
public void write(ZipOutput output) throws IOException {
|
||||
boolean debug = getLogger().isDebugEnabled();
|
||||
|
||||
|
||||
output.writeInt(0x02014b50);
|
||||
output.writeShort(versionMadeBy);
|
||||
output.writeShort(versionRequired);
|
||||
output.writeShort(generalPurposeBits);
|
||||
output.writeShort(compression);
|
||||
output.writeShort(modificationTime);
|
||||
output.writeShort(modificationDate);
|
||||
output.writeInt(crc32);
|
||||
output.writeInt(compressedSize);
|
||||
output.writeInt(size);
|
||||
output.writeShort((short) filename.length());
|
||||
output.writeShort((short) (extraData.length + numAlignBytes));
|
||||
output.writeShort((short) fileComment.length());
|
||||
output.writeShort(diskNumberStart);
|
||||
output.writeShort(internalAttributes);
|
||||
output.writeInt(externalAttributes);
|
||||
output.writeInt(localHeaderOffset);
|
||||
|
||||
output.writeString(filename);
|
||||
output.writeBytes(extraData);
|
||||
if (numAlignBytes > 0) output.writeBytes(alignBytes, 0, numAlignBytes);
|
||||
output.writeString(fileComment);
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns timetamp in Java format
|
||||
*/
|
||||
public long getTime() {
|
||||
int year = (int) (((modificationDate >> 9) & 0x007f) + 80);
|
||||
int month = (int) (((modificationDate >> 5) & 0x000f) - 1);
|
||||
int day = (int) (modificationDate & 0x001f);
|
||||
int hour = (int) ((modificationTime >> 11) & 0x001f);
|
||||
int minute = (int) ((modificationTime >> 5) & 0x003f);
|
||||
int seconds = (int) ((modificationTime << 1) & 0x003e);
|
||||
Date d = new Date(year, month, day, hour, minute, seconds);
|
||||
return d.getTime();
|
||||
}
|
||||
|
||||
/*
|
||||
* Set the file timestamp (using a Java time value).
|
||||
*/
|
||||
public void setTime(long time) {
|
||||
Date d = new Date(time);
|
||||
long dtime;
|
||||
int year = d.getYear() + 1900;
|
||||
if (year < 1980) {
|
||||
dtime = (1 << 21) | (1 << 16);
|
||||
} else {
|
||||
dtime = (year - 1980) << 25 | (d.getMonth() + 1) << 21 |
|
||||
d.getDate() << 16 | d.getHours() << 11 | d.getMinutes() << 5 |
|
||||
d.getSeconds() >> 1;
|
||||
}
|
||||
|
||||
modificationDate = (short) (dtime >> 16);
|
||||
modificationTime = (short) (dtime & 0xFFFF);
|
||||
}
|
||||
|
||||
public boolean isDirectory() {
|
||||
return filename.endsWith("/");
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
public void setName(String filename) {
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use 0 (STORED), or 8 (DEFLATE).
|
||||
*/
|
||||
public void setCompression(int compression) {
|
||||
this.compression = (short) compression;
|
||||
}
|
||||
|
||||
public short getVersionMadeBy() {
|
||||
return versionMadeBy;
|
||||
}
|
||||
|
||||
public short getVersionRequired() {
|
||||
return versionRequired;
|
||||
}
|
||||
|
||||
public short getGeneralPurposeBits() {
|
||||
return generalPurposeBits;
|
||||
}
|
||||
|
||||
public short getCompression() {
|
||||
return compression;
|
||||
}
|
||||
|
||||
public int getCrc32() {
|
||||
return crc32;
|
||||
}
|
||||
|
||||
public int getCompressedSize() {
|
||||
return compressedSize;
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public byte[] getExtraData() {
|
||||
return extraData;
|
||||
}
|
||||
|
||||
public String getFileComment() {
|
||||
return fileComment;
|
||||
}
|
||||
|
||||
public short getDiskNumberStart() {
|
||||
return diskNumberStart;
|
||||
}
|
||||
|
||||
public short getInternalAttributes() {
|
||||
return internalAttributes;
|
||||
}
|
||||
|
||||
public int getExternalAttributes() {
|
||||
return externalAttributes;
|
||||
}
|
||||
|
||||
public int getLocalHeaderOffset() {
|
||||
return localHeaderOffset;
|
||||
}
|
||||
|
||||
public long getDataPosition() {
|
||||
return dataPosition;
|
||||
}
|
||||
|
||||
public ZioEntryOutputStream getEntryOut() {
|
||||
return entryOut;
|
||||
}
|
||||
|
||||
public ZipInput getZipInput() {
|
||||
return zipInput;
|
||||
}
|
||||
|
||||
}
|
141
app/src/full/java/kellinwood/zipio/ZioEntryInputStream.java
Normal file
141
app/src/full/java/kellinwood/zipio/ZioEntryInputStream.java
Normal file
@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package kellinwood.zipio;
|
||||
|
||||
import kellinwood.logging.LoggerInterface;
|
||||
import kellinwood.logging.LoggerManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.util.Locale;
|
||||
|
||||
|
||||
/**
|
||||
* Input stream used to read just the data from a zip file entry.
|
||||
*/
|
||||
public class ZioEntryInputStream extends InputStream {
|
||||
|
||||
RandomAccessFile raf;
|
||||
int size;
|
||||
int offset;
|
||||
LoggerInterface log;
|
||||
boolean debug;
|
||||
boolean returnDummyByte = false;
|
||||
OutputStream monitor = null;
|
||||
|
||||
public ZioEntryInputStream(ZioEntry entry) throws IOException {
|
||||
|
||||
log = LoggerManager.getLogger(this.getClass().getName());
|
||||
debug = log.isDebugEnabled();
|
||||
offset = 0;
|
||||
size = entry.getCompressedSize();
|
||||
raf = entry.getZipInput().in;
|
||||
long dpos = entry.getDataPosition();
|
||||
if (dpos >= 0) {
|
||||
if (debug) log.debug(String.format(Locale.ENGLISH, "Seeking to %d", entry.getDataPosition()));
|
||||
raf.seek(entry.getDataPosition());
|
||||
} else {
|
||||
// seeks to, then reads, the local header, causing the
|
||||
// file pointer to be positioned at the start of the data.
|
||||
entry.readLocalHeader();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void setReturnDummyByte(boolean returnExtraByte) {
|
||||
returnDummyByte = returnExtraByte;
|
||||
}
|
||||
|
||||
// For debugging, if the monitor is set we write all data read to the monitor.
|
||||
public void setMonitorStream(OutputStream monitorStream) {
|
||||
monitor = monitorStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
int available = size - offset;
|
||||
if (debug) log.debug(String.format(Locale.ENGLISH, "Available = %d", available));
|
||||
if (available == 0 && returnDummyByte) return 1;
|
||||
else return available;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if ((size - offset) == 0) {
|
||||
if (returnDummyByte) {
|
||||
returnDummyByte = false;
|
||||
return 0;
|
||||
} else return -1;
|
||||
}
|
||||
int b = raf.read();
|
||||
if (b >= 0) {
|
||||
if (monitor != null) monitor.write(b);
|
||||
if (debug) log.debug("Read 1 byte");
|
||||
offset += 1;
|
||||
} else if (debug) log.debug("Read 0 bytes");
|
||||
return b;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
return readBytes(b, off, len);
|
||||
}
|
||||
|
||||
private int readBytes(byte[] b, int off, int len) throws IOException {
|
||||
if ((size - offset) == 0) {
|
||||
if (returnDummyByte) {
|
||||
returnDummyByte = false;
|
||||
b[off] = 0;
|
||||
return 1;
|
||||
} else return -1;
|
||||
}
|
||||
int numToRead = Math.min(len, available());
|
||||
int numRead = raf.read(b, off, numToRead);
|
||||
if (numRead > 0) {
|
||||
if (monitor != null) monitor.write(b, off, numRead);
|
||||
offset += numRead;
|
||||
}
|
||||
if (debug) log.debug(String.format(Locale.ENGLISH, "Read %d bytes for read(b,%d,%d)", numRead, off, len));
|
||||
return numRead;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b) throws IOException {
|
||||
return readBytes(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
long numToSkip = Math.min(n, available());
|
||||
raf.seek(raf.getFilePointer() + numToSkip);
|
||||
if (debug) log.debug(String.format(Locale.ENGLISH, "Skipped %d bytes", numToSkip));
|
||||
return numToSkip;
|
||||
}
|
||||
}
|
||||
|
||||
|
87
app/src/full/java/kellinwood/zipio/ZioEntryOutputStream.java
Normal file
87
app/src/full/java/kellinwood/zipio/ZioEntryOutputStream.java
Normal file
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package kellinwood.zipio;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.zip.CRC32;
|
||||
import java.util.zip.Deflater;
|
||||
import java.util.zip.DeflaterOutputStream;
|
||||
|
||||
public class ZioEntryOutputStream extends OutputStream {
|
||||
int size = 0; // tracks uncompressed size of data
|
||||
CRC32 crc = new CRC32();
|
||||
int crcValue = 0;
|
||||
OutputStream wrapped;
|
||||
OutputStream downstream;
|
||||
Deflater deflater;
|
||||
|
||||
public ZioEntryOutputStream(int compression, OutputStream wrapped) {
|
||||
this.wrapped = wrapped;
|
||||
if (compression != 0) {
|
||||
deflater = new Deflater(Deflater.BEST_COMPRESSION, true);
|
||||
downstream = new DeflaterOutputStream(wrapped, deflater);
|
||||
} else {
|
||||
downstream = wrapped;
|
||||
}
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
downstream.flush();
|
||||
downstream.close();
|
||||
crcValue = (int) crc.getValue();
|
||||
if (deflater != null) {
|
||||
deflater.end();
|
||||
}
|
||||
}
|
||||
|
||||
public int getCRC() {
|
||||
return crcValue;
|
||||
}
|
||||
|
||||
public void flush() throws IOException {
|
||||
downstream.flush();
|
||||
}
|
||||
|
||||
public void write(byte[] b) throws IOException {
|
||||
downstream.write(b);
|
||||
crc.update(b);
|
||||
size += b.length;
|
||||
}
|
||||
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
downstream.write(b, off, len);
|
||||
crc.update(b, off, len);
|
||||
size += len;
|
||||
}
|
||||
|
||||
public void write(int b) throws IOException {
|
||||
downstream.write(b);
|
||||
crc.update(b);
|
||||
size += 1;
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public OutputStream getWrappedStream() {
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
}
|
||||
|
232
app/src/full/java/kellinwood/zipio/ZipInput.java
Normal file
232
app/src/full/java/kellinwood/zipio/ZipInput.java
Normal file
@ -0,0 +1,232 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package kellinwood.zipio;
|
||||
|
||||
import kellinwood.logging.LoggerInterface;
|
||||
import kellinwood.logging.LoggerManager;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class ZipInput implements Closeable {
|
||||
static LoggerInterface log;
|
||||
|
||||
public String inputFilename;
|
||||
RandomAccessFile in = null;
|
||||
long fileLength;
|
||||
int scanIterations = 0;
|
||||
|
||||
Map<String, ZioEntry> zioEntries = new LinkedHashMap<String, ZioEntry>();
|
||||
CentralEnd centralEnd;
|
||||
Manifest manifest;
|
||||
|
||||
public ZipInput(String filename) throws IOException {
|
||||
this.inputFilename = filename;
|
||||
in = new RandomAccessFile(new File(inputFilename), "r");
|
||||
fileLength = in.length();
|
||||
}
|
||||
|
||||
private static LoggerInterface getLogger() {
|
||||
if (log == null) log = LoggerManager.getLogger(ZipInput.class.getName());
|
||||
return log;
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return inputFilename;
|
||||
}
|
||||
|
||||
public long getFileLength() {
|
||||
return fileLength;
|
||||
}
|
||||
|
||||
public static ZipInput read(String filename) throws IOException {
|
||||
ZipInput zipInput = new ZipInput(filename);
|
||||
zipInput.doRead();
|
||||
return zipInput;
|
||||
}
|
||||
|
||||
|
||||
public ZioEntry getEntry(String filename) {
|
||||
return zioEntries.get(filename);
|
||||
}
|
||||
|
||||
public Map<String, ZioEntry> getEntries() {
|
||||
return zioEntries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the names of immediate children in the directory with the given name.
|
||||
* The path value must end with a "/" character. Use a value of "/"
|
||||
* to get the root entries.
|
||||
*/
|
||||
public Collection<String> list(String path) {
|
||||
if (!path.endsWith("/")) throw new IllegalArgumentException("Invalid path -- does not end with '/'");
|
||||
|
||||
if (path.startsWith("/")) path = path.substring(1);
|
||||
|
||||
Pattern p = Pattern.compile(String.format("^%s([^/]+/?).*", path));
|
||||
|
||||
Set<String> names = new TreeSet<String>();
|
||||
|
||||
for (String name : zioEntries.keySet()) {
|
||||
Matcher m = p.matcher(name);
|
||||
if (m.matches()) names.add(m.group(1));
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
public Manifest getManifest() throws IOException {
|
||||
if (manifest == null) {
|
||||
ZioEntry e = zioEntries.get("META-INF/MANIFEST.MF");
|
||||
if (e != null) {
|
||||
manifest = new Manifest(e.getInputStream());
|
||||
}
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan the end of the file for the end of central directory record (EOCDR).
|
||||
* Returns the file offset of the EOCD signature. The size parameter is an
|
||||
* initial buffer size (e.g., 256).
|
||||
*/
|
||||
public long scanForEOCDR(int size) throws IOException {
|
||||
if (size > fileLength || size > 65536)
|
||||
throw new IllegalStateException("End of central directory not found in " + inputFilename);
|
||||
|
||||
int scanSize = (int) Math.min(fileLength, size);
|
||||
|
||||
byte[] scanBuf = new byte[scanSize];
|
||||
|
||||
in.seek(fileLength - scanSize);
|
||||
|
||||
in.readFully(scanBuf);
|
||||
|
||||
for (int i = scanSize - 22; i >= 0; i--) {
|
||||
scanIterations += 1;
|
||||
if (scanBuf[i] == 0x50 && scanBuf[i + 1] == 0x4b && scanBuf[i + 2] == 0x05 && scanBuf[i + 3] == 0x06) {
|
||||
return fileLength - scanSize + i;
|
||||
}
|
||||
}
|
||||
|
||||
return scanForEOCDR(size * 2);
|
||||
}
|
||||
|
||||
|
||||
private void doRead() {
|
||||
try {
|
||||
|
||||
long posEOCDR = scanForEOCDR(256);
|
||||
in.seek(posEOCDR);
|
||||
centralEnd = CentralEnd.read(this);
|
||||
|
||||
boolean debug = getLogger().isDebugEnabled();
|
||||
if (debug) {
|
||||
getLogger().debug(String.format(Locale.ENGLISH, "EOCD found in %d iterations", scanIterations));
|
||||
getLogger().debug(String.format(Locale.ENGLISH, "Directory entries=%d, size=%d, offset=%d/0x%08x", centralEnd.totalCentralEntries,
|
||||
centralEnd.centralDirectorySize, centralEnd.centralStartOffset, centralEnd.centralStartOffset));
|
||||
|
||||
ZipListingHelper.listHeader(getLogger());
|
||||
}
|
||||
|
||||
in.seek(centralEnd.centralStartOffset);
|
||||
|
||||
for (int i = 0; i < centralEnd.totalCentralEntries; i++) {
|
||||
ZioEntry entry = ZioEntry.read(this);
|
||||
zioEntries.put(entry.getName(), entry);
|
||||
if (debug) ZipListingHelper.listEntry(getLogger(), entry);
|
||||
}
|
||||
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (in != null) try {
|
||||
in.close();
|
||||
} catch (Throwable t) {
|
||||
}
|
||||
}
|
||||
|
||||
public long getFilePointer() throws IOException {
|
||||
return in.getFilePointer();
|
||||
}
|
||||
|
||||
public void seek(long position) throws IOException {
|
||||
in.seek(position);
|
||||
}
|
||||
|
||||
public byte readByte() throws IOException {
|
||||
return in.readByte();
|
||||
}
|
||||
|
||||
public int readInt() throws IOException {
|
||||
int result = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
result |= (in.readUnsignedByte() << (8 * i));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public short readShort() throws IOException {
|
||||
short result = 0;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
result |= (in.readUnsignedByte() << (8 * i));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public String readString(int length) throws IOException {
|
||||
|
||||
byte[] buffer = new byte[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
buffer[i] = in.readByte();
|
||||
}
|
||||
return new String(buffer);
|
||||
}
|
||||
|
||||
public byte[] readBytes(int length) throws IOException {
|
||||
|
||||
byte[] buffer = new byte[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
buffer[i] = in.readByte();
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public int read(byte[] b, int offset, int length) throws IOException {
|
||||
return in.read(b, offset, length);
|
||||
}
|
||||
}
|
||||
|
||||
|
53
app/src/full/java/kellinwood/zipio/ZipListingHelper.java
Normal file
53
app/src/full/java/kellinwood/zipio/ZipListingHelper.java
Normal file
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package kellinwood.zipio;
|
||||
|
||||
import kellinwood.logging.LoggerInterface;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class ZipListingHelper {
|
||||
|
||||
static DateFormat dateFormat = new SimpleDateFormat("MM-dd-yy HH:mm", Locale.ENGLISH);
|
||||
|
||||
public static void listHeader(LoggerInterface log) {
|
||||
log.debug(" Length Method Size Ratio Date Time CRC-32 Name");
|
||||
log.debug("-------- ------ ------- ----- ---- ---- ------ ----");
|
||||
|
||||
}
|
||||
|
||||
public static void listEntry(LoggerInterface log, ZioEntry entry) {
|
||||
int ratio = 0;
|
||||
if (entry.getSize() > 0) ratio = (100 * (entry.getSize() - entry.getCompressedSize())) / entry.getSize();
|
||||
log.debug(String.format(Locale.ENGLISH, "%8d %6s %8d %4d%% %s %08x %s",
|
||||
entry.getSize(),
|
||||
entry.getCompression() == 0 ? "Stored" : "Defl:N",
|
||||
entry.getCompressedSize(),
|
||||
ratio,
|
||||
dateFormat.format(new Date(entry.getTime())),
|
||||
entry.getCrc32(),
|
||||
entry.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user