Compare commits
1120
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ebbf6e349 | ||
|
|
1b71f6a60a | ||
|
|
a3241e48df | ||
|
|
69396f3d59 | ||
|
|
9042d065ae | ||
|
|
300505e127 | ||
|
|
f7bf1d624b | ||
|
|
d7b08a0181 | ||
|
|
3941f59bbc | ||
|
|
802a30e16c | ||
|
|
032563d7a4 | ||
|
|
fe7ad2032b | ||
|
|
fcde4b3fc2 | ||
|
|
1073ed6c24 | ||
|
|
e5ce3450e8 | ||
|
|
a6f1e91747 | ||
|
|
8dcf5a3654 | ||
|
|
22f9cf7c4c | ||
|
|
38f2879d82 | ||
|
|
4060695db6 | ||
|
|
81fab695ff | ||
|
|
5259428106 | ||
|
|
9774bd858f | ||
|
|
d30379251d | ||
|
|
0e6156e51c | ||
|
|
d610c9727d | ||
|
|
adba04e2dd | ||
|
|
b2aa6552b1 | ||
|
|
8b4b060425 | ||
|
|
2f11853f0d | ||
|
|
388685b784 | ||
|
|
d6939b2913 | ||
|
|
90ee3b749f | ||
|
|
e51c297a7d | ||
|
|
283e8f20b3 | ||
|
|
850c3fb8cd | ||
|
|
7046e994d7 | ||
|
|
099d802a03 | ||
|
|
453df64f69 | ||
|
|
8a2cb42546 | ||
|
|
6413ed67c6 | ||
|
|
4dc7a97322 | ||
|
|
a2d8189c74 | ||
|
|
f806c82c33 | ||
|
|
f46598cd74 | ||
|
|
919d34c90d | ||
|
|
e230af8991 | ||
|
|
09842f5d72 | ||
|
|
911ff31f69 | ||
|
|
f1c9c4e231 | ||
|
|
fa04ce5465 | ||
|
|
45ff03abfd | ||
|
|
20cebb8a05 | ||
|
|
f79e3ff51f | ||
|
|
45a67a7f3d | ||
|
|
281469fe5c | ||
|
|
461ad7fa42 | ||
|
|
899d221b67 | ||
|
|
b246e163d2 | ||
|
|
d83bb0f964 | ||
|
|
a9626475e6 | ||
|
|
d1374eca15 | ||
|
|
6289162684 | ||
|
|
25b3187094 | ||
|
|
bd2c9b172a | ||
|
|
246f953b4d | ||
|
|
433098a388 | ||
|
|
f442d9d030 | ||
|
|
61d7ae03f6 | ||
|
|
22e144cb3b | ||
|
|
a3f0d7092d | ||
|
|
27efdba185 | ||
|
|
59a1fa9992 | ||
|
|
a5140ca293 | ||
|
|
a639b02890 | ||
|
|
ead64d3c73 | ||
|
|
be761071f8 | ||
|
|
e48f19269f | ||
|
|
82ebfb99c5 | ||
|
|
4d69bf2aba | ||
|
|
4e4f77de90 | ||
|
|
fe0c3176d8 | ||
|
|
8cfbdf46e0 | ||
|
|
2234d056fc | ||
|
|
4a4c447bd2 | ||
|
|
933a95023c | ||
|
|
1d8b5f81a1 | ||
|
|
81df632e48 | ||
|
|
b0df7cbc68 | ||
|
|
25d90a3c78 | ||
|
|
384637efdf | ||
|
|
ef3d3e6443 | ||
|
|
675e98fa1a | ||
|
|
0b059c71c1 | ||
|
|
8988f08243 | ||
|
|
adb54d9d7c | ||
|
|
fdf58388a3 | ||
|
|
a5244cdb41 | ||
|
|
6caec5dbce | ||
|
|
4419ab154b | ||
|
|
67c529ffe9 | ||
|
|
057ba0c6e1 | ||
|
|
6117ea755f | ||
|
|
a56c923f0e | ||
|
|
3686241bf5 | ||
|
|
0311c6f52b | ||
|
|
f59dccd5fd | ||
|
|
01d8dabef4 | ||
|
|
773993c6f2 | ||
|
|
2c5646b1c2 | ||
|
|
b8d71e24e7 | ||
|
|
4db3f7a742 | ||
|
|
298203f909 | ||
|
|
a1cafe62cf | ||
|
|
cab75ef0d6 | ||
|
|
33a8d86684 | ||
|
|
b4d329725b | ||
|
|
6efe8922b3 | ||
|
|
f5bd097584 | ||
|
|
7492841b5f | ||
|
|
1430bd9e88 | ||
|
|
e0400305ab | ||
|
|
65d5120272 | ||
|
|
cbb398e58c | ||
|
|
57d8735624 | ||
|
|
fd99bd2ed1 | ||
|
|
46c89534fd | ||
|
|
5650ee580f | ||
|
|
62f126b5d9 | ||
|
|
51cb354f35 | ||
|
|
d4f591354d | ||
|
|
0895af0ecc | ||
|
|
fafe25fee4 | ||
|
|
3db6cc1a1c | ||
|
|
408f5a05fc | ||
|
|
28ee23c5f3 | ||
|
|
37804e67c1 | ||
|
|
a7bce9c8e9 | ||
|
|
bcf2d16a10 | ||
|
|
28d2b92abb | ||
|
|
2f3b6df529 | ||
|
|
fa2cbbb0cc | ||
|
|
d1ab9e77de | ||
|
|
3484261b50 | ||
|
|
7a03fb53ea | ||
|
|
d7f050efc4 | ||
|
|
2d11a29a79 | ||
|
|
0b5aecfdb5 | ||
|
|
487925b1f2 | ||
|
|
54c8183666 | ||
|
|
c878952237 | ||
|
|
26d6428836 | ||
|
|
cf31a31430 | ||
|
|
e10e1c665f | ||
|
|
0834ff26be | ||
|
|
014b8db20d | ||
|
|
34445e6f92 | ||
|
|
7d00e01e00 | ||
|
|
300fabca21 | ||
|
|
e17dc78aeb | ||
|
|
1b9e00efe1 | ||
|
|
152b4d3765 | ||
|
|
e0b95ab2ae | ||
|
|
e0520edccc | ||
|
|
581b32e8a2 | ||
|
|
e71d61ef37 | ||
|
|
0a51d48703 | ||
|
|
30c7f9e020 | ||
|
|
71f7771a97 | ||
|
|
b36e5b7906 | ||
|
|
bb25449b35 | ||
|
|
dc8053dfcd | ||
|
|
20be8e23ba | ||
|
|
d83f476439 | ||
|
|
29352b8838 | ||
|
|
c0390c3c5d | ||
|
|
81dd2eeb47 | ||
|
|
3beedf4dfe | ||
|
|
f5be71ecbd | ||
|
|
58ba5dfedc | ||
|
|
040d265f3c | ||
|
|
fccf4db163 | ||
|
|
1122498b54 | ||
|
|
6a98aea209 | ||
|
|
ae4d589c8e | ||
|
|
6ccb59957d | ||
|
|
edd4e865b3 | ||
|
|
6ff7614e94 | ||
|
|
065a14952a | ||
|
|
1e620931a2 | ||
|
|
a46a531a89 | ||
|
|
06c95b8d36 | ||
|
|
39967a9211 | ||
|
|
a18e6f4b63 | ||
|
|
ef475000c3 | ||
|
|
f08a2444be | ||
|
|
c746d58210 | ||
|
|
ccb48505bf | ||
|
|
c9ddd25e99 | ||
|
|
42234d9163 | ||
|
|
e058885c7c | ||
|
|
1e3556cc8e | ||
|
|
b1cda242df | ||
|
|
b593fce745 | ||
|
|
282cdeaad0 | ||
|
|
6e54c7d672 | ||
|
|
51a12ee5cb | ||
|
|
fcea40685c | ||
|
|
07450a7a51 | ||
|
|
19fa8d15a6 | ||
|
|
db2980f598 | ||
|
|
b3e4fd65aa | ||
|
|
d46eca750c | ||
|
|
d441e92adc | ||
|
|
5f70d71203 | ||
|
|
59ffb2ea8e | ||
|
|
ce59b8037f | ||
|
|
56e67ba70a | ||
|
|
108e9e216e | ||
|
|
b219b4e832 | ||
|
|
ef4e0d76ec | ||
|
|
61c75c06a9 | ||
|
|
091705b3f2 | ||
|
|
6aa5b06619 | ||
|
|
df8fc0b229 | ||
|
|
d7fcda1f83 | ||
|
|
b832c24185 | ||
|
|
3b04f8c695 | ||
|
|
0ef11b58d2 | ||
|
|
bf13bbf506 | ||
|
|
92c94aa22a | ||
|
|
1c0785432b | ||
|
|
dcb9a50061 | ||
|
|
de7f4bdb6e | ||
|
|
73d636dba6 | ||
|
|
3f1c453ff6 | ||
|
|
ecb6baaebb | ||
|
|
e1cba45486 | ||
|
|
ea4181b243 | ||
|
|
6c5b9f8851 | ||
|
|
c1fccaf240 | ||
|
|
e7c223da7d | ||
|
|
6bfe1ba876 | ||
|
|
574e5dab18 | ||
|
|
5dc9b83831 | ||
|
|
57a214a9a4 | ||
|
|
cf48eb94af | ||
|
|
683f1e488a | ||
|
|
e888ae4da7 | ||
|
|
4512f2a375 | ||
|
|
68442afa0f | ||
|
|
ddf4d0cbc3 | ||
|
|
4be185e428 | ||
|
|
f75e66ac19 | ||
|
|
abda781f02 | ||
|
|
c1533ec66d | ||
|
|
c8319446b0 | ||
|
|
7ae452cf13 | ||
|
|
4c024c5faa | ||
|
|
9d16e9d98c | ||
|
|
48357d08f7 | ||
|
|
8e0bcf40db | ||
|
|
85ef5f04dd | ||
|
|
18e0748d14 | ||
|
|
cdde5b098e | ||
|
|
f83964752d | ||
|
|
0c3e34bbf9 | ||
|
|
6dea7faac8 | ||
|
|
726f482c33 | ||
|
|
cb242f664a | ||
|
|
edc0345970 | ||
|
|
a50535dd76 | ||
|
|
505942aa25 | ||
|
|
38a593a9de | ||
|
|
da87fb2551 | ||
|
|
d4dc65a4ba | ||
|
|
53463caedd | ||
|
|
0707a0fad8 | ||
|
|
dd1024af27 | ||
|
|
7edc58a6da | ||
|
|
5a5da0cace | ||
|
|
53f02ed366 | ||
|
|
27cfa885e7 | ||
|
|
beb132d604 | ||
|
|
b1b10298b4 | ||
|
|
b1993ff7d2 | ||
|
|
2afe5fe5b2 | ||
|
|
52fa8f64cc | ||
|
|
4f7978070f | ||
|
|
a909983822 | ||
|
|
5267cad896 | ||
|
|
ce274cf65b | ||
|
|
55b6b2e34d | ||
|
|
5e4e2971b5 | ||
|
|
a933bcfb1e | ||
|
|
b4b38e27f3 | ||
|
|
bc70824b57 | ||
|
|
87ecaaa6a4 | ||
|
|
c4cd9cd802 | ||
|
|
bbe0e8c5a9 | ||
|
|
3f8984d2dc | ||
|
|
f695c4598a | ||
|
|
eada1bc023 | ||
|
|
0ebb55551c | ||
|
|
20bce419c9 | ||
|
|
ca3fc370c3 | ||
|
|
94a8b78d1a | ||
|
|
06e9ff6fcf | ||
|
|
699e74656d | ||
|
|
b3ad6ef8b8 | ||
|
|
673c8e25f5 | ||
|
|
333c8e812d | ||
|
|
f0f7a88cfb | ||
|
|
0661d78bce | ||
|
|
dc4aece272 | ||
|
|
57a312b0b4 | ||
|
|
b9897ed6ac | ||
|
|
733230f037 | ||
|
|
fc6ba3e6ad | ||
|
|
2d7c539c5f | ||
|
|
7f34965026 | ||
|
|
de8b915cff | ||
|
|
d56040581d | ||
|
|
9869b7bbb5 | ||
|
|
7df97c28c0 | ||
|
|
018a559f78 | ||
|
|
3b32ad5fed | ||
|
|
f796d2a842 | ||
|
|
2f7e84e126 | ||
|
|
996ddbf3db | ||
|
|
7779ba2ac5 | ||
|
|
4dadeab762 | ||
|
|
f4d3131ef3 | ||
|
|
efa2ac803e | ||
|
|
e0be9d46bd | ||
|
|
106b27dbec | ||
|
|
f764567523 | ||
|
|
245f8669bc | ||
|
|
b265d9bba6 | ||
|
|
72228574f9 | ||
|
|
fba1ac3d90 | ||
|
|
4a73fb565e | ||
|
|
13011ed4af | ||
|
|
b2eccdefd3 | ||
|
|
dd2d6a977b | ||
|
|
38cd1b6040 | ||
|
|
1a28bbcde8 | ||
|
|
91013daadc | ||
|
|
1ad435e79d | ||
|
|
fd5c300c53 | ||
|
|
aa7d9cf29c | ||
|
|
8db1c3400e | ||
|
|
e723336706 | ||
|
|
b5cd4c36ab | ||
|
|
d1928f2647 | ||
|
|
61621ae00b | ||
|
|
ac20755c45 | ||
|
|
610202bbbd | ||
|
|
58ce7134e1 | ||
|
|
71f91cd79e | ||
|
|
8c821ecad3 | ||
|
|
c704940462 | ||
|
|
6a7cd1b89a | ||
|
|
e29da0ba13 | ||
|
|
7791fa10f5 | ||
|
|
48ed3c8f31 | ||
|
|
7ad7a9b94e | ||
|
|
54b929ba24 | ||
|
|
7b655c6260 | ||
|
|
4e8d41b145 | ||
|
|
f036774e07 | ||
|
|
97d1bd7685 | ||
|
|
8e39449856 | ||
|
|
30f1a5a6eb | ||
|
|
627abb047c | ||
|
|
9284008afe | ||
|
|
9159f4616f | ||
|
|
73296b1430 | ||
|
|
c74e622d69 | ||
|
|
f48eb55516 | ||
|
|
b01f6ef8d3 | ||
|
|
167185ef45 | ||
|
|
777a8ffeaa | ||
|
|
b2b3e6c485 | ||
|
|
613d502fc1 | ||
|
|
79da457db4 | ||
|
|
b1a431ed82 | ||
|
|
b0f2776a23 | ||
|
|
900c346ee8 | ||
|
|
7541a87b21 | ||
|
|
2547c67ebd | ||
|
|
5ae6a0b287 | ||
|
|
f718fc0c89 | ||
|
|
57e69ba7be | ||
|
|
6168b3e047 | ||
|
|
3e500500e0 | ||
|
|
42f06ff84a | ||
|
|
a7824fc473 | ||
|
|
67e7fd6928 | ||
|
|
450e8badba | ||
|
|
704c211e11 | ||
|
|
0ad871cb5b | ||
|
|
9920605570 | ||
|
|
b1e8678dbf | ||
|
|
e2cd2ddc0d | ||
|
|
a3be0dfddf | ||
|
|
4b615ffc0b | ||
|
|
64cbd189ad | ||
|
|
0fae5a128f | ||
|
|
518b44216f | ||
|
|
9ef4512ef9 | ||
|
|
8bb5f1e955 | ||
|
|
8f8932f617 | ||
|
|
23fd1d75c4 | ||
|
|
593dcef550 | ||
|
|
d769fe9074 | ||
|
|
77a8fec790 | ||
|
|
34440edfaa | ||
|
|
3c3d8784ad | ||
|
|
1591abc92c | ||
|
|
0ffa0c34f0 | ||
|
|
c7dc94b61b | ||
|
|
a8b83871a4 | ||
|
|
6f547984ff | ||
|
|
3979c11317 | ||
|
|
2a1ec985f4 | ||
|
|
2a1d49e945 | ||
|
|
7b7e6140d2 | ||
|
|
907cfd9694 | ||
|
|
b51b1d1ec6 | ||
|
|
6a026300e4 | ||
|
|
8511907390 | ||
|
|
05a5be336e | ||
|
|
5434b1ce19 | ||
|
|
c2d4a4367c | ||
|
|
de2f277513 | ||
|
|
283e522037 | ||
|
|
caebe4c23b | ||
|
|
b195277c9a | ||
|
|
6b99cb6253 | ||
|
|
a822bb670a | ||
|
|
af7cc11647 | ||
|
|
0afe331d3d | ||
|
|
7af40f0cd7 | ||
|
|
52dc699ee2 | ||
|
|
ec28e21897 | ||
|
|
3283851dc4 | ||
|
|
ec5e0c7b25 | ||
|
|
13660cb70a | ||
|
|
ce008f5576 | ||
|
|
25f7e8b35c | ||
|
|
109398d955 | ||
|
|
4a8551087b | ||
|
|
34febe3545 | ||
|
|
53a0307a8e | ||
|
|
1c6f624fd1 | ||
|
|
780e5f08f9 | ||
|
|
1bb73b1147 | ||
|
|
09b0d4d6c4 | ||
|
|
7e3856fa05 | ||
|
|
fbcb3f5436 | ||
|
|
324b4ced06 | ||
|
|
79c2abe790 | ||
|
|
cdab9168fc | ||
|
|
ce272724de | ||
|
|
2fa0a19d3b | ||
|
|
38b9a6e04a | ||
|
|
22d3c2b6bf | ||
|
|
218a8397c4 | ||
|
|
fd990da71e | ||
|
|
4a70c89a4d | ||
|
|
7e3143678f | ||
|
|
d80e09dfba | ||
|
|
3b3b251c21 | ||
|
|
1e848bd70f | ||
|
|
fab2295445 | ||
|
|
a913bccc9e | ||
|
|
57e7f89814 | ||
|
|
f98fd5e5f1 | ||
|
|
e6af2e5c6b | ||
|
|
d219989ec7 | ||
|
|
b9daf608ff | ||
|
|
869da61fd0 | ||
|
|
9f79e2505c | ||
|
|
3000b36dbd | ||
|
|
1d21b0e5c4 | ||
|
|
8d6b0926d6 | ||
|
|
4c39d13c80 | ||
|
|
327dd1c73f | ||
|
|
b36b8f3234 | ||
|
|
5f6da284fb | ||
|
|
83d27e18cb | ||
|
|
e291bd58a8 | ||
|
|
23acdb6492 | ||
|
|
3b5e56b049 | ||
|
|
57d4967f2a | ||
|
|
80fcda149b | ||
|
|
1a86eef52c | ||
|
|
98d401f9c3 | ||
|
|
0f2fae886e | ||
|
|
f30a9093af | ||
|
|
b4334da202 | ||
|
|
416fb3bda9 | ||
|
|
7e0fc2e364 | ||
|
|
540d290be2 | ||
|
|
53974ad88d | ||
|
|
cd9b5e5a44 | ||
|
|
d9959bbcb2 | ||
|
|
d7dfd53244 | ||
|
|
84489bf61b | ||
|
|
3fdb56390e | ||
|
|
a34e766f70 | ||
|
|
d74023d4fa | ||
|
|
41572e98ba | ||
|
|
063087c87e | ||
|
|
4a5d53d713 | ||
|
|
9a262525ec | ||
|
|
e327b4e84b | ||
|
|
c128a2b518 | ||
|
|
a3263ab4af | ||
|
|
4a4efe4725 | ||
|
|
89d2433369 | ||
|
|
023175ce6d | ||
|
|
21c8d24d57 | ||
|
|
e49f5c95f4 | ||
|
|
6912e578ca | ||
|
|
8680cff2b2 | ||
|
|
aea75d44e3 | ||
|
|
80481f1778 | ||
|
|
3ea7ff8202 | ||
|
|
b5979c0528 | ||
|
|
f0f79a52b8 | ||
|
|
b549acc1de | ||
|
|
2505cc5d00 | ||
|
|
d5af66b3fc | ||
|
|
f680dc7e46 | ||
|
|
ffd8bbb1ce | ||
|
|
216a043094 | ||
|
|
021eb5f828 | ||
|
|
f3bf8849e9 | ||
|
|
128f7100d7 | ||
|
|
2cf53dd00f | ||
|
|
4a674e46e6 | ||
|
|
f6b0134fdb | ||
|
|
675a6b863e | ||
|
|
b570eeef93 | ||
|
|
71a8b4e607 | ||
|
|
89b866b0fc | ||
|
|
bcce20d9ac | ||
|
|
420e760fb3 | ||
|
|
a168c95d79 | ||
|
|
d8ccec45f9 | ||
|
|
1316875602 | ||
|
|
a85387c7c8 | ||
|
|
d357e83d06 | ||
|
|
51546f6fea | ||
|
|
b4aa5ec593 | ||
|
|
909380f592 | ||
|
|
d5f26643bf | ||
|
|
a15dc4afdf | ||
|
|
b317bc334e | ||
|
|
d0a8d08b26 | ||
|
|
f73a922a8e | ||
|
|
c6682ce718 | ||
|
|
764d1137ae | ||
|
|
02fc972a79 | ||
|
|
7fc727cf2e | ||
|
|
d77e1e6820 | ||
|
|
241d9daae9 | ||
|
|
462e0257a9 | ||
|
|
66fbc7e821 | ||
|
|
3c37f22027 | ||
|
|
0d24bc6602 | ||
|
|
5a50df8179 | ||
|
|
f1251fd150 | ||
|
|
16202efc7b | ||
|
|
26c6052286 | ||
|
|
7ce386a7b8 | ||
|
|
e43cd0a039 | ||
|
|
70be405032 | ||
|
|
9f3ad8142a | ||
|
|
51d9ece30c | ||
|
|
d8e79f879f | ||
|
|
76d50b56ee | ||
|
|
c8d1e957a1 | ||
|
|
b287af4824 | ||
|
|
416ef8d6d7 | ||
|
|
7c71abb985 | ||
|
|
900f2462a7 | ||
|
|
dd982c6cb9 | ||
|
|
927a011c9d | ||
|
|
ad0ad47a40 | ||
|
|
71a424c15d | ||
|
|
145b7049a8 | ||
|
|
323d258bc2 | ||
|
|
56ba52d7b9 | ||
|
|
54bd6f6ed8 | ||
|
|
f06167ba4b | ||
|
|
60438ae1ed | ||
|
|
1b87750b87 | ||
|
|
40cdc566c8 | ||
|
|
428ae07e7f | ||
|
|
481ad22b53 | ||
|
|
65f58b93ed | ||
|
|
45411fa14d | ||
|
|
8ba1ba4fb1 | ||
|
|
8427e1b3a5 | ||
|
|
32f0f2a0cf | ||
|
|
8b94997cde | ||
|
|
bd2d54f774 | ||
|
|
34be285510 | ||
|
|
7b906f3c3e | ||
|
|
82ba22cda2 | ||
|
|
b1a9601ae9 | ||
|
|
7c1a297a99 | ||
|
|
a8195c0e7c | ||
|
|
08713e3922 | ||
|
|
0485d52e3e | ||
|
|
8b1723e19b | ||
|
|
9cb683dc55 | ||
|
|
dce1589c19 | ||
|
|
107cf16d36 | ||
|
|
2bab97c829 | ||
|
|
5242d825c2 | ||
|
|
11d3799b03 | ||
|
|
55c7f6b896 | ||
|
|
a09d607177 | ||
|
|
626a101208 | ||
|
|
f4ca3acbf6 | ||
|
|
86835a3bd7 | ||
|
|
0ef46e2e11 | ||
|
|
04878447b7 | ||
|
|
3669d5d90e | ||
|
|
ee5e222a13 | ||
|
|
9ea44ff59a | ||
|
|
0474e2c0f5 | ||
|
|
4a909d4b93 | ||
|
|
8dbe24ab55 | ||
|
|
25f7b005cb | ||
|
|
23f55ea208 | ||
|
|
5f19fa0f1f | ||
|
|
58fbdcfcdd | ||
|
|
e33cca9fd9 | ||
|
|
b25e3d5454 | ||
|
|
583f272daa | ||
|
|
2a83ad63be | ||
|
|
6fdc908ad1 | ||
|
|
5213caac49 | ||
|
|
ca0518c747 | ||
|
|
298cd29537 | ||
|
|
1a963a3239 | ||
|
|
683c8dc5c5 | ||
|
|
2087970de6 | ||
|
|
5bd270fe6a | ||
|
|
0992fcfcb0 | ||
|
|
fc12395f7e | ||
|
|
57b291a4e8 | ||
|
|
3adf391478 | ||
|
|
a859941845 | ||
|
|
8d1fc85638 | ||
|
|
51ece1c472 | ||
|
|
45f67ec37f | ||
|
|
85b559c989 | ||
|
|
1382050b5f | ||
|
|
fb970752fd | ||
|
|
3d256a402f | ||
|
|
a677d95d34 | ||
|
|
452c0f2846 | ||
|
|
e568e688f5 | ||
|
|
a201c930c2 | ||
|
|
7d927dde6b | ||
|
|
f76fece81d | ||
|
|
069d7e2382 | ||
|
|
e2e0562719 | ||
|
|
347aaadb43 | ||
|
|
6ddd64232f | ||
|
|
e532b3b05b | ||
|
|
d7c17d80e7 | ||
|
|
b35980cf60 | ||
|
|
263853d6a7 | ||
|
|
6a100b8250 | ||
|
|
893560e589 | ||
|
|
6b7ce7de5b | ||
|
|
6082d8cc1a | ||
|
|
ec37c9cab2 | ||
|
|
309e2a715e | ||
|
|
fbb4afa4f7 | ||
|
|
90863b95f4 | ||
|
|
3576eaf3ff | ||
|
|
f14c9741ea | ||
|
|
6d04e51e10 | ||
|
|
6ea1aa6c1a | ||
|
|
c86e1a9409 | ||
|
|
f1845f2d4e | ||
|
|
a20ca4ea2e | ||
|
|
0effc0d181 | ||
|
|
7ce273ec67 | ||
|
|
39e080cdca | ||
|
|
17584ae5fb | ||
|
|
c46c72d66d | ||
|
|
36f00ab568 | ||
|
|
dae7dacf3d | ||
|
|
b69b72cd1b | ||
|
|
68d1e39410 | ||
|
|
d4482dec23 | ||
|
|
8f36cecc7a | ||
|
|
a59fbcc9c8 | ||
|
|
25cddb2d61 | ||
|
|
9a7af235b8 | ||
|
|
c9751ab2a2 | ||
|
|
5e13986cc0 | ||
|
|
35e2cc535e | ||
|
|
81355a0248 | ||
|
|
3e98502e2d | ||
|
|
6973fa4538 | ||
|
|
20191c3f46 | ||
|
|
f084fec54e | ||
|
|
367e3b3a93 | ||
|
|
36383b6f47 | ||
|
|
9ecd14820e | ||
|
|
be552e92a1 | ||
|
|
1bf676fd56 | ||
|
|
154a96b5fd | ||
|
|
443c384f41 | ||
|
|
85c7829636 | ||
|
|
68e3bb8d66 | ||
|
|
63b2e52eaf | ||
|
|
c7492f3faf | ||
|
|
9052343ef7 | ||
|
|
00c09e4a1a | ||
|
|
086c0d9f28 | ||
|
|
3ca0189f28 | ||
|
|
8f5a2ba85d | ||
|
|
1ab9811d7a | ||
|
|
0b720848ba | ||
|
|
eebd95e700 | ||
|
|
913e02f0fa | ||
|
|
10fdab2473 | ||
|
|
26710761ed | ||
|
|
17bf6ac12b | ||
|
|
6a01b7729d | ||
|
|
039e817002 | ||
|
|
3ea1e1914a | ||
|
|
e6c5152123 | ||
|
|
7bdaf186f3 | ||
|
|
b6b4e826cb | ||
|
|
949f3cb246 | ||
|
|
501855a8c0 | ||
|
|
44634be13d | ||
|
|
7c718df1af | ||
|
|
16cca9cbbd | ||
|
|
b715320298 | ||
|
|
d31d9eb137 | ||
|
|
b53c29c035 | ||
|
|
d3bf20bee6 | ||
|
|
cff445ddf5 | ||
|
|
877d96a50a | ||
|
|
12863dfa21 | ||
|
|
959959d880 | ||
|
|
cd676eabfc | ||
|
|
0a3e95c8d4 | ||
|
|
35b483e655 | ||
|
|
7dae7b72f4 | ||
|
|
620f5df241 | ||
|
|
d5ddb6eb8c | ||
|
|
33c9b9d4e4 | ||
|
|
216cafbe54 | ||
|
|
72a644bd3b | ||
|
|
7008f0bc89 | ||
|
|
289d3ea287 | ||
|
|
92ae157f00 | ||
|
|
9800f7c01b | ||
|
|
8281af4590 | ||
|
|
d609e5e113 | ||
|
|
238d769f27 | ||
|
|
1e648cc6b4 | ||
|
|
7ac20d59fc | ||
|
|
1d8ab51a22 | ||
|
|
cdfb3b74b3 | ||
|
|
6f2868f7c3 | ||
|
|
f3061fa497 | ||
|
|
468cce5f3d | ||
|
|
bfaba80d3b | ||
|
|
2ca4a2af86 | ||
|
|
7d0b245d3d | ||
|
|
96fcf69bdd | ||
|
|
63d95b1265 | ||
|
|
d173e83e01 | ||
|
|
54d11dea95 | ||
|
|
fbdab389c4 | ||
|
|
3dd3bad362 | ||
|
|
65a9a4b4f2 | ||
|
|
5f6cf5550d | ||
|
|
673dad4032 | ||
|
|
5e45ec9a9c | ||
|
|
306eb9d23c | ||
|
|
158218e501 | ||
|
|
d773332b74 | ||
|
|
5dc2eb0062 | ||
|
|
bee7febafc | ||
|
|
f49cc2d498 | ||
|
|
3f4d2b75fc | ||
|
|
af8733c2d1 | ||
|
|
545eed98a7 | ||
|
|
2da436189a | ||
|
|
906ea11733 | ||
|
|
47c5629f7d | ||
|
|
a37c676d96 | ||
|
|
d9981f15f2 | ||
|
|
27b75746fd | ||
|
|
ee80a3a841 | ||
|
|
f8d639054f | ||
|
|
7f76e58e4e | ||
|
|
1b326a86a2 | ||
|
|
607bf4c67f | ||
|
|
3e4836c91e | ||
|
|
dae8f21388 | ||
|
|
84b328b60b | ||
|
|
b98d55b444 | ||
|
|
ee9413e54a | ||
|
|
76dedce5da | ||
|
|
37f99ae706 | ||
|
|
aee58d967d | ||
|
|
5ecabeb045 | ||
|
|
408c142aad | ||
|
|
a56d541380 | ||
|
|
ae79417845 | ||
|
|
010daa583e | ||
|
|
3034957c99 | ||
|
|
99134346ed | ||
|
|
9a70708300 | ||
|
|
50f0df5012 | ||
|
|
599bb80106 | ||
|
|
aa0db1f400 | ||
|
|
82b6fd22a9 | ||
|
|
b5c730d37a | ||
|
|
a2cb6444f4 | ||
|
|
8a144eabca | ||
|
|
75c2a3b0ec | ||
|
|
beec02a69b | ||
|
|
df78b45f69 | ||
|
|
e6a3c8f165 | ||
|
|
4d948e93a3 | ||
|
|
ece082727b | ||
|
|
5b5040f4af | ||
|
|
d9d88e690b | ||
|
|
bda123fc39 | ||
|
|
2200bb9b45 | ||
|
|
884d460b52 | ||
|
|
beb97270a0 | ||
|
|
52681bd386 | ||
|
|
d772a737fc | ||
|
|
638fcfea12 | ||
|
|
cae32cca5a | ||
|
|
c429e7054a | ||
|
|
f526f11c73 | ||
|
|
892016368f | ||
|
|
d9464ac278 | ||
|
|
a68a9995a1 | ||
|
|
1a0c2cb180 | ||
|
|
d7572f5a79 | ||
|
|
a208651e13 | ||
|
|
41c231b4f3 | ||
|
|
567b6f7659 | ||
|
|
6d1e0089e5 | ||
|
|
502f532fe7 | ||
|
|
ddc47f09ab | ||
|
|
5dbcdf2fce | ||
|
|
706588c39c | ||
|
|
3e3e64942e | ||
|
|
3700ac8a84 | ||
|
|
cc0dc0abf5 | ||
|
|
5861147d17 | ||
|
|
732035a0e2 | ||
|
|
643f71009d | ||
|
|
4643e37adb | ||
|
|
1bc8854685 | ||
|
|
66ebf6a391 | ||
|
|
1239277cec | ||
|
|
9cb8fb5527 | ||
|
|
819d67dca8 | ||
|
|
f895982fa0 | ||
|
|
505c097659 | ||
|
|
783fa10157 | ||
|
|
82ffbf2a1d | ||
|
|
3c3ae47511 | ||
|
|
858d7acf68 | ||
|
|
f8b7005690 | ||
|
|
33a8cb98e6 | ||
|
|
338a22f2ed | ||
|
|
f7cecaf709 | ||
|
|
c38b3c15e7 | ||
|
|
4665678e9f | ||
|
|
07416eb74d | ||
|
|
6f37dc53c7 | ||
|
|
55b963dcf9 | ||
|
|
1a433019b3 | ||
|
|
7b292cb1ab | ||
|
|
b2f5590c04 | ||
|
|
2cf2c87a8b | ||
|
|
01420f7e72 | ||
|
|
3f699973b9 | ||
|
|
3f27c9139c | ||
|
|
9eee6fc4af | ||
|
|
a343dbe8f8 | ||
|
|
bc08a09621 | ||
|
|
586ad66bff | ||
|
|
1a426e1f5f | ||
|
|
e9a9f1070c | ||
|
|
7d3dd61bc4 | ||
|
|
f37894af88 | ||
|
|
14129545b8 | ||
|
|
f06346b6c7 | ||
|
|
13e3e8c534 | ||
|
|
4be282813c | ||
|
|
74e66a407d | ||
|
|
b19da4c82e | ||
|
|
2f49764e1b | ||
|
|
08dfb0f72d | ||
|
|
9251ed238c | ||
|
|
3b96328765 | ||
|
|
108928e524 | ||
|
|
b1191bbf9b | ||
|
|
3424260cfd | ||
|
|
e1fe56160e | ||
|
|
5838ea3e3f | ||
|
|
0906b17ed1 | ||
|
|
49b9cb72d3 | ||
|
|
01508cc3c1 | ||
|
|
79eba83812 | ||
|
|
a455dee24c | ||
|
|
f58b4959e4 | ||
|
|
a6b4182ecd | ||
|
|
9010591ffc | ||
|
|
e0dde2c982 | ||
|
|
6109d97bf7 | ||
|
|
95936e01ce | ||
|
|
beadc7b185 | ||
|
|
0bc2e45137 | ||
|
|
b8e649e995 | ||
|
|
bdcd21a535 | ||
|
|
1b779a8b77 | ||
|
|
6c59a341ee | ||
|
|
1a6e03c8f8 | ||
|
|
653c0a2ac6 | ||
|
|
e4189f367a | ||
|
|
48b7355ba4 | ||
|
|
d7d3a903ff | ||
|
|
bab3067273 | ||
|
|
fdd70a6908 | ||
|
|
3699a8fe30 | ||
|
|
32f98f8a15 | ||
|
|
c5366760a2 | ||
|
|
c98ec2a7a4 | ||
|
|
e4c729e51b | ||
|
|
f36726d8fb | ||
|
|
d2fc2ee136 | ||
|
|
6edc2164af | ||
|
|
e34dc30f1f | ||
|
|
c03081d3fa | ||
|
|
97287e4538 | ||
|
|
fc44507a34 | ||
|
|
2597004750 | ||
|
|
d1bd9c5fae | ||
|
|
54542f9246 | ||
|
|
377b23458f | ||
|
|
b7973cdb76 | ||
|
|
5f1a70886c | ||
|
|
5fbc8b0b66 | ||
|
|
780b2b6e1d | ||
|
|
039ad2032c | ||
|
|
a24222b693 | ||
|
|
6047d91d7e | ||
|
|
f0158f6946 | ||
|
|
00ecdc58e6 | ||
|
|
0a075a97af | ||
|
|
34ee70ca7d | ||
|
|
338751ef2b | ||
|
|
d4465ba864 | ||
|
|
9a8fdd4edb | ||
|
|
338848f1eb | ||
|
|
9b918ec036 | ||
|
|
24439c7853 | ||
|
|
a7f63a5b57 | ||
|
|
1e68782056 | ||
|
|
e0f7dab34f | ||
|
|
5513d47b6d | ||
|
|
e12abb66f7 | ||
|
|
106be8c0ca | ||
|
|
2e0d065e4d | ||
|
|
073298cfee | ||
|
|
ebd0f4b4f8 | ||
|
|
0ba71b060c | ||
|
|
144e384441 | ||
|
|
9bb694171b | ||
|
|
647362cd82 | ||
|
|
56996558df | ||
|
|
956b7c719e | ||
|
|
56e13559a0 | ||
|
|
261ed86047 | ||
|
|
d9b18358d2 | ||
|
|
a5161bba6f | ||
|
|
62b00c758c | ||
|
|
8663b4fa40 | ||
|
|
6304c12bd0 | ||
|
|
efe2473ecf | ||
|
|
6ba494a4f1 | ||
|
|
b81572ffff | ||
|
|
d2ae1567be | ||
|
|
d00055a5f8 | ||
|
|
896a2684d9 | ||
|
|
b98ecb17d3 | ||
|
|
cd9af7feee | ||
|
|
1c38f77b5c | ||
|
|
c5977e358e | ||
|
|
78e58e6894 | ||
|
|
30433c5cbf | ||
|
|
fe8e3766c0 | ||
|
|
90c95d9788 | ||
|
|
3930a79a47 | ||
|
|
fc852780f8 | ||
|
|
35315c38cd | ||
|
|
0b40f37418 | ||
|
|
5a5f0421d8 | ||
|
|
86c5c5bc59 | ||
|
|
776a088de9 | ||
|
|
ba54655877 | ||
|
|
9f47ae438c | ||
|
|
1717233f5a | ||
|
|
cf71feb89d | ||
|
|
21e393246a | ||
|
|
c368f234f0 | ||
|
|
c3d8610e2a | ||
|
|
1a10e330aa | ||
|
|
d992897506 | ||
|
|
a0fe6f0d7d | ||
|
|
f4826ea727 | ||
|
|
a17b254494 | ||
|
|
0190fc07bd | ||
|
|
c8dab6a88f | ||
|
|
fbafadfd98 | ||
|
|
b5832fd706 | ||
|
|
93312a0714 | ||
|
|
7e6e1f6f14 | ||
|
|
6cd498af6a | ||
|
|
2781aaca14 | ||
|
|
91b9c9af40 | ||
|
|
5c0d10b41a | ||
|
|
a3bdbd7a85 | ||
|
|
f66151f880 | ||
|
|
6c5f52995d | ||
|
|
d94b96a8eb | ||
|
|
a5d90cc97d | ||
|
|
657d566e89 | ||
|
|
5e6d24f01e | ||
|
|
e353bbe4c3 | ||
|
|
4ee2cdf19c | ||
|
|
b28831a8aa | ||
|
|
800b64d5d3 | ||
|
|
cfa6164b5c | ||
|
|
9cb22b6e17 | ||
|
|
19891e6768 | ||
|
|
c616410e0b | ||
|
|
31e1e79ed2 | ||
|
|
6191a9827d | ||
|
|
f373d8f293 | ||
|
|
c233eb5a30 | ||
|
|
360a3e9b5f | ||
|
|
01aff59d46 | ||
|
|
f0f1dca7cb | ||
|
|
6f3bd92539 | ||
|
|
a8023f7334 | ||
|
|
8c368972ec | ||
|
|
5aea90314a | ||
|
|
0c8ba49d61 | ||
|
|
ea5b8b9f81 | ||
|
|
6b21f5082c | ||
|
|
438d7f1b34 | ||
|
|
301797b477 | ||
|
|
2758be2462 | ||
|
|
8f03e4910d | ||
|
|
a09efd3eea | ||
|
|
a66d460572 | ||
|
|
162b88915e | ||
|
|
d014b6947c | ||
|
|
a6188964c9 | ||
|
|
722fbec72c | ||
|
|
22190fb686 | ||
|
|
49ea70c839 | ||
|
|
c655e7e1c9 | ||
|
|
d13cd1e07f | ||
|
|
7365fd0448 | ||
|
|
f1febca312 | ||
|
|
dbf67404c0 | ||
|
|
8cfe8c4b80 | ||
|
|
674f53315f | ||
|
|
e51cd6b90d | ||
|
|
a44f12b2e0 | ||
|
|
a2552ed5b9 | ||
|
|
763c007950 | ||
|
|
b221bf8e52 | ||
|
|
ab9a89685c | ||
|
|
41ccb16227 | ||
|
|
f6bf7b1ba6 | ||
|
|
09e7113df1 | ||
|
|
62a7e283a4 | ||
|
|
a7f7bf1316 | ||
|
|
a72fca3dfb | ||
|
|
c5e7a6e8f9 | ||
|
|
80a20f25b9 | ||
|
|
500163fe1a | ||
|
|
50c6111773 | ||
|
|
3a05649c71 | ||
|
|
9d25376e1f | ||
|
|
89935b5639 | ||
|
|
e095627aee | ||
|
|
b61e46e956 | ||
|
|
76459c5dd2 | ||
|
|
7ad67a3dbd | ||
|
|
577b49ac19 |
No files matched your search
@@ -1,19 +1,30 @@
|
||||
name: Bug 反馈 / Bug report
|
||||
description: 提交一个问题报告 / Create a bug report to help us improve
|
||||
description: 提交一个问题报告 / Create a bug report
|
||||
labels:
|
||||
- "bug"
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您愿意为 Ehviewer-Overhauled 做出贡献!
|
||||
提交问题报告前,还请首先完成文末的自查步骤
|
||||
|
||||
Thanks for your contribution on Ehviewer-Overhauled Project!
|
||||
Please finish verify steps which list in the end first before create bug report
|
||||
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: 复现步骤 / Step to reproduce
|
||||
description: |
|
||||
在此处写下复现的方式
|
||||
Describe how to reproduce here
|
||||
请从全新安装应用开始,在此处写下复现的方式。请尽量附带录屏
|
||||
Describe how to reproduce here with fresh installation, with screen recording if possible.
|
||||
placeholder: |
|
||||
0. 重新安装应用 / Fresh install
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
[录屏] / [Screen recording]
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -44,12 +55,25 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: more
|
||||
attributes:
|
||||
label: 备注 / Addition details
|
||||
description: |
|
||||
在此处写下其他您想说的内容
|
||||
Describe addition details here
|
||||
placeholder: |
|
||||
其他有用的信息与附件
|
||||
Additional details and attachments
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: EhViewer 版本号 / EhViewer version code
|
||||
description: 您可以在设置 - 关于处找到版本号 / You can get version code in Settings - About
|
||||
placeholder: 四位版本号 / 4 digits version code
|
||||
placeholder: 1.6.9.13
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -57,7 +81,7 @@ body:
|
||||
id: ci
|
||||
attributes:
|
||||
label: EhViewer CI 版本 / EhViewer CI Version
|
||||
description: 直接将对应 CI 下载页面网址填入即可 / Simply drop CI download page url here
|
||||
description: 直接将对应 GitHub Action CI 下载页面网址填入即可 / Simply drop GitHub Action CI download page url here
|
||||
placeholder: https://github.com/Ehviewer-Overhauled/Ehviewer/actions/runs/XXXXXXXXXX
|
||||
validations:
|
||||
required: true
|
||||
@@ -89,15 +113,6 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: 应用日志 / App logs
|
||||
description: 您可以通过设置-高级-导出日志来获得日志文件 / You can get logs file in Settings - Advanced - Dump logcat
|
||||
placeholder: 06-15 17:44:53.704 23382 23382 E ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: check
|
||||
attributes:
|
||||
@@ -119,4 +134,4 @@ body:
|
||||
required: true
|
||||
|
||||
- label: 您确保已使用最新 CI 版本测试,并且该问题在最新 CI 版本中并未解决 / This bug have not solved in latest CI version
|
||||
required: true
|
||||
required: true
|
||||
@@ -1,3 +1,4 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 所有其他问题 / All other questions
|
||||
url: https://github.com/Ehviewer-Overhauled/Ehviewer/discussions
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
name: 功能请求 / Feature request
|
||||
description: 提出一个功能建议 / Suggest an idea for this project
|
||||
description: 提出一个功能建议 / Suggest an idea
|
||||
labels:
|
||||
- "enhancement"
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您愿意为 Ehviewer-Overhauled 做出贡献!
|
||||
提交功能建议前,还请首先完成文末的自查步骤
|
||||
|
||||
Thanks for your contribution on Ehviewer-Overhauled Project!
|
||||
Please finish verify steps which list in the end first before suggest an idea
|
||||
|
||||
- type: textarea
|
||||
id: request
|
||||
attributes:
|
||||
@@ -33,7 +42,7 @@ body:
|
||||
- type: textarea
|
||||
id: more
|
||||
attributes:
|
||||
label: 其他细节 / Addition details
|
||||
label: 备注 / Addition details
|
||||
description: |
|
||||
在此处写下其他您想说的内容
|
||||
Describe addition details here
|
||||
@@ -48,7 +57,7 @@ body:
|
||||
attributes:
|
||||
label: EhViewer 版本号 / EhViewer version code
|
||||
description: 您可以在设置 - 关于处找到版本号 / You can get version code in Settings - About
|
||||
placeholder: 四位版本号 / 4 digits version code
|
||||
placeholder: 1.6.9.13
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -56,7 +65,7 @@ body:
|
||||
id: ci
|
||||
attributes:
|
||||
label: EhViewer CI 版本 / EhViewer CI Version
|
||||
description: 直接将对应 CI 下载页面网址填入即可 / Simply drop CI download page url here
|
||||
description: 直接将对应 GitHub Action CI 下载页面网址填入即可 / Simply drop GitHub Action CI download page url here
|
||||
placeholder: https://github.com/Ehviewer-Overhauled/Ehviewer/actions/runs/XXXXXXXXXX
|
||||
validations:
|
||||
required: true
|
||||
@@ -82,4 +91,4 @@ body:
|
||||
required: true
|
||||
|
||||
- label: 您确保已使用最新 CI 版本测试,并且该功能在最新 CI 版本中并未按照预期实现 / This feature don't implement correctly in latest CI version
|
||||
required: true
|
||||
required: true
|
||||
+21
-10
@@ -1,6 +1,11 @@
|
||||
name: CI
|
||||
|
||||
on: [push]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
tags-ignore:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
@@ -9,25 +14,24 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Setup Java 11
|
||||
- name: Setup Java 19
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 11
|
||||
|
||||
- name: Setup Android SDK Tools
|
||||
uses: android-actions/setup-android@v2
|
||||
java-version: 19
|
||||
|
||||
- name: Install NDK
|
||||
run: |
|
||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.0.8775105" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.2.9519653" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||
|
||||
- name: Gradle Build
|
||||
uses: gradle/gradle-build-action@v2
|
||||
- run: |
|
||||
git submodule update --init --recursive
|
||||
gradle assembleRelease
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
arguments: assembleRelease
|
||||
|
||||
- name: Update ARM64
|
||||
uses: actions/upload-artifact@v3
|
||||
@@ -52,3 +56,10 @@ jobs:
|
||||
with:
|
||||
name: x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-x86-release.apk
|
||||
|
||||
- name: Update mapping
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/release/mapping.txt
|
||||
retention-days: 5
|
||||
@@ -9,25 +9,23 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Setup Java 11
|
||||
- name: Setup Java 19
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 11
|
||||
|
||||
- name: Setup Android SDK Tools
|
||||
uses: android-actions/setup-android@v2
|
||||
java-version: 19
|
||||
|
||||
- name: Install NDK
|
||||
run: |
|
||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.0.8775105" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.2.9519653" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||
|
||||
- name: Gradle Build
|
||||
uses: gradle/gradle-build-action@v2
|
||||
- run: |
|
||||
git submodule update --init --recursive
|
||||
gradle assembleRelease
|
||||
with:
|
||||
arguments: assembleRelease
|
||||
|
||||
- name: Update ARM64
|
||||
uses: actions/upload-artifact@v3
|
||||
|
||||
@@ -12,33 +12,62 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Setup Java 11
|
||||
- name: Setup Java 19
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 11
|
||||
|
||||
- name: Setup Android SDK Tools
|
||||
uses: android-actions/setup-android@v2
|
||||
java-version: 19
|
||||
|
||||
- name: Install NDK
|
||||
run: |
|
||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.0.8775105" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.2.9519653" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||
|
||||
- name: Gradle Build
|
||||
uses: gradle/gradle-build-action@v2
|
||||
- run: |
|
||||
git submodule update --init --recursive
|
||||
gradle assembleRelease
|
||||
with:
|
||||
arguments: assembleRelease
|
||||
|
||||
- name: Rename Apks
|
||||
run: |
|
||||
cp app/build/outputs/apk/release/app-universal-release.apk app/build/outputs/apk/release/EhViewer-universal.apk
|
||||
cp app/build/outputs/apk/release/app-arm64-v8a-release.apk app/build/outputs/apk/release/EhViewer-arm64-v8a.apk
|
||||
cp app/build/outputs/apk/release/app-x86_64-release.apk app/build/outputs/apk/release/EhViewer-x86_64.apk
|
||||
cp app/build/outputs/apk/release/app-x86-release.apk app/build/outputs/apk/release/EhViewer-x86.apk
|
||||
cp app/build/outputs/apk/release/app-armeabi-v7a-release.apk app/build/outputs/apk/release/EhViewer-armeabi-v7a.apk
|
||||
|
||||
- name: Releases
|
||||
uses: softprops/action-gh-release@v0.1.14
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body: Bump Version
|
||||
discussion_category_name: Announcements
|
||||
files: |
|
||||
app/build/outputs/apk/release/app-arm64-v8a-release.apk
|
||||
app/build/outputs/apk/release/app-x86_64-release.apk
|
||||
app/build/outputs/apk/release/app-x86-release.apk
|
||||
app/build/outputs/apk/release/app-armeabi-v7a-release.apk
|
||||
app/build/outputs/apk/release/EhViewer-universal.apk
|
||||
app/build/outputs/apk/release/EhViewer-arm64-v8a.apk
|
||||
app/build/outputs/apk/release/EhViewer-x86_64.apk
|
||||
app/build/outputs/apk/release/EhViewer-x86.apk
|
||||
app/build/outputs/apk/release/EhViewer-armeabi-v7a.apk
|
||||
app/build/outputs/mapping/release/mapping.txt
|
||||
|
||||
- name: Collect Apks
|
||||
run: |
|
||||
mkdir app/build/outputs/apk/release/fdroid
|
||||
cp app/build/outputs/apk/release/app-arm64-v8a-release.apk app/build/outputs/apk/release/fdroid/EhViewer-v$GITHUB_REF_NAME.apk
|
||||
cp app/build/outputs/apk/release/app-armeabi-v7a-release.apk app/build/outputs/apk/release/fdroid/EhViewer-v$GITHUB_REF_NAME-armv7.apk
|
||||
|
||||
- name: Push Apks
|
||||
uses: Ehviewer-Overhauled/github-action-push-to-another-repository@main
|
||||
env:
|
||||
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
|
||||
with:
|
||||
source-directory: app/build/outputs/apk/release/fdroid
|
||||
destination-github-username: Ehviewer-Overhauled
|
||||
destination-repository-name: fdroid-repo
|
||||
user-name: 'github-actions[bot]'
|
||||
user-email: 'github-actions[bot]@users.noreply.github.com'
|
||||
target-branch: deploy
|
||||
create-target-branch: true
|
||||
commit-message: Add apks from $GITHUB_REF
|
||||
target-directory: fdroid/tmp-apks
|
||||
+6
-15
@@ -1,18 +1,9 @@
|
||||
[submodule "native/src/main/jni/image/libjpeg-turbo/libjpeg-turbo"]
|
||||
path = native/src/main/jni/image/libjpeg-turbo/libjpeg-turbo
|
||||
url = https://github.com/Ehviewer-Overhauled/libjpeg-turbo
|
||||
[submodule "native/src/main/jni/image/giflib/giflib"]
|
||||
path = native/src/main/jni/image/giflib/giflib
|
||||
url = https://github.com/Ehviewer-Overhauled/giflib
|
||||
[submodule "native/src/main/jni/image/libpng/libpng"]
|
||||
path = native/src/main/jni/image/libpng/libpng
|
||||
url = https://github.com/Ehviewer-Overhauled/libpng
|
||||
[submodule "native/src/main/jni/libarchive"]
|
||||
path = native/src/main/jni/libarchive
|
||||
[submodule "app/src/main/cpp/libarchive"]
|
||||
path = app/src/main/cpp/libarchive
|
||||
url = https://github.com/Ehviewer-Overhauled/libarchive
|
||||
[submodule "native/src/main/jni/liblzma"]
|
||||
path = native/src/main/jni/liblzma
|
||||
[submodule "app/src/main/cpp/liblzma"]
|
||||
path = app/src/main/cpp/liblzma
|
||||
url = https://github.com/Ehviewer-Overhauled/xz
|
||||
[submodule "native/src/main/jni/nettle/nettle"]
|
||||
path = native/src/main/jni/nettle/nettle
|
||||
[submodule "app/src/main/cpp/nettle/nettle"]
|
||||
path = app/src/main/cpp/nettle/nettle
|
||||
url = https://github.com/Ehviewer-Overhauled/nettle
|
||||
@@ -1,5 +1,17 @@
|
||||
<p align="right">
|
||||
<strong>English</strong>
|
||||
<span> | </span>
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-cn.md">
|
||||
简体中文
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-tw.md">
|
||||
正體中文
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">
|
||||
<img src="art/launcher_icon-web.png" width="200">
|
||||
<img src="https://github.com/Ehviewer-Overhauled/Art/blob/master/launcher_icon-web.svg" width="200">
|
||||
<br>EhViewer<br>
|
||||
</h1>
|
||||
|
||||
@@ -7,7 +19,7 @@
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml">
|
||||
<img src="https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions">
|
||||
</a>
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/master/LICENSE">
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/Ehviewer-Overhauled/Ehviewer" alt="LICENSE">
|
||||
</a>
|
||||
<a href="https://www.codefactor.io/repository/github/Ehviewer-Overhauled/ehviewer">
|
||||
@@ -21,48 +33,59 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
<h3>
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#description">
|
||||
Description
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#download">
|
||||
Download
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#screenshot">
|
||||
Screenshot
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#thanks">
|
||||
Thanks
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#license">
|
||||
License
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
|
||||
# Description
|
||||
|
||||
致力于轻量化与良好性能表现的 EhViewer 分支
|
||||
|
||||
已针对性适配 Material Design 3 及其动态取色系统
|
||||
|
||||
|
||||
EhViewer fork dedicated to lightweight and high-performance
|
||||
|
||||
with Material Design 3 and Dynamic Color Support
|
||||
|
||||
# Download
|
||||
|
||||
[Github 下载](https://github.com/Ehviewer-Overhauled/Ehviewer/releases)
|
||||
|
||||
[CI 版本下载(需登录)](https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml)
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases"><img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/></a> <a href="https://github.com/Ehviewer-Overhauled/fdroid-repo"><img alt="Get it on F-Droid" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-fdroid.svg" width="200px"/></a>
|
||||
|
||||
# Screenshot
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
# Thanks
|
||||
|
||||
本项目受到了诸多开源项目的帮助
|
||||
|
||||
Here is the libraries
|
||||
|
||||
- [AOSP](http://source.android.com/)
|
||||
- [android-advancedrecyclerview](https://github.com/h6ah4i/android-advancedrecyclerview)
|
||||
- [AOSP & AndroidX](http://source.android.com/)
|
||||
- [Coil](https://coil-kt.github.io/coil/)
|
||||
- [FullDraggableDrawer](https://github.com/PureWriter/FullDraggableDrawer)
|
||||
- [GnuTLS](https://gnutls.org/)
|
||||
- [material-components-android](https://github.com/material-components/material-components-android)
|
||||
- [material-design-icons](https://github.com/google/material-design-icons)
|
||||
- [Apache Commons Lang](https://commons.apache.org/proper/commons-lang/)
|
||||
- [apng](http://apng.sourceforge.net/)
|
||||
- [giflib](http://giflib.sourceforge.net)
|
||||
- [greenDAO](https://github.com/greenrobot/greenDAO)
|
||||
- [jsoup](https://github.com/jhy/jsoup)
|
||||
- [libjpeg-turbo](https://libjpeg-turbo.org/)
|
||||
- [libpng](http://www.libpng.org/pub/png/libpng.html)
|
||||
- [okhttp](https://github.com/square/okhttp)
|
||||
- [roaster](https://github.com/forge/roaster)
|
||||
- [TapTargetView](https://github.com/KeepSafe/TapTargetView)
|
||||
- [RikkaX](https://github.com/RikkaApps/RikkaX)
|
||||
- [libarchive](http://www.libarchive.org/)
|
||||
- [XZ Utils](https://tukaani.org/xz/)
|
||||
|
||||
@@ -71,7 +94,7 @@ Here is the libraries
|
||||
|
||||
Copyright 2014-2019 Hippo Seven
|
||||
Copyright 2020-2022 NekoInverter
|
||||
Copyright 2022 Tarsin Norbin
|
||||
Copyright 2022-2023 Tarsin Norbin
|
||||
|
||||
EhViewer 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.
|
||||
|
||||
|
||||
+2
-1
@@ -4,4 +4,5 @@ manifest-merger-release-report.txt
|
||||
/src/main/libs
|
||||
/src/main/obj
|
||||
# Intellij
|
||||
*.iml
|
||||
*.iml
|
||||
/.cxx
|
||||
@@ -1,167 +0,0 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'kotlin-kapt'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
buildToolsVersion "33.0.0"
|
||||
|
||||
Properties localProperties = new Properties()
|
||||
if (project.rootProject.file('local.properties').exists()) {
|
||||
localProperties.load(project.rootProject.file('local.properties').newDataInputStream())
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
enable = true
|
||||
reset()
|
||||
include 'arm64-v8a', 'x86_64', 'armeabi-v7a', 'x86'
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
sign {
|
||||
storeFile file(signStoreFile)
|
||||
storePassword signStorePassword
|
||||
keyAlias signkeyAlias
|
||||
keyPassword signkeyPassword
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "moe.tarsin.ehviewer"
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 33
|
||||
versionCode 114534
|
||||
versionName "1.7.29.2"
|
||||
resConfigs 'zh', 'zh-rCN', 'zh-rHK', 'zh-rTW', 'es', 'ja', 'ko', 'fr', 'de', 'th'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '11'
|
||||
}
|
||||
|
||||
lint {
|
||||
abortOnError true
|
||||
checkReleaseBuilds false
|
||||
disable 'MissingTranslation'
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += "/META-INF/**"
|
||||
excludes += "/kotlin/**"
|
||||
excludes += "**.txt"
|
||||
excludes += "**.bin"
|
||||
}
|
||||
}
|
||||
|
||||
dependenciesInfo.includeInApk false
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles 'proguard-rules.pro'
|
||||
signingConfig signingConfigs.sign
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java.srcDirs += 'src/main/java-gen'
|
||||
}
|
||||
}
|
||||
|
||||
namespace 'com.hippo.ehviewer'
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
task -> task.dependsOn ":daogenerator:executeDaoGenerator"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def activity_version = "1.6.0-alpha05"
|
||||
implementation "androidx.activity:activity:$activity_version"
|
||||
implementation "androidx.activity:activity-ktx:$activity_version"
|
||||
|
||||
implementation "androidx.annotation:annotation:1.4.0"
|
||||
implementation "androidx.annotation:annotation-experimental:1.2.0"
|
||||
implementation "androidx.appcompat:appcompat:1.6.0-alpha05"
|
||||
|
||||
def arch_core_version = "2.1.0"
|
||||
implementation "androidx.arch.core:core-common:$arch_core_version"
|
||||
implementation "androidx.arch.core:core-runtime:$arch_core_version"
|
||||
|
||||
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha04"
|
||||
implementation "androidx.browser:browser:1.4.0"
|
||||
|
||||
def collection_version = "1.3.0-alpha01"
|
||||
implementation "androidx.collection:collection:$collection_version"
|
||||
implementation "androidx.collection:collection-ktx:$collection_version"
|
||||
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.2.0-alpha03"
|
||||
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
|
||||
|
||||
def core_version = "1.9.0-alpha05"
|
||||
implementation "androidx.core:core:$core_version"
|
||||
implementation "androidx.core:core-ktx:$core_version"
|
||||
|
||||
implementation "androidx.customview:customview:1.2.0-alpha01"
|
||||
implementation "androidx.documentfile:documentfile:1.1.0-alpha01"
|
||||
implementation "androidx.dynamicanimation:dynamicanimation:1.1.0-alpha03"
|
||||
|
||||
def fragment_version = "1.5.0"
|
||||
implementation "androidx.fragment:fragment:$fragment_version"
|
||||
implementation "androidx.fragment:fragment-ktx:$fragment_version"
|
||||
|
||||
def lifecycle_version = "2.6.0-alpha01"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-core-ktx:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
|
||||
|
||||
implementation "androidx.loader:loader:1.1.0"
|
||||
implementation 'androidx.preference:preference:1.2.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.0-beta01'
|
||||
implementation 'androidx.startup:startup-runtime:1.2.0-alpha01'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
|
||||
implementation "androidx.transition:transition-ktx:1.4.1"
|
||||
implementation "androidx.vectordrawable:vectordrawable:1.2.0-beta01"
|
||||
implementation "androidx.vectordrawable:vectordrawable-animated:1.1.0"
|
||||
implementation 'androidx.viewpager:viewpager:1.1.0-alpha01'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||
implementation 'androidx.webkit:webkit:1.5.0-beta01'
|
||||
|
||||
implementation 'com.drakeet.drawer:drawer:1.0.3'
|
||||
implementation 'com.google.android.material:material:1.7.0-alpha03'
|
||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.3'
|
||||
implementation 'com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:1.0.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.10'
|
||||
implementation 'dev.rikka.rikkax.core:core-ktx:1.4.1'
|
||||
implementation 'dev.rikka.rikkax.insets:insets:1.3.0'
|
||||
implementation 'dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0'
|
||||
implementation 'dev.rikka.rikkax.preference:simplemenu-preference:1.0.3'
|
||||
implementation 'dev.rikka.rikkax.material:material-preference:2.0.0'
|
||||
implementation 'org.greenrobot:greendao:3.3.0'
|
||||
implementation 'org.jsoup:jsoup:1.15.2'
|
||||
implementation projects.native
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
exclude group: 'dev.rikka.rikkax.appcompat', module: 'appcompat'
|
||||
exclude group: 'dev.rikka.rikkax.material', module: 'material'
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
kotlin("android")
|
||||
id("kotlin-parcelize")
|
||||
kotlin("plugin.serialization")
|
||||
id("com.google.devtools.ksp")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 33
|
||||
buildToolsVersion = "33.0.1"
|
||||
ndkVersion = "25.2.9519653"
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = true
|
||||
reset()
|
||||
include("arm64-v8a", "x86_64", "armeabi-v7a", "x86")
|
||||
isUniversalApk = true
|
||||
}
|
||||
}
|
||||
|
||||
val signConfig = signingConfigs.create("release") {
|
||||
storeFile = File(projectDir.path + "/keystore/androidkey.jks")
|
||||
storePassword = "000000"
|
||||
keyAlias = "key0"
|
||||
keyPassword = "000000"
|
||||
|
||||
enableV3Signing = true
|
||||
enableV4Signing = true
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "moe.tarsin.ehviewer"
|
||||
minSdk = 28
|
||||
targetSdk = 33
|
||||
versionCode = 180031
|
||||
versionName = "1.8.6.0-alpha03"
|
||||
resourceConfigurations.addAll(
|
||||
listOf(
|
||||
"zh",
|
||||
"zh-rCN",
|
||||
"zh-rHK",
|
||||
"zh-rTW",
|
||||
"es",
|
||||
"ja",
|
||||
"ko",
|
||||
"fr",
|
||||
"de",
|
||||
"th",
|
||||
"tr",
|
||||
"nb-rNO"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path = File("src/main/cpp/CMakeLists.txt")
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_19
|
||||
targetCompatibility = JavaVersion.VERSION_19
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "19"
|
||||
freeCompilerArgs = listOf(
|
||||
// https://kotlinlang.org/docs/compiler-reference.html#progressive
|
||||
"-progressive",
|
||||
|
||||
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
|
||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
)
|
||||
}
|
||||
|
||||
lint {
|
||||
abortOnError = true
|
||||
checkReleaseBuilds = false
|
||||
disable.add("MissingTranslation")
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/CHANGES"
|
||||
excludes += "/META-INF/README.md"
|
||||
excludes += "/kotlin/**"
|
||||
excludes += "**.txt"
|
||||
excludes += "**.bin"
|
||||
}
|
||||
}
|
||||
|
||||
dependenciesInfo.includeInApk = false
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles("proguard-rules.pro")
|
||||
signingConfig = signConfig
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
compose = true
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.4.2"
|
||||
}
|
||||
|
||||
namespace = "com.hippo.ehviewer"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// https://developer.android.com/jetpack/androidx/releases/activity
|
||||
implementation("androidx.activity:activity-compose:1.6.1")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha05")
|
||||
implementation("androidx.browser:browser:1.5.0")
|
||||
implementation("androidx.collection:collection-ktx:1.3.0-alpha02")
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/compose-material3
|
||||
implementation(platform("androidx.compose:compose-bom:2023.01.00"))
|
||||
implementation("androidx.compose.material:material-icons-extended")
|
||||
implementation("androidx.compose.material3:material3")
|
||||
|
||||
implementation("androidx.core:core-ktx:1.10.0-alpha02")
|
||||
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.2.0-alpha07")
|
||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
|
||||
// https://developer.android.com/jetpack/androidx/releases/lifecycle
|
||||
implementation("androidx.lifecycle:lifecycle-process:2.5.1")
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/navigation
|
||||
val nav_version = "2.5.3"
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:$nav_version")
|
||||
implementation("androidx.navigation:navigation-ui-ktx:$nav_version")
|
||||
implementation("androidx.navigation:navigation-compose:$nav_version")
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/paging
|
||||
implementation("androidx.paging:paging-runtime-ktx:3.2.0-alpha04")
|
||||
implementation("androidx.paging:paging-compose:1.0.0-alpha18")
|
||||
implementation("androidx.preference:preference-ktx:1.2.0")
|
||||
implementation("androidx.recyclerview:recyclerview:1.3.0-rc01")
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/room
|
||||
val room_version = "2.5.0"
|
||||
ksp("androidx.room:room-compiler:$room_version")
|
||||
implementation("androidx.room:room-paging:$room_version")
|
||||
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||
implementation("com.drakeet.drawer:drawer:1.0.3")
|
||||
implementation("com.github.chrisbanes:PhotoView:2.3.0") // Dead Dependency
|
||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") // Dead Dependency
|
||||
// https://github.com/google/accompanist/releases
|
||||
implementation("com.google.accompanist:accompanist-themeadapter-material3:0.28.0")
|
||||
implementation("com.google.android.material:material:1.8.0")
|
||||
|
||||
// https://square.github.io/okhttp/changelogs/changelog/
|
||||
implementation("com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.11")
|
||||
implementation("com.squareup.okhttp3:okhttp-coroutines")
|
||||
|
||||
implementation("com.squareup.okio:okio-jvm:3.3.0")
|
||||
|
||||
implementation("dev.chrisbanes.insetter:insetter:0.6.1") // Dead Dependency
|
||||
implementation("dev.rikka.rikkax.core:core-ktx:1.4.1")
|
||||
implementation("dev.rikka.rikkax.insets:insets:1.3.0")
|
||||
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0")
|
||||
implementation("dev.rikka.rikkax.preference:simplemenu-preference:1.0.3")
|
||||
implementation("dev.rikka.rikkax.material:material-preference:2.0.0")
|
||||
// https://coil-kt.github.io/coil/changelog/
|
||||
// implementation 'io.coil-kt:coil:2.2.2'
|
||||
implementation("com.github.Ehviewer-Overhauled.coil:coil-compose:53302f2")
|
||||
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.5.0-RC")
|
||||
implementation("org.jsoup:jsoup:1.15.3")
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
exclude("dev.rikka.rikkax.appcompat", "appcompat")
|
||||
exclude("dev.rikka.rikkax.material", "material")
|
||||
exclude("org.jetbrains.kotlin", "kotlin-android-extensions-runtime")
|
||||
exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk7")
|
||||
exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8")
|
||||
}
|
||||
Vendored
+12
-22
@@ -2,33 +2,23 @@
|
||||
public static final ** CREATOR;
|
||||
}
|
||||
|
||||
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
||||
public static void check*(...);
|
||||
public static void throw*(...);
|
||||
-keepclasseswithmembernames,includedescriptorclasses class * {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
-keep class com.hippo.ehviewer.ui.fragment.* extends com.hippo.ehviewer.ui.fragment.BaseFragment { }
|
||||
-keep class com.hippo.ehviewer.ui.fragment.* extends com.hippo.ehviewer.ui.fragment.BasePreferenceFragment { }
|
||||
# https://issuetracker.google.com/issues/190382641
|
||||
-keepclassmembers class kotlin.SafePublicationLazyImpl {
|
||||
java.lang.Object _value;
|
||||
}
|
||||
|
||||
-keepnames class com.hippo.ehviewer.ui.scene.* { }
|
||||
-keep class androidx.viewpager.widget.ViewPager$LayoutParams { int position; }
|
||||
|
||||
-keep class com.hippo.ehviewer.dao.* { *; }
|
||||
# Ktor logger
|
||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||
|
||||
-keepattributes LineNumberTable,SourceFile
|
||||
-renamesourcefileattribute SourceFile
|
||||
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-renamesourcefileattribute
|
||||
-repackageclasses
|
||||
-allowaccessmodification
|
||||
-overloadaggressively
|
||||
|
||||
-dontwarn net.sqlcipher.database.**
|
||||
-dontwarn rx.**
|
||||
-dontwarn org.conscrypt.ConscryptHostnameVerifier
|
||||
-dontwarn org.conscrypt.Conscrypt$Version
|
||||
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
|
||||
-dontwarn org.bouncycastle.jsse.BCSSLSocket
|
||||
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
|
||||
-dontwarn net.sqlcipher.Cursor
|
||||
-dontwarn org.bouncycastle.jsse.BCSSLParameters
|
||||
-dontwarn org.conscrypt.Conscrypt
|
||||
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
|
||||
-dontwarn org.openjsse.net.ssl.OpenJSSE
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2016 Hippo Seven
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -16,6 +17,6 @@
|
||||
|
||||
<resources>
|
||||
|
||||
<!-- Custom preference -->
|
||||
<string name="app_name" translatable="false">EhViewer Debug</string>
|
||||
|
||||
</resources>
|
||||
@@ -1,20 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright 2016 Hippo Seven
|
||||
~
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
@@ -23,47 +9,50 @@
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<!-- Required to maintain app compatibility. -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
|
||||
<uses-feature
|
||||
android:glEsVersion="0x00020000"
|
||||
android:required="true" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
|
||||
<application
|
||||
android:name="com.hippo.ehviewer.EhApplication"
|
||||
android:name=".EhApplication"
|
||||
android:allowBackup="true"
|
||||
android:appCategory="image"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="@xml/backup_scheme"
|
||||
android:hasFragileUserData="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
tools:targetApi="tiramisu">
|
||||
<activity
|
||||
android:name="com.hippo.ehviewer.ui.MainActivity"
|
||||
android:name=".ui.login.LoginActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/AppTheme"
|
||||
android:windowSoftInputMode="adjustResize"/>
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:banner="@mipmap/ic_leanback_banner"
|
||||
android:configChanges="screenSize"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize|stateAlwaysHidden">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
@@ -84,26 +73,26 @@
|
||||
<data android:host="g.e-hentai.org" />
|
||||
<data android:host="lofi.e-hentai.org" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="@string/keyword_search">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="@string/image_search">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="image/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="com.hippo.ehviewer.ui.GalleryActivity"
|
||||
android:name="eu.kanade.tachiyomi.ui.reader.ReaderActivity"
|
||||
android:configChanges="screenSize"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
@@ -137,22 +126,33 @@
|
||||
<data android:mimeType="application/x-xz-compressed-tar" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="com.hippo.ehviewer.ui.SettingsActivity"
|
||||
android:name=".ui.SettingsActivity"
|
||||
android:configChanges="screenSize"
|
||||
android:label="@string/settings"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name="com.hippo.ehviewer.shortcuts.ShortcutsActivity"
|
||||
android:name=".ui.SecurityActivity"
|
||||
android:configChanges="screenSize"
|
||||
android:label="@string/settings"
|
||||
android:theme="@style/AppTheme" />
|
||||
<activity
|
||||
android:name=".shortcuts.ShortcutsActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
|
||||
<service
|
||||
android:name="com.hippo.ehviewer.download.DownloadService"
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
android:enabled="false"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="autoStoreLocales"
|
||||
android:value="true" />
|
||||
</service>
|
||||
<service
|
||||
android:name=".download.DownloadService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:label="@string/download_service_label" />
|
||||
|
||||
@@ -161,11 +161,10 @@
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/filepaths" />
|
||||
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
</manifest>
|
||||
@@ -1,17 +1,17 @@
|
||||
project(ehviewer)
|
||||
cmake_minimum_required(VERSION 3.4.1)
|
||||
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Oz -fvisibility=hidden -fvisibility-inlines-hidden -ffunction-sections -fdata-sections -flto\
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Ofast -fvisibility=hidden -fvisibility-inlines-hidden -ffunction-sections -fdata-sections -funroll-loops -flto=thin \
|
||||
-mllvm -polly \
|
||||
-mllvm -polly-run-dce \
|
||||
-mllvm -polly-run-inliner \
|
||||
-mllvm -polly-run-inliner \
|
||||
-mllvm -polly-isl-arg=--no-schedule-serialize-sccs \
|
||||
-mllvm -polly-ast-use-context \
|
||||
-mllvm -polly-ast-use-context \
|
||||
-mllvm -polly-detect-keep-going \
|
||||
-mllvm -polly-position=before-vectorizer \
|
||||
-mllvm -polly-vectorizer=stripmine \
|
||||
-mllvm -polly-vectorizer=stripmine \
|
||||
-mllvm -polly-detect-profitability-min-per-loop-insts=40 \
|
||||
-mllvm -polly-invariant-load-hoisting")
|
||||
-mllvm -polly-invariant-load-hoisting")
|
||||
|
||||
# Build GNUTLS libnettle
|
||||
add_subdirectory(nettle)
|
||||
@@ -44,10 +44,7 @@ SET(LIBARCHIVE_CUSTOM_LIBS "liblzma" "nettle")
|
||||
# Build libarchive
|
||||
add_subdirectory(libarchive)
|
||||
|
||||
# Build hippo's image lib
|
||||
add_subdirectory(image)
|
||||
|
||||
# Build and link our app's native lib
|
||||
add_library(${PROJECT_NAME} SHARED main.c archive.c)
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC image/image libarchive/libarchive)
|
||||
target_link_libraries(${PROJECT_NAME} image archive_static -Wl,--gc-sections)
|
||||
add_library(${PROJECT_NAME} SHARED archive.c fdutils.c gifutils.c)
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC libarchive/libarchive)
|
||||
target_link_libraries(${PROJECT_NAME} archive_static log -Wl,--gc-sections)
|
||||
@@ -0,0 +1,419 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer 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.
|
||||
*
|
||||
* EhViewer 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
|
||||
* EhViewer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/mman.h>
|
||||
#include <syscall.h>
|
||||
|
||||
#include <jni.h>
|
||||
#include <android/log.h>
|
||||
|
||||
#include <archive.h>
|
||||
#include <archive_entry.h>
|
||||
|
||||
#define LOG_TAG "libarchive_wrapper"
|
||||
|
||||
#include "ehviewer.h"
|
||||
|
||||
typedef struct {
|
||||
int using;
|
||||
int next_index;
|
||||
struct archive *arc;
|
||||
struct archive_entry *entry;
|
||||
} archive_ctx;
|
||||
|
||||
typedef struct {
|
||||
const char *filename;
|
||||
int index;
|
||||
} entry;
|
||||
|
||||
pthread_mutex_t ctx_lock;
|
||||
static archive_ctx **ctx_pool;
|
||||
#define CTX_POOL_SIZE 20
|
||||
|
||||
static bool need_encrypt = false;
|
||||
static char *passwd = NULL;
|
||||
static void *archiveAddr = NULL;
|
||||
static size_t archiveSize = 0;
|
||||
static entry *entries = NULL;
|
||||
static size_t entryCount = 0;
|
||||
|
||||
#define POPULATE_THRESHOLD (512 * 1024 * 1024) // 512 MiB
|
||||
|
||||
const char supportExt[9][6] = {
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"png",
|
||||
"gif",
|
||||
"webp",
|
||||
"bmp",
|
||||
"ico",
|
||||
"wbmp",
|
||||
"heif"
|
||||
};
|
||||
|
||||
static inline int filename_is_playable_file(const char *name) {
|
||||
const char *dotptr = strrchr(name, '.');
|
||||
if (!dotptr++)
|
||||
return false;
|
||||
int i;
|
||||
for (i = 0; i < 9; i++)
|
||||
if (strcmp(dotptr, supportExt[i]) == 0)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline int compare_entries(const void *a, const void *b) {
|
||||
const char *fa = ((entry *) a)->filename;
|
||||
const char *fb = ((entry *) b)->filename;
|
||||
int a_len = (int) strlen(fa);
|
||||
int b_len = (int) strlen(fb);
|
||||
return a_len == b_len ? strcmp(fa, fb) : a_len - b_len;
|
||||
}
|
||||
|
||||
static long archive_map_entries_index(archive_ctx *ctx) {
|
||||
long count = 0;
|
||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
|
||||
const char *name = archive_entry_pathname(ctx->entry);
|
||||
if (filename_is_playable_file(name)) {
|
||||
entries[count].filename = strdup(name);
|
||||
entries[count].index = count;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (!count)
|
||||
LOGE("%s", archive_error_string(ctx->arc));
|
||||
qsort(entries, entryCount, sizeof(entry), compare_entries);
|
||||
return count;
|
||||
}
|
||||
|
||||
static long archive_list_all_entries(archive_ctx *ctx) {
|
||||
long count = 0;
|
||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK)
|
||||
if (filename_is_playable_file(archive_entry_pathname(ctx->entry)))
|
||||
count++;
|
||||
if (!count)
|
||||
LOGE("%s", archive_error_string(ctx->arc));
|
||||
return count;
|
||||
}
|
||||
|
||||
static void archive_release_ctx(archive_ctx *ctx) {
|
||||
if (ctx) {
|
||||
archive_read_close(ctx->arc);
|
||||
archive_read_free(ctx->arc);
|
||||
free(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
static int archive_alloc_ctx(archive_ctx **ctxptr) {
|
||||
archive_ctx *ctx;
|
||||
int err;
|
||||
ctx = calloc(1, sizeof(archive_ctx));
|
||||
if (!ctx)
|
||||
return -ENOMEM;
|
||||
ctx->arc = archive_read_new();
|
||||
ctx->using = 1;
|
||||
if (!ctx->arc) {
|
||||
free(ctx);
|
||||
return -ENOMEM;
|
||||
}
|
||||
archive_read_support_format_tar(ctx->arc);
|
||||
archive_read_support_format_7zip(ctx->arc);
|
||||
archive_read_support_format_rar5(ctx->arc);
|
||||
archive_read_support_format_zip(ctx->arc);
|
||||
archive_read_support_filter_gzip(ctx->arc);
|
||||
archive_read_support_filter_xz(ctx->arc);
|
||||
archive_read_set_option(ctx->arc, "zip", "ignorecrc32", "1");
|
||||
if (passwd)
|
||||
archive_read_add_passphrase(ctx->arc, passwd);
|
||||
err = archive_read_open_memory(ctx->arc, archiveAddr, archiveSize);
|
||||
if (err) {
|
||||
archive_read_free(ctx->arc);
|
||||
free(ctx);
|
||||
return err;
|
||||
}
|
||||
*ctxptr = ctx;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int archive_skip_to_index(archive_ctx *ctx, int index) {
|
||||
assert(!(ctx->next_index > index));
|
||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
|
||||
if (!filename_is_playable_file(archive_entry_pathname(ctx->entry)))
|
||||
continue;
|
||||
if (ctx->next_index++ == index) {
|
||||
return ctx->next_index - 1;
|
||||
}
|
||||
}
|
||||
return ARCHIVE_FATAL;
|
||||
}
|
||||
|
||||
static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
|
||||
int ret;
|
||||
archive_ctx *ctx = NULL;
|
||||
pthread_mutex_lock(&ctx_lock);
|
||||
for (int i = 0; i < CTX_POOL_SIZE; i++) {
|
||||
if (!ctx_pool[i])
|
||||
continue;
|
||||
if (ctx_pool[i]->using)
|
||||
continue;
|
||||
if (ctx_pool[i]->next_index > idx)
|
||||
continue;
|
||||
if (!ctx || ctx_pool[i]->next_index > ctx->next_index)
|
||||
ctx = ctx_pool[i];
|
||||
if (ctx->next_index == idx)
|
||||
break;
|
||||
}
|
||||
if (ctx)
|
||||
ctx->using = 1;
|
||||
pthread_mutex_unlock(&ctx_lock);
|
||||
|
||||
if (!ctx) {
|
||||
archive_ctx *victimCtx = NULL;
|
||||
int victimIdx = 0;
|
||||
int replace = 1;
|
||||
ret = archive_alloc_ctx(&ctx);
|
||||
if (ret)
|
||||
return ret;
|
||||
pthread_mutex_lock(&ctx_lock);
|
||||
for (int i = 0; i < CTX_POOL_SIZE; i++) {
|
||||
if (!ctx_pool[i]) {
|
||||
ctx_pool[i] = ctx;
|
||||
replace = 0;
|
||||
break;
|
||||
}
|
||||
if (ctx_pool[i]->using)
|
||||
continue;
|
||||
if (!victimCtx || ctx_pool[i]->next_index > victimCtx->next_index) {
|
||||
victimCtx = ctx_pool[i];
|
||||
victimIdx = i;
|
||||
}
|
||||
}
|
||||
if (replace) {
|
||||
archive_release_ctx(victimCtx);
|
||||
ctx_pool[victimIdx] = ctx;
|
||||
}
|
||||
pthread_mutex_unlock(&ctx_lock);
|
||||
}
|
||||
ret = archive_skip_to_index(ctx, idx);
|
||||
if (ret != idx) {
|
||||
ret = archive_errno(ctx->arc);
|
||||
LOGE("Skip to index failed:%s", archive_error_string(ctx->arc));
|
||||
archive_release_ctx(ctx);
|
||||
return ret;
|
||||
}
|
||||
*ctxptr = ctx;
|
||||
return 0;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd, jlong size) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
archive_ctx *ctx = NULL;
|
||||
int mmap_flags = MAP_PRIVATE;
|
||||
if (size <= POPULATE_THRESHOLD)
|
||||
mmap_flags |= MAP_POPULATE;
|
||||
archiveAddr = mmap64(0, size, PROT_READ, mmap_flags, fd, 0);
|
||||
if (archiveAddr == MAP_FAILED) {
|
||||
LOGE("%s%s", "mmap64 failed with error ", strerror(errno));
|
||||
return 0;
|
||||
}
|
||||
archiveSize = size;
|
||||
madvise_log_if_error(archiveAddr, archiveSize, MADV_WILLNEED);
|
||||
pthread_mutex_init(&ctx_lock, 0);
|
||||
ctx_pool = calloc(CTX_POOL_SIZE, sizeof(archive_ctx **));
|
||||
if (!ctx_pool) {
|
||||
LOGE("Allocate archive ctx pool failed:ENOMEM");
|
||||
return 0;
|
||||
}
|
||||
long r = archive_alloc_ctx(&ctx);
|
||||
if (r) {
|
||||
if (r == -ENOMEM)
|
||||
LOGE("%s", "Mem alloc failed");
|
||||
LOGE("%s%s", "Archive open failed:", archive_error_string(ctx->arc));
|
||||
} else {
|
||||
entryCount = archive_list_all_entries(ctx);
|
||||
LOGI("%s%ld%s", "Found ", r, " image entries in archive");
|
||||
|
||||
// We must read through the file|vm then we can know whether it is encrypted
|
||||
int encryptRet = archive_read_has_encrypted_entries(ctx->arc);
|
||||
switch (encryptRet) {
|
||||
case 1: // At lease 1 encrypted entry
|
||||
need_encrypt = true;
|
||||
break;
|
||||
case 0: // format supports but no encrypted entry found
|
||||
default:
|
||||
need_encrypt = false;
|
||||
}
|
||||
|
||||
int format = archive_format(ctx->arc);
|
||||
switch (format) {
|
||||
case ARCHIVE_FORMAT_ZIP:
|
||||
case ARCHIVE_FORMAT_RAR_V5:
|
||||
madvise_log_if_error(archiveAddr, archiveSize, MADV_SEQUENTIAL);
|
||||
break;
|
||||
case ARCHIVE_FORMAT_7ZIP: // Seek is bad
|
||||
madvise_log_if_error(archiveAddr, archiveSize, MADV_RANDOM);
|
||||
break;
|
||||
default:;
|
||||
}
|
||||
}
|
||||
|
||||
archive_release_ctx(ctx);
|
||||
|
||||
r = archive_alloc_ctx(&ctx);
|
||||
if (r) {
|
||||
if (r == -ENOMEM)
|
||||
LOGE("%s", "Mem alloc failed");
|
||||
r = 0;
|
||||
LOGE("%s%s", "Archive open failed:", archive_error_string(ctx->arc));
|
||||
} else {
|
||||
entries = calloc(entryCount, sizeof(entry));
|
||||
r = archive_map_entries_index(ctx);
|
||||
}
|
||||
|
||||
archive_release_ctx(ctx);
|
||||
return r;
|
||||
}
|
||||
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_com_hippo_UriArchiveAccessor_extractToByteBuffer(JNIEnv *env, jobject thiz, jint index) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
index = entries[index].index;
|
||||
archive_ctx *ctx = NULL;
|
||||
int ret;
|
||||
ret = archive_get_ctx(&ctx, index);
|
||||
if (ret)
|
||||
return 0;
|
||||
long long size = archive_entry_size(ctx->entry);
|
||||
void *buffer = malloc(size);
|
||||
if (!buffer) {
|
||||
ctx->using = 0;
|
||||
LOGE("Allocate buffer for decompression failed:ENOMEM");
|
||||
return 0;
|
||||
}
|
||||
ret = archive_read_data(ctx->arc, buffer, size);
|
||||
ctx->using = 0;
|
||||
if (ret == size)
|
||||
return (*env)->NewDirectByteBuffer(env, buffer, size);
|
||||
if (ret != size)
|
||||
LOGE("%s", "No enough data read, WTF?");
|
||||
if (ret < 0)
|
||||
LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc));
|
||||
free(buffer);
|
||||
return 0;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_UriArchiveAccessor_closeArchive(JNIEnv *env, jobject thiz) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
for (int i = 0; i < CTX_POOL_SIZE; i++)
|
||||
archive_release_ctx(ctx_pool[i]);
|
||||
free(passwd);
|
||||
free(ctx_pool);
|
||||
pthread_mutex_destroy(&ctx_lock);
|
||||
passwd = NULL;
|
||||
need_encrypt = false;
|
||||
munmap(archiveAddr, archiveSize);
|
||||
for (int i = 0; i < entryCount; ++i) {
|
||||
free((void *) entries[i].filename);
|
||||
}
|
||||
free(entries);
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_hippo_UriArchiveAccessor_needPassword(JNIEnv *env, jobject thiz) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
return need_encrypt;
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_hippo_UriArchiveAccessor_providePassword(JNIEnv *env, jobject thiz, jstring str) {
|
||||
EH_UNUSED(thiz);
|
||||
struct archive_entry *entry;
|
||||
archive_ctx *ctx;
|
||||
jboolean ret = true;
|
||||
int len = (*env)->GetStringUTFLength(env, str);
|
||||
passwd = realloc(passwd, len * sizeof(char));
|
||||
if (!passwd) {
|
||||
LOGE("Allocate passwd buffer failed");
|
||||
return false;
|
||||
}
|
||||
strcpy(passwd, (*env)->GetStringUTFChars(env, str, NULL));
|
||||
archive_alloc_ctx(&ctx);
|
||||
void *tmpBuf = alloca(4096);
|
||||
while (archive_read_next_header(ctx->arc, &entry) == ARCHIVE_OK) {
|
||||
if (!filename_is_playable_file(archive_entry_pathname(entry)))
|
||||
continue;
|
||||
if (!archive_entry_is_encrypted(entry))
|
||||
continue;
|
||||
if (archive_read_data(ctx->arc, tmpBuf, 4096) < ARCHIVE_OK) {
|
||||
LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc));
|
||||
ret = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
archive_release_ctx(ctx);
|
||||
return ret;
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_com_hippo_UriArchiveAccessor_getFilename(JNIEnv *env, jobject thiz, jint index) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
index = entries[index].index;
|
||||
archive_ctx *ctx = NULL;
|
||||
int ret;
|
||||
ret = archive_get_ctx(&ctx, index);
|
||||
if (ret)
|
||||
return NULL;
|
||||
jstring str = (*env)->NewStringUTF(env, archive_entry_pathname(ctx->entry));
|
||||
ctx->using = 0;
|
||||
return str;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_UriArchiveAccessor_extractToFd(JNIEnv *env, jobject thiz, jint index, jint fd) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
index = entries[index].index;
|
||||
archive_ctx *ctx = NULL;
|
||||
int ret;
|
||||
ret = archive_get_ctx(&ctx, index);
|
||||
if (!ret) {
|
||||
archive_read_data_into_fd(ctx->arc, fd);
|
||||
ctx->using = 0;
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_UriArchiveAccessor_releaseByteBuffer(JNIEnv *env, jobject thiz, jobject buffer) {
|
||||
EH_UNUSED(thiz);
|
||||
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
|
||||
free(addr);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer 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.
|
||||
*
|
||||
* EhViewer 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
|
||||
* EhViewer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef EHVIEWER_EHVIEWER_H
|
||||
#define EHVIEWER_EHVIEWER_H
|
||||
|
||||
#define EH_UNUSED(x) (void)x
|
||||
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG ,__VA_ARGS__)
|
||||
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG ,__VA_ARGS__)
|
||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG ,__VA_ARGS__)
|
||||
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG ,__VA_ARGS__)
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG ,__VA_ARGS__)
|
||||
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, LOG_TAG ,__VA_ARGS__)
|
||||
|
||||
#define madvise_log_if_error(addr, len, advice) \
|
||||
if (madvise(addr, len, advice)) \
|
||||
LOGE("%s%p%s%zu%s%d%s%s%s", "madvise addr:", addr, "len:", len, "with advice ", advice, " failed with error: ", strerror(errno), ", Ignored")
|
||||
|
||||
#endif /* EHVIEWER_EHVIEWER_H */
|
||||
@@ -0,0 +1,27 @@
|
||||
#include <jni.h>
|
||||
#include <sys/mman.h>
|
||||
|
||||
#include <android/log.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
|
||||
#define LOG_TAG "mmap_utils"
|
||||
|
||||
#include "ehviewer.h"
|
||||
|
||||
// TODO: Replace it with AFileDescriptor_getFd when minsdk 31
|
||||
// https://developer.android.com/ndk/reference/group/file-descriptor
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_hippo_Native_getFd(JNIEnv *env, jclass clazz, jobject fileDescriptor) {
|
||||
jint fd = -1;
|
||||
jclass fdClass = (*env)->FindClass(env, "java/io/FileDescriptor");
|
||||
|
||||
if (fdClass != NULL) {
|
||||
jfieldID fdClassDescriptorFieldID = (*env)->GetFieldID(env, fdClass, "descriptor", "I");
|
||||
if (fdClassDescriptorFieldID != NULL && fileDescriptor != NULL) {
|
||||
fd = (*env)->GetIntField(env, fileDescriptor, fdClassDescriptorFieldID);
|
||||
}
|
||||
}
|
||||
|
||||
return fd;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#include <jni.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <android/log.h>
|
||||
|
||||
#define LOG_TAG "gifUtils"
|
||||
|
||||
#include "ehviewer.h"
|
||||
|
||||
#define GIF_HEADER_87A "GIF87a"
|
||||
#define GIF_HEADER_89A "GIF89a"
|
||||
|
||||
static int FRAME_DELAY_START_MARKER = 0x0021F904;
|
||||
|
||||
typedef signed char byte;
|
||||
|
||||
#define FRAME_DELAY_START_MARKER ((byte*)(&FRAME_DELAY_START_MARKER))
|
||||
#define MINIMUM_FRAME_DELAY 2
|
||||
#define DEFAULT_FRAME_DELAY 10
|
||||
|
||||
static inline bool isGif(void *addr) {
|
||||
return !memcmp(addr, GIF_HEADER_87A, 6) || !memcmp(addr, GIF_HEADER_89A, 6);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_image_Image_rewriteGifSource(JNIEnv *env, jclass clazz, jobject buffer) {
|
||||
byte *addr = (*env)->GetDirectBufferAddress(env, buffer);
|
||||
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
|
||||
if (size < 7 || !isGif(addr)) return;
|
||||
for (size_t i = 0; i < size - 8; i++) {
|
||||
// TODO: Optimize this hex find with SIMD?
|
||||
if (addr[i] == FRAME_DELAY_START_MARKER[3] && addr[i + 1] == FRAME_DELAY_START_MARKER[2] &&
|
||||
addr[i + 2] == FRAME_DELAY_START_MARKER[1] &&
|
||||
addr[i + 3] == FRAME_DELAY_START_MARKER[0]) {
|
||||
byte *end = addr + i + 4;
|
||||
if (end[4] != 0) continue;
|
||||
int frameDelay = end[2] << 8 | end[1];
|
||||
if (frameDelay > MINIMUM_FRAME_DELAY)
|
||||
break; // Quit if the first block looks normal, for performance
|
||||
end[1] = DEFAULT_FRAME_DELAY;
|
||||
end[2] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
Submodule
+1
Submodule app/src/main/cpp/libarchive added at c9298fd6dc.
Submodule
+1
Submodule app/src/main/cpp/liblzma added at fc9fb0ea24.
File renamed without changes.
@@ -149,7 +149,7 @@
|
||||
#define PACKAGE_NAME "nettle"
|
||||
|
||||
/* Define to the full name and version of this package. */
|
||||
#define PACKAGE_STRING "nettle 3.8"
|
||||
#define PACKAGE_STRING "nettle 3.8.1"
|
||||
|
||||
/* Define to the one symbol short name of this package. */
|
||||
#define PACKAGE_TARNAME "nettle"
|
||||
@@ -158,7 +158,7 @@
|
||||
#define PACKAGE_URL ""
|
||||
|
||||
/* Define to the version of this package. */
|
||||
#define PACKAGE_VERSION "3.8"
|
||||
#define PACKAGE_VERSION "3.8.1"
|
||||
|
||||
/* The size of `long', as computed by sizeof. */
|
||||
#define SIZEOF_LONG __SIZEOF_LONG__
|
||||
File renamed without changes.
Submodule
+1
Submodule app/src/main/cpp/nettle/nettle added at 990abad16c.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,40 @@
|
||||
package androidx.navigation.ui
|
||||
|
||||
import android.view.MenuItem
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.navigation.ActivityNavigator
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavOptions
|
||||
|
||||
fun onNavDestinationSelected2(item: MenuItem, navController: NavController): Boolean {
|
||||
if (navController.currentDestination?.matchDestination(item.itemId) == true) return false
|
||||
val builder = NavOptions.Builder().setLaunchSingleTop(true)
|
||||
if (
|
||||
navController.currentDestination!!.parent!!.findNode(item.itemId)
|
||||
is ActivityNavigator.Destination
|
||||
) {
|
||||
builder.setEnterAnim(R.anim.nav_default_enter_anim)
|
||||
.setExitAnim(R.anim.nav_default_exit_anim)
|
||||
.setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
|
||||
.setPopExitAnim(R.anim.nav_default_pop_exit_anim)
|
||||
} else {
|
||||
builder.setEnterAnim(com.hippo.ehviewer.R.anim.scene_open_enter)
|
||||
.setExitAnim(com.hippo.ehviewer.R.anim.scene_open_exit)
|
||||
.setPopEnterAnim(com.hippo.ehviewer.R.anim.scene_close_enter)
|
||||
.setPopExitAnim(com.hippo.ehviewer.R.anim.scene_close_exit)
|
||||
}
|
||||
val options = builder.build()
|
||||
return try {
|
||||
// TODO provide proper API instead of using Exceptions as Control-Flow.
|
||||
navController.navigate(item.itemId, null, options)
|
||||
// Return true only if the destination we've navigated to matches the MenuItem
|
||||
navController.currentDestination?.matchDestination(item.itemId) == true
|
||||
} catch (e: IllegalArgumentException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun NavDestination.matchDestination(@IdRes destId: Int): Boolean =
|
||||
hierarchy.any { it.id == destId }
|
||||
@@ -0,0 +1,25 @@
|
||||
package androidx.swiperefreshlayout.widget
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.ShapeDrawable
|
||||
import android.util.AttributeSet
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import rikka.core.res.resolveColor
|
||||
|
||||
|
||||
@SuppressLint("PrivateResource")
|
||||
class ThemedSwipeRefreshLayout @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : SwipeRefreshLayout(context, attrs) {
|
||||
|
||||
init {
|
||||
val surfaceColor =
|
||||
context.theme.resolveColor(com.google.android.material.R.attr.colorSurface)
|
||||
val overlayColor =
|
||||
context.getColor(com.google.android.material.R.color.m3_popupmenu_overlay_color)
|
||||
val backgroundColor = ColorUtils.compositeColors(overlayColor, surfaceColor)
|
||||
(mCircleView.background as ShapeDrawable).paint.color = backgroundColor
|
||||
setColorSchemeColors(context.theme.resolveColor(com.google.android.material.R.attr.colorAccent))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
@file:JvmName("GifDecodeUtils")
|
||||
|
||||
package coil.decode
|
||||
|
||||
import kotlin.experimental.and
|
||||
import okio.BufferedSource
|
||||
import okio.ByteString.Companion.encodeUtf8
|
||||
|
||||
// https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
|
||||
private val GIF_HEADER_87A = "GIF87a".encodeUtf8()
|
||||
private val GIF_HEADER_89A = "GIF89a".encodeUtf8()
|
||||
|
||||
// https://developers.google.com/speed/webp/docs/riff_container
|
||||
private val WEBP_HEADER_RIFF = "RIFF".encodeUtf8()
|
||||
private val WEBP_HEADER_WEBP = "WEBP".encodeUtf8()
|
||||
private val WEBP_HEADER_VPX8 = "VP8X".encodeUtf8()
|
||||
|
||||
// https://nokiatech.github.io/heif/technical.html
|
||||
private val HEIF_HEADER_FTYP = "ftyp".encodeUtf8()
|
||||
private val HEIF_HEADER_MSF1 = "msf1".encodeUtf8()
|
||||
private val HEIF_HEADER_HEVC = "hevc".encodeUtf8()
|
||||
private val HEIF_HEADER_HEVX = "hevx".encodeUtf8()
|
||||
|
||||
/**
|
||||
* Return 'true' if the [source] contains a GIF image. The [source] is not consumed.
|
||||
*/
|
||||
fun DecodeUtils.isGif(source: BufferedSource): Boolean {
|
||||
return source.rangeEquals(0, GIF_HEADER_89A) ||
|
||||
source.rangeEquals(0, GIF_HEADER_87A)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return 'true' if the [source] contains a WebP image. The [source] is not consumed.
|
||||
*/
|
||||
fun DecodeUtils.isWebP(source: BufferedSource): Boolean {
|
||||
return source.rangeEquals(0, WEBP_HEADER_RIFF) &&
|
||||
source.rangeEquals(8, WEBP_HEADER_WEBP)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return 'true' if the [source] contains an animated WebP image. The [source] is not consumed.
|
||||
*/
|
||||
fun DecodeUtils.isAnimatedWebP(source: BufferedSource): Boolean {
|
||||
return isWebP(source) &&
|
||||
source.rangeEquals(12, WEBP_HEADER_VPX8) &&
|
||||
source.request(17) &&
|
||||
(source.buffer[16] and 0b00000010) > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Return 'true' if the [source] contains an HEIF image. The [source] is not consumed.
|
||||
*/
|
||||
fun DecodeUtils.isHeif(source: BufferedSource): Boolean {
|
||||
return source.rangeEquals(4, HEIF_HEADER_FTYP)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return 'true' if the [source] contains an animated HEIF image sequence. The [source] is not
|
||||
* consumed.
|
||||
*/
|
||||
fun DecodeUtils.isAnimatedHeif(source: BufferedSource): Boolean {
|
||||
return isHeif(source) &&
|
||||
(source.rangeEquals(8, HEIF_HEADER_MSF1) ||
|
||||
source.rangeEquals(8, HEIF_HEADER_HEVC) ||
|
||||
source.rangeEquals(8, HEIF_HEADER_HEVX))
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
@file:Suppress("SameParameterValue")
|
||||
|
||||
package coil.decode
|
||||
|
||||
import okio.Buffer
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.decodeHex
|
||||
import okio.ForwardingSource
|
||||
import okio.Source
|
||||
|
||||
/**
|
||||
* A [ForwardingSource] that rewrites the GIF frame delay in every graphics control block if it's
|
||||
* below a threshold.
|
||||
*/
|
||||
internal class FrameDelayRewritingSource(delegate: Source) : ForwardingSource(delegate) {
|
||||
|
||||
// An intermediary buffer so we can read and alter the data before it's written to the destination.
|
||||
private val buffer = Buffer()
|
||||
|
||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||
// Ensure our buffer has enough bytes to satisfy this read.
|
||||
request(byteCount)
|
||||
|
||||
// Short circuit if there are no bytes in the buffer.
|
||||
if (buffer.size == 0L) {
|
||||
return if (byteCount == 0L) 0L else -1L
|
||||
}
|
||||
|
||||
// Search through the buffer and rewrite any frame delays below the threshold.
|
||||
var bytesWritten = 0L
|
||||
while (true) {
|
||||
val index = indexOf(FRAME_DELAY_START_MARKER)
|
||||
if (index == -1L) break
|
||||
|
||||
// Write up until the end of the frame delay start marker.
|
||||
bytesWritten += write(sink, index + FRAME_DELAY_START_MARKER_SIZE_BYTES)
|
||||
|
||||
// Check for the end of the graphics control extension block.
|
||||
if (!request(5) || buffer[4] != 0.toByte()) continue
|
||||
|
||||
// Rewrite the frame delay if it is below the threshold.
|
||||
// The frame delay is stored as two unsigned bytes in reverse order
|
||||
// (i.e. the most significant bits are in the byte that's read second).
|
||||
val frameDelay = (buffer[2].toUByte().toInt() shl 8) or buffer[1].toUByte().toInt()
|
||||
if (frameDelay < MINIMUM_FRAME_DELAY) {
|
||||
sink.writeByte(buffer[0].toInt())
|
||||
sink.writeByte(DEFAULT_FRAME_DELAY)
|
||||
sink.writeByte(0)
|
||||
buffer.skip(3)
|
||||
}
|
||||
}
|
||||
|
||||
// Write anything left in the source.
|
||||
if (bytesWritten < byteCount) {
|
||||
bytesWritten += write(sink, byteCount - bytesWritten)
|
||||
}
|
||||
return if (bytesWritten == 0L) -1 else bytesWritten
|
||||
}
|
||||
|
||||
private fun indexOf(bytes: ByteString): Long {
|
||||
var index = -1L
|
||||
while (true) {
|
||||
index = buffer.indexOf(bytes[0], index + 1)
|
||||
if (index == -1L) break
|
||||
if (request(bytes.size.toLong()) && buffer.rangeEquals(index, bytes)) break
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
private fun write(sink: Buffer, byteCount: Long): Long {
|
||||
return buffer.read(sink, byteCount).coerceAtLeast(0)
|
||||
}
|
||||
|
||||
private fun request(byteCount: Long): Boolean {
|
||||
if (buffer.size >= byteCount) return true
|
||||
val toRead = byteCount - buffer.size
|
||||
return super.read(buffer, toRead) == toRead
|
||||
}
|
||||
|
||||
private companion object {
|
||||
// https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
|
||||
// See: "Graphics Control Extension"
|
||||
private val FRAME_DELAY_START_MARKER = "0021F904".decodeHex()
|
||||
private const val FRAME_DELAY_START_MARKER_SIZE_BYTES = 4
|
||||
private const val MINIMUM_FRAME_DELAY = 2
|
||||
private const val DEFAULT_FRAME_DELAY = 10
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package com.davemorrissey.labs.subscaleview
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import com.davemorrissey.labs.subscaleview.provider.AssetInputProvider
|
||||
import com.davemorrissey.labs.subscaleview.provider.InputProvider
|
||||
import com.davemorrissey.labs.subscaleview.provider.OpenStreamProvider
|
||||
import com.davemorrissey.labs.subscaleview.provider.ResourceInputProvider
|
||||
import com.davemorrissey.labs.subscaleview.provider.UriInputProvider
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* Helper class used to set the source and additional attributes from a variety of sources. Supports
|
||||
* use of a bitmap, asset, resource, external file or any other URI.
|
||||
*
|
||||
*
|
||||
* When you are using a preview image, you must set the dimensions of the full size image on the
|
||||
* ImageSource object for the full size image using the [.dimensions] method.
|
||||
*/
|
||||
class ImageSource {
|
||||
val bitmap: Bitmap?
|
||||
val provider: InputProvider?
|
||||
|
||||
private var sWidth = 0
|
||||
private var sHeight = 0
|
||||
|
||||
var sRegion = Rect(0, 0, 0, 0)
|
||||
private set
|
||||
var isCached = false
|
||||
private set
|
||||
|
||||
private constructor(bitmap: Bitmap, cached: Boolean) {
|
||||
this.bitmap = bitmap
|
||||
provider = null
|
||||
sWidth = bitmap.width
|
||||
sHeight = bitmap.height
|
||||
isCached = cached
|
||||
}
|
||||
|
||||
private constructor(provider: InputProvider) {
|
||||
bitmap = null
|
||||
this.provider = provider
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a region of the source image. Region must be set independently for the full size image and the preview if
|
||||
* you are using one.
|
||||
*
|
||||
* @param sRegion the region of the source image to be displayed.
|
||||
* @return this instance for chaining.
|
||||
*/
|
||||
fun region(sRegion: Rect): ImageSource {
|
||||
this.sRegion = sRegion
|
||||
setInvariants()
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare the dimensions of the image. This is only required for a full size image, when you are specifying a URI
|
||||
* and also a preview image. When displaying a bitmap object, or not using a preview, you do not need to declare
|
||||
* the image dimensions. Note if the declared dimensions are found to be incorrect, the view will reset.
|
||||
*
|
||||
* @param sWidth width of the source image.
|
||||
* @param sHeight height of the source image.
|
||||
* @return this instance for chaining.
|
||||
*/
|
||||
fun dimensions(sWidth: Int, sHeight: Int): ImageSource {
|
||||
if (bitmap == null) {
|
||||
this.sWidth = sWidth
|
||||
this.sHeight = sHeight
|
||||
}
|
||||
setInvariants()
|
||||
return this
|
||||
}
|
||||
|
||||
private fun setInvariants() {
|
||||
if (!sRegion.isEmpty) {
|
||||
sWidth = sRegion.width()
|
||||
sHeight = sRegion.height()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Create an instance from a resource. The correct resource for the device screen resolution will be used.
|
||||
*
|
||||
* @param resId resource ID.
|
||||
* @return an [ImageSource] instance.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun resource(context: Context, resId: Int): ImageSource {
|
||||
return ImageSource(ResourceInputProvider(context, resId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance from an asset name.
|
||||
*
|
||||
* @param assetName asset name.
|
||||
* @return an [ImageSource] instance.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun asset(context: Context, assetName: String): ImageSource {
|
||||
return ImageSource(AssetInputProvider(context, assetName))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance from a URI.
|
||||
*
|
||||
* @param uri image URI.
|
||||
* @return an [ImageSource] instance.
|
||||
*/
|
||||
fun uri(context: Context, uri: Uri): ImageSource {
|
||||
return ImageSource(UriInputProvider(context, uri))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance from an input provider.
|
||||
*
|
||||
* @param provider input stream provider.
|
||||
* @return an [ImageSource] instance.
|
||||
*/
|
||||
fun provider(provider: InputProvider): ImageSource {
|
||||
return ImageSource(provider)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance from an input stream.
|
||||
*
|
||||
* @param stream open input stream.
|
||||
* @return an [ImageSource] instance.
|
||||
*/
|
||||
fun inputStream(stream: InputStream): ImageSource {
|
||||
return ImageSource(OpenStreamProvider(stream))
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a loaded bitmap for display.
|
||||
*
|
||||
* @param bitmap bitmap to be displayed.
|
||||
* @return an [ImageSource] instance.
|
||||
*/
|
||||
fun bitmap(bitmap: Bitmap): ImageSource {
|
||||
return ImageSource(bitmap, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a loaded and cached bitmap for display. This bitmap will not be recycled when it is no
|
||||
* longer needed. Use this method if you loaded the bitmap with an image loader such as Picasso
|
||||
* or Volley.
|
||||
*
|
||||
* @param bitmap bitmap to be displayed.
|
||||
* @return an [ImageSource] instance.
|
||||
*/
|
||||
fun cachedBitmap(bitmap: Bitmap): ImageSource {
|
||||
return ImageSource(bitmap, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.davemorrissey.labs.subscaleview
|
||||
|
||||
import android.graphics.PointF
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* Wraps the scale, center and orientation of a displayed image for easy restoration on screen rotate.
|
||||
*/
|
||||
class ImageViewState(val scale: Float, center: PointF) : Serializable {
|
||||
private val centerX: Float = center.x
|
||||
private val centerY: Float = center.y
|
||||
|
||||
val center: PointF
|
||||
get() = PointF(centerX, centerY)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,53 @@
|
||||
package com.davemorrissey.labs.subscaleview.decoder
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Point
|
||||
import android.graphics.Rect
|
||||
import kotlin.Throws
|
||||
import com.davemorrissey.labs.subscaleview.provider.InputProvider
|
||||
import java.lang.Exception
|
||||
|
||||
/**
|
||||
* Interface for image decoding classes.
|
||||
*/
|
||||
interface Decoder {
|
||||
/**
|
||||
* Initialise the decoder. When possible, perform initial setup work once in this method. The
|
||||
* dimensions of the image must be returned.
|
||||
*
|
||||
* @param context Application context. A reference may be held, but must be cleared on recycle.
|
||||
* @param provider Provider of the image.
|
||||
* @return Dimensions of the image.
|
||||
* @throws Exception if initialisation fails.
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
fun init(context: Context, provider: InputProvider): Point
|
||||
|
||||
/**
|
||||
* Decode a region of the image with the given sample size. This method is called off the UI
|
||||
* thread so it can safely load the image on the current thread. It is called from
|
||||
* [android.os.AsyncTask]s running in an executor that may have multiple threads, so
|
||||
* implementations must be thread safe. Adding `synchronized` to the method signature
|
||||
* is the simplest way to achieve this, but bear in mind the [.recycle] method can be
|
||||
* called concurrently.
|
||||
*
|
||||
* @param sRect Source image rectangle to decode.
|
||||
* @param sampleSize Sample size.
|
||||
* @return The decoded region. It is safe to return null if decoding fails.
|
||||
*/
|
||||
fun decodeRegion(sRect: Rect, sampleSize: Int): Bitmap
|
||||
|
||||
/**
|
||||
* Status check. Should return false before initialisation and after recycle.
|
||||
*
|
||||
* @return true if the decoder is ready to be used.
|
||||
*/
|
||||
val isReady: Boolean
|
||||
|
||||
/**
|
||||
* This method will be called when the decoder is no longer required. It should clean up any
|
||||
* resources still in use.
|
||||
*/
|
||||
fun recycle()
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.davemorrissey.labs.subscaleview.decoder
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Point
|
||||
import android.graphics.Rect
|
||||
import kotlin.Throws
|
||||
import com.davemorrissey.labs.subscaleview.provider.InputProvider
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import java.lang.Exception
|
||||
|
||||
class ImageDecoder @JvmOverloads constructor(
|
||||
private val cropBorders: Boolean,
|
||||
bitmapConfig: Bitmap.Config? = null
|
||||
) : Decoder {
|
||||
init {
|
||||
val globalBitmapConfig = SubsamplingScaleImageView.preferredBitmapConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the decoder. When possible, perform initial setup work once in this method. The
|
||||
* dimensions of the image must be returned.
|
||||
*
|
||||
* @param context Application context. A reference may be held, but must be cleared on recycle.
|
||||
* @param provider Provider of the image.
|
||||
* @return Dimensions of the image.
|
||||
* @throws Exception if initialisation fails.
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
override fun init(context: Context, provider: InputProvider): Point {
|
||||
TODO()
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a region of the image with the given sample size. This method is called off the UI
|
||||
* thread so it can safely load the image on the current thread. It is called from
|
||||
* [android.os.AsyncTask]s running in an executor that may have multiple threads, so
|
||||
* implementations must be thread safe. Adding `synchronized` to the method signature
|
||||
* is the simplest way to achieve this, but bear in mind the [.recycle] method can be
|
||||
* called concurrently.
|
||||
*
|
||||
* @param sRect Source image rectangle to decode.
|
||||
* @param sampleSize Sample size.
|
||||
* @return The decoded region. It is safe to return null if decoding fails.
|
||||
*/
|
||||
override fun decodeRegion(sRect: Rect, sampleSize: Int): Bitmap {
|
||||
TODO()
|
||||
}
|
||||
|
||||
/**
|
||||
* Status check. Should return false before initialisation and after recycle.
|
||||
*
|
||||
* @return true if the decoder is ready to be used.
|
||||
*/
|
||||
override val isReady: Boolean = TODO()
|
||||
|
||||
/**
|
||||
* This method will be called when the decoder is no longer required. It should clean up any
|
||||
* resources still in use.
|
||||
*/
|
||||
override fun recycle() {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.davemorrissey.labs.subscaleview.provider
|
||||
|
||||
import android.content.Context
|
||||
import java.io.IOException
|
||||
|
||||
class AssetInputProvider(context: Context, val assetName: String) : InputProvider {
|
||||
private val assets = context.assets
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun openStream() = assets.open(assetName)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.davemorrissey.labs.subscaleview.provider
|
||||
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import kotlin.Throws
|
||||
|
||||
interface InputProvider {
|
||||
@Throws(IOException::class)
|
||||
fun openStream(): InputStream?
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.davemorrissey.labs.subscaleview.provider
|
||||
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import kotlin.Throws
|
||||
|
||||
/**
|
||||
* Provider from an already open input stream.
|
||||
*
|
||||
*
|
||||
* Please keep in mind this provider is not reusable.
|
||||
*/
|
||||
class OpenStreamProvider(private val stream: InputStream) : InputProvider {
|
||||
@Throws(IOException::class)
|
||||
override fun openStream() = stream
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package com.davemorrissey.labs.subscaleview.provider
|
||||
|
||||
import android.content.Context
|
||||
import java.io.IOException
|
||||
|
||||
class ResourceInputProvider(context: Context, val resource: Int) : InputProvider {
|
||||
private val resources = context.resources
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun openStream() = resources.openRawResource(resource)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.davemorrissey.labs.subscaleview.provider
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import java.io.IOException
|
||||
import kotlin.Throws
|
||||
|
||||
class UriInputProvider(context: Context, val uri: Uri) : InputProvider {
|
||||
private val resolver = context.contentResolver
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun openStream() = resolver.openInputStream(uri)
|
||||
}
|
||||
@@ -15,22 +15,15 @@
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.hippo
|
||||
|
||||
package com.hippo;
|
||||
import java.io.FileDescriptor
|
||||
|
||||
public class Native {
|
||||
|
||||
public static void initialize() {
|
||||
System.loadLibrary("ehviewer");
|
||||
object Native {
|
||||
fun initialize() {
|
||||
System.loadLibrary("ehviewer")
|
||||
}
|
||||
|
||||
public static native String getlibarchiveVersion();
|
||||
|
||||
public static native String getliblzmaVersion();
|
||||
|
||||
public static native String getlibjpeg_turboVersion();
|
||||
|
||||
public static native String getzlibVersion();
|
||||
|
||||
public static native String getnettleVersion();
|
||||
@JvmStatic
|
||||
external fun getFd(fd: FileDescriptor?): Int
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer 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.
|
||||
*
|
||||
* EhViewer 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 EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.hippo
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.ImageDecoder
|
||||
import android.net.Uri
|
||||
import com.hippo.image.Image
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
class UriArchiveAccessor(ctx: Context, uri: Uri) {
|
||||
val pfd by lazy { ctx.contentResolver.openFileDescriptor(uri, "r")!! }
|
||||
fun open(): Int {
|
||||
return openArchive(pfd.fd, pfd.statSize)
|
||||
}
|
||||
|
||||
private external fun openArchive(fd: Int, size: Long): Int
|
||||
private external fun extractToByteBuffer(index: Int): ByteBuffer?
|
||||
external fun extractToFd(index: Int, fd: Int)
|
||||
external fun getFilename(index: Int): String
|
||||
external fun needPassword(): Boolean
|
||||
external fun providePassword(str: String): Boolean
|
||||
private external fun closeArchive()
|
||||
fun close() {
|
||||
closeArchive()
|
||||
pfd.close()
|
||||
}
|
||||
|
||||
fun getImageSource(index: Int): Image.CloseableSource? {
|
||||
val buffer = extractToByteBuffer(index)
|
||||
buffer ?: return null
|
||||
check(buffer.isDirect)
|
||||
Image.rewriteGifSource(buffer)
|
||||
val source = ImageDecoder.createSource(buffer)
|
||||
return object : Image.CloseableSource {
|
||||
override val source: ImageDecoder.Source
|
||||
get() = source
|
||||
|
||||
override fun close() {
|
||||
releaseByteBuffer(buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
private external fun releaseByteBuffer(buffer: ByteBuffer)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.hippo.app
|
||||
|
||||
import android.content.Context
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
||||
open class BaseDialogBuilder constructor(
|
||||
context: Context
|
||||
) : MaterialAlertDialogBuilder(context)
|
||||
@@ -18,7 +18,6 @@ package com.hippo.app
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.CheckBox
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.hippo.ehviewer.R
|
||||
|
||||
class CheckBoxDialogBuilder constructor(
|
||||
@@ -26,7 +25,7 @@ class CheckBoxDialogBuilder constructor(
|
||||
message: String?,
|
||||
checkText: String?,
|
||||
checked: Boolean
|
||||
) : MaterialAlertDialogBuilder(
|
||||
) : BaseDialogBuilder(
|
||||
context
|
||||
) {
|
||||
private val mCheckBox: CheckBox
|
||||
|
||||
@@ -23,7 +23,6 @@ import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import android.widget.TextView.OnEditorActionListener
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.hippo.ehviewer.R
|
||||
|
||||
@@ -31,7 +30,7 @@ class EditTextDialogBuilder constructor(
|
||||
context: Context,
|
||||
text: String?,
|
||||
hint: String?
|
||||
) : MaterialAlertDialogBuilder(
|
||||
) : BaseDialogBuilder(
|
||||
context
|
||||
), OnEditorActionListener {
|
||||
private val mTextInputLayout: TextInputLayout
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.hippo.app
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.HapticFeedbackConstants
|
||||
import com.google.android.material.slider.Slider
|
||||
|
||||
class HapticSlider @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : Slider(context, attrs) {
|
||||
init {
|
||||
addOnChangeListener { _, _, _ -> performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK) }
|
||||
}
|
||||
}
|
||||
@@ -23,14 +23,16 @@ import android.widget.ArrayAdapter
|
||||
import android.widget.CheckBox
|
||||
import android.widget.ListView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.yorozuya.ViewUtils
|
||||
|
||||
class ListCheckBoxDialogBuilder constructor(
|
||||
context: Context, items: Array<CharSequence>,
|
||||
listener: OnItemClickListener?, checkText: String?, checked: Boolean
|
||||
) : MaterialAlertDialogBuilder(
|
||||
class ListCheckBoxDialogBuilder(
|
||||
context: Context,
|
||||
items: List<CharSequence>,
|
||||
listener: (ListCheckBoxDialogBuilder?, AlertDialog?, Int) -> Unit,
|
||||
checkText: String?,
|
||||
checked: Boolean
|
||||
) : BaseDialogBuilder(
|
||||
context
|
||||
) {
|
||||
private val mCheckBox: CheckBox
|
||||
@@ -43,10 +45,6 @@ class ListCheckBoxDialogBuilder constructor(
|
||||
return mDialog as AlertDialog
|
||||
}
|
||||
|
||||
interface OnItemClickListener {
|
||||
fun onItemClick(builder: ListCheckBoxDialogBuilder?, dialog: AlertDialog?, position: Int)
|
||||
}
|
||||
|
||||
init {
|
||||
val view =
|
||||
LayoutInflater.from(getContext()).inflate(R.layout.dialog_list_checkbox_builder, null)
|
||||
@@ -56,7 +54,7 @@ class ListCheckBoxDialogBuilder constructor(
|
||||
listView.adapter = ArrayAdapter(getContext(), R.layout.item_select_dialog, items)
|
||||
listView.onItemClickListener =
|
||||
AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long ->
|
||||
listener?.onItemClick(this@ListCheckBoxDialogBuilder, mDialog, position)
|
||||
listener(this@ListCheckBoxDialogBuilder, mDialog, position)
|
||||
mDialog?.dismiss()
|
||||
}
|
||||
mCheckBox.text = checkText
|
||||
|
||||
@@ -1,493 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015-2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.beerbelly;
|
||||
|
||||
import android.util.Log;
|
||||
import android.util.LruCache;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.streampipe.InputStreamPipe;
|
||||
import com.hippo.streampipe.OutputStreamPipe;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public abstract class BeerBelly<V> {
|
||||
|
||||
private static final String TAG = BeerBelly.class.getSimpleName();
|
||||
private final boolean mHasMemoryCache;
|
||||
private final boolean mHasDiskCache;
|
||||
@Nullable
|
||||
private MemoryCache<V> mMemoryCache;
|
||||
@Nullable
|
||||
private DiskCache<V> mDiskCache;
|
||||
|
||||
public BeerBelly(BeerBellyParams params) {
|
||||
params.isValid();
|
||||
mHasMemoryCache = params.hasMemoryCache;
|
||||
mHasDiskCache = params.hasDiskCache;
|
||||
|
||||
if (mHasMemoryCache) {
|
||||
initMemoryCache(params.memoryCacheMaxSize);
|
||||
}
|
||||
|
||||
if (mHasDiskCache) {
|
||||
initDiskCache(params.diskCacheDir, params.diskCacheMaxSize);
|
||||
}
|
||||
}
|
||||
|
||||
private void initMemoryCache(int maxSize) {
|
||||
mMemoryCache = new MemoryCache<>(maxSize, this);
|
||||
}
|
||||
|
||||
private void initDiskCache(File cacheDir, int maxSize) {
|
||||
// Set up disk cache
|
||||
try {
|
||||
mDiskCache = new DiskCache<>(cacheDir, maxSize, this);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Can't create disk cache", e);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract int sizeOf(String key, V value);
|
||||
|
||||
protected abstract void memoryEntryAdded(V value);
|
||||
|
||||
protected abstract void memoryEntryRemoved(boolean evicted, String key, V oldValue, V newValue);
|
||||
|
||||
protected abstract V read(@NonNull InputStreamPipe isPipe);
|
||||
|
||||
protected abstract boolean write(OutputStream os, V value);
|
||||
|
||||
@Nullable
|
||||
public LruCache<String, V> getMemoryCache() {
|
||||
return mMemoryCache;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public SimpleDiskCache getDiskCache() {
|
||||
if (mDiskCache != null) {
|
||||
return mDiskCache.mDiskCache;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if have memory cache
|
||||
*
|
||||
* @return true if have memory cache
|
||||
*/
|
||||
public boolean hasMemoryCache() {
|
||||
return mHasMemoryCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if have disk cache
|
||||
*
|
||||
* @return true if have disk cache
|
||||
*/
|
||||
public boolean hasDiskCache() {
|
||||
return mHasDiskCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get memory cache usage size
|
||||
*
|
||||
* @return {@code -1} for no memory cache
|
||||
*/
|
||||
public int memorySize() {
|
||||
if (mHasMemoryCache && mMemoryCache != null) {
|
||||
return mMemoryCache.size();
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get memory cache max size
|
||||
*
|
||||
* @return {@code -1} for no memory cache
|
||||
*/
|
||||
public int memoryMaxSize() {
|
||||
if (mHasMemoryCache && mMemoryCache != null) {
|
||||
return mMemoryCache.maxSize();
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get disk cache usage size
|
||||
*
|
||||
* @return {@code -1L} for no memory cache
|
||||
*/
|
||||
public long diskSize() {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
return mDiskCache.size();
|
||||
} else {
|
||||
return -1L;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get disk cache max size
|
||||
*
|
||||
* @return {@code -1L} for no memory cache
|
||||
*/
|
||||
public long diskMaxSize() {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
return mDiskCache.getMaxSize();
|
||||
} else {
|
||||
return -1L;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from memory cache
|
||||
*
|
||||
* @param key the key to get value
|
||||
* @return the value you get, null for miss or no memory cache
|
||||
*/
|
||||
public V getFromMemory(@NonNull String key) {
|
||||
if (mHasMemoryCache && mMemoryCache != null) {
|
||||
return mMemoryCache.get(key);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from disk cache. Override {@link #read(InputStreamPipe)} to do it
|
||||
*
|
||||
* @param key the key to get value
|
||||
* @return the value you get, null for miss or no memory cache or get error
|
||||
*/
|
||||
public V getFromDisk(@NonNull String key) {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
return mDiskCache.get(key);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from memory cache and disk cache. If miss in memory cache and
|
||||
* get in disk cache, it will put value from disk cache to memory cache.
|
||||
*
|
||||
* @param key the key to get value
|
||||
* @return the value you get
|
||||
*/
|
||||
public V get(@NonNull String key) {
|
||||
V value = getFromMemory(key);
|
||||
|
||||
if (value != null) {
|
||||
// Get it in memory cache
|
||||
return value;
|
||||
}
|
||||
|
||||
value = getFromDisk(key);
|
||||
|
||||
if (value != null) {
|
||||
// Get it in disk cache
|
||||
putToMemory(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put value to memory cache
|
||||
*
|
||||
* @param key the key
|
||||
* @param value the value
|
||||
* @return false if no memory cache
|
||||
*/
|
||||
public boolean putToMemory(@NonNull String key, @NonNull V value) {
|
||||
if (mHasMemoryCache && mMemoryCache != null) {
|
||||
memoryEntryAdded(value);
|
||||
mMemoryCache.put(key, value);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Put value to disk cache
|
||||
*
|
||||
* @param key the key
|
||||
* @param value the value
|
||||
* @return false if no disk cache or get error
|
||||
*/
|
||||
public boolean putToDisk(@NonNull String key, @NonNull V value) {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
return mDiskCache.put(key, value);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Put value to memory cache and disk cache
|
||||
*
|
||||
* @param key the key
|
||||
* @param value the value
|
||||
*/
|
||||
public void put(@NonNull String key, @NonNull V value) {
|
||||
putToMemory(key, value);
|
||||
putToDisk(key, value);
|
||||
}
|
||||
|
||||
public void removeFromMemory(@NonNull String key) {
|
||||
if (mHasMemoryCache && mMemoryCache != null) {
|
||||
mMemoryCache.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeFromDisk(@NonNull String key) {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
mDiskCache.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(@NonNull String key) {
|
||||
removeFromMemory(key);
|
||||
removeFromDisk(key);
|
||||
}
|
||||
|
||||
public boolean pullFromDiskCache(@NonNull String key, @NonNull OutputStream os) {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
return mDiskCache.pull(key, os);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param key the key
|
||||
* @param is the input stream to store
|
||||
* @return false if no disk cache or get error
|
||||
*/
|
||||
public boolean putRawToDisk(@NonNull String key, @NonNull InputStream is) {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
return mDiskCache.putRaw(key, is);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evicts all of the items from the memory cache
|
||||
*/
|
||||
public void clearMemory() {
|
||||
if (mHasMemoryCache && mMemoryCache != null) {
|
||||
mMemoryCache.evictAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear disk cache
|
||||
*/
|
||||
public void clearDisk() {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
mDiskCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public void flush() {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
mDiskCache.flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evicts all of the items from the memory cache and lets the system know
|
||||
* now would be a good time to garbage collect
|
||||
*/
|
||||
public void clear() {
|
||||
clearMemory();
|
||||
clearDisk();
|
||||
}
|
||||
|
||||
/**
|
||||
* A holder class that contains cache parameters.
|
||||
*/
|
||||
public static class BeerBellyParams {
|
||||
|
||||
/**
|
||||
* is memory cache available
|
||||
*/
|
||||
public boolean hasMemoryCache = false;
|
||||
/**
|
||||
* the maximum number of bytes the memory cache should use to store
|
||||
*/
|
||||
public int memoryCacheMaxSize = 0;
|
||||
/**
|
||||
* is disk cache available
|
||||
*/
|
||||
public boolean hasDiskCache = false;
|
||||
/**
|
||||
* the dir to store disk cache
|
||||
*/
|
||||
public File diskCacheDir = null;
|
||||
/**
|
||||
* the maximum number of bytes the disk cache should use to store
|
||||
*/
|
||||
public int diskCacheMaxSize = 0;
|
||||
|
||||
/**
|
||||
* Check BeerBellyParams is valid
|
||||
*/
|
||||
public void isValid() throws IllegalStateException {
|
||||
if (!hasMemoryCache && !hasDiskCache) {
|
||||
throw new IllegalStateException("No memory cache and no disk cache. What can I do for you?");
|
||||
}
|
||||
|
||||
if (hasMemoryCache && memoryCacheMaxSize <= 0) {
|
||||
throw new IllegalStateException("Memory cache max size must be bigger than 0.");
|
||||
}
|
||||
|
||||
if (hasDiskCache) {
|
||||
if (diskCacheDir == null) {
|
||||
throw new IllegalStateException("Disk cache dir can't be null.");
|
||||
}
|
||||
if (diskCacheMaxSize <= 0) {
|
||||
throw new IllegalStateException("Disk cache max size must be bigger than 0.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class DiskCache<E> {
|
||||
|
||||
private static final int IO_BUFFER_SIZE = 8 * 1024;
|
||||
|
||||
private final SimpleDiskCache mDiskCache;
|
||||
private final BeerBelly<E> mParent;
|
||||
|
||||
private final File mCacheDir;
|
||||
private final int mMaxSize;
|
||||
|
||||
public DiskCache(File cacheDir, int size, BeerBelly<E> parent) throws IOException {
|
||||
mDiskCache = new SimpleDiskCache(cacheDir, size);
|
||||
mParent = parent;
|
||||
|
||||
mCacheDir = cacheDir;
|
||||
mMaxSize = size;
|
||||
}
|
||||
|
||||
public File getCacheDir() {
|
||||
return mCacheDir;
|
||||
}
|
||||
|
||||
public int getMaxSize() {
|
||||
return mMaxSize;
|
||||
}
|
||||
|
||||
public long size() {
|
||||
return mDiskCache.size();
|
||||
}
|
||||
|
||||
public void flush() {
|
||||
mDiskCache.flush();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
mDiskCache.clear();
|
||||
}
|
||||
|
||||
public E get(String key) {
|
||||
InputStreamPipe isPipe = mDiskCache.getInputStreamPipe(key);
|
||||
if (isPipe == null) {
|
||||
return null;
|
||||
} else {
|
||||
return mParent.read(isPipe);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean put(String key, E value) {
|
||||
OutputStreamPipe osPipe = mDiskCache.getOutputStreamPipe(key);
|
||||
BufferedOutputStream buffOut = null;
|
||||
try {
|
||||
osPipe.obtain();
|
||||
OutputStream os = osPipe.open();
|
||||
buffOut = new BufferedOutputStream(os, IO_BUFFER_SIZE);
|
||||
return mParent.write(os, value);
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
} finally {
|
||||
Util.closeQuietly(buffOut);
|
||||
osPipe.close();
|
||||
osPipe.release();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean putRaw(String key, InputStream is) {
|
||||
return mDiskCache.put(key, is);
|
||||
}
|
||||
|
||||
public void remove(String key) {
|
||||
mDiskCache.remove(key);
|
||||
}
|
||||
|
||||
public boolean pull(@NonNull String key, @NonNull OutputStream os) {
|
||||
InputStreamPipe isPipe = mDiskCache.getInputStreamPipe(key);
|
||||
if (isPipe == null) {
|
||||
return false;
|
||||
} else {
|
||||
try {
|
||||
isPipe.obtain();
|
||||
InputStream is = isPipe.open();
|
||||
Util.copy(is, os);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
} finally {
|
||||
isPipe.close();
|
||||
isPipe.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class MemoryCache<E> extends LruCache<String, E> {
|
||||
|
||||
public BeerBelly<E> mParent;
|
||||
|
||||
public MemoryCache(int maxSize, BeerBelly<E> parent) {
|
||||
super(maxSize);
|
||||
mParent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int sizeOf(String key, E value) {
|
||||
return mParent.sizeOf(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void entryRemoved(boolean evicted, String key, E oldValue, E newValue) {
|
||||
mParent.memoryEntryRemoved(evicted, key, oldValue, newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,984 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 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 com.hippo.beerbelly;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.Closeable;
|
||||
import java.io.EOFException;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A cache that uses a bounded amount of space on a filesystem. Each cache
|
||||
* entry has a string key and a fixed number of values. Each key must match
|
||||
* the regex <strong>[a-z0-9_-]{1,120}</strong>. Values are byte sequences,
|
||||
* accessible as streams or files. Each value must be between {@code 0} and
|
||||
* {@code Integer.MAX_VALUE} bytes in length.
|
||||
*
|
||||
* <p>The cache stores its data in a directory on the filesystem. This
|
||||
* directory must be exclusive to the cache; the cache may delete or overwrite
|
||||
* files from its directory. It is an error for multiple processes to use the
|
||||
* same cache directory at the same time.
|
||||
*
|
||||
* <p>This cache limits the number of bytes that it will store on the
|
||||
* filesystem. When the number of stored bytes exceeds the limit, the cache will
|
||||
* remove entries in the background until the limit is satisfied. The limit is
|
||||
* not strict: the cache may temporarily exceed it while waiting for files to be
|
||||
* deleted. The limit does not include filesystem overhead or the cache
|
||||
* journal so space-sensitive applications should set a conservative limit.
|
||||
*
|
||||
* <p>Clients call {@link #edit} to create or update the values of an entry. An
|
||||
* entry may have only one editor at one time; if a value is not available to be
|
||||
* edited then {@link #edit} will return null.
|
||||
* <ul>
|
||||
* <li>When an entry is being <strong>created</strong> it is necessary to
|
||||
* supply a full set of values; the empty value should be used as a
|
||||
* placeholder if necessary.
|
||||
* <li>When an entry is being <strong>edited</strong>, it is not necessary
|
||||
* to supply data for every value; values default to their previous
|
||||
* value.
|
||||
* </ul>
|
||||
* Every {@link #edit} call must be matched by a call to {@link Editor#commit}
|
||||
* or {@link Editor#abort}. Committing is atomic: a read observes the full set
|
||||
* of values as they were before or after the commit, but never a mix of values.
|
||||
*
|
||||
* <p>Clients call {@link #get} to read a snapshot of an entry. The read will
|
||||
* observe the value at the time that {@link #get} was called. Updates and
|
||||
* removals after the call do not impact ongoing reads.
|
||||
*
|
||||
* <p>This class is tolerant of some I/O errors. If files are missing from the
|
||||
* filesystem, the corresponding entries will be dropped from the cache. If
|
||||
* an error occurs while writing a cache value, the edit will fail silently.
|
||||
* Callers should handle other problems by catching {@code IOException} and
|
||||
* responding appropriately.
|
||||
*/
|
||||
public final class DiskLruCache implements Closeable {
|
||||
static final String JOURNAL_FILE = "journal";
|
||||
static final String JOURNAL_FILE_TEMP = "journal.tmp";
|
||||
static final String JOURNAL_FILE_BACKUP = "journal.bkp";
|
||||
static final String MAGIC = "libcore.io.DiskLruCache";
|
||||
static final String VERSION_1 = "1";
|
||||
static final long ANY_SEQUENCE_NUMBER = -1;
|
||||
static final String STRING_KEY_PATTERN = "[a-z0-9_-]{1,120}";
|
||||
static final Pattern LEGAL_KEY_PATTERN = Pattern.compile(STRING_KEY_PATTERN);
|
||||
private static final String CLEAN = "CLEAN";
|
||||
private static final String DIRTY = "DIRTY";
|
||||
private static final String REMOVE = "REMOVE";
|
||||
private static final String READ = "READ";
|
||||
|
||||
/*
|
||||
* This cache uses a journal file named "journal". A typical journal file
|
||||
* looks like this:
|
||||
* libcore.io.DiskLruCache
|
||||
* 1
|
||||
* 100
|
||||
* 2
|
||||
*
|
||||
* CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
|
||||
* DIRTY 335c4c6028171cfddfbaae1a9c313c52
|
||||
* CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
|
||||
* REMOVE 335c4c6028171cfddfbaae1a9c313c52
|
||||
* DIRTY 1ab96a171faeeee38496d8b330771a7a
|
||||
* CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
|
||||
* READ 335c4c6028171cfddfbaae1a9c313c52
|
||||
* READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
|
||||
*
|
||||
* The first five lines of the journal form its header. They are the
|
||||
* constant string "libcore.io.DiskLruCache", the disk cache's version,
|
||||
* the application's version, the value count, and a blank line.
|
||||
*
|
||||
* Each of the subsequent lines in the file is a record of the state of a
|
||||
* cache entry. Each line contains space-separated values: a state, a key,
|
||||
* and optional state-specific values.
|
||||
* o DIRTY lines track that an entry is actively being created or updated.
|
||||
* Every successful DIRTY action should be followed by a CLEAN or REMOVE
|
||||
* action. DIRTY lines without a matching CLEAN or REMOVE indicate that
|
||||
* temporary files may need to be deleted.
|
||||
* o CLEAN lines track a cache entry that has been successfully published
|
||||
* and may be read. A publish line is followed by the lengths of each of
|
||||
* its values.
|
||||
* o READ lines track accesses for LRU.
|
||||
* o REMOVE lines track entries that have been deleted.
|
||||
*
|
||||
* The journal file is appended to as cache operations occur. The journal may
|
||||
* occasionally be compacted by dropping redundant lines. A temporary file named
|
||||
* "journal.tmp" will be used during compaction; that file should be deleted if
|
||||
* it exists when the cache is opened.
|
||||
*/
|
||||
private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() {
|
||||
@Override
|
||||
public void write(int b) {
|
||||
// Eat all writes silently. Nom nom.
|
||||
}
|
||||
};
|
||||
/**
|
||||
* This cache uses a single background thread to evict entries.
|
||||
*/
|
||||
final ThreadPoolExecutor executorService =
|
||||
new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
|
||||
private final File directory;
|
||||
private final File journalFile;
|
||||
private final File journalFileTmp;
|
||||
private final File journalFileBackup;
|
||||
private final int appVersion;
|
||||
private final int valueCount;
|
||||
private final LinkedHashMap<String, Entry> lruEntries =
|
||||
new LinkedHashMap<>(0, 0.75f, true);
|
||||
private long maxSize;
|
||||
private long size = 0;
|
||||
private Writer journalWriter;
|
||||
private int redundantOpCount;
|
||||
/**
|
||||
* To differentiate between old and current snapshots, each entry is given
|
||||
* a sequence number each time an edit is committed. A snapshot is stale if
|
||||
* its sequence number is not equal to its entry's sequence number.
|
||||
*/
|
||||
private long nextSequenceNumber = 0;
|
||||
private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
|
||||
this.directory = directory;
|
||||
this.appVersion = appVersion;
|
||||
this.journalFile = new File(directory, JOURNAL_FILE);
|
||||
this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
|
||||
this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
|
||||
this.valueCount = valueCount;
|
||||
this.maxSize = maxSize;
|
||||
} private final Callable<Void> cleanupCallable = new Callable<>() {
|
||||
public Void call() throws Exception {
|
||||
synchronized (DiskLruCache.this) {
|
||||
if (journalWriter == null) {
|
||||
return null; // Closed.
|
||||
}
|
||||
trimToSize();
|
||||
if (journalRebuildRequired()) {
|
||||
rebuildJournal();
|
||||
redundantOpCount = 0;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens the cache in {@code directory}, creating a cache if none exists
|
||||
* there.
|
||||
*
|
||||
* @param directory a writable directory
|
||||
* @param valueCount the number of values per cache entry. Must be positive.
|
||||
* @param maxSize the maximum number of bytes this cache should use to store
|
||||
* @throws IOException if reading or writing the cache directory fails
|
||||
*/
|
||||
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
|
||||
throws IOException {
|
||||
if (maxSize <= 0) {
|
||||
throw new IllegalArgumentException("maxSize <= 0");
|
||||
}
|
||||
if (valueCount <= 0) {
|
||||
throw new IllegalArgumentException("valueCount <= 0");
|
||||
}
|
||||
|
||||
// If a bkp file exists, use it instead.
|
||||
File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
|
||||
if (backupFile.exists()) {
|
||||
File journalFile = new File(directory, JOURNAL_FILE);
|
||||
// If journal file also exists just delete backup file.
|
||||
if (journalFile.exists()) {
|
||||
backupFile.delete();
|
||||
} else {
|
||||
renameTo(backupFile, journalFile, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer to pick up where we left off.
|
||||
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
|
||||
if (cache.journalFile.exists()) {
|
||||
try {
|
||||
cache.readJournal();
|
||||
cache.processJournal();
|
||||
return cache;
|
||||
} catch (IOException journalIsCorrupt) {
|
||||
System.out
|
||||
.println("DiskLruCache "
|
||||
+ directory
|
||||
+ " is corrupt: "
|
||||
+ journalIsCorrupt.getMessage()
|
||||
+ ", removing");
|
||||
cache.delete();
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new empty cache.
|
||||
directory.mkdirs();
|
||||
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
|
||||
cache.rebuildJournal();
|
||||
return cache;
|
||||
}
|
||||
|
||||
private static void deleteIfExists(File file) throws IOException {
|
||||
if (file.exists() && !file.delete()) {
|
||||
throw new IOException();
|
||||
}
|
||||
}
|
||||
|
||||
private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {
|
||||
if (deleteDestination) {
|
||||
deleteIfExists(to);
|
||||
}
|
||||
if (!from.renameTo(to)) {
|
||||
throw new IOException();
|
||||
}
|
||||
}
|
||||
|
||||
private static String inputStreamToString(InputStream in) throws IOException {
|
||||
return Util.readFully(new InputStreamReader(in, Util.UTF_8));
|
||||
}
|
||||
|
||||
private void readJournal() throws IOException {
|
||||
StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
|
||||
try {
|
||||
String magic = reader.readLine();
|
||||
String version = reader.readLine();
|
||||
String appVersionString = reader.readLine();
|
||||
String valueCountString = reader.readLine();
|
||||
String blank = reader.readLine();
|
||||
if (!MAGIC.equals(magic)
|
||||
|| !VERSION_1.equals(version)
|
||||
|| !Integer.toString(appVersion).equals(appVersionString)
|
||||
|| !Integer.toString(valueCount).equals(valueCountString)
|
||||
|| !"".equals(blank)) {
|
||||
throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
|
||||
+ valueCountString + ", " + blank + "]");
|
||||
}
|
||||
|
||||
int lineCount = 0;
|
||||
while (true) {
|
||||
try {
|
||||
readJournalLine(reader.readLine());
|
||||
lineCount++;
|
||||
} catch (EOFException endOfJournal) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
redundantOpCount = lineCount - lruEntries.size();
|
||||
|
||||
// If we ended on a truncated line, rebuild the journal before appending to it.
|
||||
if (reader.hasUnterminatedLine()) {
|
||||
rebuildJournal();
|
||||
} else {
|
||||
journalWriter = new BufferedWriter(new OutputStreamWriter(
|
||||
new FileOutputStream(journalFile, true), Util.US_ASCII));
|
||||
}
|
||||
} finally {
|
||||
Util.closeQuietly(reader);
|
||||
}
|
||||
}
|
||||
|
||||
private void readJournalLine(String line) throws IOException {
|
||||
int firstSpace = line.indexOf(' ');
|
||||
if (firstSpace == -1) {
|
||||
throw new IOException("unexpected journal line: " + line);
|
||||
}
|
||||
|
||||
int keyBegin = firstSpace + 1;
|
||||
int secondSpace = line.indexOf(' ', keyBegin);
|
||||
final String key;
|
||||
if (secondSpace == -1) {
|
||||
key = line.substring(keyBegin);
|
||||
if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
|
||||
lruEntries.remove(key);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
key = line.substring(keyBegin, secondSpace);
|
||||
}
|
||||
|
||||
Entry entry = lruEntries.get(key);
|
||||
if (entry == null) {
|
||||
entry = new Entry(key);
|
||||
lruEntries.put(key, entry);
|
||||
}
|
||||
|
||||
if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
|
||||
String[] parts = line.substring(secondSpace + 1).split(" ");
|
||||
entry.readable = true;
|
||||
entry.currentEditor = null;
|
||||
entry.setLengths(parts);
|
||||
} else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
|
||||
entry.currentEditor = new Editor(entry);
|
||||
} else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
|
||||
// This work was already done by calling lruEntries.get().
|
||||
} else {
|
||||
throw new IOException("unexpected journal line: " + line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the initial size and collects garbage as a part of opening the
|
||||
* cache. Dirty entries are assumed to be inconsistent and will be deleted.
|
||||
*/
|
||||
private void processJournal() throws IOException {
|
||||
deleteIfExists(journalFileTmp);
|
||||
for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
|
||||
Entry entry = i.next();
|
||||
if (entry.currentEditor == null) {
|
||||
for (int t = 0; t < valueCount; t++) {
|
||||
size += entry.lengths[t];
|
||||
}
|
||||
} else {
|
||||
entry.currentEditor = null;
|
||||
for (int t = 0; t < valueCount; t++) {
|
||||
deleteIfExists(entry.getCleanFile(t));
|
||||
deleteIfExists(entry.getDirtyFile(t));
|
||||
}
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new journal that omits redundant information. This replaces the
|
||||
* current journal if it exists.
|
||||
*/
|
||||
private synchronized void rebuildJournal() throws IOException {
|
||||
if (journalWriter != null) {
|
||||
journalWriter.close();
|
||||
}
|
||||
|
||||
try (Writer writer = new BufferedWriter(
|
||||
new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII))) {
|
||||
writer.write(MAGIC);
|
||||
writer.write("\n");
|
||||
writer.write(VERSION_1);
|
||||
writer.write("\n");
|
||||
writer.write(Integer.toString(appVersion));
|
||||
writer.write("\n");
|
||||
writer.write(Integer.toString(valueCount));
|
||||
writer.write("\n");
|
||||
writer.write("\n");
|
||||
|
||||
for (Entry entry : lruEntries.values()) {
|
||||
if (entry.currentEditor != null) {
|
||||
writer.write(DIRTY + ' ' + entry.key + '\n');
|
||||
} else {
|
||||
writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (journalFile.exists()) {
|
||||
renameTo(journalFile, journalFileBackup, true);
|
||||
}
|
||||
renameTo(journalFileTmp, journalFile, false);
|
||||
journalFileBackup.delete();
|
||||
|
||||
journalWriter = new BufferedWriter(
|
||||
new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@code true} if it contains the key
|
||||
*/
|
||||
public synchronized boolean contain(String key) {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
Entry entry = lruEntries.get(key);
|
||||
return entry != null && entry.readable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a snapshot of the entry named {@code key}, or null if it doesn't
|
||||
* exist is not currently readable. If a value is returned, it is moved to
|
||||
* the head of the LRU queue.
|
||||
*/
|
||||
public synchronized Snapshot get(String key) throws IOException {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
Entry entry = lruEntries.get(key);
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!entry.readable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Open all streams eagerly to guarantee that we see a single published
|
||||
// snapshot. If we opened streams lazily then the streams could come
|
||||
// from different edits.
|
||||
InputStream[] ins = new InputStream[valueCount];
|
||||
try {
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
ins[i] = new FileInputStream(entry.getCleanFile(i));
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
// A file must have been deleted manually!
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
if (ins[i] != null) {
|
||||
Util.closeQuietly(ins[i]);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
redundantOpCount++;
|
||||
journalWriter.append(READ + ' ').append(key).append(String.valueOf('\n'));
|
||||
if (journalRebuildRequired()) {
|
||||
executorService.submit(cleanupCallable);
|
||||
}
|
||||
|
||||
return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an editor for the entry named {@code key}, or null if another
|
||||
* edit is in progress.
|
||||
*/
|
||||
public Editor edit(String key) throws IOException {
|
||||
return edit(key, ANY_SEQUENCE_NUMBER);
|
||||
}
|
||||
|
||||
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
Entry entry = lruEntries.get(key);
|
||||
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
|
||||
|| entry.sequenceNumber != expectedSequenceNumber)) {
|
||||
return null; // Snapshot is stale.
|
||||
}
|
||||
if (entry == null) {
|
||||
entry = new Entry(key);
|
||||
lruEntries.put(key, entry);
|
||||
} else if (entry.currentEditor != null) {
|
||||
return null; // Another edit is in progress.
|
||||
}
|
||||
|
||||
Editor editor = new Editor(entry);
|
||||
entry.currentEditor = editor;
|
||||
|
||||
// Flush the journal before creating files to prevent file leaks.
|
||||
journalWriter.write(DIRTY + ' ' + key + '\n');
|
||||
journalWriter.flush();
|
||||
return editor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the directory where this cache stores its data.
|
||||
*/
|
||||
public File getDirectory() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum number of bytes that this cache should use to store
|
||||
* its data.
|
||||
*/
|
||||
public synchronized long getMaxSize() {
|
||||
return maxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the maximum number of bytes the cache can store and queues a job
|
||||
* to trim the existing store, if necessary.
|
||||
*/
|
||||
public synchronized void setMaxSize(long maxSize) {
|
||||
this.maxSize = maxSize;
|
||||
executorService.submit(cleanupCallable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of bytes currently being used to store the values in
|
||||
* this cache. This may be greater than the max size if a background
|
||||
* deletion is pending.
|
||||
*/
|
||||
public synchronized long size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
|
||||
Entry entry = editor.entry;
|
||||
if (entry.currentEditor != editor) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
// If this edit is creating the entry for the first time, every index must have a value.
|
||||
if (success && !entry.readable) {
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
if (!editor.written[i]) {
|
||||
editor.abort();
|
||||
throw new IllegalStateException("Newly created entry didn't create value for index " + i);
|
||||
}
|
||||
if (!entry.getDirtyFile(i).exists()) {
|
||||
editor.abort();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
File dirty = entry.getDirtyFile(i);
|
||||
if (success) {
|
||||
if (dirty.exists()) {
|
||||
File clean = entry.getCleanFile(i);
|
||||
dirty.renameTo(clean);
|
||||
long oldLength = entry.lengths[i];
|
||||
long newLength = clean.length();
|
||||
entry.lengths[i] = newLength;
|
||||
size = size - oldLength + newLength;
|
||||
}
|
||||
} else {
|
||||
deleteIfExists(dirty);
|
||||
}
|
||||
}
|
||||
|
||||
redundantOpCount++;
|
||||
entry.currentEditor = null;
|
||||
if (entry.readable | success) {
|
||||
entry.readable = true;
|
||||
journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
|
||||
if (success) {
|
||||
entry.sequenceNumber = nextSequenceNumber++;
|
||||
}
|
||||
} else {
|
||||
lruEntries.remove(entry.key);
|
||||
journalWriter.write(REMOVE + ' ' + entry.key + '\n');
|
||||
}
|
||||
journalWriter.flush();
|
||||
|
||||
if (size > maxSize || journalRebuildRequired()) {
|
||||
executorService.submit(cleanupCallable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We only rebuild the journal when it will halve the size of the journal
|
||||
* and eliminate at least 2000 ops.
|
||||
*/
|
||||
private boolean journalRebuildRequired() {
|
||||
final int redundantOpCompactThreshold = 2000;
|
||||
return redundantOpCount >= redundantOpCompactThreshold //
|
||||
&& redundantOpCount >= lruEntries.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops the entry for {@code key} if it exists and can be removed. Entries
|
||||
* actively being edited cannot be removed.
|
||||
*
|
||||
* @return true if an entry was removed.
|
||||
*/
|
||||
public synchronized boolean remove(String key) throws IOException {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
Entry entry = lruEntries.get(key);
|
||||
if (entry == null || entry.currentEditor != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
File file = entry.getCleanFile(i);
|
||||
if (file.exists() && !file.delete()) {
|
||||
throw new IOException("failed to delete " + file);
|
||||
}
|
||||
size -= entry.lengths[i];
|
||||
entry.lengths[i] = 0;
|
||||
}
|
||||
|
||||
redundantOpCount++;
|
||||
journalWriter.append(REMOVE + ' ').append(key).append(String.valueOf('\n'));
|
||||
lruEntries.remove(key);
|
||||
|
||||
if (journalRebuildRequired()) {
|
||||
executorService.submit(cleanupCallable);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this cache has been closed.
|
||||
*/
|
||||
public synchronized boolean isClosed() {
|
||||
return journalWriter == null;
|
||||
}
|
||||
|
||||
private void checkNotClosed() {
|
||||
if (journalWriter == null) {
|
||||
throw new IllegalStateException("cache is closed");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force buffered operations to the filesystem.
|
||||
*/
|
||||
public synchronized void flush() throws IOException {
|
||||
checkNotClosed();
|
||||
trimToSize();
|
||||
journalWriter.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this cache. Stored values will remain on the filesystem.
|
||||
*/
|
||||
public synchronized void close() throws IOException {
|
||||
if (journalWriter == null) {
|
||||
return; // Already closed.
|
||||
}
|
||||
for (Entry entry : new ArrayList<>(lruEntries.values())) {
|
||||
if (entry.currentEditor != null) {
|
||||
entry.currentEditor.abort();
|
||||
}
|
||||
}
|
||||
trimToSize();
|
||||
journalWriter.close();
|
||||
journalWriter = null;
|
||||
}
|
||||
|
||||
private void trimToSize() throws IOException {
|
||||
while (size > maxSize) {
|
||||
Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
|
||||
remove(toEvict.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the cache and deletes all of its stored values. This will delete
|
||||
* all files in the cache directory including files that weren't created by
|
||||
* the cache.
|
||||
*/
|
||||
public void delete() throws IOException {
|
||||
close();
|
||||
Util.deleteContents(directory);
|
||||
}
|
||||
|
||||
private void validateKey(String key) {
|
||||
Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
|
||||
if (!matcher.matches()) {
|
||||
throw new IllegalArgumentException("keys must match regex "
|
||||
+ STRING_KEY_PATTERN + ": \"" + key + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A snapshot of the values for an entry.
|
||||
*/
|
||||
public final class Snapshot implements Closeable {
|
||||
private final String key;
|
||||
private final long sequenceNumber;
|
||||
private final InputStream[] ins;
|
||||
private final long[] lengths;
|
||||
|
||||
private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) {
|
||||
this.key = key;
|
||||
this.sequenceNumber = sequenceNumber;
|
||||
this.ins = ins;
|
||||
this.lengths = lengths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an editor for this snapshot's entry, or null if either the
|
||||
* entry has changed since this snapshot was created or if another edit
|
||||
* is in progress.
|
||||
*/
|
||||
public Editor edit() throws IOException {
|
||||
return DiskLruCache.this.edit(key, sequenceNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unbuffered stream with the value for {@code index}.
|
||||
*/
|
||||
public InputStream getInputStream(int index) {
|
||||
return ins[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string value for {@code index}.
|
||||
*/
|
||||
public String getString(int index) throws IOException {
|
||||
return inputStreamToString(getInputStream(index));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the byte length of the value for {@code index}.
|
||||
*/
|
||||
public long getLength(int index) {
|
||||
return lengths[index];
|
||||
}
|
||||
|
||||
public void close() {
|
||||
for (InputStream in : ins) {
|
||||
Util.closeQuietly(in);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the values for an entry.
|
||||
*/
|
||||
public final class Editor {
|
||||
private final Entry entry;
|
||||
private final boolean[] written;
|
||||
private boolean hasErrors;
|
||||
private boolean committed;
|
||||
|
||||
private Editor(Entry entry) {
|
||||
this.entry = entry;
|
||||
this.written = (entry.readable) ? null : new boolean[valueCount];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an unbuffered input stream to read the last committed value,
|
||||
* or null if no value has been committed.
|
||||
*/
|
||||
public InputStream newInputStream(int index) {
|
||||
synchronized (DiskLruCache.this) {
|
||||
if (entry.currentEditor != this) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
if (!entry.readable) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new FileInputStream(entry.getCleanFile(index));
|
||||
} catch (FileNotFoundException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last committed value as a string, or null if no value
|
||||
* has been committed.
|
||||
*/
|
||||
public String getString(int index) throws IOException {
|
||||
InputStream in = newInputStream(index);
|
||||
return in != null ? inputStreamToString(in) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new unbuffered output stream to write the value at
|
||||
* {@code index}. If the underlying output stream encounters errors
|
||||
* when writing to the filesystem, this edit will be aborted when
|
||||
* {@link #commit} is called. The returned output stream does not throw
|
||||
* IOExceptions.
|
||||
*/
|
||||
public OutputStream newOutputStream(int index) throws IOException {
|
||||
if (index < 0 || index >= valueCount) {
|
||||
throw new IllegalArgumentException("Expected index " + index + " to "
|
||||
+ "be greater than 0 and less than the maximum value count "
|
||||
+ "of " + valueCount);
|
||||
}
|
||||
synchronized (DiskLruCache.this) {
|
||||
if (entry.currentEditor != this) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
if (!entry.readable) {
|
||||
written[index] = true;
|
||||
}
|
||||
File dirtyFile = entry.getDirtyFile(index);
|
||||
FileOutputStream outputStream;
|
||||
try {
|
||||
outputStream = new FileOutputStream(dirtyFile);
|
||||
} catch (FileNotFoundException e) {
|
||||
// Attempt to recreate the cache directory.
|
||||
directory.mkdirs();
|
||||
try {
|
||||
outputStream = new FileOutputStream(dirtyFile);
|
||||
} catch (FileNotFoundException e2) {
|
||||
// We are unable to recover. Silently eat the writes.
|
||||
return NULL_OUTPUT_STREAM;
|
||||
}
|
||||
}
|
||||
return new FaultHidingOutputStream(outputStream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value at {@code index} to {@code value}.
|
||||
*/
|
||||
public void set(int index, String value) throws IOException {
|
||||
Writer writer = null;
|
||||
try {
|
||||
writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8);
|
||||
writer.write(value);
|
||||
} finally {
|
||||
Util.closeQuietly(writer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits this edit so it is visible to readers. This releases the
|
||||
* edit lock so another edit may be started on the same key.
|
||||
*/
|
||||
public void commit() throws IOException {
|
||||
if (hasErrors) {
|
||||
completeEdit(this, false);
|
||||
remove(entry.key); // The previous entry is stale.
|
||||
} else {
|
||||
completeEdit(this, true);
|
||||
}
|
||||
committed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aborts this edit. This releases the edit lock so another edit may be
|
||||
* started on the same key.
|
||||
*/
|
||||
public void abort() throws IOException {
|
||||
completeEdit(this, false);
|
||||
}
|
||||
|
||||
public void abortUnlessCommitted() {
|
||||
if (!committed) {
|
||||
try {
|
||||
abort();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class FaultHidingOutputStream extends FilterOutputStream {
|
||||
private FaultHidingOutputStream(OutputStream out) {
|
||||
super(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int oneByte) {
|
||||
try {
|
||||
out.write(oneByte);
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer, int offset, int length) {
|
||||
try {
|
||||
out.write(buffer, offset, length);
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
out.close();
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
try {
|
||||
out.flush();
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class Entry {
|
||||
private final String key;
|
||||
|
||||
/**
|
||||
* Lengths of this entry's files.
|
||||
*/
|
||||
private final long[] lengths;
|
||||
|
||||
/**
|
||||
* True if this entry has ever been published.
|
||||
*/
|
||||
private boolean readable;
|
||||
|
||||
/**
|
||||
* The ongoing edit or null if this entry is not being edited.
|
||||
*/
|
||||
private Editor currentEditor;
|
||||
|
||||
/**
|
||||
* The sequence number of the most recently committed edit to this entry.
|
||||
*/
|
||||
private long sequenceNumber;
|
||||
|
||||
private Entry(String key) {
|
||||
this.key = key;
|
||||
this.lengths = new long[valueCount];
|
||||
}
|
||||
|
||||
public String getLengths() {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (long size : lengths) {
|
||||
result.append(' ').append(size);
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set lengths using decimal numbers like "10123".
|
||||
*/
|
||||
private void setLengths(String[] strings) throws IOException {
|
||||
if (strings.length != valueCount) {
|
||||
throw invalidLengths(strings);
|
||||
}
|
||||
|
||||
try {
|
||||
for (int i = 0; i < strings.length; i++) {
|
||||
lengths[i] = Long.parseLong(strings[i]);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
throw invalidLengths(strings);
|
||||
}
|
||||
}
|
||||
|
||||
private IOException invalidLengths(String[] strings) throws IOException {
|
||||
throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));
|
||||
}
|
||||
|
||||
public File getCleanFile(int i) {
|
||||
return new File(directory, key + "." + i);
|
||||
}
|
||||
|
||||
public File getDirtyFile(int i) {
|
||||
return new File(directory, key + "." + i + ".tmp");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,522 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015-2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.beerbelly;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.streampipe.InputStreamPipe;
|
||||
import com.hippo.streampipe.OutputStreamPipe;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
public class SimpleDiskCache {
|
||||
|
||||
private static final String TAG = SimpleDiskCache.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Disk cache is free
|
||||
*/
|
||||
private static final int STATE_DISK_CACHE_NONE = 0;
|
||||
|
||||
/**
|
||||
* Disk cache is flushing or clearing
|
||||
*/
|
||||
private static final int STATE_DISK_CACHE_IN_USE = 1;
|
||||
|
||||
/**
|
||||
* Disk cache is reading or writing
|
||||
*/
|
||||
private static final int STATE_DISK_CACHE_BUSY = 2;
|
||||
|
||||
private final File mCacheDir;
|
||||
private final int mSize;
|
||||
@NonNull
|
||||
private final Map<String, CounterLock> mLockMap = new HashMap<>();
|
||||
@NonNull
|
||||
private final LockPool mLockPool = new LockPool();
|
||||
private int mDiskCacheState;
|
||||
@Nullable
|
||||
private DiskLruCache mDiskLruCache;
|
||||
|
||||
public SimpleDiskCache(File cacheDir, int size) {
|
||||
mCacheDir = cacheDir;
|
||||
mSize = size;
|
||||
|
||||
try {
|
||||
init();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A hashing method that changes a string (like a URL) into a hash suitable
|
||||
* for using as a disk filename.
|
||||
*
|
||||
* @param key The key used to store the file
|
||||
*/
|
||||
private static String hashKeyForDisk(final String key) {
|
||||
String cacheKey;
|
||||
try {
|
||||
final MessageDigest digest = MessageDigest.getInstance("MD5");
|
||||
digest.update(key.getBytes());
|
||||
cacheKey = bytesToHexString(digest.digest());
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
cacheKey = String.valueOf(key.hashCode());
|
||||
}
|
||||
return cacheKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* http://stackoverflow.com/questions/332079
|
||||
*
|
||||
* @param bytes The bytes to convert.
|
||||
* @return A {@link String} converted from the bytes of a hashable key used
|
||||
* to store a filename on the disk, to hex digits.
|
||||
*/
|
||||
private static String bytesToHexString(final byte[] bytes) {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
for (final byte b : bytes) {
|
||||
final String hex = Integer.toHexString(0xFF & b);
|
||||
if (hex.length() == 1) {
|
||||
builder.append('0');
|
||||
}
|
||||
builder.append(hex);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public synchronized boolean isValid() {
|
||||
return mDiskLruCache != null;
|
||||
}
|
||||
|
||||
private synchronized void init() throws IOException {
|
||||
if (!isValid()) {
|
||||
mDiskLruCache = DiskLruCache.open(mCacheDir, 1, 1, mSize);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized long size() {
|
||||
if (null != mDiskLruCache) {
|
||||
return mDiskLruCache.size();
|
||||
} else {
|
||||
return -1L;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void flush() {
|
||||
// Wait for cache available
|
||||
while (mDiskCacheState != STATE_DISK_CACHE_NONE) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
mDiskCacheState = STATE_DISK_CACHE_BUSY;
|
||||
|
||||
if (null != mDiskLruCache) {
|
||||
try {
|
||||
mDiskLruCache.flush();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "SimpleDiskCache flush", e);
|
||||
}
|
||||
}
|
||||
|
||||
mDiskCacheState = STATE_DISK_CACHE_NONE;
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
public synchronized boolean clear() {
|
||||
boolean result = true;
|
||||
if (null == mDiskLruCache) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wait for cache available
|
||||
while (mDiskCacheState != STATE_DISK_CACHE_NONE) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
mDiskCacheState = STATE_DISK_CACHE_BUSY;
|
||||
|
||||
try {
|
||||
mDiskLruCache.delete();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "SimpleDiskCache clearCache", e);
|
||||
}
|
||||
mDiskLruCache = null;
|
||||
try {
|
||||
init();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "SimpleDiskCache init", e);
|
||||
result = false;
|
||||
}
|
||||
|
||||
mDiskCacheState = STATE_DISK_CACHE_NONE;
|
||||
notifyAll();
|
||||
return result;
|
||||
}
|
||||
|
||||
private synchronized CounterLock obtainLock(String key) {
|
||||
// Wait for clear over
|
||||
while (mDiskCacheState == STATE_DISK_CACHE_BUSY) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
// Empty
|
||||
}
|
||||
}
|
||||
mDiskCacheState = STATE_DISK_CACHE_IN_USE;
|
||||
|
||||
// Get lock from key
|
||||
CounterLock lock;
|
||||
lock = mLockMap.get(key);
|
||||
if (lock == null) {
|
||||
lock = mLockPool.get();
|
||||
mLockMap.put(key, lock);
|
||||
}
|
||||
lock.obtain();
|
||||
|
||||
return lock;
|
||||
}
|
||||
|
||||
private synchronized void releaseLock(String key, CounterLock lock) {
|
||||
lock.release();
|
||||
if (lock.isFree()) {
|
||||
mLockMap.remove(key);
|
||||
mLockPool.push(lock);
|
||||
}
|
||||
|
||||
if (mLockMap.isEmpty()) {
|
||||
mDiskCacheState = STATE_DISK_CACHE_NONE;
|
||||
notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean contain(@NonNull String key) {
|
||||
String diskKey = hashKeyForDisk(key);
|
||||
CounterLock lock = obtainLock(diskKey);
|
||||
if (null == mDiskLruCache) {
|
||||
releaseLock(diskKey, lock);
|
||||
return false;
|
||||
}
|
||||
lock.readLock().lock();
|
||||
|
||||
boolean result = mDiskLruCache.contain(diskKey);
|
||||
|
||||
lock.readLock().unlock();
|
||||
releaseLock(diskKey, lock);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean remove(@NonNull String key) {
|
||||
String diskKey = hashKeyForDisk(key);
|
||||
CounterLock lock = obtainLock(diskKey);
|
||||
if (null == mDiskLruCache) {
|
||||
releaseLock(diskKey, lock);
|
||||
return false;
|
||||
}
|
||||
lock.writeLock().lock();
|
||||
|
||||
boolean result;
|
||||
try {
|
||||
result = mDiskLruCache.remove(diskKey);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
result = false;
|
||||
}
|
||||
|
||||
lock.writeLock().unlock();
|
||||
releaseLock(diskKey, lock);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean put(@NonNull String key, @NonNull InputStream is) {
|
||||
String diskKey = hashKeyForDisk(key);
|
||||
CounterLock lock = obtainLock(diskKey);
|
||||
if (null == mDiskLruCache) {
|
||||
releaseLock(diskKey, lock);
|
||||
return false;
|
||||
}
|
||||
lock.writeLock().lock();
|
||||
|
||||
boolean result = false;
|
||||
if (isValid()) {
|
||||
result = putToDisk(diskKey, is);
|
||||
}
|
||||
|
||||
lock.writeLock().unlock();
|
||||
releaseLock(diskKey, lock);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean putToDisk(String key, InputStream is) {
|
||||
DiskLruCache.Editor editor = null;
|
||||
OutputStream os = null;
|
||||
boolean completeEdit = false;
|
||||
try {
|
||||
//noinspection ConstantConditions
|
||||
editor = mDiskLruCache.edit(key);
|
||||
if (editor == null) {
|
||||
// The editor is in progress
|
||||
return false;
|
||||
}
|
||||
|
||||
os = editor.newOutputStream(0);
|
||||
if (os != null) {
|
||||
Util.copy(is, os);
|
||||
completeEdit = true;
|
||||
editor.commit();
|
||||
return true;
|
||||
} else {
|
||||
// Can't get OutputStream
|
||||
completeEdit = true;
|
||||
editor.abort();
|
||||
return false;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Util.closeQuietly(os);
|
||||
try {
|
||||
if (!completeEdit && editor != null) {
|
||||
editor.abort();
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param key the key of the target
|
||||
* @return the InputStreamPipe, <code>null</code> for missing
|
||||
*/
|
||||
@Nullable
|
||||
public InputStreamPipe getInputStreamPipe(@NonNull String key) {
|
||||
if (contain(key)) {
|
||||
String diskKey = hashKeyForDisk(key);
|
||||
return new CacheInputStreamPipe(diskKey);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public OutputStreamPipe getOutputStreamPipe(@NonNull String key) {
|
||||
String diskKey = hashKeyForDisk(key);
|
||||
return new CacheOutputStreamPipe(diskKey);
|
||||
}
|
||||
|
||||
private class CacheInputStreamPipe implements InputStreamPipe {
|
||||
|
||||
private final String mKey;
|
||||
private CounterLock mLock;
|
||||
private DiskLruCache.Snapshot mCurrentSnapshot;
|
||||
|
||||
private CacheInputStreamPipe(String key) {
|
||||
mKey = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void obtain() {
|
||||
if (mLock == null) {
|
||||
mLock = obtainLock(mKey);
|
||||
mLock.readLock().lock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (mCurrentSnapshot != null) {
|
||||
throw new IllegalStateException("Please close it first");
|
||||
}
|
||||
|
||||
if (mLock != null) {
|
||||
mLock.readLock().unlock();
|
||||
releaseLock(mKey, mLock);
|
||||
mLock = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
InputStream open() throws IOException {
|
||||
if (mLock == null) {
|
||||
throw new IllegalStateException("Please obtain it first");
|
||||
}
|
||||
if (mCurrentSnapshot != null) {
|
||||
throw new IllegalStateException("Please close it before reopen");
|
||||
}
|
||||
if (null == mDiskLruCache) {
|
||||
throw new IOException("Can't find disk lru cache");
|
||||
}
|
||||
|
||||
DiskLruCache.Snapshot snapshot;
|
||||
snapshot = mDiskLruCache.get(mKey);
|
||||
if (snapshot == null) {
|
||||
throw new IOException("Miss the key " + mKey);
|
||||
}
|
||||
mCurrentSnapshot = snapshot;
|
||||
return snapshot.getInputStream(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
Util.closeQuietly(mCurrentSnapshot);
|
||||
mCurrentSnapshot = null;
|
||||
}
|
||||
}
|
||||
|
||||
private class CacheOutputStreamPipe implements OutputStreamPipe {
|
||||
|
||||
private final String mKey;
|
||||
private CounterLock mLock;
|
||||
private DiskLruCache.Editor mCurrentEditor;
|
||||
|
||||
private CacheOutputStreamPipe(String key) {
|
||||
mKey = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void obtain() {
|
||||
if (mLock == null) {
|
||||
mLock = obtainLock(mKey);
|
||||
mLock.writeLock().lock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (mCurrentEditor != null) {
|
||||
throw new IllegalStateException("Please close it first");
|
||||
}
|
||||
|
||||
if (mLock != null) {
|
||||
mLock.writeLock().unlock();
|
||||
releaseLock(mKey, mLock);
|
||||
mLock = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
OutputStream open() throws IOException {
|
||||
if (mLock == null) {
|
||||
throw new IllegalStateException("Please obtain it first");
|
||||
}
|
||||
if (mCurrentEditor != null) {
|
||||
throw new IllegalStateException("Please close it before reopen");
|
||||
}
|
||||
if (null == mDiskLruCache) {
|
||||
throw new IOException("Can't find disk lru cache");
|
||||
}
|
||||
|
||||
DiskLruCache.Editor editor = mDiskLruCache.edit(mKey);
|
||||
if (editor == null) {
|
||||
throw new IOException("Miss the key " + mKey);
|
||||
}
|
||||
|
||||
OutputStream os;
|
||||
try {
|
||||
os = editor.newOutputStream(0);
|
||||
mCurrentEditor = editor;
|
||||
return os;
|
||||
} catch (IOException e) {
|
||||
// Get trouble
|
||||
try {
|
||||
editor.abort();
|
||||
} catch (IOException ex) {
|
||||
// Ignore
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (mCurrentEditor != null) {
|
||||
try {
|
||||
mCurrentEditor.commit();
|
||||
} catch (IOException e) {
|
||||
// Empty
|
||||
}
|
||||
mCurrentEditor = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class CounterLock extends ReentrantReadWriteLock {
|
||||
|
||||
private int mCount;
|
||||
|
||||
public boolean isFree() {
|
||||
return mCount == 0;
|
||||
}
|
||||
|
||||
public void release() {
|
||||
if (--mCount < 0) {
|
||||
throw new IllegalStateException("Release time is more than occupy time");
|
||||
}
|
||||
}
|
||||
|
||||
public void obtain() {
|
||||
++mCount;
|
||||
}
|
||||
}
|
||||
|
||||
private class LockPool {
|
||||
|
||||
private static final int MAX_SIZE = 10;
|
||||
|
||||
private final CounterLock[] mArray = new CounterLock[MAX_SIZE];
|
||||
private final int mMaxSize = MAX_SIZE;
|
||||
private int mSize;
|
||||
|
||||
public void push(CounterLock lock) {
|
||||
if (lock != null && mSize < mMaxSize) {
|
||||
mArray[mSize++] = lock;
|
||||
}
|
||||
}
|
||||
|
||||
public CounterLock get() {
|
||||
if (mSize > 0) {
|
||||
CounterLock lock = mArray[--mSize];
|
||||
mArray[mSize] = null;
|
||||
return lock;
|
||||
} else {
|
||||
return new CounterLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2012 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 com.hippo.beerbelly;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
/**
|
||||
* Buffers input from an {@link InputStream} for reading lines.
|
||||
*
|
||||
* <p>This class is used for buffered reading of lines. For purposes of this class, a line ends
|
||||
* with "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated
|
||||
* line at end of input is invalid and will be ignored, the caller may use {@code
|
||||
* hasUnterminatedLine()} to detect it after catching the {@code EOFException}.
|
||||
*
|
||||
* <p>This class is intended for reading input that strictly consists of lines, such as line-based
|
||||
* cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction
|
||||
* with {@link java.io.InputStreamReader} provides similar functionality, this class uses different
|
||||
* end-of-input reporting and a more restrictive definition of a line.
|
||||
*
|
||||
* <p>This class supports only charsets that encode '\r' and '\n' as a single byte with value 13
|
||||
* and 10, respectively, and the representation of no other character contains these values.
|
||||
* We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1.
|
||||
* The default charset is US_ASCII.
|
||||
*/
|
||||
class StrictLineReader implements Closeable {
|
||||
private static final byte CR = (byte) '\r';
|
||||
private static final byte LF = (byte) '\n';
|
||||
|
||||
private final InputStream in;
|
||||
private final Charset charset;
|
||||
|
||||
/*
|
||||
* Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end
|
||||
* and the data in the range [pos, end) is buffered for reading. At end of input, if there is
|
||||
* an unterminated line, we set end == -1, otherwise end == pos. If the underlying
|
||||
* {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.
|
||||
*/
|
||||
private byte[] buf;
|
||||
private int pos;
|
||||
private int end;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code LineReader} with the specified charset and the default capacity.
|
||||
*
|
||||
* @param in the {@code InputStream} to read data from.
|
||||
* @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
|
||||
* supported.
|
||||
* @throws NullPointerException if {@code in} or {@code charset} is null.
|
||||
* @throws IllegalArgumentException if the specified charset is not supported.
|
||||
*/
|
||||
public StrictLineReader(InputStream in, Charset charset) {
|
||||
this(in, 8192, charset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code LineReader} with the specified capacity and charset.
|
||||
*
|
||||
* @param in the {@code InputStream} to read data from.
|
||||
* @param capacity the capacity of the buffer.
|
||||
* @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
|
||||
* supported.
|
||||
* @throws NullPointerException if {@code in} or {@code charset} is null.
|
||||
* @throws IllegalArgumentException if {@code capacity} is negative or zero
|
||||
* or the specified charset is not supported.
|
||||
*/
|
||||
public StrictLineReader(InputStream in, int capacity, Charset charset) {
|
||||
if (in == null || charset == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
if (capacity < 0) {
|
||||
throw new IllegalArgumentException("capacity <= 0");
|
||||
}
|
||||
if (!(charset.equals(Util.US_ASCII))) {
|
||||
throw new IllegalArgumentException("Unsupported encoding");
|
||||
}
|
||||
|
||||
this.in = in;
|
||||
this.charset = charset;
|
||||
buf = new byte[capacity];
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the reader by closing the underlying {@code InputStream} and
|
||||
* marking this reader as closed.
|
||||
*
|
||||
* @throws IOException for errors when closing the underlying {@code InputStream}.
|
||||
*/
|
||||
public void close() throws IOException {
|
||||
synchronized (in) {
|
||||
if (buf != null) {
|
||||
buf = null;
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"},
|
||||
* this end of line marker is not included in the result.
|
||||
*
|
||||
* @return the next line from the input.
|
||||
* @throws IOException for underlying {@code InputStream} errors.
|
||||
* @throws EOFException for the end of source stream.
|
||||
*/
|
||||
public String readLine() throws IOException {
|
||||
synchronized (in) {
|
||||
if (buf == null) {
|
||||
throw new IOException("LineReader is closed");
|
||||
}
|
||||
|
||||
// Read more data if we are at the end of the buffered data.
|
||||
// Though it's an error to read after an exception, we will let {@code fillBuf()}
|
||||
// throw again if that happens; thus we need to handle end == -1 as well as end == pos.
|
||||
if (pos >= end) {
|
||||
fillBuf();
|
||||
}
|
||||
// Try to find LF in the buffered data and return the line if successful.
|
||||
for (int i = pos; i != end; ++i) {
|
||||
if (buf[i] == LF) {
|
||||
int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i;
|
||||
String res = new String(buf, pos, lineEnd - pos, charset.name());
|
||||
pos = i + 1;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
// Let's anticipate up to 80 characters on top of those already read.
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) {
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
|
||||
try {
|
||||
return new String(buf, 0, length, charset.name());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new AssertionError(e); // Since we control the charset this will never happen.
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
while (true) {
|
||||
out.write(buf, pos, end - pos);
|
||||
// Mark unterminated line in case fillBuf throws EOFException or IOException.
|
||||
end = -1;
|
||||
fillBuf();
|
||||
// Try to find LF in the buffered data and return the line if successful.
|
||||
for (int i = pos; i != end; ++i) {
|
||||
if (buf[i] == LF) {
|
||||
if (i != pos) {
|
||||
out.write(buf, pos, i - pos);
|
||||
}
|
||||
pos = i + 1;
|
||||
return out.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasUnterminatedLine() {
|
||||
return end == -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads new input data into the buffer. Call only with pos == end or end == -1,
|
||||
* depending on the desired outcome if the function throws.
|
||||
*/
|
||||
private void fillBuf() throws IOException {
|
||||
int result = in.read(buf, 0, buf.length);
|
||||
if (result == -1) {
|
||||
throw new EOFException();
|
||||
}
|
||||
pos = 0;
|
||||
end = result;
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2010 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 com.hippo.beerbelly;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Reader;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Junk drawer of utility methods.
|
||||
*/
|
||||
final class Util {
|
||||
static final Charset US_ASCII = StandardCharsets.US_ASCII;
|
||||
static final Charset UTF_8 = StandardCharsets.UTF_8;
|
||||
private static final int EOF = -1;
|
||||
private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
|
||||
|
||||
private Util() {
|
||||
}
|
||||
|
||||
static String readFully(Reader reader) throws IOException {
|
||||
try (reader) {
|
||||
StringWriter writer = new StringWriter();
|
||||
char[] buffer = new char[1024];
|
||||
int count;
|
||||
while ((count = reader.read(buffer)) != -1) {
|
||||
writer.write(buffer, 0, count);
|
||||
}
|
||||
return writer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the contents of {@code dir}. Throws an IOException if any file
|
||||
* could not be deleted, or if {@code dir} is not a readable directory.
|
||||
*/
|
||||
static void deleteContents(File dir) throws IOException {
|
||||
File[] files = dir.listFiles();
|
||||
if (files == null) {
|
||||
throw new IOException("not a readable directory: " + dir);
|
||||
}
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
deleteContents(file);
|
||||
}
|
||||
if (!file.delete()) {
|
||||
throw new IOException("failed to delete file: " + file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void closeQuietly(/*Auto*/Closeable closeable) {
|
||||
if (closeable != null) {
|
||||
try {
|
||||
closeable.close();
|
||||
} catch (RuntimeException rethrown) {
|
||||
throw rethrown;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy bytes from an <code>InputStream</code> to an
|
||||
* <code>OutputStream</code>.
|
||||
*
|
||||
* @param input the InputStream
|
||||
* @param output the OutputStream
|
||||
* @return the number of bytes copied
|
||||
*/
|
||||
public static long copy(InputStream input, OutputStream output)
|
||||
throws IOException {
|
||||
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
|
||||
long count = 0;
|
||||
int n;
|
||||
while (EOF != (n = input.read(buffer))) {
|
||||
output.write(buffer, 0, n);
|
||||
count += n;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.hippo.compat
|
||||
|
||||
import com.hippo.ehviewer.spider.SpiderInfo
|
||||
import com.hippo.yorozuya.IOUtils
|
||||
import java.io.InputStream
|
||||
|
||||
fun readLegacySpiderInfo(inputStream: InputStream): SpiderInfo {
|
||||
fun read(): String {
|
||||
return IOUtils.readAsciiLine(inputStream)
|
||||
}
|
||||
|
||||
fun readInt(): Int {
|
||||
return read().toInt()
|
||||
}
|
||||
|
||||
fun readLong(): Long {
|
||||
return read().toLong()
|
||||
}
|
||||
repeat(2) { read() } // We assert that only info v2
|
||||
val gid = readLong()
|
||||
val token = read()
|
||||
read()
|
||||
val previewPages = readInt()
|
||||
val previewPerPage = readInt()
|
||||
val pages = read().toInt()
|
||||
val pTokenMap = hashMapOf<Int, String>()
|
||||
val info = SpiderInfo(gid, pages, pTokenMap, 0, token, previewPages, previewPerPage)
|
||||
runCatching {
|
||||
val line = read()
|
||||
val pos = line.indexOf(" ")
|
||||
if (pos > 0) {
|
||||
val index = line.substring(0, pos).toInt()
|
||||
val pToken = line.substring(pos + 1)
|
||||
if (pToken.isNotEmpty()) {
|
||||
pTokenMap[index] = pToken
|
||||
}
|
||||
}
|
||||
}
|
||||
return info
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015-2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.conaco;
|
||||
|
||||
import android.os.Process;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
import com.hippo.beerbelly.BeerBelly;
|
||||
import com.hippo.yorozuya.thread.PriorityThreadFactory;
|
||||
import com.hippo.yorozuya.thread.SerialThreadExecutor;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.LinkedList;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public final class Conaco<V> {
|
||||
|
||||
public static final int SOURCE_MEMORY = 0;
|
||||
public static final int SOURCE_DISK = 1;
|
||||
public static final int SOURCE_NETWORK = 2;
|
||||
private static final String TAG = Conaco.class.getSimpleName();
|
||||
private final ValueHelper<V> mHelper;
|
||||
private final ValueCache<V> mCache;
|
||||
private final OkHttpClient mOkHttpClient;
|
||||
private final Register<V> mRegister;
|
||||
private final SerialThreadExecutor mDiskExecutor;
|
||||
private final ThreadPoolExecutor mNetworkExecutor;
|
||||
private final AtomicInteger mIdGenerator;
|
||||
private final boolean mDebug;
|
||||
|
||||
private Conaco(Builder<V> builder) {
|
||||
mHelper = builder.objectHelper;
|
||||
|
||||
BeerBelly.BeerBellyParams beerBellyParams = new BeerBelly.BeerBellyParams();
|
||||
beerBellyParams.hasMemoryCache = builder.hasMemoryCache;
|
||||
beerBellyParams.memoryCacheMaxSize = builder.memoryCacheMaxSize;
|
||||
beerBellyParams.hasDiskCache = builder.hasDiskCache;
|
||||
beerBellyParams.diskCacheDir = builder.diskCacheDir;
|
||||
beerBellyParams.diskCacheMaxSize = builder.diskCacheMaxSize;
|
||||
|
||||
mCache = new ValueCache<>(beerBellyParams, mHelper);
|
||||
mOkHttpClient = builder.okHttpClient;
|
||||
|
||||
mRegister = new Register<>();
|
||||
|
||||
mDiskExecutor = new SerialThreadExecutor(3000L, new LinkedList<>(),
|
||||
new PriorityThreadFactory("Conaco-Disk", Process.THREAD_PRIORITY_BACKGROUND));
|
||||
mNetworkExecutor = new ThreadPoolExecutor(3, 3, 5L, TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue<>(),
|
||||
new PriorityThreadFactory("Conaco-Network", Process.THREAD_PRIORITY_BACKGROUND));
|
||||
|
||||
mIdGenerator = new AtomicInteger();
|
||||
|
||||
mDebug = builder.debug;
|
||||
}
|
||||
|
||||
public BeerBelly getBeerBelly() {
|
||||
return mCache;
|
||||
}
|
||||
|
||||
private void onUnregisterConacoTask(ConacoTask<V> task) {
|
||||
ConacoTask<V> next = mRegister.getByKey(task.getKey());
|
||||
if (next != null) {
|
||||
startConacoTask(next);
|
||||
}
|
||||
}
|
||||
|
||||
private void startConacoTask(ConacoTask<V> task) {
|
||||
Unikery<V> unikery = task.getUnikery();
|
||||
|
||||
// Can't unikery is gone, finish the task now
|
||||
if (unikery == null) {
|
||||
finishConacoTask(task);
|
||||
return;
|
||||
}
|
||||
|
||||
String key = task.getKey();
|
||||
V value = null;
|
||||
|
||||
// Get from memory
|
||||
if (key != null && task.useMemoryCache() && mHelper.useMemoryCache(key, null)) {
|
||||
value = mCache.getFromMemory(key);
|
||||
}
|
||||
|
||||
if (value == null || !unikery.onGetValue(value, SOURCE_MEMORY)) {
|
||||
unikery.onMiss(SOURCE_MEMORY);
|
||||
task.start();
|
||||
} else {
|
||||
// Get the object, finish the task
|
||||
finishConacoTask(task);
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void load(ConacoTask.Builder<V> builder) {
|
||||
builder.isValid();
|
||||
|
||||
if (mDebug) {
|
||||
Log.d(TAG, "Key " + builder.getKey());
|
||||
Log.d(TAG, "Url " + builder.getUrl());
|
||||
}
|
||||
|
||||
Unikery<V> unikery = builder.getUnikery();
|
||||
|
||||
// Cancel first
|
||||
cancel(unikery);
|
||||
|
||||
// Build conaco task
|
||||
int id;
|
||||
//noinspection StatementWithEmptyBody
|
||||
while ((id = mIdGenerator.getAndIncrement()) == Unikery.INVALID_ID) ;
|
||||
unikery.setTaskId(id);
|
||||
builder.setId(id)
|
||||
.setHelper(mHelper)
|
||||
.setCache(mCache)
|
||||
.setOkHttpClient(mOkHttpClient)
|
||||
.setDiskExecutor(mDiskExecutor)
|
||||
.setNetworkExecutor(mNetworkExecutor)
|
||||
.setConaco(this);
|
||||
ConacoTask<V> task = builder.build();
|
||||
|
||||
if (!mRegister.register(id, task)) {
|
||||
startConacoTask(task);
|
||||
} else {
|
||||
// The key is repeated, wait
|
||||
unikery.onWait();
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void cancel(Unikery<V> unikery) {
|
||||
int id = unikery.getTaskId();
|
||||
if (id != Unikery.INVALID_ID) {
|
||||
unikery.setTaskId(Unikery.INVALID_ID);
|
||||
ConacoTask<V> task = mRegister.unregister(id);
|
||||
if (task != null) {
|
||||
task.stop();
|
||||
// Don't need unikery anymore
|
||||
task.clearUnikery();
|
||||
// Callback
|
||||
onUnregisterConacoTask(task);
|
||||
} else {
|
||||
Log.e(TAG, "Can't find conaco task by id " + id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isLoading(Unikery<V> unikery) {
|
||||
int id = unikery.getTaskId();
|
||||
return id != Unikery.INVALID_ID && mRegister.contain(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister task, reset unikery id, clear unikery in task, call next
|
||||
*/
|
||||
void finishConacoTask(ConacoTask<V> task) {
|
||||
mRegister.unregister(task.getId());
|
||||
Unikery unikery = task.getUnikery();
|
||||
if (unikery != null) {
|
||||
unikery.setTaskId(Unikery.INVALID_ID);
|
||||
}
|
||||
task.clearUnikery();
|
||||
|
||||
onUnregisterConacoTask(task);
|
||||
}
|
||||
|
||||
@IntDef({SOURCE_MEMORY, SOURCE_DISK, SOURCE_NETWORK})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface Source {
|
||||
}
|
||||
|
||||
public static class Builder<T> extends BeerBelly.BeerBellyParams {
|
||||
/**
|
||||
* The client to get image from internet
|
||||
*/
|
||||
public OkHttpClient okHttpClient = null;
|
||||
|
||||
/**
|
||||
* Decode, get size and others
|
||||
*/
|
||||
public ValueHelper<T> objectHelper = null;
|
||||
|
||||
public boolean debug = false;
|
||||
|
||||
@Override
|
||||
public void isValid() throws IllegalStateException {
|
||||
super.isValid();
|
||||
|
||||
if (okHttpClient == null) {
|
||||
throw new IllegalStateException("No http client? How can I load image via url?");
|
||||
}
|
||||
}
|
||||
|
||||
public Conaco<T> build() {
|
||||
isValid();
|
||||
return new Conaco<>(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,588 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015-2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.conaco;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
import com.hippo.beerbelly.SimpleDiskCache;
|
||||
import com.hippo.okhttp.ChromeRequestBuilder;
|
||||
import com.hippo.streampipe.InputStreamPipe;
|
||||
import com.hippo.streampipe.OutputStreamPipe;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import okhttp3.Call;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
@SuppressWarnings({"rawtypes", "deprecation"})
|
||||
public final class ConacoTask<V> {
|
||||
|
||||
private static final String TAG = ConacoTask.class.getSimpleName();
|
||||
|
||||
private final int mId;
|
||||
private final WeakReference<Unikery<V>> mUnikeryWeakReference;
|
||||
private final String mKey;
|
||||
private final String mUrl;
|
||||
private final DataContainer mDataContainer;
|
||||
private final boolean mUseMemoryCache;
|
||||
private final boolean mUseDiskCache;
|
||||
private final boolean mUseNetwork;
|
||||
private final ValueHelper<V> mHelper;
|
||||
private final ValueCache<V> mCache;
|
||||
private final OkHttpClient mOkHttpClient;
|
||||
private final Executor mDiskExecutor;
|
||||
private final Executor mNetworkExecutor;
|
||||
private final Conaco<V> mConaco;
|
||||
|
||||
private DiskLoadTask mDiskLoadTask;
|
||||
private NetworkLoadTask mNetworkLoadTask;
|
||||
private Call mCall;
|
||||
private boolean mStart;
|
||||
private volatile boolean mStop;
|
||||
|
||||
private ConacoTask(Builder<V> builder) {
|
||||
mId = builder.mId;
|
||||
mUnikeryWeakReference = new WeakReference<>(builder.mUnikery);
|
||||
mKey = builder.mKey;
|
||||
mUrl = builder.mUrl;
|
||||
mDataContainer = builder.mDataContainer;
|
||||
mUseMemoryCache = builder.mUseMemoryCache;
|
||||
mUseDiskCache = builder.mUseDiskCache;
|
||||
mUseNetwork = builder.mUseNetwork;
|
||||
mHelper = builder.mHelper;
|
||||
mCache = builder.mCache;
|
||||
mOkHttpClient = builder.mOkHttpClient;
|
||||
mDiskExecutor = builder.mDiskExecutor;
|
||||
mNetworkExecutor = builder.mNetworkExecutor;
|
||||
mConaco = builder.mConaco;
|
||||
}
|
||||
|
||||
private static void putFromDiskCacheToDataContainer(String key, ValueCache cache, DataContainer container) {
|
||||
SimpleDiskCache diskCache = cache.getDiskCache();
|
||||
if (diskCache != null) {
|
||||
InputStreamPipe pipe = diskCache.getInputStreamPipe(key);
|
||||
if (pipe != null) {
|
||||
try {
|
||||
pipe.obtain();
|
||||
container.save(pipe.open(), -1L, null, null);
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "Can't save value from disk cache to data container");
|
||||
e.printStackTrace();
|
||||
container.remove();
|
||||
} finally {
|
||||
pipe.close();
|
||||
pipe.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void putFromDataContainerToDiskCache(String key, ValueCache cache, DataContainer container) {
|
||||
InputStreamPipe pipe = container.get();
|
||||
if (pipe != null) {
|
||||
try {
|
||||
pipe.obtain();
|
||||
cache.putRawToDisk(key, pipe.open());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Can't save value from data container to disk cache", e);
|
||||
cache.removeFromDisk(key);
|
||||
} finally {
|
||||
pipe.close();
|
||||
pipe.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int getId() {
|
||||
return mId;
|
||||
}
|
||||
|
||||
String getKey() {
|
||||
return mKey;
|
||||
}
|
||||
|
||||
boolean useMemoryCache() {
|
||||
return mUseMemoryCache;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Unikery<V> getUnikery() {
|
||||
return mUnikeryWeakReference.get();
|
||||
}
|
||||
|
||||
void clearUnikery() {
|
||||
mUnikeryWeakReference.clear();
|
||||
}
|
||||
|
||||
private void onFinish() {
|
||||
if (!mStop) {
|
||||
mConaco.finishConacoTask(this);
|
||||
}/* else {
|
||||
// It is done by Conaco
|
||||
}*/
|
||||
}
|
||||
|
||||
@UiThread
|
||||
void start() {
|
||||
if (mStop || mStart) {
|
||||
return;
|
||||
}
|
||||
|
||||
mStart = true;
|
||||
|
||||
Unikery unikery = mUnikeryWeakReference.get();
|
||||
if (unikery != null && unikery.getTaskId() == mId) {
|
||||
if (mUseDiskCache) {
|
||||
mDiskLoadTask = new DiskLoadTask();
|
||||
mDiskLoadTask.executeOnExecutor(mDiskExecutor);
|
||||
return;
|
||||
} else if (mUseNetwork) {
|
||||
unikery.onMiss(Conaco.SOURCE_DISK);
|
||||
unikery.onRequest();
|
||||
mNetworkLoadTask = new NetworkLoadTask();
|
||||
mNetworkLoadTask.executeOnExecutor(mNetworkExecutor);
|
||||
return;
|
||||
} else {
|
||||
unikery.onMiss(Conaco.SOURCE_DISK);
|
||||
unikery.onMiss(Conaco.SOURCE_NETWORK);
|
||||
unikery.onFailure();
|
||||
}
|
||||
}
|
||||
|
||||
onFinish();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
void stop() {
|
||||
if (mStop) {
|
||||
return;
|
||||
}
|
||||
|
||||
mStop = true;
|
||||
|
||||
// Stop jobs
|
||||
if (mDiskLoadTask != null) { // Getting from disk
|
||||
mDiskLoadTask.cancel(false);
|
||||
} else if (mNetworkLoadTask != null) { // Getting from network
|
||||
mNetworkLoadTask.cancel(false);
|
||||
if (mCall != null) {
|
||||
mCall.cancel();
|
||||
mCall = null;
|
||||
}
|
||||
}
|
||||
|
||||
Unikery unikery = mUnikeryWeakReference.get();
|
||||
if (unikery != null) {
|
||||
unikery.onCancel();
|
||||
}
|
||||
|
||||
// Conaco handle the clean up
|
||||
}
|
||||
|
||||
private boolean isNotNecessary(AsyncTask asyncTask) {
|
||||
Unikery unikery = mUnikeryWeakReference.get();
|
||||
return mStop || asyncTask.isCancelled() || unikery == null || unikery.getTaskId() != mId;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unused", "UnusedReturnValue", "RedundantSuppression"})
|
||||
public static class Builder<T> {
|
||||
|
||||
private int mId;
|
||||
private Unikery<T> mUnikery;
|
||||
private String mKey;
|
||||
private String mUrl;
|
||||
private DataContainer mDataContainer;
|
||||
private boolean mUseMemoryCache = true;
|
||||
private boolean mUseDiskCache = true;
|
||||
private boolean mUseNetwork = true;
|
||||
private ValueHelper<T> mHelper;
|
||||
private ValueCache<T> mCache;
|
||||
private OkHttpClient mOkHttpClient;
|
||||
private Executor mDiskExecutor;
|
||||
private Executor mNetworkExecutor;
|
||||
private Conaco<T> mConaco;
|
||||
|
||||
public Builder<T> setId(int id) {
|
||||
mId = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Unikery<T> getUnikery() {
|
||||
return mUnikery;
|
||||
}
|
||||
|
||||
public Builder<T> setUnikery(Unikery<T> unikery) {
|
||||
mUnikery = unikery;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return mKey;
|
||||
}
|
||||
|
||||
public Builder<T> setKey(String key) {
|
||||
mKey = key;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return mUrl;
|
||||
}
|
||||
|
||||
public Builder<T> setUrl(String url) {
|
||||
mUrl = url;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> setDataContainer(DataContainer dataContainer) {
|
||||
mDataContainer = dataContainer;
|
||||
return this;
|
||||
}
|
||||
|
||||
boolean isUseMemoryCache() {
|
||||
return mUseMemoryCache;
|
||||
}
|
||||
|
||||
public Builder<T> setUseMemoryCache(boolean useMemoryCache) {
|
||||
mUseMemoryCache = useMemoryCache;
|
||||
return this;
|
||||
}
|
||||
|
||||
boolean isUseDiskCache() {
|
||||
return mUseDiskCache;
|
||||
}
|
||||
|
||||
public Builder<T> setUseDiskCache(boolean useDiskCache) {
|
||||
mUseDiskCache = useDiskCache;
|
||||
return this;
|
||||
}
|
||||
|
||||
boolean isUseNetwork() {
|
||||
return mUseNetwork;
|
||||
}
|
||||
|
||||
public Builder<T> setUseNetwork(boolean useNetwork) {
|
||||
mUseNetwork = useNetwork;
|
||||
return this;
|
||||
}
|
||||
|
||||
ValueHelper<T> getHelper() {
|
||||
return mHelper;
|
||||
}
|
||||
|
||||
Builder<T> setHelper(ValueHelper<T> helper) {
|
||||
mHelper = helper;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder<T> setCache(ValueCache<T> cache) {
|
||||
mCache = cache;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder<T> setOkHttpClient(OkHttpClient okHttpClient) {
|
||||
mOkHttpClient = okHttpClient;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder<T> setDiskExecutor(Executor diskExecutor) {
|
||||
mDiskExecutor = diskExecutor;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder<T> setNetworkExecutor(Executor networkExecutor) {
|
||||
mNetworkExecutor = networkExecutor;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder<T> setConaco(Conaco<T> conaco) {
|
||||
mConaco = conaco;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void isValid() {
|
||||
if (mUnikery == null) {
|
||||
throw new IllegalStateException("Must set unikery");
|
||||
}
|
||||
if (mKey == null && mUrl == null && mDataContainer == null) {
|
||||
throw new IllegalStateException("At least one of mKey and mUrl and mDataContainer have to not be null");
|
||||
}
|
||||
}
|
||||
|
||||
public ConacoTask<T> build() {
|
||||
return new ConacoTask<>(this);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private class DiskLoadTask extends AsyncTask<Void, Void, V> {
|
||||
|
||||
@Override
|
||||
protected V doInBackground(Void... params) {
|
||||
if (isNotNecessary(this)) {
|
||||
return null;
|
||||
} else {
|
||||
V value = null;
|
||||
|
||||
// First check data container
|
||||
if (mDataContainer != null && mDataContainer.isEnabled()) {
|
||||
InputStreamPipe isp = mDataContainer.get();
|
||||
if (isp != null) {
|
||||
value = mHelper.decode(isp);
|
||||
}
|
||||
}
|
||||
|
||||
// Then check disk cache
|
||||
if (mKey != null) {
|
||||
if (value == null && mUseDiskCache) {
|
||||
value = mCache.getFromDisk(mKey);
|
||||
// Put back to data container
|
||||
if (value != null && mDataContainer != null && mDataContainer.isEnabled()) {
|
||||
putFromDiskCacheToDataContainer(mKey, mCache, mDataContainer);
|
||||
}
|
||||
}
|
||||
|
||||
if (value != null && mUseMemoryCache && mHelper.useMemoryCache(mKey, value)) {
|
||||
// Put it to memory
|
||||
mCache.putToMemory(mKey, value);
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(V value) {
|
||||
mDiskLoadTask = null;
|
||||
|
||||
if (isCancelled() || mStop) {
|
||||
onCancelled(value);
|
||||
} else {
|
||||
Unikery<V> unikery = mUnikeryWeakReference.get();
|
||||
if (unikery != null && unikery.getTaskId() == mId) {
|
||||
boolean getValue = false;
|
||||
if ((value == null || !(getValue = unikery.onGetValue(value, Conaco.SOURCE_DISK))) && mUseNetwork) {
|
||||
unikery.onMiss(Conaco.SOURCE_DISK);
|
||||
unikery.onRequest();
|
||||
mNetworkLoadTask = new NetworkLoadTask();
|
||||
mNetworkLoadTask.executeOnExecutor(mNetworkExecutor);
|
||||
return;
|
||||
} else if (!getValue) {
|
||||
unikery.onMiss(Conaco.SOURCE_DISK);
|
||||
unikery.onFailure();
|
||||
}
|
||||
}
|
||||
onFinish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCancelled(V holder) {
|
||||
onFinish();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private class NetworkLoadTask extends AsyncTask<Void, Long, V> implements ProgressNotifier {
|
||||
|
||||
@Override
|
||||
public void notifyProgress(long singleReceivedSize, long receivedSize, long totalSize) {
|
||||
if (!isNotNecessary(this)) {
|
||||
publishProgress(singleReceivedSize, receivedSize, totalSize);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean putToDiskCache(InputStream is, long length) {
|
||||
SimpleDiskCache diskCache = mCache.getDiskCache();
|
||||
if (diskCache == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
OutputStreamPipe pipe = diskCache.getOutputStreamPipe(mKey);
|
||||
try {
|
||||
pipe.obtain();
|
||||
OutputStream os = pipe.open();
|
||||
|
||||
final byte[] buffer = new byte[1024 * 4];
|
||||
long receivedSize = 0;
|
||||
int bytesRead;
|
||||
|
||||
while ((bytesRead = is.read(buffer)) != -1) {
|
||||
os.write(buffer, 0, bytesRead);
|
||||
receivedSize += bytesRead;
|
||||
notifyProgress(bytesRead, receivedSize, length);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
} finally {
|
||||
pipe.close();
|
||||
pipe.release();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean putToDataContainer(InputStream is, ResponseBody body) {
|
||||
// Get media type
|
||||
String mediaType;
|
||||
MediaType mt = body.contentType();
|
||||
if (mt != null) {
|
||||
mediaType = mt.type() + '/' + mt.subtype();
|
||||
} else {
|
||||
mediaType = null;
|
||||
}
|
||||
return mDataContainer.save(is, body.contentLength(), mediaType, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected V doInBackground(Void... params) {
|
||||
if (isNotNecessary(this)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
V value;
|
||||
InputStream is = null;
|
||||
try {
|
||||
Log.d("TAG", "Conaco " + mUrl);
|
||||
|
||||
// Load it from internet
|
||||
Request request = new ChromeRequestBuilder(mUrl).build();
|
||||
mCall = mOkHttpClient.newCall(request);
|
||||
|
||||
Response response = mCall.execute();
|
||||
ResponseBody body = response.body();
|
||||
if (body == null) {
|
||||
return null;
|
||||
}
|
||||
is = body.byteStream();
|
||||
|
||||
if (isNotNecessary(this)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((mDataContainer == null || !mDataContainer.isEnabled()) && mKey != null) {
|
||||
if (putToDiskCache(is, body.contentLength())) {
|
||||
// Get object from disk cache
|
||||
value = mCache.getFromDisk(mKey);
|
||||
if (value == null) {
|
||||
// Maybe bad download, remove it from disk cache
|
||||
mCache.removeFromDisk(mKey);
|
||||
} else if (mUseMemoryCache && mHelper.useMemoryCache(mKey, value)) {
|
||||
// Put it to memory
|
||||
mCache.putToMemory(mKey, value);
|
||||
}
|
||||
return value;
|
||||
} else {
|
||||
// Maybe bad download, remove it from disk cache
|
||||
mCache.removeFromDisk(mKey);
|
||||
return null;
|
||||
}
|
||||
} else if (mDataContainer != null && mDataContainer.isEnabled()) {
|
||||
// Check url Moved
|
||||
HttpUrl requestHttpUrl = request.url();
|
||||
HttpUrl responseHttpUrl = response.request().url();
|
||||
if (!responseHttpUrl.equals(requestHttpUrl)) {
|
||||
mDataContainer.onUrlMoved(mUrl, responseHttpUrl.url().toString());
|
||||
}
|
||||
|
||||
// Put to data container
|
||||
if (!putToDataContainer(is, body)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get value from data container
|
||||
InputStreamPipe isp = mDataContainer.get();
|
||||
if (isp == null) {
|
||||
return null;
|
||||
}
|
||||
value = mHelper.decode(isp);
|
||||
if (value == null) {
|
||||
mDataContainer.remove();
|
||||
} else if (mKey != null) {
|
||||
// Put to disk cache
|
||||
putFromDataContainerToDiskCache(mKey, mCache, mDataContainer);
|
||||
|
||||
if (mUseMemoryCache && mHelper.useMemoryCache(mKey, value)) {
|
||||
// Put it to memory
|
||||
mCache.putToMemory(mKey, value);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
} finally {
|
||||
mCall = null;
|
||||
try {
|
||||
if (is != null) {
|
||||
is.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(V holder) {
|
||||
mNetworkLoadTask = null;
|
||||
|
||||
if (isCancelled() || mStop) {
|
||||
onCancelled(holder);
|
||||
} else {
|
||||
Unikery<V> unikery = mUnikeryWeakReference.get();
|
||||
if (unikery != null && unikery.getTaskId() == mId) {
|
||||
if (holder == null || !unikery.onGetValue(holder, Conaco.SOURCE_NETWORK)) {
|
||||
unikery.onFailure();
|
||||
}
|
||||
}
|
||||
onFinish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCancelled(V value) {
|
||||
onFinish();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onProgressUpdate(Long... values) {
|
||||
Unikery unikery = mUnikeryWeakReference.get();
|
||||
if (!mStop && !isCancelled() && unikery != null && unikery.getTaskId() == mId) {
|
||||
unikery.onProgress(values[0], values[1], values[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015-2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.conaco;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.streampipe.InputStreamPipe;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public interface DataContainer {
|
||||
|
||||
/**
|
||||
* Returns the enabled status for this DataContainer.
|
||||
*/
|
||||
boolean isEnabled();
|
||||
|
||||
/**
|
||||
* Get 304 or something like that
|
||||
*/
|
||||
void onUrlMoved(String requestUrl, String responseUrl);
|
||||
|
||||
/**
|
||||
* Save the {@code InputStream}
|
||||
*/
|
||||
boolean save(InputStream is, long length, @Nullable String mediaType, @Nullable ProgressNotifier notify);
|
||||
|
||||
/**
|
||||
* Get {@code InputStreamPipe} for saved before
|
||||
*/
|
||||
@Nullable
|
||||
InputStreamPipe get();
|
||||
|
||||
/**
|
||||
* Remove saved stuff
|
||||
*/
|
||||
void remove();
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.conaco;
|
||||
|
||||
public interface ProgressNotifier {
|
||||
|
||||
void notifyProgress(long singleReceivedSize, long receivedSize, long totalSize);
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.conaco;
|
||||
|
||||
import com.hippo.yorozuya.collect.Multimap;
|
||||
import com.hippo.yorozuya.collect.SparseILArray;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
class Register<V> {
|
||||
|
||||
private final SparseILArray<ConacoTask<V>> mIdMap = new SparseILArray<>();
|
||||
private final Multimap<String, ConacoTask<V>> mKeyMap = Multimap.create();
|
||||
|
||||
/**
|
||||
* @return true for the key is already registered
|
||||
*/
|
||||
public synchronized boolean register(int id, ConacoTask<V> task) {
|
||||
boolean repeatedKey = false;
|
||||
String taskKey = task.getKey();
|
||||
|
||||
// For no-key task, there no need to check
|
||||
if (taskKey != null) {
|
||||
repeatedKey = mKeyMap.countElements(taskKey) != 0;
|
||||
}
|
||||
|
||||
// Append task
|
||||
mKeyMap.putElement(taskKey, task);
|
||||
mIdMap.append(id, task);
|
||||
|
||||
return repeatedKey;
|
||||
}
|
||||
|
||||
public synchronized ConacoTask<V> unregister(int id) {
|
||||
ConacoTask<V> task = mIdMap.remove(id);
|
||||
if (task != null) {
|
||||
mKeyMap.removeElement(task.getKey(), task);
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
public synchronized boolean contain(int id) {
|
||||
return mIdMap.indexOfKey(id) >= 0;
|
||||
}
|
||||
|
||||
public synchronized ConacoTask<V> getByKey(String key) {
|
||||
if (key == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<ConacoTask<V>> list = mKeyMap.get(key);
|
||||
if (list != null && list.size() > 0) {
|
||||
return list.get(0);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.conaco;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public interface Unikery<V> {
|
||||
|
||||
int INVALID_ID = -1;
|
||||
|
||||
/**
|
||||
* Get the conaco task id
|
||||
*
|
||||
* @return the conaco task id
|
||||
*/
|
||||
int getTaskId();
|
||||
|
||||
/**
|
||||
* Store the conaco task id
|
||||
*
|
||||
* @param id the conaco task id
|
||||
*/
|
||||
void setTaskId(int id);
|
||||
|
||||
/**
|
||||
* On miss in the source
|
||||
*/
|
||||
void onMiss(@Conaco.Source int source);
|
||||
|
||||
/**
|
||||
* On start http request
|
||||
*/
|
||||
void onRequest();
|
||||
|
||||
/**
|
||||
* On http request progress
|
||||
*/
|
||||
void onProgress(long singleReceivedSize, long receivedSize, long totalSize);
|
||||
|
||||
/**
|
||||
* On start to wait because repeated key
|
||||
*/
|
||||
void onWait();
|
||||
|
||||
/**
|
||||
* On get the value
|
||||
*
|
||||
* @return {@code true} for the {@code Unikery} accepts the value
|
||||
* and the task ends. {@code false} for the {@code Unikery}
|
||||
* reject the value and the task goes on or failed
|
||||
*/
|
||||
boolean onGetValue(@NonNull V value, @Conaco.Source int source);
|
||||
|
||||
/**
|
||||
* On failed to get value
|
||||
*/
|
||||
void onFailure();
|
||||
|
||||
/**
|
||||
* On user cancel the conaco task
|
||||
*/
|
||||
void onCancel();
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.conaco;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.hippo.beerbelly.BeerBelly;
|
||||
import com.hippo.streampipe.InputStreamPipe;
|
||||
|
||||
import java.io.OutputStream;
|
||||
|
||||
class ValueCache<V> extends BeerBelly<V> {
|
||||
|
||||
private final ValueHelper<V> mHelper;
|
||||
|
||||
public ValueCache(BeerBelly.BeerBellyParams params, ValueHelper<V> helper) {
|
||||
super(params);
|
||||
mHelper = helper;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int sizeOf(String key, V value) {
|
||||
return mHelper.sizeOf(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void memoryEntryAdded(V value) {
|
||||
mHelper.onAddToMemoryCache(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void memoryEntryRemoved(boolean evicted, String key, V oldValue, V newValue) {
|
||||
if (oldValue != null) {
|
||||
mHelper.onRemoveFromMemoryCache(key, oldValue);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected V read(@NonNull InputStreamPipe isPipe) {
|
||||
return mHelper.decode(isPipe);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean write(OutputStream os, V value) {
|
||||
throw new UnsupportedOperationException("Not support write object");
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.conaco;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.streampipe.InputStreamPipe;
|
||||
|
||||
public interface ValueHelper<V> {
|
||||
|
||||
/**
|
||||
* Decode object for the {@code InputStreamPipe}
|
||||
*
|
||||
* @param isPipe the {@code InputStreamPipe}
|
||||
* @return the decoded object
|
||||
*/
|
||||
@Nullable
|
||||
V decode(@NonNull InputStreamPipe isPipe);
|
||||
|
||||
/**
|
||||
* Get the size of the object
|
||||
*/
|
||||
int sizeOf(@NonNull String key, @NonNull V value);
|
||||
|
||||
/**
|
||||
* Called when the object added to memory cache
|
||||
*/
|
||||
void onAddToMemoryCache(@NonNull V oldValue);
|
||||
|
||||
/**
|
||||
* Called when the object removed from memory cache
|
||||
*/
|
||||
void onRemoveFromMemoryCache(@NonNull String key, @NonNull V oldValue);
|
||||
|
||||
/**
|
||||
* Use cache memory or not. Sometimes large object
|
||||
* should not be stored in memory cache.
|
||||
*/
|
||||
boolean useMemoryCache(@NonNull String key, V holder);
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.drawable;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import com.hippo.yorozuya.MathUtils;
|
||||
|
||||
public class BatteryDrawable extends Drawable {
|
||||
|
||||
public static final int WARN_LIMIT = 15;
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = BatteryDrawable.class.getSimpleName();
|
||||
private final Paint mPaint;
|
||||
private final Rect mTopRect;
|
||||
private final Rect mBottomRect;
|
||||
private final Rect mRightRect;
|
||||
private final Rect mHeadRect;
|
||||
private final Rect mElectRect;
|
||||
private int mColor = Color.WHITE;
|
||||
private int mWarningColor = Color.RED;
|
||||
private int mElect = -1;
|
||||
private int mStart;
|
||||
private int mStop;
|
||||
|
||||
public BatteryDrawable() {
|
||||
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
|
||||
mPaint.setStyle(Paint.Style.FILL);
|
||||
|
||||
mTopRect = new Rect();
|
||||
mBottomRect = new Rect();
|
||||
mRightRect = new Rect();
|
||||
mHeadRect = new Rect();
|
||||
mElectRect = new Rect();
|
||||
updatePaint();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("SuspiciousNameCombination")
|
||||
protected void onBoundsChange(Rect bounds) {
|
||||
int width = bounds.width();
|
||||
int height = bounds.height();
|
||||
int strokeWidth = (int) (Math.sqrt(width * width + height * height) * 0.06f);
|
||||
int turn1 = width * 6 / 7;
|
||||
int turn2 = height / 3;
|
||||
int secBottom = height - strokeWidth;
|
||||
mStart = strokeWidth;
|
||||
mStop = turn1 - strokeWidth;
|
||||
|
||||
mTopRect.set(0, 0, turn1, strokeWidth);
|
||||
mBottomRect.set(0, secBottom, turn1, height);
|
||||
mRightRect.set(turn1 - strokeWidth, strokeWidth, turn1, secBottom);
|
||||
mHeadRect.set(turn1, turn2, width, height - turn2);
|
||||
mElectRect.set(0, strokeWidth, mStop, secBottom);
|
||||
}
|
||||
|
||||
/**
|
||||
* How to draw:<br>
|
||||
* |------------------------------|<br>
|
||||
* |\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\|<br>
|
||||
* |------------------------------|---|<br>
|
||||
* |/////////////////| |//|\\\|<br>
|
||||
* |/////////////////| |//|\\\|<br>
|
||||
* |------------------------------|---|<br>
|
||||
* |\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\|<br>
|
||||
* |------------------------------|
|
||||
*/
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
if (mElect == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
mElectRect.right = MathUtils.lerp(mStart, mStop, mElect / 100.0f);
|
||||
|
||||
canvas.drawRect(mTopRect, mPaint);
|
||||
canvas.drawRect(mBottomRect, mPaint);
|
||||
canvas.drawRect(mRightRect, mPaint);
|
||||
canvas.drawRect(mHeadRect, mPaint);
|
||||
canvas.drawRect(mElectRect, mPaint);
|
||||
}
|
||||
|
||||
private boolean isWarn() {
|
||||
return mElect <= WARN_LIMIT;
|
||||
}
|
||||
|
||||
public void setColor(int color) {
|
||||
if (mColor == color) {
|
||||
return;
|
||||
}
|
||||
|
||||
mColor = color;
|
||||
if (!isWarn()) {
|
||||
mPaint.setColor(mColor);
|
||||
invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
public void setWarningColor(int color) {
|
||||
if (mWarningColor == color) {
|
||||
return;
|
||||
}
|
||||
|
||||
mWarningColor = color;
|
||||
if (isWarn()) {
|
||||
mPaint.setColor(mWarningColor);
|
||||
invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
public void setElect(int elect) {
|
||||
if (mElect == elect) {
|
||||
return;
|
||||
}
|
||||
|
||||
mElect = elect;
|
||||
updatePaint();
|
||||
}
|
||||
|
||||
public void setElect(int elect, boolean warn) {
|
||||
if (mElect == elect) {
|
||||
return;
|
||||
}
|
||||
|
||||
mElect = elect;
|
||||
updatePaint(warn);
|
||||
}
|
||||
|
||||
private void updatePaint() {
|
||||
updatePaint(isWarn());
|
||||
}
|
||||
|
||||
private void updatePaint(boolean warn) {
|
||||
if (warn) {
|
||||
mPaint.setColor(mWarningColor);
|
||||
} else {
|
||||
mPaint.setColor(mColor);
|
||||
}
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
mPaint.setAlpha(alpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(ColorFilter cf) {
|
||||
mPaint.setColorFilter(cf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import android.graphics.ColorFilter;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Region;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -29,17 +30,25 @@ import androidx.core.graphics.drawable.DrawableCompat;
|
||||
|
||||
public class DrawableWrapper extends Drawable implements Drawable.Callback {
|
||||
private Drawable mDrawable;
|
||||
private Rect mBounds;
|
||||
|
||||
public DrawableWrapper(Drawable drawable) {
|
||||
this.setWrappedDrawable(drawable);
|
||||
}
|
||||
|
||||
public void draw(@NonNull Canvas canvas) {
|
||||
this.mDrawable.draw(canvas);
|
||||
if (mDrawable instanceof BitmapDrawable bitmapDrawable) {
|
||||
canvas.drawBitmap(bitmapDrawable.getBitmap(), null, mBounds, bitmapDrawable.getPaint());
|
||||
} else {
|
||||
synchronized (mDrawable) {
|
||||
mDrawable.setBounds(mBounds);
|
||||
mDrawable.draw(canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void onBoundsChange(Rect bounds) {
|
||||
this.mDrawable.setBounds(bounds);
|
||||
mBounds = bounds;
|
||||
}
|
||||
|
||||
public int getChangingConfigurations() {
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.drawable;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.yorozuya.MathUtils;
|
||||
|
||||
/**
|
||||
* A drawable that can draw a "Drawer hamburger" menu or an Arrow and animate between them.
|
||||
*/
|
||||
public class DrawerArrowDrawable extends Drawable {
|
||||
|
||||
// The angle in degrees that the arrow head is inclined at.
|
||||
private static final float ARROW_HEAD_ANGLE = (float) Math.toRadians(45);
|
||||
private final Paint mPaint = new Paint();
|
||||
private final float mBarThickness;
|
||||
// The length of top and bottom bars when they merge into an arrow
|
||||
private final float mTopBottomArrowSize;
|
||||
// The length of middle bar
|
||||
private final float mBarSize;
|
||||
// The length of the middle bar when arrow is shaped
|
||||
private final float mMiddleArrowSize;
|
||||
// The space between bars when they are parallel
|
||||
private final float mBarGap;
|
||||
// Whether bars should spin or not during progress
|
||||
private final boolean mSpin;
|
||||
// Use Path instead of canvas operations so that if color has transparency, overlapping sections
|
||||
// wont look different
|
||||
private final Path mPath = new Path();
|
||||
// The reported intrinsic size of the drawable.
|
||||
private final int mSize;
|
||||
// the amount that overlaps w/ bar size when rotation is max
|
||||
private final float mMaxCutForBarSize;
|
||||
// Whether we should mirror animation when animation is reversed.
|
||||
private boolean mVerticalMirror = false;
|
||||
// The interpolated version of the original progress
|
||||
private float mProgress;
|
||||
|
||||
/**
|
||||
* @param context used to get the configuration for the drawable from
|
||||
*/
|
||||
public DrawerArrowDrawable(Context context, int color) {
|
||||
Resources resources = context.getResources();
|
||||
|
||||
mPaint.setAntiAlias(true);
|
||||
mPaint.setColor(color);
|
||||
mSize = resources.getDimensionPixelSize(R.dimen.dad_drawable_size);
|
||||
// round this because having this floating may cause bad measurements
|
||||
mBarSize = Math.round(resources.getDimension(R.dimen.dad_bar_size));
|
||||
// round this because having this floating may cause bad measurements
|
||||
mTopBottomArrowSize = Math.round(resources.getDimension(R.dimen.dad_top_bottom_bar_arrow_size));
|
||||
mBarThickness = resources.getDimension(R.dimen.dad_thickness);
|
||||
// round this because having this floating may cause bad measurements
|
||||
mBarGap = Math.round(resources.getDimension(R.dimen.dad_gap_between_bars));
|
||||
mSpin = resources.getBoolean(R.bool.dad_spin_bars);
|
||||
mMiddleArrowSize = resources.getDimension(R.dimen.dad_middle_bar_arrow_size);
|
||||
|
||||
mPaint.setStyle(Paint.Style.STROKE);
|
||||
mPaint.setStrokeJoin(Paint.Join.MITER);
|
||||
mPaint.setStrokeCap(Paint.Cap.BUTT);
|
||||
mPaint.setStrokeWidth(mBarThickness);
|
||||
|
||||
mMaxCutForBarSize = (float) (mBarThickness / 2 * Math.cos(ARROW_HEAD_ANGLE));
|
||||
}
|
||||
|
||||
/**
|
||||
* If set, canvas is flipped when progress reached to end and going back to start.
|
||||
*/
|
||||
protected void setVerticalMirror(boolean verticalMirror) {
|
||||
mVerticalMirror = verticalMirror;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
Rect bounds = getBounds();
|
||||
// Interpolated widths of arrow bars
|
||||
final float arrowSize = MathUtils.lerp(mBarSize, mTopBottomArrowSize, mProgress);
|
||||
final float middleBarSize = MathUtils.lerp(mBarSize, mMiddleArrowSize, mProgress);
|
||||
// Interpolated size of middle bar
|
||||
final float middleBarCut = Math.round(MathUtils.lerp(0, mMaxCutForBarSize, mProgress));
|
||||
// The rotation of the top and bottom bars (that make the arrow head)
|
||||
final float rotation = MathUtils.lerp(0, ARROW_HEAD_ANGLE, mProgress);
|
||||
|
||||
// The whole canvas rotates as the transition happens
|
||||
final float canvasRotate = MathUtils.lerp(-180, 0, mProgress);
|
||||
final float arrowWidth = Math.round(arrowSize * Math.cos(rotation));
|
||||
final float arrowHeight = Math.round(arrowSize * Math.sin(rotation));
|
||||
|
||||
mPath.rewind();
|
||||
final float topBottomBarOffset = MathUtils.lerp(mBarGap + mBarThickness, -mMaxCutForBarSize,
|
||||
mProgress);
|
||||
|
||||
final float arrowEdge = -middleBarSize / 2;
|
||||
// draw middle bar
|
||||
mPath.moveTo(arrowEdge + middleBarCut, 0);
|
||||
mPath.rLineTo(middleBarSize - middleBarCut * 2, 0);
|
||||
|
||||
// bottom bar
|
||||
mPath.moveTo(arrowEdge, topBottomBarOffset);
|
||||
mPath.rLineTo(arrowWidth, arrowHeight);
|
||||
|
||||
// top bar
|
||||
mPath.moveTo(arrowEdge, -topBottomBarOffset);
|
||||
mPath.rLineTo(arrowWidth, -arrowHeight);
|
||||
|
||||
mPath.close();
|
||||
|
||||
canvas.save();
|
||||
// Rotate the whole canvas if spinning, if not, rotate it 180 to get
|
||||
// the arrow pointing the other way for RTL.
|
||||
canvas.translate(bounds.centerX(), bounds.centerY());
|
||||
if (mSpin) {
|
||||
canvas.rotate(canvasRotate * (mVerticalMirror ? -1 : 1));
|
||||
}
|
||||
canvas.drawPath(mPath, mPaint);
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
public void setColor(@ColorInt int color) {
|
||||
mPaint.setColor(color);
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int i) {
|
||||
mPaint.setAlpha(i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(ColorFilter colorFilter) {
|
||||
mPaint.setColorFilter(colorFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicHeight() {
|
||||
return mSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicWidth() {
|
||||
return mSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
|
||||
@Keep
|
||||
public float getProgress() {
|
||||
return mProgress;
|
||||
}
|
||||
|
||||
@Keep
|
||||
public void setProgress(float progress) {
|
||||
if (progress == 1f) {
|
||||
setVerticalMirror(true);
|
||||
} else if (progress == 0f) {
|
||||
setVerticalMirror(false);
|
||||
}
|
||||
mProgress = progress;
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
public void setMenu(long duration) {
|
||||
setShape(false, duration);
|
||||
}
|
||||
|
||||
public void setArrow(long duration) {
|
||||
setShape(true, duration);
|
||||
}
|
||||
|
||||
public void setShape(boolean arrow, long duration) {
|
||||
if (!((!arrow && mProgress == 0f) || (arrow && mProgress == 1f))) {
|
||||
float endProgress = arrow ? 1f : 0f;
|
||||
if (duration <= 0) {
|
||||
setProgress(endProgress);
|
||||
} else {
|
||||
ObjectAnimator oa = ObjectAnimator.ofFloat(this, "progress", endProgress);
|
||||
oa.setDuration(duration);
|
||||
oa.setAutoCancel(true);
|
||||
oa.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,31 +16,25 @@
|
||||
|
||||
package com.hippo.drawable;
|
||||
|
||||
import android.graphics.drawable.AnimatedImageDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.hippo.conaco.Conaco;
|
||||
import com.hippo.conaco.ConacoTask;
|
||||
import com.hippo.conaco.Unikery;
|
||||
import com.hippo.image.ImageBitmap;
|
||||
import com.hippo.image.ImageDrawable;
|
||||
import com.hippo.widget.ObservedTextView;
|
||||
|
||||
public class UnikeryDrawable extends WrapDrawable implements Unikery<ImageBitmap>,
|
||||
ObservedTextView.OnWindowAttachListener {
|
||||
import coil.Coil;
|
||||
import coil.request.Disposable;
|
||||
import coil.request.ImageRequest;
|
||||
|
||||
public class UnikeryDrawable extends WrapDrawable implements ObservedTextView.OnWindowAttachListener {
|
||||
|
||||
private static final String TAG = UnikeryDrawable.class.getSimpleName();
|
||||
private final ObservedTextView mTextView;
|
||||
private final Conaco<ImageBitmap> mConaco;
|
||||
private int mTaskId = Unikery.INVALID_ID;
|
||||
private String mUrl;
|
||||
private Disposable task;
|
||||
|
||||
public UnikeryDrawable(ObservedTextView textView, Conaco<ImageBitmap> conaco) {
|
||||
public UnikeryDrawable(ObservedTextView textView) {
|
||||
mTextView = textView;
|
||||
mTextView.setOnWindowAttachListener(this);
|
||||
mConaco = conaco;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -50,22 +44,28 @@ public class UnikeryDrawable extends WrapDrawable implements Unikery<ImageBitmap
|
||||
|
||||
@Override
|
||||
public void onDetachedFromWindow() {
|
||||
mConaco.cancel(this);
|
||||
if (task != null && !task.isDisposed())
|
||||
task.dispose();
|
||||
clearDrawable();
|
||||
}
|
||||
|
||||
public void load(String url) {
|
||||
if (url != null) {
|
||||
mUrl = url;
|
||||
mConaco.load(new ConacoTask.Builder<ImageBitmap>().setUnikery(this).setUrl(url).setKey(url));
|
||||
var imageLoader = Coil.imageLoader(mTextView.getContext());
|
||||
var request = new ImageRequest.Builder(mTextView.getContext()).data(url).memoryCacheKey(url).diskCacheKey(url).target(
|
||||
drawable -> null,
|
||||
drawable -> null,
|
||||
drawable -> {
|
||||
onGetValue(drawable);
|
||||
return null;
|
||||
}
|
||||
).build();
|
||||
task = imageLoader.enqueue(request);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearDrawable() {
|
||||
Drawable drawable = getDrawable();
|
||||
if (drawable instanceof ImageDrawable) {
|
||||
((ImageDrawable) drawable).recycle();
|
||||
}
|
||||
setDrawable(null);
|
||||
}
|
||||
|
||||
@@ -89,63 +89,16 @@ public class UnikeryDrawable extends WrapDrawable implements Unikery<ImageBitmap
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTaskId() {
|
||||
return mTaskId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTaskId(int id) {
|
||||
mTaskId = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateSelf() {
|
||||
CharSequence cs = mTextView.getText();
|
||||
mTextView.setText(cs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMiss(int source) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequest() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgress(long singleReceivedSize, long receivedSize, long totalSize) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWait() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onGetValue(@NonNull ImageBitmap value, int source) {
|
||||
ImageDrawable drawable;
|
||||
try {
|
||||
drawable = new ImageDrawable(value);
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "The ImageBitmap is recycled", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
public void onGetValue(Drawable drawable) {
|
||||
clearDrawable();
|
||||
|
||||
setDrawable(drawable);
|
||||
drawable.start();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
// Empty
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel() {
|
||||
// Empty
|
||||
if (drawable instanceof AnimatedImageDrawable animatedImageDrawable)
|
||||
animatedImageDrawable.start();
|
||||
}
|
||||
}
|
||||
@@ -64,8 +64,7 @@ public final class LayoutManagerUtils {
|
||||
RecyclerView.LayoutManager layoutManager, Context context, int position,
|
||||
int millisecondsPerInch) {
|
||||
SimpleSmoothScroller smoothScroller;
|
||||
if (layoutManager instanceof LinearLayoutManager) {
|
||||
final LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
|
||||
if (layoutManager instanceof final LinearLayoutManager linearLayoutManager) {
|
||||
smoothScroller = new SimpleSmoothScroller(context, millisecondsPerInch) {
|
||||
@Override
|
||||
public PointF computeScrollVectorForPosition(int targetPosition) {
|
||||
@@ -73,8 +72,7 @@ public final class LayoutManagerUtils {
|
||||
}
|
||||
};
|
||||
|
||||
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
|
||||
final StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager;
|
||||
} else if (layoutManager instanceof final StaggeredGridLayoutManager staggeredGridLayoutManager) {
|
||||
smoothScroller = new SimpleSmoothScroller(context, millisecondsPerInch) {
|
||||
@Override
|
||||
public PointF computeScrollVectorForPosition(int targetPosition) {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package com.hippo.ehviewer;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
@@ -25,7 +24,6 @@ import android.os.Debug;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.hippo.scene.StageActivity;
|
||||
import com.hippo.util.PackageUtils;
|
||||
import com.hippo.util.ReadableTime;
|
||||
import com.hippo.yorozuya.FileUtils;
|
||||
@@ -78,27 +76,10 @@ public final class Crash {
|
||||
|
||||
// Runtime
|
||||
String topActivityClazzName = "null";
|
||||
String topSceneClazzName = "null";
|
||||
try {
|
||||
Activity topActivity = ((EhApplication) context.getApplicationContext()).getTopActivity();
|
||||
if (null != topActivity) {
|
||||
topActivityClazzName = topActivity.getClass().getName();
|
||||
if (topActivity instanceof StageActivity) {
|
||||
Class<?> clazz = ((StageActivity) topActivity).getTopSceneClass();
|
||||
if (clazz != null) {
|
||||
topSceneClazzName = clazz.getName();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
// Ignore
|
||||
}
|
||||
fw.write("======== Runtime ========\r\n");
|
||||
fw.write("TopActivity=");
|
||||
fw.write(avoidNull(topActivityClazzName));
|
||||
fw.write("\r\n");
|
||||
fw.write("TopScene=");
|
||||
fw.write(avoidNull(topSceneClazzName));
|
||||
fw.write("\r\n");
|
||||
fw.write("\r\n");
|
||||
|
||||
|
||||
@@ -1,596 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.ComponentCallbacks2;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Debug;
|
||||
import android.text.Html;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.collection.LruCache;
|
||||
import androidx.core.os.LocaleListCompat;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.hippo.Native;
|
||||
import com.hippo.beerbelly.SimpleDiskCache;
|
||||
import com.hippo.conaco.Conaco;
|
||||
import com.hippo.ehviewer.client.EhClient;
|
||||
import com.hippo.ehviewer.client.EhCookieStore;
|
||||
import com.hippo.ehviewer.client.EhDns;
|
||||
import com.hippo.ehviewer.client.EhEngine;
|
||||
import com.hippo.ehviewer.client.EhRequestBuilder;
|
||||
import com.hippo.ehviewer.client.EhSSLSocketFactory;
|
||||
import com.hippo.ehviewer.client.EhUrl;
|
||||
import com.hippo.ehviewer.client.EhX509TrustManager;
|
||||
import com.hippo.ehviewer.client.data.GalleryDetail;
|
||||
import com.hippo.ehviewer.client.parser.EventPaneParser;
|
||||
import com.hippo.ehviewer.download.DownloadManager;
|
||||
import com.hippo.ehviewer.spider.SpiderDen;
|
||||
import com.hippo.ehviewer.ui.CommonOperations;
|
||||
import com.hippo.image.Image;
|
||||
import com.hippo.image.ImageBitmap;
|
||||
import com.hippo.network.StatusCodeException;
|
||||
import com.hippo.scene.SceneApplication;
|
||||
import com.hippo.unifile.UniFile;
|
||||
import com.hippo.util.BitmapUtils;
|
||||
import com.hippo.util.ClipboardUtil;
|
||||
import com.hippo.util.ExceptionUtils;
|
||||
import com.hippo.util.IoThreadPoolExecutor;
|
||||
import com.hippo.util.ReadableTime;
|
||||
import com.hippo.yorozuya.FileUtils;
|
||||
import com.hippo.yorozuya.IntIdGenerator;
|
||||
import com.hippo.yorozuya.OSUtils;
|
||||
import com.hippo.yorozuya.SimpleHandler;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyStore;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import okhttp3.Cache;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
public class EhApplication extends SceneApplication {
|
||||
|
||||
public static final boolean BETA = false;
|
||||
private static final String TAG = EhApplication.class.getSimpleName();
|
||||
private static final String KEY_GLOBAL_STUFF_NEXT_ID = "global_stuff_next_id";
|
||||
private static final boolean DEBUG_CONACO = false;
|
||||
private static final boolean DEBUG_PRINT_NATIVE_MEMORY = false;
|
||||
private static final boolean DEBUG_PRINT_IMAGE_COUNT = false;
|
||||
private static final long DEBUG_PRINT_INTERVAL = 3000L;
|
||||
|
||||
public static boolean locked = false;
|
||||
|
||||
private static EhApplication instance;
|
||||
|
||||
private final IntIdGenerator mIdGenerator = new IntIdGenerator();
|
||||
private final HashMap<Integer, Object> mGlobalStuffMap = new HashMap<>();
|
||||
private final List<Activity> mActivityList = new ArrayList<>();
|
||||
private EhCookieStore mEhCookieStore;
|
||||
private EhClient mEhClient;
|
||||
private EhProxySelector mEhProxySelector;
|
||||
private OkHttpClient mOkHttpClient;
|
||||
private Cache mOkHttpCache;
|
||||
private ImageBitmapHelper mImageBitmapHelper;
|
||||
private Conaco<ImageBitmap> mConaco;
|
||||
private LruCache<Long, GalleryDetail> mGalleryDetailCache;
|
||||
private SimpleDiskCache mSpiderInfoCache;
|
||||
private DownloadManager mDownloadManager;
|
||||
private Hosts mHosts;
|
||||
private FavouriteStatusRouter mFavouriteStatusRouter;
|
||||
private boolean initialized = false;
|
||||
|
||||
public static EhApplication getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static EhCookieStore getEhCookieStore(@NonNull Context context) {
|
||||
EhApplication application = ((EhApplication) context.getApplicationContext());
|
||||
if (application.mEhCookieStore == null) {
|
||||
application.mEhCookieStore = new EhCookieStore(context);
|
||||
}
|
||||
return application.mEhCookieStore;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static EhClient getEhClient(@NonNull Context context) {
|
||||
EhApplication application = ((EhApplication) context.getApplicationContext());
|
||||
if (application.mEhClient == null) {
|
||||
application.mEhClient = new EhClient(application);
|
||||
}
|
||||
return application.mEhClient;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static EhProxySelector getEhProxySelector(@NonNull Context context) {
|
||||
EhApplication application = ((EhApplication) context.getApplicationContext());
|
||||
if (application.mEhProxySelector == null) {
|
||||
application.mEhProxySelector = new EhProxySelector();
|
||||
}
|
||||
return application.mEhProxySelector;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static OkHttpClient getOkHttpClient(@NonNull Context context) {
|
||||
EhApplication application = ((EhApplication) context.getApplicationContext());
|
||||
if (application.mOkHttpClient == null) {
|
||||
OkHttpClient.Builder builder = new OkHttpClient.Builder()
|
||||
.connectTimeout(5, TimeUnit.SECONDS)
|
||||
.readTimeout(5, TimeUnit.SECONDS)
|
||||
.writeTimeout(5, TimeUnit.SECONDS)
|
||||
.callTimeout(10, TimeUnit.SECONDS)
|
||||
.cookieJar(getEhCookieStore(application))
|
||||
.cache(getOkHttpCache(application))
|
||||
.dns(new EhDns(application))
|
||||
.addNetworkInterceptor(chain -> {
|
||||
try {
|
||||
return chain.proceed(chain.request());
|
||||
} catch (NullPointerException e) {
|
||||
// crash on meizu devices due to old Android version
|
||||
// https://github.com/square/okhttp/issues/3301#issuecomment-348415095
|
||||
throw new IOException(e.getMessage());
|
||||
}
|
||||
})
|
||||
.proxySelector(getEhProxySelector(application));
|
||||
try {
|
||||
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
|
||||
TrustManagerFactory.getDefaultAlgorithm());
|
||||
trustManagerFactory.init((KeyStore) null);
|
||||
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
|
||||
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
|
||||
throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
|
||||
}
|
||||
X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
|
||||
builder.sslSocketFactory(new EhSSLSocketFactory(), trustManager);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
builder.sslSocketFactory(new EhSSLSocketFactory(), new EhX509TrustManager());
|
||||
}
|
||||
application.mOkHttpClient = builder.build();
|
||||
}
|
||||
return application.mOkHttpClient;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Cache getOkHttpCache(@NonNull Context context) {
|
||||
EhApplication application = ((EhApplication) context.getApplicationContext());
|
||||
if (application.mOkHttpCache == null) {
|
||||
application.mOkHttpCache = new Cache(new File(application.getCacheDir(), "http_cache"), 50L * 1024L * 1024L);
|
||||
}
|
||||
return application.mOkHttpCache;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static ImageBitmapHelper getImageBitmapHelper(@NonNull Context context) {
|
||||
EhApplication application = ((EhApplication) context.getApplicationContext());
|
||||
if (application.mImageBitmapHelper == null) {
|
||||
application.mImageBitmapHelper = new ImageBitmapHelper();
|
||||
}
|
||||
return application.mImageBitmapHelper;
|
||||
}
|
||||
|
||||
private static int getMemoryCacheMaxSize() {
|
||||
return Math.min(20 * 1024 * 1024, (int) OSUtils.getAppMaxMemory());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Conaco<ImageBitmap> getConaco(@NonNull Context context) {
|
||||
EhApplication application = ((EhApplication) context.getApplicationContext());
|
||||
if (application.mConaco == null) {
|
||||
Conaco.Builder<ImageBitmap> builder = new Conaco.Builder<>();
|
||||
builder.hasMemoryCache = true;
|
||||
builder.memoryCacheMaxSize = getMemoryCacheMaxSize();
|
||||
builder.hasDiskCache = true;
|
||||
builder.diskCacheDir = new File(context.getCacheDir(), "thumb");
|
||||
builder.diskCacheMaxSize = 320 * 1024 * 1024; // 320MB
|
||||
builder.okHttpClient = getOkHttpClient(context);
|
||||
builder.objectHelper = getImageBitmapHelper(context);
|
||||
builder.debug = DEBUG_CONACO;
|
||||
application.mConaco = builder.build();
|
||||
}
|
||||
return application.mConaco;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static LruCache<Long, GalleryDetail> getGalleryDetailCache(@NonNull Context context) {
|
||||
EhApplication application = ((EhApplication) context.getApplicationContext());
|
||||
if (application.mGalleryDetailCache == null) {
|
||||
// Max size 25, 3 min timeout
|
||||
application.mGalleryDetailCache = new LruCache<>(25);
|
||||
getFavouriteStatusRouter().addListener((gid, slot) -> {
|
||||
GalleryDetail gd = application.mGalleryDetailCache.get(gid);
|
||||
if (gd != null) {
|
||||
gd.favoriteSlot = slot;
|
||||
}
|
||||
});
|
||||
}
|
||||
return application.mGalleryDetailCache;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static SimpleDiskCache getSpiderInfoCache(@NonNull Context context) {
|
||||
EhApplication application = ((EhApplication) context.getApplicationContext());
|
||||
if (null == application.mSpiderInfoCache) {
|
||||
application.mSpiderInfoCache = new SimpleDiskCache(
|
||||
new File(context.getCacheDir(), "spider_info"), 20 * 1024 * 1024); // 20M
|
||||
}
|
||||
return application.mSpiderInfoCache;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static DownloadManager getDownloadManager() {
|
||||
return getDownloadManager(instance);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static DownloadManager getDownloadManager(@NonNull Context context) {
|
||||
EhApplication application = ((EhApplication) context.getApplicationContext());
|
||||
if (application.mDownloadManager == null) {
|
||||
application.mDownloadManager = new DownloadManager(application);
|
||||
}
|
||||
return application.mDownloadManager;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Hosts getHosts(@NonNull Context context) {
|
||||
EhApplication application = ((EhApplication) context.getApplicationContext());
|
||||
if (application.mHosts == null) {
|
||||
application.mHosts = new Hosts(application, "hosts.db");
|
||||
}
|
||||
return application.mHosts;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static FavouriteStatusRouter getFavouriteStatusRouter() {
|
||||
return getFavouriteStatusRouter(getInstance());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static FavouriteStatusRouter getFavouriteStatusRouter(@NonNull Context context) {
|
||||
EhApplication application = ((EhApplication) context.getApplicationContext());
|
||||
if (application.mFavouriteStatusRouter == null) {
|
||||
application.mFavouriteStatusRouter = new FavouriteStatusRouter();
|
||||
}
|
||||
return application.mFavouriteStatusRouter;
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Override
|
||||
public void onCreate() {
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(new EhlifecycleObserver());
|
||||
instance = this;
|
||||
|
||||
Thread.UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler();
|
||||
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
|
||||
try {
|
||||
// Always save crash file if onCreate() is not done
|
||||
if (!initialized || Settings.getSaveCrashLog()) {
|
||||
Crash.saveCrashLog(instance, e);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
if (handler != null) {
|
||||
handler.uncaughtException(t, e);
|
||||
}
|
||||
});
|
||||
|
||||
super.onCreate();
|
||||
|
||||
Native.initialize();
|
||||
ClipboardUtil.initialize(this);
|
||||
GetText.initialize(this);
|
||||
StatusCodeException.initialize(this);
|
||||
Settings.initialize(this);
|
||||
ReadableTime.initialize(this);
|
||||
AppConfig.initialize(this);
|
||||
SpiderDen.initialize(this);
|
||||
EhDB.initialize(this);
|
||||
EhEngine.initialize();
|
||||
BitmapUtils.initialize(this);
|
||||
|
||||
if (EhDB.needMerge()) {
|
||||
EhDB.mergeOldDB(this);
|
||||
}
|
||||
|
||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.create(Settings.getLocale()));
|
||||
AppCompatDelegate.setDefaultNightMode(Settings.getTheme());
|
||||
|
||||
// Do io tasks in new thread
|
||||
//noinspection deprecation
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
// Check no media file
|
||||
try {
|
||||
UniFile downloadLocation = Settings.getDownloadLocation();
|
||||
if (Settings.getMediaScan()) {
|
||||
CommonOperations.removeNoMediaFile(downloadLocation);
|
||||
} else {
|
||||
CommonOperations.ensureNoMediaFile(downloadLocation);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
ExceptionUtils.throwIfFatal(t);
|
||||
}
|
||||
|
||||
// Clear temp files
|
||||
try {
|
||||
clearTempDir();
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
ExceptionUtils.throwIfFatal(t);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(IoThreadPoolExecutor.getInstance());
|
||||
|
||||
// Check app update
|
||||
update();
|
||||
|
||||
// Update version code
|
||||
try {
|
||||
PackageInfo pi = getPackageManager().getPackageInfo(getPackageName(), 0);
|
||||
Settings.putVersionCode(pi.versionCode);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
mIdGenerator.setNextId(Settings.getInt(KEY_GLOBAL_STUFF_NEXT_ID, 0));
|
||||
|
||||
if (DEBUG_PRINT_NATIVE_MEMORY || DEBUG_PRINT_IMAGE_COUNT) {
|
||||
debugPrint();
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
theDawnOfNewDay();
|
||||
}
|
||||
|
||||
private void theDawnOfNewDay() {
|
||||
if (!Settings.getRequestNews()) {
|
||||
return;
|
||||
}
|
||||
EhCookieStore store = getEhCookieStore(this);
|
||||
HttpUrl eh = HttpUrl.get(EhUrl.HOST_E);
|
||||
|
||||
if (store.contains(eh, EhCookieStore.KEY_IPD_MEMBER_ID) || store.contains(eh, EhCookieStore.KEY_IPD_PASS_HASH)) {
|
||||
IoThreadPoolExecutor.getInstance().execute(() -> {
|
||||
String referer = EhUrl.REFERER_E;
|
||||
Request request = new EhRequestBuilder(EhUrl.HOST_E + "news.php", referer).build();
|
||||
Call call = getOkHttpClient(this).newCall(request);
|
||||
try {
|
||||
Response response = call.execute();
|
||||
ResponseBody responseBody = response.body();
|
||||
if (responseBody == null) {
|
||||
return;
|
||||
}
|
||||
String body = responseBody.string();
|
||||
String html = EventPaneParser.parse(body);
|
||||
if (html != null) {
|
||||
showEventPane(html);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void showEventPane(String html) {
|
||||
Activity activity = getTopActivity();
|
||||
if (activity != null) {
|
||||
activity.runOnUiThread(() -> {
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(activity)
|
||||
.setMessage(Html.fromHtml(html))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.create();
|
||||
dialog.setOnShowListener(d -> {
|
||||
final View messageView = dialog.findViewById(android.R.id.message);
|
||||
if (messageView instanceof TextView) {
|
||||
((TextView) messageView).setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
});
|
||||
try {
|
||||
dialog.show();
|
||||
} catch (Throwable t) {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void clearTempDir() {
|
||||
File dir = AppConfig.getTempDir();
|
||||
if (null != dir) {
|
||||
FileUtils.deleteContent(dir);
|
||||
}
|
||||
dir = AppConfig.getExternalTempDir();
|
||||
if (null != dir) {
|
||||
FileUtils.deleteContent(dir);
|
||||
}
|
||||
}
|
||||
|
||||
private void update() {
|
||||
int version = Settings.getVersionCode();
|
||||
if (version < 52) {
|
||||
Settings.putGuideGallery(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearMemoryCache() {
|
||||
if (null != mConaco) {
|
||||
mConaco.getBeerBelly().clearMemory();
|
||||
}
|
||||
if (null != mGalleryDetailCache) {
|
||||
mGalleryDetailCache.evictAll();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrimMemory(int level) {
|
||||
super.onTrimMemory(level);
|
||||
|
||||
if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) {
|
||||
clearMemoryCache();
|
||||
}
|
||||
}
|
||||
|
||||
private void debugPrint() {
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (DEBUG_PRINT_NATIVE_MEMORY) {
|
||||
Log.i(TAG, "Native memory: " + FileUtils.humanReadableByteCount(
|
||||
Debug.getNativeHeapAllocatedSize(), false));
|
||||
}
|
||||
if (DEBUG_PRINT_IMAGE_COUNT) {
|
||||
Log.i(TAG, "Image count: " + Image.getImageCount());
|
||||
}
|
||||
SimpleHandler.getInstance().postDelayed(this, DEBUG_PRINT_INTERVAL);
|
||||
}
|
||||
}.run();
|
||||
}
|
||||
|
||||
public int putGlobalStuff(@NonNull Object o) {
|
||||
int id = mIdGenerator.nextId();
|
||||
mGlobalStuffMap.put(id, o);
|
||||
Settings.putInt(KEY_GLOBAL_STUFF_NEXT_ID, mIdGenerator.nextId());
|
||||
return id;
|
||||
}
|
||||
|
||||
public boolean containGlobalStuff(int id) {
|
||||
return mGlobalStuffMap.containsKey(id);
|
||||
}
|
||||
|
||||
public Object getGlobalStuff(int id) {
|
||||
return mGlobalStuffMap.get(id);
|
||||
}
|
||||
|
||||
public Object removeGlobalStuff(int id) {
|
||||
return mGlobalStuffMap.remove(id);
|
||||
}
|
||||
|
||||
public void removeGlobalStuff(Object o) {
|
||||
mGlobalStuffMap.values().removeAll(Collections.singleton(o));
|
||||
}
|
||||
|
||||
public void registerActivity(Activity activity) {
|
||||
mActivityList.add(activity);
|
||||
}
|
||||
|
||||
public void unregisterActivity(Activity activity) {
|
||||
mActivityList.remove(activity);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Activity getTopActivity() {
|
||||
if (!mActivityList.isEmpty()) {
|
||||
return mActivityList.get(mActivityList.size() - 1);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid crash on some "energy saving" devices
|
||||
@Override
|
||||
public ComponentName startService(Intent service) {
|
||||
try {
|
||||
return super.startService(service);
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
ExceptionUtils.throwIfFatal(t);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid crash on some "energy saving" devices
|
||||
@Override
|
||||
public boolean bindService(Intent service, ServiceConnection conn, int flags) {
|
||||
try {
|
||||
return super.bindService(service, conn, flags);
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
ExceptionUtils.throwIfFatal(t);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid crash on some "energy saving" devices
|
||||
@Override
|
||||
public void unbindService(ServiceConnection conn) {
|
||||
try {
|
||||
super.unbindService(conn);
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
ExceptionUtils.throwIfFatal(t);
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid crash on some "energy saving" devices
|
||||
@Override
|
||||
public ComponentName startForegroundService(Intent service) {
|
||||
try {
|
||||
return super.startForegroundService(service);
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
ExceptionUtils.throwIfFatal(t);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static class EhlifecycleObserver implements DefaultLifecycleObserver {
|
||||
@Override
|
||||
public void onPause(@NonNull LifecycleOwner owner) {
|
||||
locked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.ActivityManager
|
||||
import android.app.Application
|
||||
import android.content.ComponentCallbacks2
|
||||
import android.text.Html
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.collection.LruCache
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import com.hippo.Native
|
||||
import com.hippo.app.BaseDialogBuilder
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.EhDns
|
||||
import com.hippo.ehviewer.client.EhRequestBuilder
|
||||
import com.hippo.ehviewer.client.EhSSLSocketFactory
|
||||
import com.hippo.ehviewer.client.EhTagDatabase
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.data.GalleryDetail
|
||||
import com.hippo.ehviewer.client.parser.EventPaneParser
|
||||
import com.hippo.ehviewer.dao.buildMainDB
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
import com.hippo.ehviewer.ui.CommonOperations
|
||||
import com.hippo.ehviewer.ui.EhActivity
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import com.hippo.util.ReadableTime
|
||||
import com.hippo.yorozuya.FileUtils
|
||||
import com.hippo.yorozuya.IntIdGenerator
|
||||
import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import okhttp3.OkHttpClient
|
||||
import java.net.Proxy
|
||||
import java.security.KeyStore
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||
private val mIdGenerator = IntIdGenerator()
|
||||
private val mGlobalStuffMap = HashMap<Int, Any>()
|
||||
private val mActivityList = ArrayList<Activity>()
|
||||
val topActivity: EhActivity?
|
||||
get() = if (mActivityList.isNotEmpty()) {
|
||||
mActivityList[mActivityList.size - 1] as EhActivity
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun recreateAllActivity() {
|
||||
mActivityList.forEach { it.recreate() }
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
override fun onCreate() {
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
application = this
|
||||
val handler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
Thread.setDefaultUncaughtExceptionHandler { t, e ->
|
||||
try {
|
||||
if (Settings.getSaveCrashLog()) {
|
||||
Crash.saveCrashLog(application, e)
|
||||
}
|
||||
} catch (ignored: Throwable) {
|
||||
}
|
||||
handler?.uncaughtException(t, e)
|
||||
}
|
||||
super<Application>.onCreate()
|
||||
Native.initialize()
|
||||
GetText.initialize(this)
|
||||
Settings.initialize()
|
||||
ReadableTime.initialize(this)
|
||||
AppConfig.initialize(this)
|
||||
EhTagDatabase.update()
|
||||
AppCompatDelegate.setDefaultNightMode(Settings.getTheme())
|
||||
launchIO {
|
||||
launchIO {
|
||||
ehDatabase
|
||||
}
|
||||
launchIO {
|
||||
downloadManager
|
||||
}
|
||||
launchIO {
|
||||
cleanupDownload()
|
||||
}
|
||||
launchIO {
|
||||
theDawnOfNewDay()
|
||||
}
|
||||
}
|
||||
mIdGenerator.setNextId(Settings.getInt(KEY_GLOBAL_STUFF_NEXT_ID, 0))
|
||||
}
|
||||
|
||||
private fun cleanupDownload() {
|
||||
try {
|
||||
val downloadLocation = Settings.getDownloadLocation()
|
||||
if (Settings.getMediaScan()) {
|
||||
CommonOperations.removeNoMediaFile(downloadLocation)
|
||||
} else {
|
||||
CommonOperations.ensureNoMediaFile(downloadLocation)
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
t.printStackTrace()
|
||||
ExceptionUtils.throwIfFatal(t)
|
||||
}
|
||||
|
||||
// Clear temp files
|
||||
try {
|
||||
clearTempDir()
|
||||
} catch (t: Throwable) {
|
||||
t.printStackTrace()
|
||||
ExceptionUtils.throwIfFatal(t)
|
||||
}
|
||||
}
|
||||
|
||||
private fun theDawnOfNewDay() {
|
||||
if (!Settings.getRequestNews()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (ehCookieStore.hasSignedIn()) {
|
||||
val referer = EhUrl.REFERER_E
|
||||
val request = EhRequestBuilder(EhUrl.HOST_E + "news.php", referer).build()
|
||||
val call = okHttpClient.newCall(request)
|
||||
try {
|
||||
call.execute().use { response ->
|
||||
val responseBody = response.body
|
||||
val body = responseBody.string()
|
||||
val html = EventPaneParser.parse(body)
|
||||
if (html != null) {
|
||||
showEventPane(html)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showEventPane(html: String) {
|
||||
if (Settings.getHideHvEvents() && html.contains("You have encountered a monster!")) {
|
||||
return
|
||||
}
|
||||
val activity = topActivity
|
||||
activity?.runOnUiThread {
|
||||
val dialog = BaseDialogBuilder(activity)
|
||||
.setMessage(Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.create()
|
||||
dialog.setOnShowListener {
|
||||
val messageView = dialog.findViewById<View>(android.R.id.message)
|
||||
if (messageView is TextView) {
|
||||
messageView.movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
}
|
||||
try {
|
||||
dialog.show()
|
||||
} catch (t: Throwable) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearTempDir() {
|
||||
var dir = AppConfig.getTempDir()
|
||||
if (null != dir) {
|
||||
FileUtils.deleteContent(dir)
|
||||
}
|
||||
dir = AppConfig.getExternalTempDir()
|
||||
if (null != dir) {
|
||||
FileUtils.deleteContent(dir)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
super.onTrimMemory(level)
|
||||
if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) {
|
||||
galleryDetailCache.evictAll()
|
||||
}
|
||||
}
|
||||
|
||||
fun putGlobalStuff(o: Any): Int {
|
||||
val id = mIdGenerator.nextId()
|
||||
mGlobalStuffMap[id] = o
|
||||
Settings.putInt(KEY_GLOBAL_STUFF_NEXT_ID, mIdGenerator.nextId())
|
||||
return id
|
||||
}
|
||||
|
||||
fun containGlobalStuff(id: Int): Boolean {
|
||||
return mGlobalStuffMap.containsKey(id)
|
||||
}
|
||||
|
||||
fun getGlobalStuff(id: Int): Any? {
|
||||
return mGlobalStuffMap[id]
|
||||
}
|
||||
|
||||
fun removeGlobalStuff(id: Int): Any? {
|
||||
return mGlobalStuffMap.remove(id)
|
||||
}
|
||||
|
||||
fun removeGlobalStuff(o: Any) {
|
||||
mGlobalStuffMap.values.removeAll(setOf(o))
|
||||
}
|
||||
|
||||
fun registerActivity(activity: Activity) {
|
||||
mActivityList.add(activity)
|
||||
}
|
||||
|
||||
fun unregisterActivity(activity: Activity) {
|
||||
mActivityList.remove(activity)
|
||||
}
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
if (!locked)
|
||||
locked_last_leave_time = System.currentTimeMillis() / 1000
|
||||
locked = true
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
return ImageLoader.Builder(this).apply {
|
||||
okHttpClient(okHttpClient)
|
||||
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
||||
// Coil spawns a new thread for every image load by default
|
||||
fetcherDispatcher(Dispatchers.IO.limitedParallelism(8))
|
||||
decoderDispatcher(Dispatchers.IO.limitedParallelism(2))
|
||||
transformationDispatcher(Dispatchers.IO.limitedParallelism(2))
|
||||
crossfade(true)
|
||||
}.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_GLOBAL_STUFF_NEXT_ID = "global_stuff_next_id"
|
||||
var locked = true
|
||||
var locked_last_leave_time: Long = 0
|
||||
|
||||
@JvmStatic
|
||||
lateinit var application: EhApplication
|
||||
private set
|
||||
|
||||
@JvmStatic
|
||||
val ehCookieStore by lazy { EhCookieStore }
|
||||
|
||||
@JvmStatic
|
||||
val ehProxySelector by lazy { EhProxySelector() }
|
||||
|
||||
@JvmStatic
|
||||
val okHttpClient by lazy {
|
||||
val builder = OkHttpClient.Builder()
|
||||
.cookieJar(ehCookieStore)
|
||||
.dns(EhDns)
|
||||
.proxySelector(ehProxySelector)
|
||||
|
||||
if (Settings.getDF()) {
|
||||
val factory =
|
||||
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())!!
|
||||
factory.init(null as KeyStore?)
|
||||
val manager = factory.trustManagers!!
|
||||
|
||||
val trustManager = manager.filterIsInstance<X509TrustManager>().first()
|
||||
builder.sslSocketFactory(EhSSLSocketFactory, trustManager)
|
||||
builder.proxy(Proxy.NO_PROXY)
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val galleryDetailCache by lazy {
|
||||
LruCache<Long, GalleryDetail>(25).also {
|
||||
favouriteStatusRouter.addListener { gid, slot ->
|
||||
it[gid]?.favoriteSlot = slot
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val downloadManager by lazy { DownloadManager() }
|
||||
|
||||
@JvmStatic
|
||||
val hosts by lazy { Hosts(application, "hosts.db") }
|
||||
|
||||
@JvmStatic
|
||||
val favouriteStatusRouter by lazy { FavouriteStatusRouter() }
|
||||
|
||||
@JvmStatic
|
||||
val readerPreferences by lazy { ReaderPreferences(AndroidPreferenceStore(application)) }
|
||||
|
||||
@JvmStatic
|
||||
val ehDatabase by lazy { buildMainDB(application) }
|
||||
}
|
||||
}
|
||||
@@ -17,27 +17,24 @@
|
||||
package com.hippo.ehviewer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.paging.PagingSource;
|
||||
import androidx.room.Room;
|
||||
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo;
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder;
|
||||
import com.hippo.ehviewer.dao.DaoMaster;
|
||||
import com.hippo.ehviewer.dao.DaoSession;
|
||||
import com.hippo.ehviewer.dao.BasicDao;
|
||||
import com.hippo.ehviewer.dao.DownloadDirname;
|
||||
import com.hippo.ehviewer.dao.DownloadDirnameDao;
|
||||
import com.hippo.ehviewer.dao.DownloadInfo;
|
||||
import com.hippo.ehviewer.dao.DownloadLabel;
|
||||
import com.hippo.ehviewer.dao.DownloadLabelDao;
|
||||
import com.hippo.ehviewer.dao.DownloadsDao;
|
||||
import com.hippo.ehviewer.dao.EhDatabase;
|
||||
import com.hippo.ehviewer.dao.Filter;
|
||||
import com.hippo.ehviewer.dao.FilterDao;
|
||||
import com.hippo.ehviewer.dao.HistoryDao;
|
||||
import com.hippo.ehviewer.dao.HistoryInfo;
|
||||
import com.hippo.ehviewer.dao.LocalFavoriteInfo;
|
||||
@@ -46,14 +43,8 @@ import com.hippo.ehviewer.dao.QuickSearch;
|
||||
import com.hippo.ehviewer.dao.QuickSearchDao;
|
||||
import com.hippo.ehviewer.download.DownloadManager;
|
||||
import com.hippo.util.ExceptionUtils;
|
||||
import com.hippo.util.SqlUtils;
|
||||
import com.hippo.yorozuya.IOUtils;
|
||||
import com.hippo.yorozuya.ObjectUtils;
|
||||
import com.hippo.yorozuya.collect.SparseJLArray;
|
||||
|
||||
import org.greenrobot.greendao.AbstractDao;
|
||||
import org.greenrobot.greendao.query.CloseableListIterator;
|
||||
import org.greenrobot.greendao.query.LazyList;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
@@ -65,15 +56,9 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class EhDB {
|
||||
private static final int CUR_DB_VER = 4;
|
||||
|
||||
private static final String TAG = EhDB.class.getSimpleName();
|
||||
|
||||
private static final int MAX_HISTORY_COUNT = 200;
|
||||
|
||||
private static DaoSession sDaoSession;
|
||||
|
||||
private static boolean sHasOldDB;
|
||||
private static boolean sNewDB;
|
||||
private static final EhDatabase db = EhApplication.getEhDatabase();
|
||||
|
||||
private static void upgradeDB(SQLiteDatabase db, int oldVersion) {
|
||||
switch (oldVersion) {
|
||||
@@ -114,220 +99,9 @@ public class EhDB {
|
||||
}
|
||||
}
|
||||
|
||||
public static void initialize(Context context) {
|
||||
sHasOldDB = context.getDatabasePath("data").exists();
|
||||
|
||||
DBOpenHelper helper = new DBOpenHelper(
|
||||
context.getApplicationContext(), "eh.db", null);
|
||||
|
||||
SQLiteDatabase db = helper.getWritableDatabase();
|
||||
DaoMaster daoMaster = new DaoMaster(db);
|
||||
|
||||
sDaoSession = daoMaster.newSession();
|
||||
}
|
||||
|
||||
public static boolean needMerge() {
|
||||
return sNewDB && sHasOldDB;
|
||||
}
|
||||
|
||||
public static void mergeOldDB(Context context) {
|
||||
sNewDB = false;
|
||||
|
||||
OldDBHelper oldDBHelper = new OldDBHelper(context);
|
||||
SQLiteDatabase oldDB;
|
||||
try {
|
||||
oldDB = oldDBHelper.getReadableDatabase();
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get GalleryInfo list
|
||||
SparseJLArray<GalleryInfo> map = new SparseJLArray<>();
|
||||
try {
|
||||
Cursor cursor = oldDB.rawQuery("select * from " + OldDBHelper.TABLE_GALLERY, null);
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToFirst()) {
|
||||
while (!cursor.isAfterLast()) {
|
||||
GalleryInfo gi = new GalleryInfo();
|
||||
gi.gid = cursor.getInt(0);
|
||||
gi.token = cursor.getString(1);
|
||||
gi.title = cursor.getString(2);
|
||||
gi.posted = cursor.getString(3);
|
||||
gi.category = cursor.getInt(4);
|
||||
gi.thumb = cursor.getString(5);
|
||||
gi.uploader = cursor.getString(6);
|
||||
try {
|
||||
// In 0.6.x version, NaN is stored
|
||||
gi.rating = cursor.getFloat(7);
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
gi.rating = -1.0f;
|
||||
}
|
||||
|
||||
map.put(gi.gid, gi);
|
||||
|
||||
cursor.moveToNext();
|
||||
}
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
// Ignore
|
||||
}
|
||||
|
||||
// Merge local favorites
|
||||
try {
|
||||
Cursor cursor = oldDB.rawQuery("select * from " + OldDBHelper.TABLE_LOCAL_FAVOURITE, null);
|
||||
if (cursor != null) {
|
||||
LocalFavoritesDao dao = sDaoSession.getLocalFavoritesDao();
|
||||
if (cursor.moveToFirst()) {
|
||||
long i = 0L;
|
||||
while (!cursor.isAfterLast()) {
|
||||
// Get GalleryInfo first
|
||||
long gid = cursor.getInt(0);
|
||||
GalleryInfo gi = map.get(gid);
|
||||
if (gi == null) {
|
||||
Log.e(TAG, "Can't get GalleryInfo with gid: " + gid);
|
||||
cursor.moveToNext();
|
||||
continue;
|
||||
}
|
||||
|
||||
LocalFavoriteInfo info = new LocalFavoriteInfo(gi);
|
||||
info.setTime(i);
|
||||
dao.insert(info);
|
||||
cursor.moveToNext();
|
||||
i++;
|
||||
}
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
// Ignore
|
||||
}
|
||||
|
||||
|
||||
// Merge quick search
|
||||
try {
|
||||
Cursor cursor = oldDB.rawQuery("select * from " + OldDBHelper.TABLE_TAG, null);
|
||||
if (cursor != null) {
|
||||
QuickSearchDao dao = sDaoSession.getQuickSearchDao();
|
||||
if (cursor.moveToFirst()) {
|
||||
while (!cursor.isAfterLast()) {
|
||||
QuickSearch quickSearch = new QuickSearch();
|
||||
|
||||
int mode = cursor.getInt(2);
|
||||
String search = cursor.getString(4);
|
||||
String tag = cursor.getString(7);
|
||||
if (mode == ListUrlBuilder.MODE_UPLOADER && search != null &&
|
||||
search.startsWith("uploader:")) {
|
||||
search = search.substring("uploader:".length());
|
||||
}
|
||||
|
||||
quickSearch.setTime(cursor.getInt(0));
|
||||
quickSearch.setName(cursor.getString(1));
|
||||
quickSearch.setMode(mode);
|
||||
quickSearch.setCategory(cursor.getInt(3));
|
||||
quickSearch.setKeyword(mode == ListUrlBuilder.MODE_TAG ? tag : search);
|
||||
quickSearch.setAdvanceSearch(cursor.getInt(5));
|
||||
quickSearch.setMinRating(cursor.getInt(6));
|
||||
|
||||
dao.insert(quickSearch);
|
||||
cursor.moveToNext();
|
||||
}
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
// Ignore
|
||||
}
|
||||
|
||||
// Merge download info
|
||||
try {
|
||||
Cursor cursor = oldDB.rawQuery("select * from " + OldDBHelper.TABLE_DOWNLOAD, null);
|
||||
if (cursor != null) {
|
||||
DownloadsDao dao = sDaoSession.getDownloadsDao();
|
||||
if (cursor.moveToFirst()) {
|
||||
long i = 0L;
|
||||
while (!cursor.isAfterLast()) {
|
||||
// Get GalleryInfo first
|
||||
long gid = cursor.getInt(0);
|
||||
GalleryInfo gi = map.get(gid);
|
||||
if (gi == null) {
|
||||
Log.e(TAG, "Can't get GalleryInfo with gid: " + gid);
|
||||
cursor.moveToNext();
|
||||
continue;
|
||||
}
|
||||
|
||||
DownloadInfo info = new DownloadInfo(gi);
|
||||
int state = cursor.getInt(2);
|
||||
int legacy = cursor.getInt(3);
|
||||
if (state == DownloadInfo.STATE_FINISH && legacy > 0) {
|
||||
state = DownloadInfo.STATE_FAILED;
|
||||
}
|
||||
info.setState(state);
|
||||
info.setLegacy(legacy);
|
||||
if (cursor.getColumnCount() == 5) {
|
||||
info.setTime(cursor.getLong(4));
|
||||
} else {
|
||||
info.setTime(i);
|
||||
}
|
||||
dao.insert(info);
|
||||
cursor.moveToNext();
|
||||
i++;
|
||||
}
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
// Ignore
|
||||
}
|
||||
|
||||
try {
|
||||
// Merge history info
|
||||
Cursor cursor = oldDB.rawQuery("select * from " + OldDBHelper.TABLE_HISTORY, null);
|
||||
if (cursor != null) {
|
||||
HistoryDao dao = sDaoSession.getHistoryDao();
|
||||
if (cursor.moveToFirst()) {
|
||||
while (!cursor.isAfterLast()) {
|
||||
// Get GalleryInfo first
|
||||
long gid = cursor.getInt(0);
|
||||
GalleryInfo gi = map.get(gid);
|
||||
if (gi == null) {
|
||||
Log.e(TAG, "Can't get GalleryInfo with gid: " + gid);
|
||||
cursor.moveToNext();
|
||||
continue;
|
||||
}
|
||||
|
||||
HistoryInfo info = new HistoryInfo(gi);
|
||||
info.setMode(cursor.getInt(1));
|
||||
info.setTime(cursor.getLong(2));
|
||||
dao.insert(info);
|
||||
cursor.moveToNext();
|
||||
}
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
// Ignore
|
||||
}
|
||||
|
||||
try {
|
||||
oldDBHelper.close();
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized List<DownloadInfo> getAllDownloadInfo() {
|
||||
DownloadsDao dao = sDaoSession.getDownloadsDao();
|
||||
List<DownloadInfo> list = dao.queryBuilder().orderDesc(DownloadsDao.Properties.Time).list();
|
||||
DownloadsDao dao = db.downloadsDao();
|
||||
List<DownloadInfo> list = dao.list();
|
||||
// Fix state
|
||||
for (DownloadInfo info : list) {
|
||||
if (info.state == DownloadInfo.STATE_WAIT || info.state == DownloadInfo.STATE_DOWNLOAD) {
|
||||
@@ -337,10 +111,15 @@ public class EhDB {
|
||||
return list;
|
||||
}
|
||||
|
||||
public static synchronized void updateDownloadInfo(List<DownloadInfo> downloadInfos) {
|
||||
DownloadsDao dao = db.downloadsDao();
|
||||
dao.update(downloadInfos);
|
||||
}
|
||||
|
||||
// Insert or update
|
||||
public static synchronized void putDownloadInfo(DownloadInfo downloadInfo) {
|
||||
DownloadsDao dao = sDaoSession.getDownloadsDao();
|
||||
if (null != dao.load(downloadInfo.gid)) {
|
||||
DownloadsDao dao = db.downloadsDao();
|
||||
if (null != dao.load(downloadInfo.getGid())) {
|
||||
// Update
|
||||
dao.update(downloadInfo);
|
||||
} else {
|
||||
@@ -349,13 +128,13 @@ public class EhDB {
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void removeDownloadInfo(long gid) {
|
||||
sDaoSession.getDownloadsDao().deleteByKey(gid);
|
||||
public static synchronized void removeDownloadInfo(DownloadInfo downloadInfo) {
|
||||
db.downloadsDao().delete(downloadInfo);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static synchronized String getDownloadDirname(long gid) {
|
||||
DownloadDirnameDao dao = sDaoSession.getDownloadDirnameDao();
|
||||
DownloadDirnameDao dao = db.downloadDirnameDao();
|
||||
DownloadDirname raw = dao.load(gid);
|
||||
if (raw != null) {
|
||||
return raw.getDirname();
|
||||
@@ -368,7 +147,7 @@ public class EhDB {
|
||||
* Insert or update
|
||||
*/
|
||||
public static synchronized void putDownloadDirname(long gid, String dirname) {
|
||||
DownloadDirnameDao dao = sDaoSession.getDownloadDirnameDao();
|
||||
DownloadDirnameDao dao = db.downloadDirnameDao();
|
||||
DownloadDirname raw = dao.load(gid);
|
||||
if (raw != null) { // Update
|
||||
raw.setDirname(dirname);
|
||||
@@ -382,23 +161,23 @@ public class EhDB {
|
||||
}
|
||||
|
||||
public static synchronized void removeDownloadDirname(long gid) {
|
||||
DownloadDirnameDao dao = sDaoSession.getDownloadDirnameDao();
|
||||
DownloadDirnameDao dao = db.downloadDirnameDao();
|
||||
dao.deleteByKey(gid);
|
||||
}
|
||||
|
||||
public static synchronized void clearDownloadDirname() {
|
||||
DownloadDirnameDao dao = sDaoSession.getDownloadDirnameDao();
|
||||
DownloadDirnameDao dao = db.downloadDirnameDao();
|
||||
dao.deleteAll();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static synchronized List<DownloadLabel> getAllDownloadLabelList() {
|
||||
DownloadLabelDao dao = sDaoSession.getDownloadLabelDao();
|
||||
return dao.queryBuilder().orderAsc(DownloadLabelDao.Properties.Time).list();
|
||||
DownloadLabelDao dao = db.downloadLabelDao();
|
||||
return dao.list();
|
||||
}
|
||||
|
||||
public static synchronized DownloadLabel addDownloadLabel(String label) {
|
||||
DownloadLabelDao dao = sDaoSession.getDownloadLabelDao();
|
||||
DownloadLabelDao dao = db.downloadLabelDao();
|
||||
DownloadLabel raw = new DownloadLabel();
|
||||
raw.setLabel(label);
|
||||
raw.setTime(System.currentTimeMillis());
|
||||
@@ -409,13 +188,13 @@ public class EhDB {
|
||||
public static synchronized DownloadLabel addDownloadLabel(DownloadLabel raw) {
|
||||
// Reset id
|
||||
raw.setId(null);
|
||||
DownloadLabelDao dao = sDaoSession.getDownloadLabelDao();
|
||||
DownloadLabelDao dao = db.downloadLabelDao();
|
||||
raw.setId(dao.insert(raw));
|
||||
return raw;
|
||||
}
|
||||
|
||||
public static synchronized void updateDownloadLabel(DownloadLabel raw) {
|
||||
DownloadLabelDao dao = sDaoSession.getDownloadLabelDao();
|
||||
DownloadLabelDao dao = db.downloadLabelDao();
|
||||
dao.update(raw);
|
||||
}
|
||||
|
||||
@@ -428,9 +207,8 @@ public class EhDB {
|
||||
int offset = reverse ? toPosition : fromPosition;
|
||||
int limit = reverse ? fromPosition - toPosition + 1 : toPosition - fromPosition + 1;
|
||||
|
||||
DownloadLabelDao dao = sDaoSession.getDownloadLabelDao();
|
||||
List<DownloadLabel> list = dao.queryBuilder().orderAsc(DownloadLabelDao.Properties.Time)
|
||||
.offset(offset).limit(limit).list();
|
||||
DownloadLabelDao dao = db.downloadLabelDao();
|
||||
List<DownloadLabel> list = dao.list(offset, limit);
|
||||
|
||||
int step = reverse ? 1 : -1;
|
||||
int start = reverse ? limit - 1 : 0;
|
||||
@@ -441,51 +219,45 @@ public class EhDB {
|
||||
}
|
||||
list.get(start).setTime(toTime);
|
||||
|
||||
dao.updateInTx(list);
|
||||
dao.update(list);
|
||||
}
|
||||
|
||||
public static synchronized void removeDownloadLabel(DownloadLabel raw) {
|
||||
DownloadLabelDao dao = sDaoSession.getDownloadLabelDao();
|
||||
DownloadLabelDao dao = db.downloadLabelDao();
|
||||
dao.delete(raw);
|
||||
}
|
||||
|
||||
public static synchronized List<GalleryInfo> getAllLocalFavorites() {
|
||||
LocalFavoritesDao dao = sDaoSession.getLocalFavoritesDao();
|
||||
List<LocalFavoriteInfo> list = dao.queryBuilder().orderDesc(LocalFavoritesDao.Properties.Time).list();
|
||||
List<GalleryInfo> result = new ArrayList<>();
|
||||
result.addAll(list);
|
||||
return result;
|
||||
LocalFavoritesDao dao = db.localFavoritesDao();
|
||||
List<LocalFavoriteInfo> list = dao.list();
|
||||
return new ArrayList<>(list);
|
||||
}
|
||||
|
||||
public static synchronized List<GalleryInfo> searchLocalFavorites(String query) {
|
||||
query = SqlUtils.sqlEscapeString("%" + query + "%");
|
||||
LocalFavoritesDao dao = sDaoSession.getLocalFavoritesDao();
|
||||
List<LocalFavoriteInfo> list = dao.queryBuilder().orderDesc(LocalFavoritesDao.Properties.Time)
|
||||
.where(LocalFavoritesDao.Properties.Title.like(query)).list();
|
||||
List<GalleryInfo> result = new ArrayList<>();
|
||||
result.addAll(list);
|
||||
return result;
|
||||
LocalFavoritesDao dao = db.localFavoritesDao();
|
||||
List<LocalFavoriteInfo> list = dao.list("%" + query + "%");
|
||||
return new ArrayList<>(list);
|
||||
}
|
||||
|
||||
public static synchronized void removeLocalFavorites(long gid) {
|
||||
sDaoSession.getLocalFavoritesDao().deleteByKey(gid);
|
||||
db.localFavoritesDao().deleteByKey(gid);
|
||||
}
|
||||
|
||||
public static synchronized void removeLocalFavorites(long[] gidArray) {
|
||||
LocalFavoritesDao dao = sDaoSession.getLocalFavoritesDao();
|
||||
LocalFavoritesDao dao = db.localFavoritesDao();
|
||||
for (long gid : gidArray) {
|
||||
dao.deleteByKey(gid);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized boolean containLocalFavorites(long gid) {
|
||||
LocalFavoritesDao dao = sDaoSession.getLocalFavoritesDao();
|
||||
LocalFavoritesDao dao = db.localFavoritesDao();
|
||||
return null != dao.load(gid);
|
||||
}
|
||||
|
||||
public static synchronized void putLocalFavorites(GalleryInfo galleryInfo) {
|
||||
LocalFavoritesDao dao = sDaoSession.getLocalFavoritesDao();
|
||||
if (null == dao.load(galleryInfo.gid)) {
|
||||
LocalFavoritesDao dao = db.localFavoritesDao();
|
||||
if (null == dao.load(galleryInfo.getGid())) {
|
||||
LocalFavoriteInfo info;
|
||||
if (galleryInfo instanceof LocalFavoriteInfo) {
|
||||
info = (LocalFavoriteInfo) galleryInfo;
|
||||
@@ -504,31 +276,26 @@ public class EhDB {
|
||||
}
|
||||
|
||||
public static synchronized List<QuickSearch> getAllQuickSearch() {
|
||||
QuickSearchDao dao = sDaoSession.getQuickSearchDao();
|
||||
return dao.queryBuilder().orderAsc(QuickSearchDao.Properties.Time).list();
|
||||
QuickSearchDao dao = db.quickSearchDao();
|
||||
return dao.list();
|
||||
}
|
||||
|
||||
public static synchronized void insertQuickSearch(QuickSearch quickSearch) {
|
||||
QuickSearchDao dao = sDaoSession.getQuickSearchDao();
|
||||
quickSearch.id = null;
|
||||
quickSearch.time = System.currentTimeMillis();
|
||||
quickSearch.id = dao.insert(quickSearch);
|
||||
QuickSearchDao dao = db.quickSearchDao();
|
||||
quickSearch.setId(null);
|
||||
quickSearch.setTime(System.currentTimeMillis());
|
||||
quickSearch.setId(dao.insert(quickSearch));
|
||||
}
|
||||
|
||||
public static synchronized void importQuickSearch(List<QuickSearch> quickSearchList) {
|
||||
QuickSearchDao dao = sDaoSession.getQuickSearchDao();
|
||||
QuickSearchDao dao = db.quickSearchDao();
|
||||
for (QuickSearch quickSearch : quickSearchList) {
|
||||
dao.insert(quickSearch);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void updateQuickSearch(QuickSearch quickSearch) {
|
||||
QuickSearchDao dao = sDaoSession.getQuickSearchDao();
|
||||
dao.update(quickSearch);
|
||||
}
|
||||
|
||||
public static synchronized void deleteQuickSearch(QuickSearch quickSearch) {
|
||||
QuickSearchDao dao = sDaoSession.getQuickSearchDao();
|
||||
QuickSearchDao dao = db.quickSearchDao();
|
||||
dao.delete(quickSearch);
|
||||
}
|
||||
|
||||
@@ -541,9 +308,8 @@ public class EhDB {
|
||||
int offset = reverse ? toPosition : fromPosition;
|
||||
int limit = reverse ? fromPosition - toPosition + 1 : toPosition - fromPosition + 1;
|
||||
|
||||
QuickSearchDao dao = sDaoSession.getQuickSearchDao();
|
||||
List<QuickSearch> list = dao.queryBuilder().orderAsc(QuickSearchDao.Properties.Time)
|
||||
.offset(offset).limit(limit).list();
|
||||
QuickSearchDao dao = db.quickSearchDao();
|
||||
List<QuickSearch> list = dao.list(offset, limit);
|
||||
|
||||
int step = reverse ? 1 : -1;
|
||||
int start = reverse ? limit - 1 : 0;
|
||||
@@ -554,68 +320,77 @@ public class EhDB {
|
||||
}
|
||||
list.get(start).setTime(toTime);
|
||||
|
||||
dao.updateInTx(list);
|
||||
dao.update(list);
|
||||
}
|
||||
|
||||
public static synchronized LazyList<HistoryInfo> getHistoryLazyList() {
|
||||
return sDaoSession.getHistoryDao().queryBuilder().orderDesc(HistoryDao.Properties.Time).listLazy();
|
||||
public static synchronized PagingSource<Integer, HistoryInfo> getHistoryLazyList() {
|
||||
return db.historyDao().listLazy();
|
||||
}
|
||||
|
||||
public static synchronized void putHistoryInfo(GalleryInfo galleryInfo) {
|
||||
HistoryDao dao = sDaoSession.getHistoryDao();
|
||||
HistoryInfo info = dao.load(galleryInfo.gid);
|
||||
if (null != info) {
|
||||
// Update time
|
||||
info.time = System.currentTimeMillis();
|
||||
HistoryDao dao = db.historyDao();
|
||||
HistoryInfo info;
|
||||
if (galleryInfo instanceof HistoryInfo) {
|
||||
info = (HistoryInfo) galleryInfo;
|
||||
} else {
|
||||
info = new HistoryInfo(galleryInfo);
|
||||
}
|
||||
info.time = System.currentTimeMillis();
|
||||
if (null != dao.load(info.getGid())) {
|
||||
dao.update(info);
|
||||
} else {
|
||||
// New history
|
||||
info = new HistoryInfo(galleryInfo);
|
||||
info.time = System.currentTimeMillis();
|
||||
dao.insert(info);
|
||||
List<HistoryInfo> list = dao.queryBuilder().orderDesc(HistoryDao.Properties.Time)
|
||||
.limit(-1).offset(MAX_HISTORY_COUNT).list();
|
||||
dao.deleteInTx(list);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void putHistoryInfoNonRefresh(GalleryInfo info) {
|
||||
HistoryDao dao = db.historyDao();
|
||||
var i = dao.load(info.getGid());
|
||||
if (null != i) {
|
||||
HistoryInfo historyInfo;
|
||||
if (info instanceof HistoryInfo) {
|
||||
historyInfo = (HistoryInfo) info;
|
||||
} else {
|
||||
historyInfo = new HistoryInfo(info);
|
||||
historyInfo.time = i.time;
|
||||
}
|
||||
dao.update(historyInfo);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void putHistoryInfo(List<HistoryInfo> historyInfoList) {
|
||||
HistoryDao dao = sDaoSession.getHistoryDao();
|
||||
HistoryDao dao = db.historyDao();
|
||||
for (HistoryInfo info : historyInfoList) {
|
||||
if (null == dao.load(info.gid)) {
|
||||
if (null == dao.load(info.getGid())) {
|
||||
dao.insert(info);
|
||||
}
|
||||
}
|
||||
|
||||
List<HistoryInfo> list = dao.queryBuilder().orderDesc(HistoryDao.Properties.Time)
|
||||
.limit(-1).offset(MAX_HISTORY_COUNT).list();
|
||||
dao.deleteInTx(list);
|
||||
}
|
||||
|
||||
public static synchronized void deleteHistoryInfo(HistoryInfo info) {
|
||||
HistoryDao dao = sDaoSession.getHistoryDao();
|
||||
HistoryDao dao = db.historyDao();
|
||||
dao.delete(info);
|
||||
}
|
||||
|
||||
public static synchronized void clearHistoryInfo() {
|
||||
HistoryDao dao = sDaoSession.getHistoryDao();
|
||||
HistoryDao dao = db.historyDao();
|
||||
dao.deleteAll();
|
||||
}
|
||||
|
||||
public static synchronized List<Filter> getAllFilter() {
|
||||
return sDaoSession.getFilterDao().queryBuilder().list();
|
||||
return db.filterDao().list();
|
||||
}
|
||||
|
||||
public static synchronized boolean addFilter(Filter filter) {
|
||||
Filter existFilter;
|
||||
try {
|
||||
existFilter = sDaoSession.getFilterDao().queryBuilder().where(FilterDao.Properties.Text.eq(filter.text), FilterDao.Properties.Mode.eq(filter.mode)).unique();
|
||||
existFilter = db.filterDao().load(filter.text, filter.mode);
|
||||
} catch (Exception e) {
|
||||
existFilter = null;
|
||||
}
|
||||
if (existFilter == null) {
|
||||
filter.setId(null);
|
||||
filter.setId(sDaoSession.getFilterDao().insert(filter));
|
||||
filter.setId(db.filterDao().insert(filter));
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
@@ -623,23 +398,18 @@ public class EhDB {
|
||||
}
|
||||
|
||||
public static synchronized void deleteFilter(Filter filter) {
|
||||
sDaoSession.getFilterDao().delete(filter);
|
||||
db.filterDao().delete(filter);
|
||||
}
|
||||
|
||||
public static synchronized void triggerFilter(Filter filter) {
|
||||
filter.setEnable(!filter.enable);
|
||||
sDaoSession.getFilterDao().update(filter);
|
||||
filter.enable = !filter.enable;
|
||||
db.filterDao().update(filter);
|
||||
}
|
||||
|
||||
private static <T> boolean copyDao(AbstractDao<T, ?> from, AbstractDao<T, ?> to) {
|
||||
try (CloseableListIterator<T> iterator = from.queryBuilder().listIterator()) {
|
||||
while (iterator.hasNext()) {
|
||||
to.insert(iterator.next());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
private static <T> void copyDao(BasicDao<T> from, BasicDao<T> to) {
|
||||
List<T> list = from.list();
|
||||
for (T item : list)
|
||||
to.insert(item);
|
||||
}
|
||||
|
||||
public static synchronized boolean exportDB(Context context, Uri uri) {
|
||||
@@ -647,30 +417,21 @@ public class EhDB {
|
||||
|
||||
// Delete old export db
|
||||
context.deleteDatabase(ehExportName);
|
||||
|
||||
DBOpenHelper helper = new DBOpenHelper(context.getApplicationContext(), ehExportName, null);
|
||||
EhDatabase newDb = Room.databaseBuilder(context, EhDatabase.class, ehExportName).allowMainThreadQueries().build();
|
||||
|
||||
try {
|
||||
// Copy data to a export db
|
||||
try (SQLiteDatabase db = helper.getWritableDatabase()) {
|
||||
DaoMaster daoMaster = new DaoMaster(db);
|
||||
DaoSession exportSession = daoMaster.newSession();
|
||||
if (!copyDao(sDaoSession.getDownloadsDao(), exportSession.getDownloadsDao()))
|
||||
return false;
|
||||
if (!copyDao(sDaoSession.getDownloadLabelDao(), exportSession.getDownloadLabelDao()))
|
||||
return false;
|
||||
if (!copyDao(sDaoSession.getDownloadDirnameDao(), exportSession.getDownloadDirnameDao()))
|
||||
return false;
|
||||
if (!copyDao(sDaoSession.getHistoryDao(), exportSession.getHistoryDao()))
|
||||
return false;
|
||||
if (!copyDao(sDaoSession.getQuickSearchDao(), exportSession.getQuickSearchDao()))
|
||||
return false;
|
||||
if (!copyDao(sDaoSession.getLocalFavoritesDao(), exportSession.getLocalFavoritesDao()))
|
||||
return false;
|
||||
if (!copyDao(sDaoSession.getBookmarksDao(), exportSession.getBookmarksDao()))
|
||||
return false;
|
||||
if (!copyDao(sDaoSession.getFilterDao(), exportSession.getFilterDao()))
|
||||
return false;
|
||||
copyDao(db.downloadsDao(), newDb.downloadsDao());
|
||||
copyDao(db.downloadLabelDao(), newDb.downloadLabelDao());
|
||||
copyDao(db.downloadDirnameDao(), newDb.downloadDirnameDao());
|
||||
copyDao(db.historyDao(), newDb.historyDao());
|
||||
copyDao(db.quickSearchDao(), newDb.quickSearchDao());
|
||||
copyDao(db.localFavoritesDao(), newDb.localFavoritesDao());
|
||||
copyDao(db.filterDao(), newDb.filterDao());
|
||||
|
||||
// Close export db so we can copy it
|
||||
if (newDb.isOpen()) {
|
||||
newDb.close();
|
||||
}
|
||||
|
||||
// Copy export db to data dir
|
||||
@@ -691,6 +452,7 @@ public class EhDB {
|
||||
IOUtils.closeQuietly(is);
|
||||
IOUtils.closeQuietly(os);
|
||||
}
|
||||
|
||||
// Delete failed file
|
||||
return false;
|
||||
} finally {
|
||||
@@ -704,6 +466,8 @@ public class EhDB {
|
||||
public static synchronized String importDB(Context context, Uri uri) {
|
||||
try {
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(uri);
|
||||
|
||||
// Copy file to cache dir
|
||||
File file = File.createTempFile("importDatabase", "");
|
||||
FileOutputStream outputStream = new FileOutputStream(file);
|
||||
byte[] buff = new byte[1024];
|
||||
@@ -718,77 +482,105 @@ public class EhDB {
|
||||
inputStream.close();
|
||||
outputStream.close();
|
||||
|
||||
SQLiteDatabase db = SQLiteDatabase.openDatabase(
|
||||
// Check database version
|
||||
SQLiteDatabase oldDB = SQLiteDatabase.openDatabase(
|
||||
file.getPath(), null, SQLiteDatabase.NO_LOCALIZED_COLLATORS);
|
||||
int newVersion = DaoMaster.SCHEMA_VERSION;
|
||||
int oldVersion = db.getVersion();
|
||||
int newVersion = CUR_DB_VER;
|
||||
int oldVersion = oldDB.getVersion();
|
||||
if (oldVersion < newVersion) {
|
||||
upgradeDB(db, oldVersion);
|
||||
db.setVersion(newVersion);
|
||||
upgradeDB(oldDB, oldVersion);
|
||||
oldDB.setVersion(newVersion);
|
||||
} else if (oldVersion > newVersion) {
|
||||
return context.getString(R.string.cant_read_the_file);
|
||||
return context.getString(R.string.db_version_too_high);
|
||||
}
|
||||
|
||||
DaoMaster daoMaster = new DaoMaster(db);
|
||||
DaoSession session = daoMaster.newSession();
|
||||
|
||||
// Downloads
|
||||
DownloadManager manager = EhApplication.getDownloadManager(context);
|
||||
List<DownloadInfo> downloadInfoList = session.getDownloadsDao().queryBuilder().list();
|
||||
manager.addDownload(downloadInfoList, false);
|
||||
// Crete temp room database from cache file
|
||||
String tmpDBName = "tmp.db";
|
||||
context.deleteDatabase(tmpDBName);
|
||||
EhDatabase oldRoomDatabase = Room.databaseBuilder(context, EhDatabase.class, tmpDBName)
|
||||
.createFromFile(file).allowMainThreadQueries().build();
|
||||
|
||||
// Download label
|
||||
List<DownloadLabel> downloadLabelList = session.getDownloadLabelDao().queryBuilder().list();
|
||||
manager.addDownloadLabel(downloadLabelList);
|
||||
DownloadManager manager = EhApplication.getDownloadManager();
|
||||
try {
|
||||
List<DownloadLabel> downloadLabelList = oldRoomDatabase.downloadLabelDao().list();
|
||||
manager.addDownloadLabel(downloadLabelList);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// Downloads
|
||||
try {
|
||||
List<DownloadInfo> downloadInfoList = oldRoomDatabase.downloadsDao().list();
|
||||
manager.addDownload(downloadInfoList, false);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// Download dirname
|
||||
List<DownloadDirname> downloadDirnameList = session.getDownloadDirnameDao().queryBuilder().list();
|
||||
for (DownloadDirname dirname : downloadDirnameList) {
|
||||
putDownloadDirname(dirname.getGid(), dirname.getDirname());
|
||||
try {
|
||||
List<DownloadDirname> downloadDirnameList = oldRoomDatabase.downloadDirnameDao().list();
|
||||
for (DownloadDirname dirname : downloadDirnameList) {
|
||||
putDownloadDirname(dirname.getGid(), dirname.getDirname());
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// History
|
||||
List<HistoryInfo> historyInfoList = session.getHistoryDao().queryBuilder().list();
|
||||
putHistoryInfo(historyInfoList);
|
||||
try {
|
||||
List<HistoryInfo> historyInfoList = oldRoomDatabase.historyDao().list();
|
||||
putHistoryInfo(historyInfoList);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// QuickSearch
|
||||
List<QuickSearch> quickSearchList = session.getQuickSearchDao().queryBuilder().list();
|
||||
List<QuickSearch> currentQuickSearchList = sDaoSession.getQuickSearchDao().queryBuilder().list();
|
||||
List<QuickSearch> importList = new ArrayList<>();
|
||||
for (QuickSearch quickSearch : quickSearchList) {
|
||||
String name = quickSearch.name;
|
||||
for (QuickSearch q : currentQuickSearchList) {
|
||||
if (ObjectUtils.equal(q.name, name)) {
|
||||
// The same name
|
||||
name = null;
|
||||
break;
|
||||
try {
|
||||
List<QuickSearch> quickSearchList = oldRoomDatabase.quickSearchDao().list();
|
||||
List<QuickSearch> currentQuickSearchList = db.quickSearchDao().list();
|
||||
List<QuickSearch> importList = new ArrayList<>();
|
||||
for (QuickSearch quickSearch : quickSearchList) {
|
||||
String name = quickSearch.getName();
|
||||
for (QuickSearch q : currentQuickSearchList) {
|
||||
if (ObjectUtils.equal(q.getName(), name)) {
|
||||
// The same name
|
||||
name = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (null == name) {
|
||||
continue;
|
||||
}
|
||||
importList.add(quickSearch);
|
||||
}
|
||||
if (null == name) {
|
||||
continue;
|
||||
}
|
||||
importList.add(quickSearch);
|
||||
importQuickSearch(importList);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
importQuickSearch(importList);
|
||||
|
||||
// LocalFavorites
|
||||
List<LocalFavoriteInfo> localFavoriteInfoList = session.getLocalFavoritesDao().queryBuilder().list();
|
||||
for (LocalFavoriteInfo info : localFavoriteInfoList) {
|
||||
putLocalFavorites(info);
|
||||
try {
|
||||
List<LocalFavoriteInfo> localFavoriteInfoList = oldRoomDatabase.localFavoritesDao().list();
|
||||
for (LocalFavoriteInfo info : localFavoriteInfoList) {
|
||||
putLocalFavorites(info);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// Bookmarks
|
||||
// TODO
|
||||
|
||||
// Filter
|
||||
List<Filter> filterList = session.getFilterDao().queryBuilder().list();
|
||||
List<Filter> currentFilterList = sDaoSession.getFilterDao().queryBuilder().list();
|
||||
for (Filter filter : filterList) {
|
||||
if (!currentFilterList.contains(filter)) {
|
||||
addFilter(filter);
|
||||
try {
|
||||
List<Filter> filterList = oldRoomDatabase.filterDao().list();
|
||||
List<Filter> currentFilterList = db.filterDao().list();
|
||||
for (Filter filter : filterList) {
|
||||
if (!currentFilterList.contains(filter)) {
|
||||
addFilter(filter);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// Delete temp database
|
||||
if (oldRoomDatabase.isOpen()) {
|
||||
oldRoomDatabase.close();
|
||||
}
|
||||
context.deleteDatabase(tmpDBName);
|
||||
|
||||
return null;
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
@@ -796,46 +588,4 @@ public class EhDB {
|
||||
return context.getString(R.string.cant_read_the_file);
|
||||
}
|
||||
}
|
||||
|
||||
private static class DBOpenHelper extends DaoMaster.OpenHelper {
|
||||
|
||||
public DBOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory) {
|
||||
super(context, name, factory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
super.onCreate(db);
|
||||
sNewDB = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
upgradeDB(db, oldVersion);
|
||||
}
|
||||
}
|
||||
|
||||
private static class OldDBHelper extends SQLiteOpenHelper {
|
||||
|
||||
private static final String DB_NAME = "data";
|
||||
private static final int VERSION = 4;
|
||||
|
||||
private static final String TABLE_GALLERY = "gallery";
|
||||
private static final String TABLE_LOCAL_FAVOURITE = "local_favourite";
|
||||
private static final String TABLE_TAG = "tag";
|
||||
private static final String TABLE_DOWNLOAD = "download";
|
||||
private static final String TABLE_HISTORY = "history";
|
||||
|
||||
public OldDBHelper(Context context) {
|
||||
super(context, DB_NAME, null, VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ public class FavouriteStatusRouter {
|
||||
for (Map<Long, GalleryInfo> map : maps.values()) {
|
||||
GalleryInfo info = map.get(gid);
|
||||
if (info != null) {
|
||||
info.favoriteSlot = slot;
|
||||
info.setFavoriteSlot(slot);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -291,7 +291,7 @@ public class Hosts {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean contains(String host) {
|
||||
public boolean contains(String host) {
|
||||
try (Cursor cursor = db.rawQuery("SELECT * FROM " + TABLE_HOSTS + " WHERE " + COLUMN_HOST + " = ?;", new String[]{host})) {
|
||||
return cursor.moveToNext();
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.conaco.ValueHelper;
|
||||
import com.hippo.image.ImageBitmap;
|
||||
import com.hippo.streampipe.InputStreamPipe;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class ImageBitmapHelper implements ValueHelper<ImageBitmap> {
|
||||
|
||||
private static final int MAX_CACHE_SIZE = 512 * 512;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ImageBitmap decode(@NonNull InputStreamPipe isPipe) {
|
||||
try {
|
||||
isPipe.obtain();
|
||||
return ImageBitmap.decode(isPipe.open());
|
||||
} catch (OutOfMemoryError | IOException e) {
|
||||
return null;
|
||||
} finally {
|
||||
isPipe.close();
|
||||
isPipe.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int sizeOf(@NonNull String key, @NonNull ImageBitmap value) {
|
||||
return value.getWidth() * value.getHeight() * 4 /* value.getByteCount() TODO Update Image */;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddToMemoryCache(@NonNull ImageBitmap oldValue) {
|
||||
oldValue.obtain();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoveFromMemoryCache(@NonNull String key, @NonNull ImageBitmap oldValue) {
|
||||
oldValue.release();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useMemoryCache(@NonNull String key, ImageBitmap value) {
|
||||
if (value != null) {
|
||||
return value.getWidth() * value.getHeight() <= MAX_CACHE_SIZE
|
||||
/* value.getByteCount() <= MAX_CACHE_BYTE_COUNT TODO Update Image */;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package com.hippo.ehviewer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
@@ -27,19 +26,13 @@ import androidx.annotation.Nullable;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.hippo.ehviewer.client.EhConfig;
|
||||
import com.hippo.ehviewer.client.EhUtils;
|
||||
import com.hippo.ehviewer.client.data.FavListUrlBuilder;
|
||||
import com.hippo.ehviewer.ui.CommonOperations;
|
||||
import com.hippo.ehviewer.ui.scene.GalleryListScene;
|
||||
import com.hippo.glgallery.GalleryView;
|
||||
import com.hippo.unifile.UniFile;
|
||||
import com.hippo.util.ExceptionUtils;
|
||||
import com.hippo.yorozuya.AssertUtils;
|
||||
import com.hippo.yorozuya.FileUtils;
|
||||
import com.hippo.yorozuya.MathUtils;
|
||||
import com.hippo.yorozuya.NumberUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -50,21 +43,36 @@ public class Settings {
|
||||
********************/
|
||||
|
||||
public static final String KEY_THEME = "theme";
|
||||
|
||||
public static final String KEY_ACCOUNT = "account";
|
||||
public static final String KEY_IMAGE_LIMITS = "image_limits";
|
||||
public static final String KEY_U_CONFIG = "uconfig";
|
||||
public static final String KEY_MY_TAGS = "mytags";
|
||||
public static final String KEY_BLACK_DARK_THEME = "black_dark_theme";
|
||||
public static final int THEME_LIGHT = 1;
|
||||
public static final int THEME_SYSTEM = -1;
|
||||
public static final int THEME_BLACK = 2;
|
||||
public static final String KEY_APPLY_NAV_BAR_THEME_COLOR = "apply_nav_bar_theme_color";
|
||||
public static final String KEY_GALLERY_SITE = "gallery_site";
|
||||
public static final String KEY_LIST_MODE = "list_mode";
|
||||
public static final String KEY_DETAIL_SIZE = "detail_size";
|
||||
public static final String KEY_THUMB_SIZE = "thumb_size";
|
||||
public static final String KEY_THUMB_SIZE = "thumb_size_";
|
||||
public static final String KEY_THUMB_RESOLUTION = "thumb_resolution";
|
||||
public static final String KEY_SHOW_JPN_TITLE = "show_jpn_title";
|
||||
public static final String KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations";
|
||||
public static final String KEY_TAG_TRANSLATIONS_SOURCE = "tag_translations_source";
|
||||
public static final String KEY_DEFAULT_CATEGORIES = "default_categories";
|
||||
public static final int DEFAULT_DEFAULT_CATEGORIES = EhUtils.ALL_CATEGORY;
|
||||
public static final int DEFAULT_DEFAULT_CATEGORIES = EhConfig.ALL_CATEGORY;
|
||||
public static final String KEY_EXCLUDED_TAG_NAMESPACES = "excluded_tag_namespaces";
|
||||
public static final String KEY_EXCLUDED_LANGUAGES = "excluded_languages";
|
||||
public static final String KEY_REQUEST_NEWS = "request_news";
|
||||
public static final String KEY_HIDE_HV_EVENTS = "hide_hv_events";
|
||||
public static final String[] SIGN_IN_REQUIRED = {
|
||||
KEY_IMAGE_LIMITS,
|
||||
KEY_GALLERY_SITE,
|
||||
KEY_U_CONFIG,
|
||||
KEY_MY_TAGS,
|
||||
KEY_SHOW_JPN_TITLE,
|
||||
KEY_REQUEST_NEWS,
|
||||
KEY_HIDE_HV_EVENTS
|
||||
};
|
||||
/********************
|
||||
****** Privacy and Security
|
||||
********************/
|
||||
@@ -76,54 +84,47 @@ public class Settings {
|
||||
public static final String KEY_DOWNLOAD_SAVE_SCHEME = "image_scheme";
|
||||
public static final String KEY_DOWNLOAD_SAVE_AUTHORITY = "image_authority";
|
||||
public static final String KEY_DOWNLOAD_SAVE_PATH = "image_path";
|
||||
public static final String KEY_NOTIFICATION_REQUIRED = "notification_required";
|
||||
public static final String KEY_DOWNLOAD_SAVE_QUERY = "image_query";
|
||||
public static final String KEY_DOWNLOAD_SAVE_FRAGMENT = "image_fragment";
|
||||
public static final String KEY_MEDIA_SCAN = "media_scan";
|
||||
public static final String KEY_IMAGE_RESOLUTION = "image_size";
|
||||
public static final String DEFAULT_IMAGE_RESOLUTION = EhConfig.IMAGE_SIZE_AUTO;
|
||||
public static final int INVALID_DEFAULT_FAV_SLOT = -2;
|
||||
public static final String KEY_ENABLE_ANALYTICS = "enable_analytics";
|
||||
/********************
|
||||
****** Advanced
|
||||
********************/
|
||||
public static final String KEY_SAVE_PARSE_ERROR_BODY = "save_parse_error_body";
|
||||
public static final String KEY_SECURITY = "require_unlock";
|
||||
public static final String DEFAULT_SECURITY = "";
|
||||
public static final String KEY_ENABLE_FINGERPRINT = "enable_fingerprint";
|
||||
public static final String KEY_SECURITY_DELAY = "require_unlock_delay";
|
||||
public static final String KEY_READ_CACHE_SIZE = "read_cache_size";
|
||||
public static final int DEFAULT_READ_CACHE_SIZE = 320;
|
||||
public static final String KEY_BUILT_IN_HOSTS = "built_in_hosts_2";
|
||||
public static final String KEY_DOMAIN_FRONTING = "domain_fronting";
|
||||
public static final String KEY_APP_LANGUAGE = "app_language";
|
||||
public static final String KEY_BYPASS_VPN = "bypass_vpn";
|
||||
public static final String KEY_LIST_THUMB_SIZE = "list_tile_size";
|
||||
private static final boolean DEFAULT_HIDE_HV_EVENTS = true;
|
||||
private static final String KEY_SHOW_COMMENTS = "show_gallery_comments";
|
||||
private static final boolean DEFAULT_SHOW_COMMENTS = true;
|
||||
private static final String TAG = Settings.class.getSimpleName();
|
||||
private static final String KEY_VERSION_CODE = "version_code";
|
||||
private static final int DEFAULT_VERSION_CODE = 0;
|
||||
private static final String KEY_DISPLAY_NAME = "display_name";
|
||||
private static final String DEFAULT_DISPLAY_NAME = null;
|
||||
private static final int DEFAULT_LIST_THUMB_SIZE = 40;
|
||||
private static final String KEY_AVATAR = "avatar";
|
||||
private static final String DEFAULT_AVATAR = null;
|
||||
private static final String KEY_SHOW_WARNING = "show_warning";
|
||||
private static final boolean DEFAULT_SHOW_WARNING = true;
|
||||
private static final String KEY_REMOVE_IMAGE_FILES = "include_pic";
|
||||
private static final boolean DEFAULT_REMOVE_IMAGE_FILES = true;
|
||||
private static final String KEY_NEED_SIGN_IN = "need_sign_in";
|
||||
private static final boolean DEFAULT_NEED_SIGN_IN = true;
|
||||
private static final String KEY_SELECT_SITE = "select_site";
|
||||
private static final boolean DEFAULT_SELECT_SITE = true;
|
||||
private static final String KEY_QUICK_SEARCH_TIP = "quick_search_tip";
|
||||
private static final boolean DEFAULT_QUICK_SEARCH_TIP = true;
|
||||
private static final int DEFAULT_THEME = THEME_SYSTEM;
|
||||
private static final boolean DEFAULT_APPLY_NAV_BAR_THEME_COLOR = true;
|
||||
private static final int DEFAULT_GALLERY_SITE = 1;
|
||||
private static final String KEY_LAUNCH_PAGE = "launch_page";
|
||||
private static final int DEFAULT_LAUNCH_PAGE = 0;
|
||||
private static final int DEFAULT_LIST_MODE = 0;
|
||||
private static final int DEFAULT_DETAIL_SIZE = 0;
|
||||
private static final int DEFAULT_THUMB_SIZE = 1;
|
||||
private static final int DEFAULT_THUMB_SIZE = 120;
|
||||
private static final int DEFAULT_THUMB_RESOLUTION = 0;
|
||||
private static final String KEY_FIX_THUMB_URL = "fix_thumb_url";
|
||||
private static final boolean DEFAULT_FIX_THUMB_URL = false;
|
||||
private static final String KEY_SHOW_JPN_TITLE = "show_jpn_title";
|
||||
private static final boolean DEFAULT_SHOW_JPN_TITLE = false;
|
||||
private static final String KEY_SHOW_GALLERY_PAGES = "show_gallery_pages";
|
||||
private static final boolean DEFAULT_SHOW_GALLERY_PAGES = false;
|
||||
@@ -134,41 +135,6 @@ public class Settings {
|
||||
private static final boolean DEFAULT_METERED_NETWORK_WARNING = false;
|
||||
private static final String KEY_APP_LINK_VERIFY_TIP = "app_link_verify_tip";
|
||||
private static final boolean DEFAULT_APP_LINK_VERIFY_TIP = false;
|
||||
private static final String KEY_NIGHT_MODE = "night_mode";
|
||||
private static final String DEFAULT_NIGHT_MODE = "-1";
|
||||
private static final String KEY_E_INK_MODE = "e_ink_mode_2";
|
||||
private static final boolean DEFAULT_E_INK_MODE = false;
|
||||
/********************
|
||||
****** Read
|
||||
********************/
|
||||
private static final String KEY_SCREEN_ROTATION = "screen_rotation";
|
||||
private static final int DEFAULT_SCREEN_ROTATION = 0;
|
||||
private static final String KEY_READING_DIRECTION = "reading_direction";
|
||||
private static final int DEFAULT_READING_DIRECTION = GalleryView.LAYOUT_RIGHT_TO_LEFT;
|
||||
private static final String KEY_PAGE_SCALING = "page_scaling";
|
||||
private static final int DEFAULT_PAGE_SCALING = GalleryView.SCALE_FIT;
|
||||
private static final String KEY_START_POSITION = "start_position";
|
||||
private static final int DEFAULT_START_POSITION = GalleryView.START_POSITION_TOP_RIGHT;
|
||||
private static final String KEY_KEEP_SCREEN_ON = "keep_screen_on";
|
||||
private static final boolean DEFAULT_KEEP_SCREEN_ON = false;
|
||||
private static final String KEY_SHOW_CLOCK = "gallery_show_clock";
|
||||
private static final boolean DEFAULT_SHOW_CLOCK = true;
|
||||
private static final String KEY_SHOW_PROGRESS = "gallery_show_progress";
|
||||
private static final boolean DEFAULT_SHOW_PROGRESS = true;
|
||||
private static final String KEY_SHOW_BATTERY = "gallery_show_battery";
|
||||
private static final boolean DEFAULT_SHOW_BATTERY = true;
|
||||
private static final String KEY_SHOW_PAGE_INTERVAL = "gallery_show_page_interval";
|
||||
private static final boolean DEFAULT_SHOW_PAGE_INTERVAL = true;
|
||||
private static final String KEY_VOLUME_PAGE = "volume_page";
|
||||
private static final boolean DEFAULT_VOLUME_PAGE = false;
|
||||
private static final String KEY_REVERSE_VOLUME_PAGE = "reserve_volume_page";
|
||||
private static final boolean DEFAULT_REVERSE_VOLUME_PAGE = false;
|
||||
private static final String KEY_READING_FULLSCREEN = "reading_fullscreen";
|
||||
private static final boolean VALUE_READING_FULLSCREEN = true;
|
||||
private static final String KEY_CUSTOM_SCREEN_LIGHTNESS = "custom_screen_lightness";
|
||||
private static final boolean DEFAULT_CUSTOM_SCREEN_LIGHTNESS = false;
|
||||
private static final String KEY_SCREEN_LIGHTNESS = "screen_lightness";
|
||||
private static final int DEFAULT_SCREEN_LIGHTNESS = 50;
|
||||
private static final boolean DEFAULT_MEDIA_SCAN = false;
|
||||
private static final String KEY_RECENT_DOWNLOAD_LABEL = "recent_download_label";
|
||||
private static final String DEFAULT_RECENT_DOWNLOAD_LABEL = null;
|
||||
@@ -182,8 +148,6 @@ public class Settings {
|
||||
private static final int DEFAULT_PRELOAD_IMAGE = 5;
|
||||
private static final String KEY_DOWNLOAD_ORIGIN_IMAGE = "download_origin_image";
|
||||
private static final boolean DEFAULT_DOWNLOAD_ORIGIN_IMAGE = false;
|
||||
private static final String KEY_READ_THEME = "read_theme";
|
||||
private static final int DEFAULT_READ_THEME = 1;
|
||||
/********************
|
||||
****** Favorites
|
||||
********************/
|
||||
@@ -225,70 +189,38 @@ public class Settings {
|
||||
// -1 for local, 0 - 9 for cloud favorite, other for no default fav slot
|
||||
private static final String KEY_DEFAULT_FAV_SLOT = "default_favorite_2";
|
||||
private static final int DEFAULT_DEFAULT_FAV_SLOT = INVALID_DEFAULT_FAV_SLOT;
|
||||
/********************
|
||||
****** Analytics
|
||||
********************/
|
||||
private static final String KEY_ASK_ANALYTICS = "ask_analytics";
|
||||
private static final boolean DEFAULT_ASK_ANALYTICS = true;
|
||||
private static final boolean DEFAULT_ENABLE_ANALYTICS = false;
|
||||
private static final String KEY_USER_ID = "user_id";
|
||||
private static final String FILENAME_USER_ID = ".user_id";
|
||||
private static final int LENGTH_USER_ID = 32;
|
||||
/********************
|
||||
****** Update
|
||||
********************/
|
||||
private static final String KEY_BETA_UPDATE_CHANNEL = "beta_update_channel";
|
||||
private static final boolean DEFAULT_BETA_UPDATE_CHANNEL = EhApplication.BETA;
|
||||
private static final String KEY_SKIP_UPDATE_VERSION = "skip_update_version";
|
||||
private static final int DEFAULT_SKIP_UPDATE_VERSION = 0;
|
||||
private static final boolean DEFAULT_SAVE_PARSE_ERROR_BODY = true;
|
||||
private static final String KEY_SAVE_CRASH_LOG = "save_crash_log";
|
||||
private static final boolean DEFAULT_SAVE_CRASH_LOG = true;
|
||||
private static final boolean DEFAULT_BUILT_IN_HOSTS = false;
|
||||
private static final boolean DEFAULT_FRONTING = false;
|
||||
private static final String DEFAULT_APP_LANGUAGE = "system";
|
||||
private static final boolean DEFAULT_BYPASS_VPN = true;
|
||||
private static final String KEY_PROXY_TYPE = "proxy_type";
|
||||
private static final int DEFAULT_PROXY_TYPE = EhProxySelector.TYPE_SYSTEM;
|
||||
private static final String KEY_PROXY_IP = "proxy_ip";
|
||||
private static final String DEFAULT_PROXY_IP = null;
|
||||
private static final String KEY_PROXY_PORT = "proxy_port";
|
||||
private static final int DEFAULT_PROXY_PORT = -1;
|
||||
/********************
|
||||
****** Guide
|
||||
********************/
|
||||
private static final String KEY_GUIDE_QUICK_SEARCH = "guide_quick_search";
|
||||
private static final boolean DEFAULT_GUIDE_QUICK_SEARCH = true;
|
||||
private static final String KEY_GUIDE_COLLECTIONS = "guide_collections";
|
||||
private static final boolean DEFAULT_GUIDE_COLLECTIONS = true;
|
||||
private static final String KEY_GUIDE_DOWNLOAD_THUMB = "guide_download_thumb";
|
||||
private static final boolean DEFAULT_GUIDE_DOWNLOAD_THUMB = true;
|
||||
private static final String KEY_GUIDE_DOWNLOAD_LABELS = "guide_download_labels";
|
||||
private static final boolean DEFAULT_GUIDE_DOWNLOAD_LABELS = true;
|
||||
private static final String KEY_GUIDE_GALLERY = "guide_gallery";
|
||||
private static final boolean DEFAULT_GUIDE_GALLERY = true;
|
||||
private static final String KEY_CLIPBOARD_TEXT_HASH_CODE = "clipboard_text_hash_code";
|
||||
private static final int DEFAULT_CLIPBOARD_TEXT_HASH_CODE = 0;
|
||||
private static final String KEY_DOWNLOAD_DELAY = "download_delay";
|
||||
private static final int DEFAULT_DOWNLOAD_DELAY = 0;
|
||||
private static final String KEY_REQUEST_NEWS = "request_news";
|
||||
private static final boolean DEFAULT_REQUEST_NEWS = true;
|
||||
|
||||
|
||||
private static final String KEY_ARCHIVE_PASSWDS = "archive_passwds";
|
||||
|
||||
|
||||
private static Context sContext;
|
||||
private static SharedPreferences sSettingsPre;
|
||||
private static final String KEY_QS_SAVE_PROGRESS = "qs_save_progress";
|
||||
private static final boolean DEFAULT_QS_SAVE_PROGRESS = true;
|
||||
public static boolean LIST_THUMB_SIZE_INITED = false;
|
||||
public static SharedPreferences sSettingsPre;
|
||||
private static int LIST_THUMB_SIZE = 40;
|
||||
private static EhConfig sEhConfig;
|
||||
|
||||
public static void initialize(Context context) {
|
||||
sContext = context.getApplicationContext();
|
||||
sSettingsPre = PreferenceManager.getDefaultSharedPreferences(sContext);
|
||||
public static void initialize() {
|
||||
sSettingsPre = PreferenceManager.getDefaultSharedPreferences(EhApplication.getApplication());
|
||||
sEhConfig = loadEhConfig();
|
||||
fixDefaultValue(context);
|
||||
fixDefaultValue();
|
||||
}
|
||||
|
||||
private static void fixDefaultValue(Context context) {
|
||||
private static void fixDefaultValue() {
|
||||
if ("CN".equals(Locale.getDefault().getCountry())) {
|
||||
// Enable domain fronting if the country is CN
|
||||
if (!sSettingsPre.contains(KEY_BUILT_IN_HOSTS)) {
|
||||
@@ -305,16 +237,6 @@ public class Settings {
|
||||
}
|
||||
}
|
||||
|
||||
public static Locale getLocale() {
|
||||
Locale locale = null;
|
||||
String language = Settings.getAppLanguage();
|
||||
if (language != null && !language.equals("system")) {
|
||||
return Locale.forLanguageTag(language);
|
||||
} else {
|
||||
return Locale.getDefault();
|
||||
}
|
||||
}
|
||||
|
||||
private static EhConfig loadEhConfig() {
|
||||
EhConfig ehConfig = new EhConfig();
|
||||
ehConfig.imageSize = getImageResolution();
|
||||
@@ -351,32 +273,6 @@ public class Settings {
|
||||
sSettingsPre.edit().putInt(key, value).apply();
|
||||
}
|
||||
|
||||
public static long getLong(String key, long defValue) {
|
||||
try {
|
||||
return sSettingsPre.getLong(key, defValue);
|
||||
} catch (ClassCastException e) {
|
||||
Log.d(TAG, "Get ClassCastException when get " + key + " value", e);
|
||||
return defValue;
|
||||
}
|
||||
}
|
||||
|
||||
public static void putLong(String key, long value) {
|
||||
sSettingsPre.edit().putLong(key, value).apply();
|
||||
}
|
||||
|
||||
public static float getFloat(String key, float defValue) {
|
||||
try {
|
||||
return sSettingsPre.getFloat(key, defValue);
|
||||
} catch (ClassCastException e) {
|
||||
Log.d(TAG, "Get ClassCastException when get " + key + " value", e);
|
||||
return defValue;
|
||||
}
|
||||
}
|
||||
|
||||
public static void putFloat(String key, float value) {
|
||||
sSettingsPre.edit().putFloat(key, value).apply();
|
||||
}
|
||||
|
||||
public static String getString(String key, String defValue) {
|
||||
try {
|
||||
return sSettingsPre.getString(key, defValue);
|
||||
@@ -418,14 +314,7 @@ public class Settings {
|
||||
sSettingsPre.edit().putString(key, Integer.toString(value)).apply();
|
||||
}
|
||||
|
||||
public static int getVersionCode() {
|
||||
return getInt(KEY_VERSION_CODE, DEFAULT_VERSION_CODE);
|
||||
}
|
||||
|
||||
public static void putVersionCode(int value) {
|
||||
putInt(KEY_VERSION_CODE, value);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getDisplayName() {
|
||||
return getString(KEY_DISPLAY_NAME, DEFAULT_DISPLAY_NAME);
|
||||
}
|
||||
@@ -434,22 +323,10 @@ public class Settings {
|
||||
putString(KEY_DISPLAY_NAME, value);
|
||||
}
|
||||
|
||||
public static String getAvatar() {
|
||||
return getString(KEY_AVATAR, DEFAULT_AVATAR);
|
||||
}
|
||||
|
||||
public static void putAvatar(String value) {
|
||||
putString(KEY_AVATAR, value);
|
||||
}
|
||||
|
||||
public static boolean getShowWarning() {
|
||||
return getBoolean(KEY_SHOW_WARNING, DEFAULT_SHOW_WARNING);
|
||||
}
|
||||
|
||||
public static void putShowWarning(boolean value) {
|
||||
putBoolean(KEY_SHOW_WARNING, value);
|
||||
}
|
||||
|
||||
public static boolean getRemoveImageFiles() {
|
||||
return getBoolean(KEY_REMOVE_IMAGE_FILES, DEFAULT_REMOVE_IMAGE_FILES);
|
||||
}
|
||||
@@ -482,14 +359,6 @@ public class Settings {
|
||||
return getIntFromStr(KEY_THEME, DEFAULT_THEME);
|
||||
}
|
||||
|
||||
public static void putTheme(int theme) {
|
||||
putIntToStr(KEY_THEME, theme);
|
||||
}
|
||||
|
||||
public static boolean getApplyNavBarThemeColor() {
|
||||
return getBoolean(KEY_APPLY_NAV_BAR_THEME_COLOR, DEFAULT_APPLY_NAV_BAR_THEME_COLOR);
|
||||
}
|
||||
|
||||
public static int getGallerySite() {
|
||||
return getIntFromStr(KEY_GALLERY_SITE, DEFAULT_GALLERY_SITE);
|
||||
}
|
||||
@@ -500,17 +369,13 @@ public class Settings {
|
||||
|
||||
public static String getLaunchPageGalleryListSceneAction() {
|
||||
int value = getIntFromStr(KEY_LAUNCH_PAGE, DEFAULT_LAUNCH_PAGE);
|
||||
switch (value) {
|
||||
default:
|
||||
case 0:
|
||||
return GalleryListScene.ACTION_HOMEPAGE;
|
||||
case 1:
|
||||
return GalleryListScene.ACTION_SUBSCRIPTION;
|
||||
case 2:
|
||||
return GalleryListScene.ACTION_WHATS_HOT;
|
||||
case 3:
|
||||
return GalleryListScene.ACTION_TOP_LIST;
|
||||
}
|
||||
return switch (value) {
|
||||
case 0 -> GalleryListScene.ACTION_HOMEPAGE;
|
||||
case 1 -> GalleryListScene.ACTION_SUBSCRIPTION;
|
||||
case 2 -> GalleryListScene.ACTION_WHATS_HOT;
|
||||
case 3 -> GalleryListScene.ACTION_TOP_LIST;
|
||||
default -> throw new IllegalStateException("Unexpected value: " + value);
|
||||
};
|
||||
}
|
||||
|
||||
public static int getListMode() {
|
||||
@@ -523,44 +388,38 @@ public class Settings {
|
||||
|
||||
@DimenRes
|
||||
public static int getDetailSizeResId() {
|
||||
switch (getDetailSize()) {
|
||||
default:
|
||||
case 0:
|
||||
return R.dimen.gallery_list_column_width_long;
|
||||
case 1:
|
||||
return R.dimen.gallery_list_column_width_short;
|
||||
}
|
||||
return switch (getDetailSize()) {
|
||||
case 0 -> R.dimen.gallery_list_column_width_long;
|
||||
case 1 -> R.dimen.gallery_list_column_width_short;
|
||||
default -> throw new IllegalStateException("Unexpected value: " + getDetailSize());
|
||||
};
|
||||
}
|
||||
|
||||
public static int getThumbSize() {
|
||||
return getIntFromStr(KEY_THUMB_SIZE, DEFAULT_THUMB_SIZE);
|
||||
return dip2px(getInt(KEY_THUMB_SIZE, DEFAULT_THUMB_SIZE));
|
||||
}
|
||||
|
||||
@DimenRes
|
||||
public static int getThumbSizeResId() {
|
||||
switch (getThumbSize()) {
|
||||
case 0:
|
||||
return R.dimen.gallery_grid_column_width_large;
|
||||
default:
|
||||
case 1:
|
||||
return R.dimen.gallery_grid_column_width_middle;
|
||||
case 2:
|
||||
return R.dimen.gallery_grid_column_width_small;
|
||||
}
|
||||
public static int dip2px(int dpValue) {
|
||||
final float scale = EhApplication.getApplication().getResources().getDisplayMetrics().density;
|
||||
return (int) (dpValue * scale + 0.5f);
|
||||
}
|
||||
|
||||
public static int getThumbResolution() {
|
||||
return getIntFromStr(KEY_THUMB_RESOLUTION, DEFAULT_THUMB_RESOLUTION);
|
||||
}
|
||||
|
||||
public static boolean getFixThumbUrl() {
|
||||
return getBoolean(KEY_FIX_THUMB_URL, DEFAULT_FIX_THUMB_URL);
|
||||
public static boolean getShowComments() {
|
||||
return getBoolean(KEY_SHOW_COMMENTS, DEFAULT_SHOW_COMMENTS);
|
||||
}
|
||||
|
||||
public static boolean getRequestNews() {
|
||||
return getBoolean(KEY_REQUEST_NEWS, DEFAULT_REQUEST_NEWS);
|
||||
}
|
||||
|
||||
public static boolean getHideHvEvents() {
|
||||
return getBoolean(KEY_HIDE_HV_EVENTS, DEFAULT_HIDE_HV_EVENTS);
|
||||
}
|
||||
|
||||
public static boolean getShowJpnTitle() {
|
||||
return getBoolean(KEY_SHOW_JPN_TITLE, DEFAULT_SHOW_JPN_TITLE);
|
||||
}
|
||||
@@ -581,32 +440,14 @@ public class Settings {
|
||||
return getInt(KEY_DEFAULT_CATEGORIES, DEFAULT_DEFAULT_CATEGORIES);
|
||||
}
|
||||
|
||||
public static void putDefaultCategories(int value) {
|
||||
sEhConfig.defaultCategories = value;
|
||||
sEhConfig.setDirty();
|
||||
putInt(KEY_DEFAULT_CATEGORIES, value);
|
||||
}
|
||||
|
||||
public static int getExcludedTagNamespaces() {
|
||||
return getInt(KEY_EXCLUDED_TAG_NAMESPACES, DEFAULT_EXCLUDED_TAG_NAMESPACES);
|
||||
}
|
||||
|
||||
public static void putExcludedTagNamespaces(int value) {
|
||||
sEhConfig.excludedNamespaces = value;
|
||||
sEhConfig.setDirty();
|
||||
putInt(KEY_EXCLUDED_TAG_NAMESPACES, value);
|
||||
}
|
||||
|
||||
public static String getExcludedLanguages() {
|
||||
return getString(KEY_EXCLUDED_LANGUAGES, DEFAULT_EXCLUDED_LANGUAGES);
|
||||
}
|
||||
|
||||
public static void putExcludedLanguages(String value) {
|
||||
sEhConfig.excludedLanguages = value;
|
||||
sEhConfig.setDirty();
|
||||
putString(KEY_EXCLUDED_LANGUAGES, value);
|
||||
}
|
||||
|
||||
public static boolean getMeteredNetworkWarning() {
|
||||
return getBoolean(KEY_METERED_NETWORK_WARNING, DEFAULT_METERED_NETWORK_WARNING);
|
||||
}
|
||||
@@ -619,152 +460,20 @@ public class Settings {
|
||||
putBoolean(KEY_APP_LINK_VERIFY_TIP, value);
|
||||
}
|
||||
|
||||
public static int getScreenRotation() {
|
||||
return getIntFromStr(KEY_SCREEN_ROTATION, DEFAULT_SCREEN_ROTATION);
|
||||
}
|
||||
|
||||
public static void putScreenRotation(int value) {
|
||||
putIntToStr(KEY_SCREEN_ROTATION, value);
|
||||
}
|
||||
|
||||
@GalleryView.LayoutMode
|
||||
public static int getReadingDirection() {
|
||||
return GalleryView.sanitizeLayoutMode(getIntFromStr(KEY_READING_DIRECTION, DEFAULT_READING_DIRECTION));
|
||||
}
|
||||
|
||||
public static void putReadingDirection(int value) {
|
||||
putIntToStr(KEY_READING_DIRECTION, value);
|
||||
}
|
||||
|
||||
@GalleryView.ScaleMode
|
||||
public static int getPageScaling() {
|
||||
return GalleryView.sanitizeScaleMode(getIntFromStr(KEY_PAGE_SCALING, DEFAULT_PAGE_SCALING));
|
||||
}
|
||||
|
||||
public static void putPageScaling(int value) {
|
||||
putIntToStr(KEY_PAGE_SCALING, value);
|
||||
}
|
||||
|
||||
@GalleryView.StartPosition
|
||||
public static int getStartPosition() {
|
||||
return GalleryView.sanitizeStartPosition(getIntFromStr(KEY_START_POSITION, DEFAULT_START_POSITION));
|
||||
}
|
||||
|
||||
public static void putStartPosition(int value) {
|
||||
putIntToStr(KEY_START_POSITION, value);
|
||||
}
|
||||
|
||||
public static boolean getKeepScreenOn() {
|
||||
return getBoolean(KEY_KEEP_SCREEN_ON, DEFAULT_KEEP_SCREEN_ON);
|
||||
}
|
||||
|
||||
public static void putKeepScreenOn(boolean value) {
|
||||
putBoolean(KEY_KEEP_SCREEN_ON, value);
|
||||
}
|
||||
|
||||
public static boolean getShowClock() {
|
||||
return getBoolean(KEY_SHOW_CLOCK, DEFAULT_SHOW_CLOCK);
|
||||
}
|
||||
|
||||
public static void putShowClock(boolean value) {
|
||||
putBoolean(KEY_SHOW_CLOCK, value);
|
||||
}
|
||||
|
||||
public static boolean getShowProgress() {
|
||||
return getBoolean(KEY_SHOW_PROGRESS, DEFAULT_SHOW_PROGRESS);
|
||||
}
|
||||
|
||||
public static void putShowProgress(boolean value) {
|
||||
putBoolean(KEY_SHOW_PROGRESS, value);
|
||||
}
|
||||
|
||||
public static boolean getShowBattery() {
|
||||
return getBoolean(KEY_SHOW_BATTERY, DEFAULT_SHOW_BATTERY);
|
||||
}
|
||||
|
||||
public static void putShowBattery(boolean value) {
|
||||
putBoolean(KEY_SHOW_BATTERY, value);
|
||||
}
|
||||
|
||||
public static boolean getShowPageInterval() {
|
||||
return getBoolean(KEY_SHOW_PAGE_INTERVAL, DEFAULT_SHOW_PAGE_INTERVAL);
|
||||
}
|
||||
|
||||
public static void putShowPageInterval(boolean value) {
|
||||
putBoolean(KEY_SHOW_PAGE_INTERVAL, value);
|
||||
}
|
||||
|
||||
public static boolean getVolumePage() {
|
||||
return getBoolean(KEY_VOLUME_PAGE, DEFAULT_VOLUME_PAGE);
|
||||
}
|
||||
|
||||
public static void putVolumePage(boolean value) {
|
||||
putBoolean(KEY_VOLUME_PAGE, value);
|
||||
}
|
||||
|
||||
public static boolean getReverseVolumePage() {
|
||||
return getBoolean(KEY_REVERSE_VOLUME_PAGE, DEFAULT_REVERSE_VOLUME_PAGE);
|
||||
}
|
||||
|
||||
public static void putReverseVolumePage(boolean value) {
|
||||
putBoolean(KEY_REVERSE_VOLUME_PAGE, value);
|
||||
}
|
||||
|
||||
public static boolean getReadingFullscreen() {
|
||||
return getBoolean(KEY_READING_FULLSCREEN, VALUE_READING_FULLSCREEN);
|
||||
}
|
||||
|
||||
public static void putReadingFullscreen(boolean value) {
|
||||
putBoolean(KEY_READING_FULLSCREEN, value);
|
||||
}
|
||||
|
||||
public static boolean getCustomScreenLightness() {
|
||||
return getBoolean(KEY_CUSTOM_SCREEN_LIGHTNESS, DEFAULT_CUSTOM_SCREEN_LIGHTNESS);
|
||||
}
|
||||
|
||||
public static void putCustomScreenLightness(boolean value) {
|
||||
putBoolean(KEY_CUSTOM_SCREEN_LIGHTNESS, value);
|
||||
}
|
||||
|
||||
public static int getScreenLightness() {
|
||||
return getInt(KEY_SCREEN_LIGHTNESS, DEFAULT_SCREEN_LIGHTNESS);
|
||||
}
|
||||
|
||||
public static void putScreenLightness(int value) {
|
||||
putInt(KEY_SCREEN_LIGHTNESS, value);
|
||||
}
|
||||
|
||||
public static int getReadTheme() {
|
||||
return getIntFromStr(KEY_READ_THEME, DEFAULT_READ_THEME);
|
||||
}
|
||||
|
||||
public static void putReadTheme(int value) {
|
||||
putIntToStr(KEY_READ_THEME, value);
|
||||
}
|
||||
|
||||
public static boolean getEnabledSecurity() {
|
||||
return getBoolean(KEY_SEC_SECURITY, VALUE_SEC_SECURITY);
|
||||
}
|
||||
|
||||
public static void putEnabledSecurity(boolean value) {
|
||||
putBoolean(KEY_READING_FULLSCREEN, value);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static UniFile getDownloadLocation() {
|
||||
UniFile dir = null;
|
||||
try {
|
||||
Uri.Builder builder = new Uri.Builder();
|
||||
builder.scheme(getString(KEY_DOWNLOAD_SAVE_SCHEME, null));
|
||||
builder.encodedAuthority(getString(KEY_DOWNLOAD_SAVE_AUTHORITY, null));
|
||||
builder.encodedPath(getString(KEY_DOWNLOAD_SAVE_PATH, null));
|
||||
builder.encodedQuery(getString(KEY_DOWNLOAD_SAVE_QUERY, null));
|
||||
builder.encodedFragment(getString(KEY_DOWNLOAD_SAVE_FRAGMENT, null));
|
||||
dir = UniFile.fromUri(sContext, builder.build());
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
// Ignore
|
||||
}
|
||||
UniFile dir;
|
||||
Uri.Builder builder = new Uri.Builder();
|
||||
builder.scheme(getString(KEY_DOWNLOAD_SAVE_SCHEME, null));
|
||||
builder.encodedAuthority(getString(KEY_DOWNLOAD_SAVE_AUTHORITY, null));
|
||||
builder.encodedPath(getString(KEY_DOWNLOAD_SAVE_PATH, null));
|
||||
builder.encodedQuery(getString(KEY_DOWNLOAD_SAVE_QUERY, null));
|
||||
builder.encodedFragment(getString(KEY_DOWNLOAD_SAVE_FRAGMENT, null));
|
||||
dir = UniFile.fromUri(EhApplication.getApplication(), builder.build());
|
||||
return dir != null ? dir : UniFile.fromFile(AppConfig.getDefaultDownloadDir());
|
||||
}
|
||||
|
||||
@@ -815,26 +524,14 @@ public class Settings {
|
||||
return getIntFromStr(KEY_MULTI_THREAD_DOWNLOAD, DEFAULT_MULTI_THREAD_DOWNLOAD);
|
||||
}
|
||||
|
||||
public static void putMultiThreadDownload(int value) {
|
||||
putIntToStr(KEY_MULTI_THREAD_DOWNLOAD, value);
|
||||
}
|
||||
|
||||
public static int getDownloadDelay() {
|
||||
return getIntFromStr(KEY_DOWNLOAD_DELAY, DEFAULT_DOWNLOAD_DELAY);
|
||||
}
|
||||
|
||||
public static void putDownloadDelay(int value) {
|
||||
putIntToStr(KEY_DOWNLOAD_DELAY, value);
|
||||
}
|
||||
|
||||
public static int getPreloadImage() {
|
||||
return getIntFromStr(KEY_PRELOAD_IMAGE, DEFAULT_PRELOAD_IMAGE);
|
||||
}
|
||||
|
||||
public static void putPreloadImage(int value) {
|
||||
putIntToStr(KEY_PRELOAD_IMAGE, value);
|
||||
}
|
||||
|
||||
public static String getImageResolution() {
|
||||
return getString(KEY_IMAGE_RESOLUTION, DEFAULT_IMAGE_RESOLUTION);
|
||||
}
|
||||
@@ -849,10 +546,6 @@ public class Settings {
|
||||
return getBoolean(KEY_DOWNLOAD_ORIGIN_IMAGE, DEFAULT_DOWNLOAD_ORIGIN_IMAGE);
|
||||
}
|
||||
|
||||
public static void putDownloadOriginImage(boolean value) {
|
||||
putBoolean(KEY_DOWNLOAD_ORIGIN_IMAGE, value);
|
||||
}
|
||||
|
||||
public static String[] getFavCat() {
|
||||
String[] favCat = new String[10];
|
||||
favCat[0] = sSettingsPre.getString(KEY_FAV_CAT_0, DEFAULT_FAV_CAT_0);
|
||||
@@ -947,104 +640,18 @@ public class Settings {
|
||||
putInt(KEY_DEFAULT_FAV_SLOT, value);
|
||||
}
|
||||
|
||||
public static boolean getAskAnalytics() {
|
||||
return getBoolean(KEY_ASK_ANALYTICS, DEFAULT_ASK_ANALYTICS);
|
||||
public static boolean getQSSaveProgress() {
|
||||
return getBoolean(KEY_QS_SAVE_PROGRESS, DEFAULT_QS_SAVE_PROGRESS);
|
||||
}
|
||||
|
||||
public static void putAskAnalytics(boolean value) {
|
||||
putBoolean(KEY_ASK_ANALYTICS, value);
|
||||
}
|
||||
|
||||
public static boolean getEnableAnalytics() {
|
||||
return getBoolean(KEY_ENABLE_ANALYTICS, DEFAULT_ENABLE_ANALYTICS);
|
||||
}
|
||||
|
||||
public static void putEnableAnalytics(boolean value) {
|
||||
putBoolean(KEY_ENABLE_ANALYTICS, value);
|
||||
}
|
||||
|
||||
public static String getUserID() {
|
||||
boolean writeXml = false;
|
||||
boolean writeFile = false;
|
||||
String userID = getString(KEY_USER_ID, null);
|
||||
File file = AppConfig.getFileInExternalAppDir(FILENAME_USER_ID);
|
||||
if (null == userID || !isValidUserID(userID)) {
|
||||
writeXml = true;
|
||||
// Get use ID from out sd card file
|
||||
userID = FileUtils.read(file);
|
||||
if (null == userID || !isValidUserID(userID)) {
|
||||
writeFile = true;
|
||||
userID = generateUserID();
|
||||
}
|
||||
} else {
|
||||
writeFile = true;
|
||||
}
|
||||
|
||||
if (writeXml) {
|
||||
putString(KEY_USER_ID, userID);
|
||||
}
|
||||
if (writeFile) {
|
||||
FileUtils.write(file, userID);
|
||||
}
|
||||
|
||||
return userID;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static String generateUserID() {
|
||||
int length = LENGTH_USER_ID;
|
||||
StringBuilder sb = new StringBuilder(length);
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (MathUtils.random(0, ('9' - '0' + 1) + ('z' - 'a' + 1)) <= '9' - '0') {
|
||||
sb.append((char) MathUtils.random('0', '9' + 1));
|
||||
} else {
|
||||
sb.append((char) MathUtils.random('a', 'z' + 1));
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static boolean isValidUserID(@Nullable String userID) {
|
||||
if (null == userID || LENGTH_USER_ID != userID.length()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < LENGTH_USER_ID; i++) {
|
||||
char ch = userID.charAt(i);
|
||||
if (!(ch >= '0' && ch <= '9') && !(ch >= 'a' && ch <= 'z')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean getBetaUpdateChannel() {
|
||||
return getBoolean(KEY_BETA_UPDATE_CHANNEL, DEFAULT_BETA_UPDATE_CHANNEL);
|
||||
}
|
||||
|
||||
public static void putBetaUpdateChannel(boolean value) {
|
||||
putBoolean(KEY_BETA_UPDATE_CHANNEL, value);
|
||||
}
|
||||
|
||||
public static int getSkipUpdateVersion() {
|
||||
return getInt(KEY_SKIP_UPDATE_VERSION, DEFAULT_SKIP_UPDATE_VERSION);
|
||||
}
|
||||
|
||||
public static void putSkipUpdateVersion(int value) {
|
||||
putInt(KEY_SKIP_UPDATE_VERSION, value);
|
||||
public static void putQSSaveProgress(boolean value) {
|
||||
putBoolean(KEY_QS_SAVE_PROGRESS, value);
|
||||
}
|
||||
|
||||
public static boolean getSaveParseErrorBody() {
|
||||
return getBoolean(KEY_SAVE_PARSE_ERROR_BODY, DEFAULT_SAVE_PARSE_ERROR_BODY);
|
||||
}
|
||||
|
||||
public static void putSaveParseErrorBody(boolean value) {
|
||||
putBoolean(KEY_SAVE_PARSE_ERROR_BODY, value);
|
||||
}
|
||||
|
||||
public static boolean getSaveCrashLog() {
|
||||
return getBoolean(KEY_SAVE_CRASH_LOG, DEFAULT_SAVE_CRASH_LOG);
|
||||
}
|
||||
@@ -1057,8 +664,8 @@ public class Settings {
|
||||
putBoolean(KEY_SECURITY, value);
|
||||
}
|
||||
|
||||
public static boolean getEnableFingerprint() {
|
||||
return getBoolean(KEY_ENABLE_FINGERPRINT, true);
|
||||
public static int getSecurityDelay() {
|
||||
return getInt(KEY_SECURITY_DELAY, 0);
|
||||
}
|
||||
|
||||
public static int getReadCacheSize() {
|
||||
@@ -1081,12 +688,8 @@ public class Settings {
|
||||
putBoolean(KEY_DOMAIN_FRONTING, value);
|
||||
}
|
||||
|
||||
public static String getAppLanguage() {
|
||||
return getString(KEY_APP_LANGUAGE, DEFAULT_APP_LANGUAGE);
|
||||
}
|
||||
|
||||
public static void putAppLanguage(String value) {
|
||||
putString(KEY_APP_LANGUAGE, value);
|
||||
public static boolean getBypassVpn() {
|
||||
return getBoolean(KEY_BYPASS_VPN, DEFAULT_BYPASS_VPN);
|
||||
}
|
||||
|
||||
public static int getProxyType() {
|
||||
@@ -1113,46 +716,6 @@ public class Settings {
|
||||
putInt(KEY_PROXY_PORT, value);
|
||||
}
|
||||
|
||||
public static boolean getGuideQuickSearch() {
|
||||
return getBoolean(KEY_GUIDE_QUICK_SEARCH, DEFAULT_GUIDE_QUICK_SEARCH);
|
||||
}
|
||||
|
||||
public static void putGuideQuickSearch(boolean value) {
|
||||
putBoolean(KEY_GUIDE_QUICK_SEARCH, value);
|
||||
}
|
||||
|
||||
public static boolean getGuideCollections() {
|
||||
return getBoolean(KEY_GUIDE_COLLECTIONS, DEFAULT_GUIDE_COLLECTIONS);
|
||||
}
|
||||
|
||||
public static void putGuideCollections(boolean value) {
|
||||
putBoolean(KEY_GUIDE_COLLECTIONS, value);
|
||||
}
|
||||
|
||||
public static boolean getGuideDownloadThumb() {
|
||||
return getBoolean(KEY_GUIDE_DOWNLOAD_THUMB, DEFAULT_GUIDE_DOWNLOAD_THUMB);
|
||||
}
|
||||
|
||||
public static void putGuideDownloadThumb(boolean value) {
|
||||
putBoolean(KEY_GUIDE_DOWNLOAD_THUMB, value);
|
||||
}
|
||||
|
||||
public static boolean getGuideDownloadLabels() {
|
||||
return getBoolean(KEY_GUIDE_DOWNLOAD_LABELS, DEFAULT_GUIDE_DOWNLOAD_LABELS);
|
||||
}
|
||||
|
||||
public static void puttGuideDownloadLabels(boolean value) {
|
||||
putBoolean(KEY_GUIDE_DOWNLOAD_LABELS, value);
|
||||
}
|
||||
|
||||
public static boolean getGuideGallery() {
|
||||
return getBoolean(KEY_GUIDE_GALLERY, DEFAULT_GUIDE_GALLERY);
|
||||
}
|
||||
|
||||
public static void putGuideGallery(boolean value) {
|
||||
putBoolean(KEY_GUIDE_GALLERY, value);
|
||||
}
|
||||
|
||||
public static int getClipboardTextHashCode() {
|
||||
return getInt(KEY_CLIPBOARD_TEXT_HASH_CODE, DEFAULT_CLIPBOARD_TEXT_HASH_CODE);
|
||||
}
|
||||
@@ -1168,4 +731,22 @@ public class Settings {
|
||||
public static void putPasswdToArchivePasswds(String value) {
|
||||
putStringToStringSet(KEY_ARCHIVE_PASSWDS, value);
|
||||
}
|
||||
|
||||
public static int getListThumbSize() {
|
||||
if (LIST_THUMB_SIZE_INITED) {
|
||||
return LIST_THUMB_SIZE;
|
||||
}
|
||||
int size = 3 * getInt(KEY_LIST_THUMB_SIZE, DEFAULT_LIST_THUMB_SIZE);
|
||||
LIST_THUMB_SIZE = size;
|
||||
LIST_THUMB_SIZE_INITED = true;
|
||||
return size;
|
||||
}
|
||||
|
||||
public static boolean getNotificationRequired() {
|
||||
return getBoolean(KEY_NOTIFICATION_REQUIRED, false);
|
||||
}
|
||||
|
||||
public static void putNotificationRequired() {
|
||||
putBoolean(KEY_NOTIFICATION_REQUIRED, true);
|
||||
}
|
||||
}
|
||||
@@ -26,13 +26,10 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.browser.customtabs.CustomTabsIntent;
|
||||
|
||||
import com.hippo.ehviewer.client.EhUrlOpener;
|
||||
import com.hippo.ehviewer.client.data.GalleryDetail;
|
||||
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser;
|
||||
import com.hippo.ehviewer.ui.GalleryActivity;
|
||||
import com.hippo.ehviewer.ui.MainActivity;
|
||||
import com.hippo.scene.Announcer;
|
||||
import com.hippo.scene.StageActivity;
|
||||
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
|
||||
|
||||
public final class UrlOpener {
|
||||
|
||||
@@ -55,20 +52,20 @@ public final class UrlOpener {
|
||||
if (galleryDetail != null) {
|
||||
GalleryPageUrlParser.Result result = GalleryPageUrlParser.parse(url);
|
||||
if (result != null) {
|
||||
if (result.gid == galleryDetail.gid) {
|
||||
intent = new Intent(context, GalleryActivity.class);
|
||||
intent.setAction(GalleryActivity.ACTION_EH);
|
||||
intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, galleryDetail);
|
||||
intent.putExtra(GalleryActivity.KEY_PAGE, result.page);
|
||||
if (result.gid == galleryDetail.getGid()) {
|
||||
intent = new Intent(context, ReaderActivity.class);
|
||||
intent.setAction(ReaderActivity.ACTION_EH);
|
||||
intent.putExtra(ReaderActivity.KEY_GALLERY_INFO, galleryDetail);
|
||||
intent.putExtra(ReaderActivity.KEY_PAGE, result.page);
|
||||
context.startActivity(intent);
|
||||
return;
|
||||
}
|
||||
} else if (url.startsWith("#c")) {
|
||||
try {
|
||||
intent = new Intent(context, GalleryActivity.class);
|
||||
intent.setAction(GalleryActivity.ACTION_EH);
|
||||
intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, galleryDetail);
|
||||
intent.putExtra(GalleryActivity.KEY_PAGE, Integer.parseInt(url.replace("#c", "")) - 1);
|
||||
intent = new Intent(context, ReaderActivity.class);
|
||||
intent.setAction(ReaderActivity.ACTION_EH);
|
||||
intent.putExtra(ReaderActivity.KEY_GALLERY_INFO, galleryDetail);
|
||||
intent.putExtra(ReaderActivity.KEY_PAGE, Integer.parseInt(url.replace("#c", "")) - 1);
|
||||
context.startActivity(intent);
|
||||
return;
|
||||
} catch (NumberFormatException e) {
|
||||
@@ -76,16 +73,6 @@ public final class UrlOpener {
|
||||
}
|
||||
}
|
||||
}
|
||||
Announcer announcer = EhUrlOpener.parseUrl(url);
|
||||
if (null != announcer) {
|
||||
intent = new Intent(context, MainActivity.class);
|
||||
intent.setAction(StageActivity.ACTION_START_SCENE);
|
||||
intent.putExtra(StageActivity.KEY_SCENE_NAME, announcer.getClazz().getName());
|
||||
intent.putExtra(StageActivity.KEY_SCENE_ARGS, announcer.getArgs());
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
CustomTabsIntent.Builder customTabsIntent = new CustomTabsIntent.Builder();
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client;
|
||||
|
||||
public class EhCacheKeyFactory {
|
||||
|
||||
public static String getThumbKey(long gid) {
|
||||
return "preview:large:" + gid + ":" + 0; // "thumb:" + gid;
|
||||
}
|
||||
|
||||
public static String getNormalPreviewKey(long gid, int index) {
|
||||
return "preview:normal:" + gid + ":" + index;
|
||||
}
|
||||
|
||||
public static String getLargePreviewKey(long gid, int index) {
|
||||
return "preview:large:" + gid + ":" + index;
|
||||
}
|
||||
|
||||
public static String getLargePreviewSetKey(long gid, int index) {
|
||||
return "large_preview_set:" + gid + ":" + index;
|
||||
}
|
||||
|
||||
public static String getImageKey(long gid, int index) {
|
||||
return "image:" + gid + ":" + index;
|
||||
}
|
||||
}
|
||||
+5
-7
@@ -13,12 +13,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.client
|
||||
|
||||
package com.hippo.ehviewer.client.exception;
|
||||
|
||||
public class Image509Exception extends EhException {
|
||||
|
||||
public Image509Exception() {
|
||||
super("509");
|
||||
object EhCacheKeyFactory {
|
||||
fun getImageKey(gid: Long, index: Int): String {
|
||||
return "image:$gid:$index"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.client.exception.CancelledException;
|
||||
import com.hippo.util.ExceptionUtils;
|
||||
import com.hippo.util.IoThreadPoolExecutor;
|
||||
import com.hippo.yorozuya.SimpleHandler;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import okhttp3.Call;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
public class EhClient {
|
||||
|
||||
public static final String TAG = EhClient.class.getSimpleName();
|
||||
|
||||
public static final int METHOD_SIGN_IN = 0;
|
||||
public static final int METHOD_GET_GALLERY_LIST = 1;
|
||||
public static final int METHOD_GET_GALLERY_DETAIL = 3;
|
||||
public static final int METHOD_GET_PREVIEW_SET = 4;
|
||||
public static final int METHOD_GET_RATE_GALLERY = 5;
|
||||
public static final int METHOD_GET_COMMENT_GALLERY = 6;
|
||||
public static final int METHOD_GET_GALLERY_TOKEN = 7;
|
||||
public static final int METHOD_GET_FAVORITES = 8;
|
||||
public static final int METHOD_ADD_FAVORITES = 9;
|
||||
public static final int METHOD_ADD_FAVORITES_RANGE = 10;
|
||||
public static final int METHOD_MODIFY_FAVORITES = 11;
|
||||
public static final int METHOD_GET_TORRENT_LIST = 12;
|
||||
public static final int METHOD_GET_PROFILE = 14;
|
||||
public static final int METHOD_VOTE_COMMENT = 15;
|
||||
public static final int METHOD_IMAGE_SEARCH = 16;
|
||||
public static final int METHOD_ARCHIVE_LIST = 17;
|
||||
public static final int METHOD_DOWNLOAD_ARCHIVE = 18;
|
||||
public static final int METHOD_VOTE_TAG = 19;
|
||||
|
||||
private final ThreadPoolExecutor mRequestThreadPool;
|
||||
private final OkHttpClient mOkHttpClient;
|
||||
|
||||
public EhClient(Context context) {
|
||||
mRequestThreadPool = IoThreadPoolExecutor.getInstance();
|
||||
mOkHttpClient = EhApplication.getOkHttpClient(context);
|
||||
}
|
||||
|
||||
public void execute(EhRequest request) {
|
||||
if (!request.isCancelled()) {
|
||||
Task task = new Task(request.getMethod(), request.getCallback(), request.getEhConfig());
|
||||
task.executeOnExecutor(mRequestThreadPool, request.getArgs());
|
||||
request.task = task;
|
||||
} else {
|
||||
request.getCallback().onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callback<E> {
|
||||
|
||||
void onSuccess(E result);
|
||||
|
||||
void onFailure(Exception e);
|
||||
|
||||
void onCancel();
|
||||
}
|
||||
|
||||
public class Task extends AsyncTask<Object, Void, Object> {
|
||||
|
||||
private final int mMethod;
|
||||
private final AtomicReference<Call> mCall = new AtomicReference<>();
|
||||
private final AtomicBoolean mStop = new AtomicBoolean();
|
||||
private Callback mCallback;
|
||||
private EhConfig mEhConfig;
|
||||
|
||||
public Task(int method, Callback callback, EhConfig ehConfig) {
|
||||
mMethod = method;
|
||||
mCallback = callback;
|
||||
mEhConfig = ehConfig;
|
||||
}
|
||||
|
||||
// Called in Job thread
|
||||
public void setCall(Call call) throws CancelledException {
|
||||
if (mStop.get()) {
|
||||
// Stopped Job thread
|
||||
throw new CancelledException();
|
||||
} else {
|
||||
mCall.lazySet(call);
|
||||
}
|
||||
}
|
||||
|
||||
public EhConfig getEhConfig() {
|
||||
return mEhConfig;
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (!mStop.get()) {
|
||||
mStop.lazySet(true);
|
||||
|
||||
if (mCallback != null) {
|
||||
// TODO Avoid new runnable
|
||||
SimpleHandler.getInstance().post(mCallback::onCancel);
|
||||
}
|
||||
|
||||
Status status = getStatus();
|
||||
if (status == Status.PENDING) {
|
||||
cancel(false);
|
||||
} else if (status == Status.RUNNING) {
|
||||
// It is running, cancel call if it is created
|
||||
Call call = mCall.get();
|
||||
if (call != null) {
|
||||
call.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
// Clear
|
||||
mCallback = null;
|
||||
mEhConfig = null;
|
||||
mCall.lazySet(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object doInBackground(Object... params) {
|
||||
try {
|
||||
switch (mMethod) {
|
||||
case METHOD_SIGN_IN:
|
||||
return EhEngine.signIn(this, mOkHttpClient, (String) params[0], (String) params[1]);
|
||||
case METHOD_GET_GALLERY_LIST:
|
||||
return EhEngine.getGalleryList(this, mOkHttpClient, (String) params[0]);
|
||||
case METHOD_GET_GALLERY_DETAIL:
|
||||
return EhEngine.getGalleryDetail(this, mOkHttpClient, (String) params[0]);
|
||||
case METHOD_GET_PREVIEW_SET:
|
||||
return EhEngine.getPreviewSet(this, mOkHttpClient, (String) params[0]);
|
||||
case METHOD_GET_RATE_GALLERY:
|
||||
return EhEngine.rateGallery(this, mOkHttpClient, (Long) params[0], (String) params[1], (Long) params[2], (String) params[3], (Float) params[4]);
|
||||
case METHOD_GET_COMMENT_GALLERY:
|
||||
return EhEngine.commentGallery(this, mOkHttpClient, (String) params[0], (String) params[1], (String) params[2]);
|
||||
case METHOD_GET_GALLERY_TOKEN:
|
||||
return EhEngine.getGalleryToken(this, mOkHttpClient, (Long) params[0], (String) params[1], (Integer) params[2]);
|
||||
case METHOD_GET_FAVORITES:
|
||||
return EhEngine.getFavorites(this, mOkHttpClient, (String) params[0], (Boolean) params[1]);
|
||||
case METHOD_ADD_FAVORITES:
|
||||
return EhEngine.addFavorites(this, mOkHttpClient, (Long) params[0], (String) params[1], (Integer) params[2], (String) params[3]);
|
||||
case METHOD_ADD_FAVORITES_RANGE:
|
||||
return EhEngine.addFavoritesRange(this, mOkHttpClient, (long[]) params[0], (String[]) params[1], (Integer) params[2]);
|
||||
case METHOD_MODIFY_FAVORITES:
|
||||
return EhEngine.modifyFavorites(this, mOkHttpClient, (String) params[0], (long[]) params[1], (Integer) params[2], (Boolean) params[3]);
|
||||
case METHOD_GET_TORRENT_LIST:
|
||||
return EhEngine.getTorrentList(this, mOkHttpClient, (String) params[0], (Long) params[1], (String) params[2]);
|
||||
case METHOD_GET_PROFILE:
|
||||
return EhEngine.getProfile(this, mOkHttpClient);
|
||||
case METHOD_VOTE_COMMENT:
|
||||
return EhEngine.voteComment(this, mOkHttpClient, (Long) params[0], (String) params[1], (Long) params[2], (String) params[3], (Long) params[4], (Integer) params[5]);
|
||||
case METHOD_IMAGE_SEARCH:
|
||||
return EhEngine.imageSearch(this, mOkHttpClient, (File) params[0], (Boolean) params[1], (Boolean) params[2], (Boolean) params[3]);
|
||||
case METHOD_ARCHIVE_LIST:
|
||||
return EhEngine.getArchiveList(this, mOkHttpClient, (String) params[0], (Long) params[1], (String) params[2]);
|
||||
case METHOD_DOWNLOAD_ARCHIVE:
|
||||
return EhEngine.downloadArchive(this, mOkHttpClient, (Long) params[0], (String) params[1], (String) params[2], (String) params[3]);
|
||||
case METHOD_VOTE_TAG:
|
||||
return EhEngine.voteTag(this, mOkHttpClient, (Long) params[0], (String) params[1], (Long) params[2], (String) params[3], (String) params[4], (Integer) params[5]);
|
||||
default:
|
||||
return new IllegalStateException("Can't detect method " + mMethod);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
protected void onPostExecute(Object result) {
|
||||
if (mCallback != null) {
|
||||
//noinspection StatementWithEmptyBody
|
||||
if (!(result instanceof CancelledException)) {
|
||||
if (result instanceof Exception) {
|
||||
mCallback.onFailure((Exception) result);
|
||||
} else {
|
||||
mCallback.onSuccess(result);
|
||||
}
|
||||
} else {
|
||||
// onCancel is called in stop
|
||||
}
|
||||
}
|
||||
|
||||
// Clear
|
||||
mCallback = null;
|
||||
mEhConfig = null;
|
||||
mCall.lazySet(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.client
|
||||
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.io.File
|
||||
|
||||
object EhClient {
|
||||
internal fun enqueue(request: EhRequest, scope: CoroutineScope) {
|
||||
check(!request.isActive) // Abort if attempt to execute an active request
|
||||
request.job = scope.launchIO {
|
||||
val callback: Callback<Any?>? = request.callback
|
||||
try {
|
||||
val result: Any? = request.run {
|
||||
if (args == null)
|
||||
execute(method)
|
||||
else
|
||||
execute(method, *args!!)
|
||||
}
|
||||
withUIContext { callback?.onSuccess(result) }
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException)
|
||||
throw e // Don't catch coroutine CancellationException
|
||||
e.printStackTrace()
|
||||
withUIContext { callback?.onFailure(e) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun execute(method: Int, vararg params: Any?): Any? {
|
||||
return when (method) {
|
||||
METHOD_SIGN_IN -> EhEngine.signIn(
|
||||
params[0] as String,
|
||||
params[1] as String
|
||||
)
|
||||
|
||||
METHOD_GET_GALLERY_LIST -> EhEngine.getGalleryList(
|
||||
params[0] as String
|
||||
)
|
||||
|
||||
METHOD_GET_GALLERY_DETAIL -> EhEngine.getGalleryDetail(
|
||||
params[0] as String?
|
||||
)
|
||||
|
||||
METHOD_GET_PREVIEW_SET -> EhEngine.getPreviewSet(
|
||||
params[0] as String?
|
||||
)
|
||||
|
||||
METHOD_GET_RATE_GALLERY -> EhEngine.rateGallery(
|
||||
params[0] as Long,
|
||||
params[1] as String?,
|
||||
params[2] as Long,
|
||||
params[3] as String?,
|
||||
params[4] as Float
|
||||
)
|
||||
|
||||
METHOD_GET_COMMENT_GALLERY -> EhEngine.commentGallery(
|
||||
params[0] as String?,
|
||||
params[1] as String,
|
||||
params[2] as String?
|
||||
)
|
||||
|
||||
METHOD_GET_GALLERY_TOKEN -> EhEngine.getGalleryToken(
|
||||
params[0] as Long,
|
||||
params[1] as String?,
|
||||
params[2] as Int
|
||||
)
|
||||
|
||||
METHOD_GET_FAVORITES -> EhEngine.getFavorites(
|
||||
params[0] as String
|
||||
)
|
||||
|
||||
METHOD_ADD_FAVORITES -> EhEngine.addFavorites(
|
||||
params[0] as Long,
|
||||
params[1] as String?,
|
||||
params[2] as Int,
|
||||
params[3] as String?
|
||||
)
|
||||
|
||||
METHOD_ADD_FAVORITES_RANGE -> @Suppress("UNCHECKED_CAST") EhEngine.addFavoritesRange(
|
||||
params[0] as LongArray,
|
||||
params[1] as Array<String?>,
|
||||
params[2] as Int
|
||||
)
|
||||
|
||||
METHOD_MODIFY_FAVORITES -> EhEngine.modifyFavorites(
|
||||
params[0] as String,
|
||||
params[1] as LongArray,
|
||||
params[2] as Int
|
||||
)
|
||||
|
||||
METHOD_GET_TORRENT_LIST -> EhEngine.getTorrentList(
|
||||
params[0] as String?,
|
||||
params[1] as Long,
|
||||
params[2] as String?
|
||||
)
|
||||
|
||||
METHOD_GET_PROFILE -> EhEngine.getProfile()
|
||||
METHOD_VOTE_COMMENT -> EhEngine.voteComment(
|
||||
params[0] as Long,
|
||||
params[1] as String?,
|
||||
params[2] as Long,
|
||||
params[3] as String?,
|
||||
params[4] as Long,
|
||||
params[5] as Int
|
||||
)
|
||||
|
||||
METHOD_IMAGE_SEARCH -> EhEngine.imageSearch(
|
||||
params[0] as File,
|
||||
params[1] as Boolean,
|
||||
params[2] as Boolean,
|
||||
params[3] as Boolean
|
||||
)
|
||||
|
||||
METHOD_ARCHIVE_LIST -> EhEngine.getArchiveList(
|
||||
params[0] as String?,
|
||||
params[1] as Long,
|
||||
params[2] as String?
|
||||
)
|
||||
|
||||
METHOD_DOWNLOAD_ARCHIVE -> EhEngine.downloadArchive(
|
||||
params[0] as Long,
|
||||
params[1] as String?,
|
||||
params[2] as String?,
|
||||
params[3] as String?,
|
||||
params[4] as Boolean
|
||||
)
|
||||
|
||||
METHOD_VOTE_TAG -> EhEngine.voteTag(
|
||||
params[0] as Long,
|
||||
params[1] as String?,
|
||||
params[2] as Long,
|
||||
params[3] as String?,
|
||||
params[4] as String?,
|
||||
params[5] as Int
|
||||
)
|
||||
|
||||
METHOD_GET_UCONFIG -> EhEngine.getUConfig()
|
||||
else -> throw IllegalStateException("Can't detect method $method")
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback<E> {
|
||||
fun onSuccess(result: E)
|
||||
fun onFailure(e: Exception)
|
||||
fun onCancel()
|
||||
}
|
||||
|
||||
const val METHOD_SIGN_IN = 0
|
||||
const val METHOD_GET_GALLERY_LIST = 1
|
||||
const val METHOD_GET_GALLERY_DETAIL = 3
|
||||
const val METHOD_GET_PREVIEW_SET = 4
|
||||
const val METHOD_GET_RATE_GALLERY = 5
|
||||
const val METHOD_GET_COMMENT_GALLERY = 6
|
||||
const val METHOD_GET_GALLERY_TOKEN = 7
|
||||
const val METHOD_GET_FAVORITES = 8
|
||||
const val METHOD_ADD_FAVORITES = 9
|
||||
const val METHOD_ADD_FAVORITES_RANGE = 10
|
||||
const val METHOD_MODIFY_FAVORITES = 11
|
||||
const val METHOD_GET_TORRENT_LIST = 12
|
||||
const val METHOD_GET_PROFILE = 14
|
||||
const val METHOD_VOTE_COMMENT = 15
|
||||
const val METHOD_IMAGE_SEARCH = 16
|
||||
const val METHOD_ARCHIVE_LIST = 17
|
||||
const val METHOD_DOWNLOAD_ARCHIVE = 18
|
||||
const val METHOD_VOTE_TAG = 19
|
||||
const val METHOD_GET_UCONFIG = 20
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.hippo.network.CookieRepository;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.Cookie;
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
public class EhCookieStore extends CookieRepository {
|
||||
|
||||
public static final String KEY_IPD_MEMBER_ID = "ipb_member_id";
|
||||
public static final String KEY_IPD_PASS_HASH = "ipb_pass_hash";
|
||||
public static final String KEY_IGNEOUS = "igneous";
|
||||
|
||||
public static final Cookie sTipsCookie =
|
||||
new Cookie.Builder()
|
||||
.name(EhConfig.KEY_CONTENT_WARNING)
|
||||
.value(EhConfig.CONTENT_WARNING_NOT_SHOW)
|
||||
.domain(EhUrl.DOMAIN_E)
|
||||
.path("/")
|
||||
.expiresAt(Long.MAX_VALUE)
|
||||
.build();
|
||||
|
||||
public EhCookieStore(Context context) {
|
||||
super(context, "okhttp3-cookie.db");
|
||||
}
|
||||
|
||||
public static Cookie newCookie(Cookie cookie, String newDomain, boolean forcePersistent,
|
||||
boolean forceLongLive, boolean forceNotHostOnly) {
|
||||
Cookie.Builder builder = new Cookie.Builder();
|
||||
builder.name(cookie.name());
|
||||
builder.value(cookie.value());
|
||||
|
||||
if (forceLongLive) {
|
||||
builder.expiresAt(Long.MAX_VALUE);
|
||||
} else if (cookie.persistent()) {
|
||||
builder.expiresAt(cookie.expiresAt());
|
||||
} else if (forcePersistent) {
|
||||
builder.expiresAt(Long.MAX_VALUE);
|
||||
}
|
||||
if (cookie.hostOnly() && !forceNotHostOnly) {
|
||||
builder.hostOnlyDomain(newDomain);
|
||||
} else {
|
||||
builder.domain(newDomain);
|
||||
}
|
||||
builder.path(cookie.path());
|
||||
if (cookie.secure()) {
|
||||
builder.secure();
|
||||
}
|
||||
if (cookie.httpOnly()) {
|
||||
builder.httpOnly();
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public void signOut() {
|
||||
clear();
|
||||
}
|
||||
|
||||
public boolean hasSignedIn() {
|
||||
HttpUrl url = HttpUrl.parse(EhUrl.HOST_E);
|
||||
return contains(url, KEY_IPD_MEMBER_ID) &&
|
||||
contains(url, KEY_IPD_PASS_HASH);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<Cookie> loadForRequest(@NonNull HttpUrl url) {
|
||||
List<Cookie> cookies = super.loadForRequest(url);
|
||||
|
||||
boolean checkTips = domainMatch(url, EhUrl.DOMAIN_E);
|
||||
|
||||
if (checkTips) {
|
||||
List<Cookie> result = new ArrayList<>(cookies.size() + 1);
|
||||
// Add all but skip some
|
||||
for (Cookie cookie : cookies) {
|
||||
String name = cookie.name();
|
||||
if (EhConfig.KEY_CONTENT_WARNING.equals(name)) {
|
||||
continue;
|
||||
}
|
||||
if (EhConfig.KEY_UCONFIG.equals(name)) {
|
||||
continue;
|
||||
}
|
||||
result.add(cookie);
|
||||
}
|
||||
// Add some
|
||||
result.add(sTipsCookie);
|
||||
return Collections.unmodifiableList(result);
|
||||
} else {
|
||||
return cookies;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.client
|
||||
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.network.CookieDatabase
|
||||
import com.hippo.network.CookieSet
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import java.util.Collections
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object EhCookieStore : CookieJar {
|
||||
private val db: CookieDatabase = CookieDatabase(EhApplication.application, "okhttp3-cookie.db")
|
||||
private val map: MutableMap<String, CookieSet> = db.allCookies
|
||||
|
||||
fun signOut() {
|
||||
clear()
|
||||
}
|
||||
|
||||
fun hasSignedIn(): Boolean {
|
||||
val url = EhUrl.HOST_E.toHttpUrl()
|
||||
return contains(url, KEY_IPB_MEMBER_ID) &&
|
||||
contains(url, KEY_IPB_PASS_HASH)
|
||||
}
|
||||
|
||||
const val KEY_IPB_MEMBER_ID = "ipb_member_id"
|
||||
const val KEY_IPB_PASS_HASH = "ipb_pass_hash"
|
||||
const val KEY_IGNEOUS = "igneous"
|
||||
private val sTipsCookie: Cookie = Cookie.Builder()
|
||||
.name(EhConfig.KEY_CONTENT_WARNING)
|
||||
.value(EhConfig.CONTENT_WARNING_NOT_SHOW)
|
||||
.domain(EhUrl.DOMAIN_E)
|
||||
.path("/")
|
||||
.expiresAt(Long.MAX_VALUE)
|
||||
.build()
|
||||
|
||||
fun newCookie(
|
||||
cookie: Cookie, newDomain: String, forcePersistent: Boolean,
|
||||
forceLongLive: Boolean, forceNotHostOnly: Boolean
|
||||
): Cookie {
|
||||
val builder = Cookie.Builder()
|
||||
builder.name(cookie.name)
|
||||
builder.value(cookie.value)
|
||||
if (forceLongLive) {
|
||||
builder.expiresAt(Long.MAX_VALUE)
|
||||
} else if (cookie.persistent) {
|
||||
builder.expiresAt(cookie.expiresAt)
|
||||
} else if (forcePersistent) {
|
||||
builder.expiresAt(Long.MAX_VALUE)
|
||||
}
|
||||
if (cookie.hostOnly && !forceNotHostOnly) {
|
||||
builder.hostOnlyDomain(newDomain)
|
||||
} else {
|
||||
builder.domain(newDomain)
|
||||
}
|
||||
builder.path(cookie.path)
|
||||
if (cookie.secure) {
|
||||
builder.secure()
|
||||
}
|
||||
if (cookie.httpOnly) {
|
||||
builder.httpOnly()
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addCookie(cookie: Cookie) {
|
||||
// For cookie database
|
||||
var toAdd: Cookie? = null
|
||||
var toUpdate: Cookie? = null
|
||||
var toRemove: Cookie? = null
|
||||
var set = map[cookie.domain]
|
||||
if (set == null) {
|
||||
set = CookieSet()
|
||||
map[cookie.domain] = set
|
||||
}
|
||||
if (cookie.expiresAt <= System.currentTimeMillis()) {
|
||||
toRemove = set.remove(cookie)
|
||||
// If the cookie is not persistent, it's not in database
|
||||
if (toRemove != null && !toRemove.persistent) {
|
||||
toRemove = null
|
||||
}
|
||||
} else {
|
||||
toAdd = cookie
|
||||
toUpdate = set.add(cookie)
|
||||
// If the cookie is not persistent, it's not in database
|
||||
if (!toAdd.persistent) toAdd = null
|
||||
if (toUpdate != null && !toUpdate.persistent) toUpdate = null
|
||||
// Remove the cookie if it updates to null
|
||||
if (toAdd == null && toUpdate != null) {
|
||||
toRemove = toUpdate
|
||||
toUpdate = null
|
||||
}
|
||||
}
|
||||
if (toRemove != null) {
|
||||
db.remove(toRemove)
|
||||
}
|
||||
if (toAdd != null) {
|
||||
if (toUpdate != null) {
|
||||
db.update(toUpdate, toAdd)
|
||||
} else {
|
||||
db.add(toAdd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getCookieHeader(url: HttpUrl): String {
|
||||
val cookies = getCookies(url)
|
||||
val cookieHeader = StringBuilder()
|
||||
var i = 0
|
||||
val size = cookies.size
|
||||
while (i < size) {
|
||||
if (i > 0) {
|
||||
cookieHeader.append("; ")
|
||||
}
|
||||
val cookie = cookies[i]
|
||||
cookieHeader.append(cookie.name).append('=').append(cookie.value)
|
||||
i++
|
||||
}
|
||||
return cookieHeader.toString()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun getCookies(url: HttpUrl): List<Cookie> {
|
||||
val accepted: MutableList<Cookie> = ArrayList()
|
||||
val expired: MutableList<Cookie> = ArrayList()
|
||||
for ((domain, cookieSet) in map) {
|
||||
if (domainMatch(url, domain)) {
|
||||
cookieSet[url, accepted, expired]
|
||||
}
|
||||
}
|
||||
for (cookie in expired) {
|
||||
if (cookie.persistent) {
|
||||
db.remove(cookie)
|
||||
}
|
||||
}
|
||||
|
||||
// RFC 6265 Section-5.4 step 2, sort the cookie-list
|
||||
// Cookies with longer paths are listed before cookies with shorter paths.
|
||||
// Ignore creation-time, we don't store them.
|
||||
accepted.sortWith { o1: Cookie, o2: Cookie -> o2.path.length - o1.path.length }
|
||||
return accepted
|
||||
}
|
||||
|
||||
fun contains(url: HttpUrl, name: String?): Boolean {
|
||||
for (cookie in getCookies(url)) {
|
||||
if (cookie.name == name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all cookies in this `CookieRepository`.
|
||||
*/
|
||||
@Synchronized
|
||||
fun clear() {
|
||||
map.clear()
|
||||
db.clear()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun close() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
for (cookie in cookies) {
|
||||
addCookie(cookie)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick and dirty pattern to differentiate IP addresses from hostnames. This is an approximation
|
||||
* of Android's private InetAddress#isNumeric API.
|
||||
*
|
||||
*
|
||||
* This matches IPv6 addresses as a hex string containing at least one colon, and possibly
|
||||
* including dots after the first colon. It matches IPv4 addresses as strings containing only
|
||||
* decimal digits and dots. This pattern matches strings like "a:.23" and "54" that are neither IP
|
||||
* addresses nor hostnames; they will be verified as IP addresses (which is a more strict
|
||||
* verification).
|
||||
*/
|
||||
private val VERIFY_AS_IP_ADDRESS =
|
||||
Pattern.compile("([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)")
|
||||
|
||||
/**
|
||||
* Returns true if `host` is not a host name and might be an IP address.
|
||||
*/
|
||||
private fun verifyAsIpAddress(host: String): Boolean {
|
||||
return VERIFY_AS_IP_ADDRESS.matcher(host).matches()
|
||||
}
|
||||
|
||||
// okhttp3.Cookie.domainMatch(HttpUrl, String)
|
||||
@JvmStatic
|
||||
fun domainMatch(url: HttpUrl, domain: String?): Boolean {
|
||||
val urlHost = url.host
|
||||
return if (urlHost == domain) {
|
||||
true // As in 'example.com' matching 'example.com'.
|
||||
} else urlHost.endsWith(domain!!)
|
||||
&& urlHost[urlHost.length - domain.length - 1] == '.' && !verifyAsIpAddress(
|
||||
urlHost
|
||||
)
|
||||
// As in 'example.com' matching 'www.example.com'.
|
||||
}
|
||||
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||
val cookies = getCookies(url)
|
||||
val checkTips = domainMatch(url, EhUrl.DOMAIN_E)
|
||||
return if (checkTips) {
|
||||
val result: MutableList<Cookie> = ArrayList(cookies.size + 1)
|
||||
// Add all but skip some
|
||||
for (cookie in cookies) {
|
||||
val name = cookie.name
|
||||
if (EhConfig.KEY_CONTENT_WARNING == name) {
|
||||
continue
|
||||
}
|
||||
if (EhConfig.KEY_UCONFIG == name) {
|
||||
continue
|
||||
}
|
||||
result.add(cookie)
|
||||
}
|
||||
// Add some
|
||||
result.add(sTipsCookie)
|
||||
Collections.unmodifiableList(result)
|
||||
} else {
|
||||
cookies
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client;
|
||||
|
||||
/*
|
||||
* Created by Hippo on 2018/3/23.
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.Hosts;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import okhttp3.Dns;
|
||||
|
||||
public class EhDns implements Dns {
|
||||
|
||||
private static final Map<String, List<InetAddress>> builtInHosts;
|
||||
|
||||
static {
|
||||
Map<String, List<InetAddress>> map = new HashMap<>();
|
||||
put(map, "e-hentai.org", "104.20.134.21", "104.20.135.21", "172.67.0.127");
|
||||
put(map, "exhentai.org", "178.175.128.252", "178.175.129.252", "178.175.129.254", "178.175.128.254",
|
||||
"178.175.132.20", "178.175.132.22");
|
||||
put(map, "repo.e-hentai.org", "94.100.28.57", "94.100.29.73");
|
||||
put(map, "forums.e-hentai.org", "94.100.18.243");
|
||||
put(map, "ehgt.org", "37.48.89.44", "81.171.10.48", "178.162.139.24", "178.162.140.212"
|
||||
, "2001:1af8:4700:a062:8::47de", "2001:1af8:4700:a062:9::47de", "2001:1af8:4700:a0c9:4::47de", "2001:1af8:4700:a0c9:3::47de");
|
||||
put(map, "gt0.ehgt.org", "37.48.89.44", "81.171.10.48", "178.162.139.24", "178.162.140.212"
|
||||
, "2001:1af8:4700:a062:8::47de", "2001:1af8:4700:a062:9::47de", "2001:1af8:4700:a0c9:4::47de", "2001:1af8:4700:a0c9:3::47de");
|
||||
put(map, "gt1.ehgt.org", "37.48.89.44", "81.171.10.48", "178.162.139.24", "178.162.140.212"
|
||||
, "2001:1af8:4700:a062:8::47de", "2001:1af8:4700:a062:9::47de", "2001:1af8:4700:a0c9:4::47de", "2001:1af8:4700:a0c9:3::47de");
|
||||
put(map, "gt2.ehgt.org", "37.48.89.44", "81.171.10.48", "178.162.139.24", "178.162.140.212"
|
||||
, "2001:1af8:4700:a062:8::47de", "2001:1af8:4700:a062:9::47de", "2001:1af8:4700:a0c9:4::47de", "2001:1af8:4700:a0c9:3::47de");
|
||||
put(map, "gt3.ehgt.org", "37.48.89.44", "81.171.10.48", "178.162.139.24", "178.162.140.212"
|
||||
, "2001:1af8:4700:a062:8::47de", "2001:1af8:4700:a062:9::47de", "2001:1af8:4700:a0c9:4::47de", "2001:1af8:4700:a0c9:3::47de");
|
||||
put(map, "ul.ehgt.org", "94.100.24.82", "94.100.24.72");
|
||||
put(map, "raw.githubusercontent.com", "151.101.0.133", "151.101.64.133", "151.101.128.133", "151.101.192.133");
|
||||
builtInHosts = map;
|
||||
}
|
||||
|
||||
private final Hosts hosts;
|
||||
|
||||
public EhDns(Context context) {
|
||||
hosts = EhApplication.getHosts(context);
|
||||
}
|
||||
|
||||
private static void put(Map<String, List<InetAddress>> map, String host, String... ips) {
|
||||
List<InetAddress> addresses = new ArrayList<>();
|
||||
for (String ip : ips) {
|
||||
addresses.add(Hosts.toInetAddress(host, ip));
|
||||
}
|
||||
map.put(host, addresses);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<InetAddress> lookup(@NonNull String hostname) throws UnknownHostException {
|
||||
List<InetAddress> inetAddresses = hosts.get(hostname);
|
||||
if (inetAddresses != null) {
|
||||
return inetAddresses;
|
||||
}
|
||||
if (Settings.getBuiltInHosts()) {
|
||||
inetAddresses = builtInHosts.get(hostname);
|
||||
if (inetAddresses != null) {
|
||||
return inetAddresses;
|
||||
}
|
||||
}
|
||||
return SYSTEM.lookup(hostname);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright 2018 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.client
|
||||
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.Hosts
|
||||
import com.hippo.ehviewer.Settings
|
||||
import okhttp3.Dns
|
||||
import java.net.InetAddress
|
||||
import java.net.UnknownHostException
|
||||
|
||||
object EhDns : Dns {
|
||||
private val hosts = EhApplication.hosts
|
||||
private val builtInHosts: MutableMap<String, List<InetAddress>> = mutableMapOf()
|
||||
|
||||
init {
|
||||
/* Pair(ip: String!, blockedByCCP: Boolean!) */
|
||||
val ehgtHosts = arrayOf(
|
||||
Pair("37.48.89.44", false),
|
||||
Pair("81.171.10.48", false),
|
||||
Pair("178.162.139.24", false),
|
||||
Pair("178.162.140.212", false),
|
||||
Pair("2001:1af8:4700:a062:8::47de", false),
|
||||
Pair("2001:1af8:4700:a062:9::47de", true),
|
||||
Pair("2001:1af8:4700:a0c9:4::47de", false),
|
||||
Pair("2001:1af8:4700:a0c9:3::47de", true),
|
||||
)
|
||||
|
||||
put(
|
||||
builtInHosts,
|
||||
"e-hentai.org",
|
||||
Pair("104.20.134.21", false),
|
||||
Pair("104.20.135.21", false),
|
||||
Pair("172.67.0.127", false),
|
||||
)
|
||||
put(
|
||||
builtInHosts,
|
||||
"exhentai.org",
|
||||
Pair("178.175.128.252", false),
|
||||
Pair("178.175.129.252", false),
|
||||
Pair("178.175.129.254", false),
|
||||
Pair("178.175.128.254", false),
|
||||
Pair("178.175.132.20", false),
|
||||
Pair("178.175.132.22", false),
|
||||
)
|
||||
put(
|
||||
builtInHosts,
|
||||
"repo.e-hentai.org",
|
||||
Pair("94.100.28.57", true),
|
||||
Pair("94.100.29.73", true),
|
||||
)
|
||||
put(
|
||||
builtInHosts,
|
||||
"forums.e-hentai.org",
|
||||
Pair("94.100.18.243", false),
|
||||
)
|
||||
put(
|
||||
builtInHosts,
|
||||
"ehgt.org",
|
||||
*ehgtHosts
|
||||
)
|
||||
put(
|
||||
builtInHosts,
|
||||
"gt0.ehgt.org",
|
||||
*ehgtHosts
|
||||
)
|
||||
put(
|
||||
builtInHosts,
|
||||
"gt1.ehgt.org",
|
||||
*ehgtHosts
|
||||
)
|
||||
put(
|
||||
builtInHosts,
|
||||
"gt2.ehgt.org",
|
||||
*ehgtHosts
|
||||
)
|
||||
put(
|
||||
builtInHosts,
|
||||
"gt3.ehgt.org",
|
||||
*ehgtHosts
|
||||
)
|
||||
put(
|
||||
builtInHosts,
|
||||
"ul.ehgt.org",
|
||||
Pair("94.100.24.82", true),
|
||||
Pair("94.100.24.72", true),
|
||||
)
|
||||
put(
|
||||
builtInHosts,
|
||||
"raw.githubusercontent.com",
|
||||
Pair("151.101.0.133", false),
|
||||
Pair("151.101.64.133", false),
|
||||
Pair("151.101.128.133", false),
|
||||
Pair("151.101.192.133", false),
|
||||
)
|
||||
}
|
||||
|
||||
private fun put(
|
||||
map: MutableMap<String, List<InetAddress>>,
|
||||
host: String,
|
||||
vararg ips: Pair<String, Boolean>
|
||||
) {
|
||||
map[host] = ips.mapNotNull { pair ->
|
||||
Hosts.toInetAddress(host, pair.first).takeUnless { Settings.getDF() && pair.second }
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(UnknownHostException::class)
|
||||
override fun lookup(hostname: String): List<InetAddress> {
|
||||
return hosts[hostname] ?: builtInHosts[hostname].takeIf { Settings.getBuiltInHosts() }
|
||||
?: Dns.SYSTEM.lookup(hostname)
|
||||
}
|
||||
|
||||
fun isInHosts(hostname: String): Boolean {
|
||||
return hosts.contains(hostname) || (builtInHosts.contains(hostname) && Settings.getBuiltInHosts())
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1,275 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.hippo.ehviewer.EhDB;
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo;
|
||||
import com.hippo.ehviewer.dao.Filter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class EhFilter {
|
||||
|
||||
public static final int MODE_TITLE = 0;
|
||||
public static final int MODE_UPLOADER = 1;
|
||||
public static final int MODE_TAG = 2;
|
||||
public static final int MODE_TAG_NAMESPACE = 3;
|
||||
private static final String TAG = EhFilter.class.getSimpleName();
|
||||
private static EhFilter sInstance;
|
||||
private final List<Filter> mTitleFilterList = new ArrayList<>();
|
||||
private final List<Filter> mUploaderFilterList = new ArrayList<>();
|
||||
private final List<Filter> mTagFilterList = new ArrayList<>();
|
||||
private final List<Filter> mTagNamespaceFilterList = new ArrayList<>();
|
||||
|
||||
private EhFilter() {
|
||||
List<Filter> list = EhDB.getAllFilter();
|
||||
for (int i = 0, n = list.size(); i < n; i++) {
|
||||
Filter filter = list.get(i);
|
||||
switch (filter.mode) {
|
||||
case MODE_TITLE:
|
||||
filter.text = filter.text.toLowerCase();
|
||||
mTitleFilterList.add(filter);
|
||||
break;
|
||||
case MODE_UPLOADER:
|
||||
mUploaderFilterList.add(filter);
|
||||
break;
|
||||
case MODE_TAG:
|
||||
filter.text = filter.text.toLowerCase();
|
||||
mTagFilterList.add(filter);
|
||||
break;
|
||||
case MODE_TAG_NAMESPACE:
|
||||
filter.text = filter.text.toLowerCase();
|
||||
mTagNamespaceFilterList.add(filter);
|
||||
break;
|
||||
default:
|
||||
Log.d(TAG, "Unknown mode: " + filter.mode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static EhFilter getInstance() {
|
||||
if (sInstance == null) {
|
||||
sInstance = new EhFilter();
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
public List<Filter> getTitleFilterList() {
|
||||
return mTitleFilterList;
|
||||
}
|
||||
|
||||
public List<Filter> getUploaderFilterList() {
|
||||
return mUploaderFilterList;
|
||||
}
|
||||
|
||||
public List<Filter> getTagFilterList() {
|
||||
return mTagFilterList;
|
||||
}
|
||||
|
||||
public List<Filter> getTagNamespaceFilterList() {
|
||||
return mTagNamespaceFilterList;
|
||||
}
|
||||
|
||||
public synchronized boolean addFilter(Filter filter) {
|
||||
// enable filter by default before it is added to database
|
||||
filter.enable = true;
|
||||
if (!EhDB.addFilter(filter)) return false;
|
||||
|
||||
switch (filter.mode) {
|
||||
case MODE_TITLE:
|
||||
filter.text = filter.text.toLowerCase();
|
||||
mTitleFilterList.add(filter);
|
||||
break;
|
||||
case MODE_UPLOADER:
|
||||
mUploaderFilterList.add(filter);
|
||||
break;
|
||||
case MODE_TAG:
|
||||
filter.text = filter.text.toLowerCase();
|
||||
mTagFilterList.add(filter);
|
||||
break;
|
||||
case MODE_TAG_NAMESPACE:
|
||||
filter.text = filter.text.toLowerCase();
|
||||
mTagNamespaceFilterList.add(filter);
|
||||
break;
|
||||
default:
|
||||
Log.d(TAG, "Unknown mode: " + filter.mode);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public synchronized void triggerFilter(Filter filter) {
|
||||
EhDB.triggerFilter(filter);
|
||||
}
|
||||
|
||||
public synchronized void deleteFilter(Filter filter) {
|
||||
EhDB.deleteFilter(filter);
|
||||
|
||||
switch (filter.mode) {
|
||||
case MODE_TITLE:
|
||||
mTitleFilterList.remove(filter);
|
||||
break;
|
||||
case MODE_UPLOADER:
|
||||
mUploaderFilterList.remove(filter);
|
||||
break;
|
||||
case MODE_TAG:
|
||||
mTagFilterList.remove(filter);
|
||||
break;
|
||||
case MODE_TAG_NAMESPACE:
|
||||
mTagNamespaceFilterList.remove(filter);
|
||||
break;
|
||||
default:
|
||||
Log.d(TAG, "Unknown mode: " + filter.mode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean needTags() {
|
||||
return 0 != mTagFilterList.size() || 0 != mTagNamespaceFilterList.size();
|
||||
}
|
||||
|
||||
public synchronized boolean filterTitle(GalleryInfo info) {
|
||||
if (null == info) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Title
|
||||
String title = info.title;
|
||||
List<Filter> filters = mTitleFilterList;
|
||||
if (null != title && filters.size() > 0) {
|
||||
for (int i = 0, n = filters.size(); i < n; i++) {
|
||||
if (filters.get(i).enable && title.toLowerCase().contains(filters.get(i).text)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public synchronized boolean filterUploader(GalleryInfo info) {
|
||||
if (null == info) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Uploader
|
||||
String uploader = info.uploader;
|
||||
List<Filter> filters = mUploaderFilterList;
|
||||
if (null != uploader && filters.size() > 0) {
|
||||
for (int i = 0, n = filters.size(); i < n; i++) {
|
||||
if (filters.get(i).enable && uploader.equals(filters.get(i).text)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean matchTag(String tag, String filter) {
|
||||
if (null == tag || null == filter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String tagNamespace;
|
||||
String tagName;
|
||||
String filterNamespace;
|
||||
String filterName;
|
||||
int index = tag.indexOf(':');
|
||||
if (index < 0) {
|
||||
tagNamespace = null;
|
||||
tagName = tag;
|
||||
} else {
|
||||
tagNamespace = tag.substring(0, index);
|
||||
tagName = tag.substring(index + 1);
|
||||
}
|
||||
index = filter.indexOf(':');
|
||||
if (index < 0) {
|
||||
filterNamespace = null;
|
||||
filterName = filter;
|
||||
} else {
|
||||
filterNamespace = filter.substring(0, index);
|
||||
filterName = filter.substring(index + 1);
|
||||
}
|
||||
|
||||
if (null != tagNamespace && null != filterNamespace &&
|
||||
!tagNamespace.equals(filterNamespace)) {
|
||||
return false;
|
||||
}
|
||||
return tagName.equals(filterName);
|
||||
}
|
||||
|
||||
public synchronized boolean filterTag(GalleryInfo info) {
|
||||
if (null == info) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Tag
|
||||
String[] tags = info.simpleTags;
|
||||
List<Filter> filters = mTagFilterList;
|
||||
if (null != tags && filters.size() > 0) {
|
||||
for (String tag : tags) {
|
||||
for (int i = 0, n = filters.size(); i < n; i++) {
|
||||
if (filters.get(i).enable && matchTag(tag, filters.get(i).text)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean matchTagNamespace(String tag, String filter) {
|
||||
if (null == tag || null == filter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String tagNamespace;
|
||||
int index = tag.indexOf(':');
|
||||
if (index >= 0) {
|
||||
tagNamespace = tag.substring(0, index);
|
||||
return tagNamespace.equals(filter);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean filterTagNamespace(GalleryInfo info) {
|
||||
if (null == info) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String[] tags = info.simpleTags;
|
||||
List<Filter> filters = mTagNamespaceFilterList;
|
||||
if (null != tags && filters.size() > 0) {
|
||||
for (String tag : tags) {
|
||||
for (int i = 0, n = filters.size(); i < n; i++) {
|
||||
if (filters.get(i).enable && matchTagNamespace(tag, filters.get(i).text)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.client
|
||||
|
||||
import android.util.Log
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.dao.Filter
|
||||
import java.util.Locale
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object EhFilter {
|
||||
private val mTitleFilterList: MutableList<Filter> = ArrayList()
|
||||
private val mUploaderFilterList: MutableList<Filter> = ArrayList()
|
||||
private val mTagFilterList: MutableList<Filter> = ArrayList()
|
||||
private val mTagNamespaceFilterList: MutableList<Filter> = ArrayList()
|
||||
private val mCommenterFilterList: MutableList<Filter> = ArrayList()
|
||||
private val mCommentFilterList: MutableList<Filter> = ArrayList()
|
||||
|
||||
const val MODE_TITLE = 0
|
||||
const val MODE_UPLOADER = 1
|
||||
const val MODE_TAG = 2
|
||||
const val MODE_TAG_NAMESPACE = 3
|
||||
const val MODE_COMMENTER = 4
|
||||
const val MODE_COMMENT = 5
|
||||
private val TAG = EhFilter::class.java.simpleName
|
||||
|
||||
init {
|
||||
val list = EhDB.getAllFilter()
|
||||
var i = 0
|
||||
val n = list.size
|
||||
while (i < n) {
|
||||
val filter = list[i]
|
||||
when (filter.mode) {
|
||||
MODE_TITLE -> {
|
||||
filter.text = filter.text!!.lowercase(Locale.getDefault())
|
||||
mTitleFilterList.add(filter)
|
||||
}
|
||||
|
||||
MODE_TAG -> {
|
||||
filter.text = filter.text!!.lowercase(Locale.getDefault())
|
||||
mTagFilterList.add(filter)
|
||||
}
|
||||
|
||||
MODE_TAG_NAMESPACE -> {
|
||||
filter.text = filter.text!!.lowercase(Locale.getDefault())
|
||||
mTagNamespaceFilterList.add(filter)
|
||||
}
|
||||
|
||||
MODE_UPLOADER -> mUploaderFilterList.add(filter)
|
||||
MODE_COMMENTER -> mCommenterFilterList.add(filter)
|
||||
MODE_COMMENT -> mCommentFilterList.add(filter)
|
||||
else -> Log.d(TAG, "Unknown mode: " + filter.mode)
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
val titleFilterList: List<Filter>
|
||||
get() = mTitleFilterList
|
||||
val uploaderFilterList: List<Filter>
|
||||
get() = mUploaderFilterList
|
||||
val tagFilterList: List<Filter>
|
||||
get() = mTagFilterList
|
||||
val tagNamespaceFilterList: List<Filter>
|
||||
get() = mTagNamespaceFilterList
|
||||
val commenterFilterList: List<Filter>
|
||||
get() = mCommenterFilterList
|
||||
val commentFilterList: List<Filter>
|
||||
get() = mCommentFilterList
|
||||
|
||||
@Synchronized
|
||||
fun addFilter(filter: Filter): Boolean {
|
||||
// enable filter by default before it is added to database
|
||||
filter.enable = true
|
||||
if (!EhDB.addFilter(filter)) return false
|
||||
when (filter.mode) {
|
||||
MODE_TITLE -> {
|
||||
filter.text = filter.text!!.lowercase(Locale.getDefault())
|
||||
mTitleFilterList.add(filter)
|
||||
}
|
||||
|
||||
MODE_TAG -> {
|
||||
filter.text = filter.text!!.lowercase(Locale.getDefault())
|
||||
mTagFilterList.add(filter)
|
||||
}
|
||||
|
||||
MODE_TAG_NAMESPACE -> {
|
||||
filter.text = filter.text!!.lowercase(Locale.getDefault())
|
||||
mTagNamespaceFilterList.add(filter)
|
||||
}
|
||||
|
||||
MODE_UPLOADER -> mUploaderFilterList.add(filter)
|
||||
MODE_COMMENTER -> mCommenterFilterList.add(filter)
|
||||
MODE_COMMENT -> mCommentFilterList.add(filter)
|
||||
else -> Log.d(TAG, "Unknown mode: " + filter.mode)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun triggerFilter(filter: Filter?) {
|
||||
EhDB.triggerFilter(filter)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun deleteFilter(filter: Filter) {
|
||||
EhDB.deleteFilter(filter)
|
||||
when (filter.mode) {
|
||||
MODE_TITLE -> mTitleFilterList.remove(filter)
|
||||
MODE_TAG -> mTagFilterList.remove(filter)
|
||||
MODE_TAG_NAMESPACE -> mTagNamespaceFilterList.remove(filter)
|
||||
MODE_UPLOADER -> mUploaderFilterList.remove(filter)
|
||||
MODE_COMMENTER -> mCommenterFilterList.remove(filter)
|
||||
MODE_COMMENT -> mCommentFilterList.remove(filter)
|
||||
else -> Log.d(TAG, "Unknown mode: " + filter.mode)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun needTags(): Boolean {
|
||||
return 0 != mTagFilterList.size || 0 != mTagNamespaceFilterList.size
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun filterTitle(info: GalleryInfo?): Boolean {
|
||||
if (null == info) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Title
|
||||
val title = info.title
|
||||
val filters: List<Filter> = mTitleFilterList
|
||||
if (null != title && filters.isNotEmpty()) {
|
||||
var i = 0
|
||||
val n = filters.size
|
||||
while (i < n) {
|
||||
if (filters[i].enable!! && title.lowercase(Locale.getDefault()).contains(
|
||||
filters[i].text!!
|
||||
)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun filterUploader(info: GalleryInfo?): Boolean {
|
||||
if (null == info) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Uploader
|
||||
val uploader = info.uploader
|
||||
val filters: List<Filter> = mUploaderFilterList
|
||||
if (null != uploader && filters.isNotEmpty()) {
|
||||
var i = 0
|
||||
val n = filters.size
|
||||
while (i < n) {
|
||||
if (filters[i].enable!! && uploader == filters[i].text) {
|
||||
return false
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun matchTag(tag: String?, filter: String?): Boolean {
|
||||
if (null == tag || null == filter) {
|
||||
return false
|
||||
}
|
||||
val tagNamespace: String?
|
||||
val tagName: String
|
||||
val filterNamespace: String?
|
||||
val filterName: String
|
||||
var index = tag.indexOf(':')
|
||||
if (index < 0) {
|
||||
tagNamespace = null
|
||||
tagName = tag
|
||||
} else {
|
||||
tagNamespace = tag.substring(0, index)
|
||||
tagName = tag.substring(index + 1)
|
||||
}
|
||||
index = filter.indexOf(':')
|
||||
if (index < 0) {
|
||||
filterNamespace = null
|
||||
filterName = filter
|
||||
} else {
|
||||
filterNamespace = filter.substring(0, index)
|
||||
filterName = filter.substring(index + 1)
|
||||
}
|
||||
return if (null != tagNamespace && null != filterNamespace &&
|
||||
tagNamespace != filterNamespace
|
||||
) {
|
||||
false
|
||||
} else tagName == filterName
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun filterTag(info: GalleryInfo?): Boolean {
|
||||
if (null == info) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Tag
|
||||
val tags = info.simpleTags
|
||||
val filters: List<Filter> = mTagFilterList
|
||||
if (null != tags && filters.isNotEmpty()) {
|
||||
for (tag in tags) {
|
||||
var i = 0
|
||||
val n = filters.size
|
||||
while (i < n) {
|
||||
if (filters[i].enable!! && matchTag(tag, filters[i].text)) {
|
||||
return false
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun matchTagNamespace(tag: String?, filter: String?): Boolean {
|
||||
if (null == tag || null == filter) {
|
||||
return false
|
||||
}
|
||||
val tagNamespace: String
|
||||
val index = tag.indexOf(':')
|
||||
return if (index >= 0) {
|
||||
tagNamespace = tag.substring(0, index)
|
||||
tagNamespace == filter
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun filterTagNamespace(info: GalleryInfo?): Boolean {
|
||||
if (null == info) {
|
||||
return false
|
||||
}
|
||||
val tags = info.simpleTags
|
||||
val filters: List<Filter> = mTagNamespaceFilterList
|
||||
if (null != tags && filters.isNotEmpty()) {
|
||||
for (tag in tags) {
|
||||
var i = 0
|
||||
val n = filters.size
|
||||
while (i < n) {
|
||||
if (filters[i].enable!! && matchTagNamespace(tag, filters[i].text)) {
|
||||
return false
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun filterCommenter(commenter: String?): Boolean {
|
||||
if (null == commenter) {
|
||||
return false
|
||||
}
|
||||
val filters: List<Filter> = mCommenterFilterList
|
||||
if (filters.isNotEmpty()) {
|
||||
var i = 0
|
||||
val n = filters.size
|
||||
while (i < n) {
|
||||
if (filters[i].enable!! && commenter == filters[i].text) {
|
||||
return false
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun filterComment(comment: String?): Boolean {
|
||||
if (null == comment) {
|
||||
return false
|
||||
}
|
||||
val filters: List<Filter> = mCommentFilterList
|
||||
if (filters.isNotEmpty()) {
|
||||
var i = 0
|
||||
val n = filters.size
|
||||
while (i < n) {
|
||||
if (filters[i].enable!!) {
|
||||
val p = Pattern.compile(filters[i].text!!)
|
||||
val m = p.matcher(comment)
|
||||
if (m.find()) return false
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client;
|
||||
|
||||
import com.hippo.ehviewer.Settings;
|
||||
|
||||
public class EhRequest {
|
||||
|
||||
EhClient.Task task;
|
||||
private int mMethod;
|
||||
private Object[] mArgs;
|
||||
private EhClient.Callback mCallback;
|
||||
private EhConfig mEhConfig;
|
||||
private boolean mCancel = false;
|
||||
|
||||
public int getMethod() {
|
||||
return mMethod;
|
||||
}
|
||||
|
||||
public EhRequest setMethod(int method) {
|
||||
mMethod = method;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Object[] getArgs() {
|
||||
return mArgs;
|
||||
}
|
||||
|
||||
public EhRequest setArgs(Object... args) {
|
||||
mArgs = args;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EhClient.Callback getCallback() {
|
||||
return mCallback;
|
||||
}
|
||||
|
||||
public EhRequest setCallback(EhClient.Callback callback) {
|
||||
mCallback = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EhConfig getEhConfig() {
|
||||
return mEhConfig != null ? mEhConfig : Settings.getEhConfig();
|
||||
}
|
||||
|
||||
public EhRequest setEhConfig(EhConfig ehConfig) {
|
||||
mEhConfig = ehConfig;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
if (!mCancel) {
|
||||
mCancel = true;
|
||||
if (task != null) {
|
||||
task.stop();
|
||||
task = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isCancelled() {
|
||||
return mCancel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.client
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
class EhRequest {
|
||||
internal var job: Job? = null
|
||||
val isActive
|
||||
get() = job?.isActive ?: false
|
||||
var method = 0
|
||||
private set
|
||||
var args: Array<out Any?>? = null
|
||||
private set
|
||||
var callback: EhClient.Callback<Any?>? = null
|
||||
private set
|
||||
|
||||
fun setMethod(method: Int): EhRequest {
|
||||
this.method = method
|
||||
return this
|
||||
}
|
||||
|
||||
fun setArgs(vararg args: Any?): EhRequest {
|
||||
this.args = args
|
||||
return this
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun setCallback(callback: EhClient.Callback<*>?): EhRequest {
|
||||
this.callback = callback as EhClient.Callback<Any?>?
|
||||
return this
|
||||
}
|
||||
|
||||
fun enqueue(scope: CoroutineScope) {
|
||||
EhClient.enqueue(this, scope)
|
||||
}
|
||||
|
||||
@DelicateCoroutinesApi
|
||||
fun enqueue() {
|
||||
EhClient.enqueue(this, GlobalScope)
|
||||
}
|
||||
|
||||
fun enqueue(fragment: Fragment) {
|
||||
enqueue(fragment.viewLifecycleOwner.lifecycleScope)
|
||||
}
|
||||
|
||||
fun enqueue(activity: Activity) {
|
||||
check(activity is FragmentActivity)
|
||||
enqueue(activity.lifecycleScope)
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun cancel() {
|
||||
if (isActive) {
|
||||
job?.cancel()
|
||||
callback?.onCancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client;
|
||||
|
||||
import com.hippo.okhttp.ChromeRequestBuilder;
|
||||
|
||||
public class EhRequestBuilder extends ChromeRequestBuilder {
|
||||
|
||||
public EhRequestBuilder(String url, String referer) {
|
||||
this(url, referer, null);
|
||||
}
|
||||
|
||||
public EhRequestBuilder(String url, String referer, String origin) {
|
||||
super(url);
|
||||
if (referer != null) {
|
||||
addHeader("Referer", referer);
|
||||
}
|
||||
if (origin != null) {
|
||||
addHeader("Origin", origin);
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-9
@@ -13,16 +13,17 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.client
|
||||
|
||||
package com.hippo.ehviewer.client.exception;
|
||||
import com.hippo.okhttp.ChromeRequestBuilder
|
||||
|
||||
public class EhException extends Exception {
|
||||
|
||||
public EhException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
}
|
||||
|
||||
public EhException(String detailMessage, Throwable cause) {
|
||||
super(detailMessage, cause);
|
||||
class EhRequestBuilder @JvmOverloads constructor(
|
||||
url: String,
|
||||
referer: String?,
|
||||
origin: String? = null
|
||||
) : ChromeRequestBuilder(url) {
|
||||
init {
|
||||
referer?.let { addHeader("Referer", it) }
|
||||
origin?.let { addHeader("Origin", it) }
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer 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.
|
||||
*
|
||||
* EhViewer 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 EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.hippo.ehviewer.Settings;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
public class EhSSLSocketFactory extends SSLSocketFactory {
|
||||
@Override
|
||||
public String[] getDefaultCipherSuites() {
|
||||
return ((SSLSocketFactory) getDefault()).getDefaultCipherSuites();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedCipherSuites() {
|
||||
return ((SSLSocketFactory) getDefault()).getSupportedCipherSuites();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
|
||||
SSLSocketFactory defaultFactory = (SSLSocketFactory) getDefault();
|
||||
if (!Settings.getDF()) {
|
||||
return defaultFactory.createSocket(s, host, port, autoClose);
|
||||
}
|
||||
String address = s.getInetAddress().getHostAddress();
|
||||
SSLSocket socket = (SSLSocket) defaultFactory.createSocket(s, address, port, autoClose);
|
||||
SSLSession sslSession = socket.getSession();
|
||||
Log.d("EhSSLSocketFactory", "Host: " + host + " Address: " + address + " Protocol:" + sslSession.getProtocol() + " CipherSuite:" + sslSession.getCipherSuite());
|
||||
socket.setEnabledProtocols(socket.getSupportedProtocols());
|
||||
return socket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port) throws IOException {
|
||||
return getDefault().createSocket(host, port);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
|
||||
return getDefault().createSocket(host, port, localHost, localPort);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(InetAddress host, int port) throws IOException {
|
||||
return getDefault().createSocket(host, port);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
|
||||
return getDefault().createSocket(address, port, localAddress, localPort);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer 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.
|
||||
*
|
||||
* EhViewer 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 EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.hippo.ehviewer.client
|
||||
|
||||
import android.util.Log
|
||||
import java.net.InetAddress
|
||||
import java.net.Socket
|
||||
import javax.net.ssl.SSLSocket
|
||||
import javax.net.ssl.SSLSocketFactory
|
||||
|
||||
object EhSSLSocketFactory : SSLSocketFactory() {
|
||||
private val sslSocketFactory = getDefault() as SSLSocketFactory
|
||||
override fun getDefaultCipherSuites(): Array<String> {
|
||||
return sslSocketFactory.defaultCipherSuites
|
||||
}
|
||||
|
||||
override fun getSupportedCipherSuites(): Array<String> {
|
||||
return sslSocketFactory.supportedCipherSuites
|
||||
}
|
||||
|
||||
override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket {
|
||||
val address = s.inetAddress.hostAddress.takeIf { EhDns.isInHosts(host) }
|
||||
val socket = sslSocketFactory.createSocket(s, address ?: host, port, autoClose) as SSLSocket
|
||||
val sslSession = socket.session
|
||||
Log.d(
|
||||
"EhSSLSocketFactory",
|
||||
"Host: " + host + " Address: " + address + " Protocol:" + sslSession.protocol + " CipherSuite:" + sslSession.cipherSuite
|
||||
)
|
||||
return socket
|
||||
}
|
||||
|
||||
override fun createSocket(host: String, port: Int): Socket {
|
||||
return sslSocketFactory.createSocket(host, port)
|
||||
}
|
||||
|
||||
override fun createSocket(
|
||||
host: String,
|
||||
port: Int,
|
||||
localHost: InetAddress,
|
||||
localPort: Int
|
||||
): Socket {
|
||||
return sslSocketFactory.createSocket(host, port, localHost, localPort)
|
||||
}
|
||||
|
||||
override fun createSocket(host: InetAddress, port: Int): Socket {
|
||||
return sslSocketFactory.createSocket(host, port)
|
||||
}
|
||||
|
||||
override fun createSocket(
|
||||
address: InetAddress,
|
||||
port: Int,
|
||||
localAddress: InetAddress,
|
||||
localPort: Int
|
||||
): Socket {
|
||||
return sslSocketFactory.createSocket(address, port, localAddress, localPort)
|
||||
}
|
||||
}
|
||||
@@ -1,436 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Base64;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.Pair;
|
||||
|
||||
import com.hippo.ehviewer.AppConfig;
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.util.ExceptionUtils;
|
||||
import com.hippo.util.IoThreadPoolExecutor;
|
||||
import com.hippo.yorozuya.FileUtils;
|
||||
import com.hippo.yorozuya.IOUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import okhttp3.Call;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import okio.BufferedSource;
|
||||
import okio.Okio;
|
||||
|
||||
public class EhTagDatabase {
|
||||
|
||||
private static final Map<String, String> NAMESPACE_TO_PREFIX = new HashMap<>();
|
||||
// TODO more lock for different language
|
||||
private static final Lock lock = new ReentrantLock();
|
||||
private static volatile EhTagDatabase instance;
|
||||
|
||||
static {
|
||||
NAMESPACE_TO_PREFIX.put("artist", "a:");
|
||||
NAMESPACE_TO_PREFIX.put("cosplayer", "cos:");
|
||||
NAMESPACE_TO_PREFIX.put("character", "c:");
|
||||
NAMESPACE_TO_PREFIX.put("female", "f:");
|
||||
NAMESPACE_TO_PREFIX.put("group", "g:");
|
||||
NAMESPACE_TO_PREFIX.put("language", "l:");
|
||||
NAMESPACE_TO_PREFIX.put("male", "m:");
|
||||
NAMESPACE_TO_PREFIX.put("mixed", "x:");
|
||||
NAMESPACE_TO_PREFIX.put("other", "o:");
|
||||
NAMESPACE_TO_PREFIX.put("parody", "p:");
|
||||
NAMESPACE_TO_PREFIX.put("reclass", "r:");
|
||||
}
|
||||
|
||||
private final String name;
|
||||
private final byte[] tags;
|
||||
|
||||
public EhTagDatabase(String name, BufferedSource source) throws IOException {
|
||||
this.name = name;
|
||||
String[] tmp;
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
source.readInt();
|
||||
for (String i : source.readUtf8().split("\n")) {
|
||||
tmp = i.split("\r", 2);
|
||||
buffer.append(tmp[0]);
|
||||
buffer.append("\r");
|
||||
try {
|
||||
buffer.append(new String(Base64.decode(tmp[1], Base64.DEFAULT), StandardCharsets.UTF_8));
|
||||
} catch (Exception e) {
|
||||
buffer.append(tmp[1]);
|
||||
}
|
||||
buffer.append("\n");
|
||||
}
|
||||
byte[] b = buffer.toString().getBytes(StandardCharsets.UTF_8);
|
||||
int totalBytes = b.length;
|
||||
tags = new byte[totalBytes];
|
||||
System.arraycopy(b, 0, tags, 0, totalBytes);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static EhTagDatabase getInstance(Context context) {
|
||||
if (isPossible(context)) {
|
||||
return instance;
|
||||
} else {
|
||||
instance = null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String namespaceToPrefix(String namespace) {
|
||||
return NAMESPACE_TO_PREFIX.get(namespace);
|
||||
}
|
||||
|
||||
private static String[] getMetadata(Context context) {
|
||||
String[] metadata = context.getResources().getStringArray(R.array.tag_translation_metadata);
|
||||
if (metadata.length == 4) {
|
||||
return metadata;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isPossible(Context context) {
|
||||
return getMetadata(context) != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static byte[] getFileContent(File file, int length) {
|
||||
try (BufferedSource source = Okio.buffer(Okio.source(file))) {
|
||||
byte[] content = new byte[length];
|
||||
source.readFully(content);
|
||||
return content;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static byte[] getFileSha1(File file) {
|
||||
try (InputStream is = new FileInputStream(file)) {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||
int n;
|
||||
byte[] buffer = new byte[4 * 1024];
|
||||
while ((n = is.read(buffer)) != -1) {
|
||||
digest.update(buffer, 0, n);
|
||||
}
|
||||
return digest.digest();
|
||||
} catch (IOException | NoSuchAlgorithmException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean equals(byte[] b1, byte[] b2) {
|
||||
if (b1 == null && b2 == null) {
|
||||
return true;
|
||||
}
|
||||
if (b1 == null || b2 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (b1.length != b2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < b1.length; i++) {
|
||||
if (b1[i] != b2[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean checkData(File sha1File, File dataFile) {
|
||||
byte[] s1 = getFileContent(sha1File, 20);
|
||||
if (s1 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] s2 = getFileSha1(dataFile);
|
||||
if (s2 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return equals(s1, s2);
|
||||
}
|
||||
|
||||
private static boolean save(OkHttpClient client, String url, File file) {
|
||||
Request request = new Request.Builder().url(url).build();
|
||||
Call call = client.newCall(request);
|
||||
try (Response response = call.execute()) {
|
||||
if (!response.isSuccessful()) {
|
||||
return false;
|
||||
}
|
||||
ResponseBody body = response.body();
|
||||
if (body == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try (InputStream is = body.byteStream(); OutputStream os = new FileOutputStream(file)) {
|
||||
IOUtils.copy(is, os);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
ExceptionUtils.throwIfFatal(t);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void update(Context context) {
|
||||
String[] urls = getMetadata(context);
|
||||
if (urls == null || urls.length != 4) {
|
||||
// Clear tags if it's not possible
|
||||
instance = null;
|
||||
return;
|
||||
}
|
||||
|
||||
String sha1Name = urls[0];
|
||||
String sha1Url = urls[1];
|
||||
String dataName = urls[2];
|
||||
String dataUrl = urls[3];
|
||||
|
||||
// Clear tags if name if different
|
||||
EhTagDatabase tmp = instance;
|
||||
if (tmp != null && !tmp.name.equals(dataName)) {
|
||||
instance = null;
|
||||
}
|
||||
|
||||
IoThreadPoolExecutor.getInstance().execute(() -> {
|
||||
if (!lock.tryLock()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
File dir = AppConfig.getFilesDir("tag-translations");
|
||||
if (dir == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check current sha1 and current data
|
||||
File sha1File = new File(dir, sha1Name);
|
||||
File dataFile = new File(dir, dataName);
|
||||
if (!checkData(sha1File, dataFile)) {
|
||||
FileUtils.delete(sha1File);
|
||||
FileUtils.delete(dataFile);
|
||||
}
|
||||
|
||||
// Read current EhTagDatabase
|
||||
if (instance == null && dataFile.exists()) {
|
||||
try (BufferedSource source = Okio.buffer(Okio.source(dataFile))) {
|
||||
instance = new EhTagDatabase(dataName, source);
|
||||
} catch (IOException e) {
|
||||
FileUtils.delete(sha1File);
|
||||
FileUtils.delete(dataFile);
|
||||
}
|
||||
}
|
||||
|
||||
OkHttpClient client = EhApplication.getOkHttpClient(context);
|
||||
|
||||
// Save new sha1
|
||||
File tempSha1File = new File(dir, sha1Name + ".tmp");
|
||||
if (!save(client, sha1Url, tempSha1File)) {
|
||||
FileUtils.delete(tempSha1File);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check new sha1 and current data
|
||||
if (checkData(tempSha1File, dataFile)) {
|
||||
// The data is the same
|
||||
FileUtils.delete(tempSha1File);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save new data
|
||||
File tempDataFile = new File(dir, dataName + ".tmp");
|
||||
if (!save(client, dataUrl, tempDataFile)) {
|
||||
FileUtils.delete(tempDataFile);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check new sha1 and new data
|
||||
if (!checkData(tempSha1File, tempDataFile)) {
|
||||
FileUtils.delete(tempSha1File);
|
||||
FileUtils.delete(tempDataFile);
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace current sha1 and current data with new sha1 and new data
|
||||
FileUtils.delete(sha1File);
|
||||
FileUtils.delete(dataFile);
|
||||
tempSha1File.renameTo(sha1File);
|
||||
tempDataFile.renameTo(dataFile);
|
||||
|
||||
// Read new EhTagDatabase
|
||||
try (BufferedSource source = Okio.buffer(Okio.source(dataFile))) {
|
||||
instance = new EhTagDatabase(dataName, source);
|
||||
} catch (IOException e) {
|
||||
// Ignore
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public String getTranslation(String tag) {
|
||||
return search(tags, tag.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String search(byte[] tags, byte[] tag) {
|
||||
int low = 0;
|
||||
int high = tags.length;
|
||||
while (low < high) {
|
||||
int start = (low + high) / 2;
|
||||
// Look for the starting '\n'
|
||||
while (start > -1 && tags[start] != '\n') {
|
||||
start--;
|
||||
}
|
||||
start++;
|
||||
|
||||
// Look for the middle '\r'.
|
||||
int middle = 1;
|
||||
while (tags[start + middle] != '\r') {
|
||||
middle++;
|
||||
}
|
||||
|
||||
// Look for the ending '\n'
|
||||
int end = middle + 1;
|
||||
while (tags[start + end] != '\n') {
|
||||
end++;
|
||||
}
|
||||
|
||||
int compare;
|
||||
int tagIndex = 0;
|
||||
int curIndex = start;
|
||||
|
||||
for (; ; ) {
|
||||
int tagByte = tag[tagIndex] & 0xff;
|
||||
int curByte = tags[curIndex] & 0xff;
|
||||
compare = tagByte - curByte;
|
||||
if (compare != 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
tagIndex++;
|
||||
curIndex++;
|
||||
if (tagIndex == tag.length && curIndex == start + middle) {
|
||||
break;
|
||||
}
|
||||
if (tagIndex == tag.length) {
|
||||
compare = -1;
|
||||
break;
|
||||
}
|
||||
if (curIndex == start + middle) {
|
||||
compare = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (compare < 0) {
|
||||
high = start - 1;
|
||||
} else if (compare > 0) {
|
||||
low = start + end + 1;
|
||||
} else {
|
||||
return new String(tags, start + middle + 1, end - middle - 1, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ArrayList<Pair<String, String>> suggest(String keyword) {
|
||||
return searchTag(tags, keyword);
|
||||
}
|
||||
|
||||
private ArrayList<Pair<String, String>> searchTag(byte[] tags, String keyword) {
|
||||
ArrayList<Pair<String, String>> searchHints = new ArrayList<>();
|
||||
int begin = 0;
|
||||
while (begin < tags.length - 1) {
|
||||
int start = begin;
|
||||
// Look for the starting '\n'
|
||||
while (tags[start] != '\n') {
|
||||
start++;
|
||||
}
|
||||
|
||||
// Look for the middle '\r'.
|
||||
int middle = 1;
|
||||
while (tags[start + middle] != '\r') {
|
||||
middle++;
|
||||
}
|
||||
|
||||
// Look for the ending '\n'
|
||||
int end = middle + 1;
|
||||
while (tags[start + end] != '\n') {
|
||||
end++;
|
||||
}
|
||||
|
||||
begin = start + end;
|
||||
|
||||
byte[] hintBytes = new byte[end - middle - 1];
|
||||
System.arraycopy(tags, start + middle + 1, hintBytes, 0, end - middle - 1);
|
||||
String hint = new String(hintBytes, StandardCharsets.UTF_8);
|
||||
byte[] tagBytes = new byte[middle];
|
||||
System.arraycopy(tags, start + 1, tagBytes, 0, middle);
|
||||
String tag = new String(tagBytes, StandardCharsets.UTF_8);
|
||||
int index = tag.indexOf(':');
|
||||
boolean keywordMatches;
|
||||
if (index == -1 || index >= tag.length() - 1 || keyword.length() > 2) {
|
||||
keywordMatches = containsIgnoreSpace(tag, keyword);
|
||||
} else {
|
||||
keywordMatches = containsIgnoreSpace(tag.substring(index + 1), keyword);
|
||||
}
|
||||
|
||||
if (keywordMatches || containsIgnoreSpace(hint, keyword)) {
|
||||
Pair<String, String> pair = new Pair<>(hint, tag);
|
||||
if (!searchHints.contains(pair)) {
|
||||
searchHints.add(pair);
|
||||
}
|
||||
}
|
||||
if (searchHints.size() > 20) {
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
return searchHints;
|
||||
}
|
||||
|
||||
private boolean containsIgnoreSpace(String text, String key) {
|
||||
return text.replace(" ", "").contains(key.replace(" ", ""));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
/*
|
||||
* Copyright 2019 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.client
|
||||
|
||||
import android.content.Context
|
||||
import com.hippo.ehviewer.AppConfig
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.EhApplication.Companion.okHttpClient
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import com.hippo.util.HashCodeUtils
|
||||
import com.hippo.yorozuya.FileUtils
|
||||
import com.hippo.yorozuya.IOUtils
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okio.BufferedSource
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
|
||||
private typealias TagGroup = Map<String, String>
|
||||
private typealias TagGroups = Map<String, TagGroup>
|
||||
|
||||
object EhTagDatabase {
|
||||
private const val NAMESPACE_PREFIX = "n"
|
||||
private lateinit var tagGroups: TagGroups
|
||||
|
||||
fun isInitialized(): Boolean {
|
||||
return this::tagGroups.isInitialized
|
||||
}
|
||||
|
||||
private fun JSONObject.toTagGroups(): TagGroups =
|
||||
keys().asSequence().associateWith { getJSONObject(it).toTagGroup() }
|
||||
|
||||
private fun JSONObject.toTagGroup(): TagGroup =
|
||||
keys().asSequence().associateWith { getString(it) }
|
||||
|
||||
private fun updateData(source: BufferedSource) {
|
||||
try {
|
||||
tagGroups = JSONObject(source.readString(StandardCharsets.UTF_8)).toTagGroups()
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun getTranslation(prefix: String? = NAMESPACE_PREFIX, tag: String?): String? {
|
||||
return tagGroups[prefix]?.get(tag)?.trim()?.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
private fun internalSuggestFlow(
|
||||
tags: Map<String, String>,
|
||||
keyword: String,
|
||||
translate: Boolean,
|
||||
exactly: Boolean
|
||||
): Flow<Pair<String?, String>> = flow {
|
||||
if (exactly) {
|
||||
tags[keyword]?.let {
|
||||
emit(Pair(it.takeIf { translate }, keyword))
|
||||
}
|
||||
} else {
|
||||
if (translate) {
|
||||
tags.forEach { (tag, hint) ->
|
||||
if (tag != keyword &&
|
||||
(tag.containsIgnoreSpace(keyword) || hint.containsIgnoreSpace(keyword))
|
||||
) {
|
||||
emit(Pair(hint, tag))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tags.keys.forEach { tag ->
|
||||
if (tag != keyword && tag.containsIgnoreSpace(keyword)) {
|
||||
emit(Pair(null, tag))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Construct a cold flow for tag database suggestions */
|
||||
fun suggestFlow(
|
||||
keyword: String,
|
||||
translate: Boolean,
|
||||
exactly: Boolean = false
|
||||
): Flow<Pair<String?, String>> = flow {
|
||||
val keywordPrefix = keyword.substringBefore(':')
|
||||
val keywordTag = keyword.drop(keywordPrefix.length + 1)
|
||||
val prefix = namespaceToPrefix(keywordPrefix) ?: keywordPrefix
|
||||
val tags = tagGroups[prefix.takeIf { keywordTag.isNotEmpty() && it != NAMESPACE_PREFIX }]
|
||||
tags?.let {
|
||||
internalSuggestFlow(it, keywordTag, translate, exactly).collect { (hint, tag) ->
|
||||
emit(Pair(hint, "$prefix:$tag"))
|
||||
}
|
||||
} ?: tagGroups.forEach { (prefix, tags) ->
|
||||
if (prefix != NAMESPACE_PREFIX) {
|
||||
internalSuggestFlow(tags, keyword, translate, exactly).collect { (hint, tag) ->
|
||||
emit(Pair(hint, "$prefix:$tag"))
|
||||
}
|
||||
} else {
|
||||
internalSuggestFlow(tags, keyword, translate, exactly).collect { (hint, tag) ->
|
||||
emit(Pair(hint, "$tag:"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.removeSpace(): String = replace(" ", "")
|
||||
|
||||
private fun String.containsIgnoreSpace(other: String, ignoreCase: Boolean = true): Boolean =
|
||||
removeSpace().contains(other.removeSpace(), ignoreCase)
|
||||
|
||||
|
||||
private val NAMESPACE_TO_PREFIX = HashMap<String, String>().also {
|
||||
it["artist"] = "a"
|
||||
it["cosplayer"] = "cos"
|
||||
it["character"] = "c"
|
||||
it["female"] = "f"
|
||||
it["group"] = "g"
|
||||
it["language"] = "l"
|
||||
it["male"] = "m"
|
||||
it["mixed"] = "x"
|
||||
it["other"] = "o"
|
||||
it["parody"] = "p"
|
||||
it["reclass"] = "r"
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun namespaceToPrefix(namespace: String): String? {
|
||||
return NAMESPACE_TO_PREFIX[namespace]
|
||||
}
|
||||
|
||||
private fun getMetadata(context: Context): Array<String>? {
|
||||
return context.resources.getStringArray(R.array.tag_translation_metadata)
|
||||
.takeIf { it.size == 4 }
|
||||
}
|
||||
|
||||
fun isTranslatable(context: Context): Boolean {
|
||||
return context.resources.getBoolean(R.bool.tag_translatable)
|
||||
}
|
||||
|
||||
private fun getFileContent(file: File): String? {
|
||||
try {
|
||||
file.source().buffer().use { return it.readString(StandardCharsets.UTF_8) }
|
||||
} catch (e: IOException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFileSha1(file: File): String? {
|
||||
try {
|
||||
FileInputStream(file).use { stream ->
|
||||
val digest = MessageDigest.getInstance("SHA-1")
|
||||
var n: Int
|
||||
val buffer = ByteArray(4 * 1024)
|
||||
while (stream.read(buffer).also { n = it } != -1) {
|
||||
digest.update(buffer, 0, n)
|
||||
}
|
||||
return HashCodeUtils.bytesToHexString(digest.digest())
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
return null
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkData(sha1File: File, dataFile: File): Boolean {
|
||||
val s1 = getFileContent(sha1File) ?: return false
|
||||
val s2 = getFileSha1(dataFile) ?: return false
|
||||
return s1 == s2
|
||||
}
|
||||
|
||||
private fun save(client: OkHttpClient, url: String, file: File): Boolean {
|
||||
val request: Request = Request.Builder().url(url).build()
|
||||
val call = client.newCall(request)
|
||||
try {
|
||||
call.execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
return false
|
||||
}
|
||||
val body = response.body
|
||||
body.byteStream()
|
||||
.use { ins -> FileOutputStream(file).use { os -> IOUtils.copy(ins, os) } }
|
||||
return true
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
FileUtils.delete(file)
|
||||
t.printStackTrace()
|
||||
ExceptionUtils.throwIfFatal(t)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
@JvmStatic
|
||||
fun update() {
|
||||
launchIO {
|
||||
updateInternal()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun updateInternal() {
|
||||
val urls = getMetadata(EhApplication.application)
|
||||
urls?.let {
|
||||
val sha1Name = urls[0]
|
||||
val sha1Url = urls[1]
|
||||
val dataName = urls[2]
|
||||
val dataUrl = urls[3]
|
||||
|
||||
try {
|
||||
val dir = AppConfig.getFilesDir("tag-translations")
|
||||
checkNotNull(dir)
|
||||
|
||||
// Check current sha1 and current data
|
||||
val sha1File = File(dir, sha1Name)
|
||||
val dataFile = File(dir, dataName)
|
||||
if (!checkData(sha1File, dataFile)) {
|
||||
FileUtils.delete(sha1File)
|
||||
FileUtils.delete(dataFile)
|
||||
}
|
||||
|
||||
// Read current EhTagDatabase
|
||||
if (!isInitialized() && dataFile.exists()) {
|
||||
try {
|
||||
dataFile.source().buffer().use { updateData(it) }
|
||||
} catch (e: IOException) {
|
||||
FileUtils.delete(sha1File)
|
||||
FileUtils.delete(dataFile)
|
||||
}
|
||||
}
|
||||
val client = okHttpClient
|
||||
|
||||
// Save new sha1
|
||||
val tempSha1File = File(dir, "$sha1Name.tmp")
|
||||
check(save(client, sha1Url, tempSha1File))
|
||||
|
||||
// Check new sha1 and current data
|
||||
if (checkData(tempSha1File, dataFile)) {
|
||||
// The data is the same
|
||||
FileUtils.delete(tempSha1File)
|
||||
return
|
||||
}
|
||||
|
||||
// Save new data
|
||||
val tempDataFile = File(dir, "$dataName.tmp")
|
||||
check(save(client, dataUrl, tempDataFile))
|
||||
|
||||
// Check new sha1 and new data
|
||||
if (!checkData(tempSha1File, tempDataFile)) {
|
||||
FileUtils.delete(tempSha1File)
|
||||
FileUtils.delete(tempDataFile)
|
||||
return
|
||||
}
|
||||
|
||||
// Replace current sha1 and current data with new sha1 and new data
|
||||
FileUtils.delete(sha1File)
|
||||
FileUtils.delete(dataFile)
|
||||
tempSha1File.renameTo(sha1File)
|
||||
tempDataFile.renameTo(dataFile)
|
||||
|
||||
// Read new EhTagDatabase
|
||||
try {
|
||||
dataFile.source().buffer().use { updateData(it) }
|
||||
} catch (_: IOException) {
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,243 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.network.UrlBuilder;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
public class EhUrl {
|
||||
|
||||
public static final int SITE_E = 0;
|
||||
public static final int SITE_EX = 1;
|
||||
|
||||
public static final String DOMAIN_EX = "exhentai.org";
|
||||
public static final String DOMAIN_E = "e-hentai.org";
|
||||
public static final String DOMAIN_LOFI = "lofi.e-hentai.org";
|
||||
|
||||
public static final String HOST_EX = "https://" + DOMAIN_EX + "/";
|
||||
public static final String API_EX = HOST_EX + "api.php";
|
||||
public static final String URL_FAVORITES_EX = HOST_EX + "favorites.php";
|
||||
public static final String URL_UCONFIG_EX = HOST_EX + "uconfig.php";
|
||||
public static final String URL_MY_TAGS_EX = HOST_EX + "mytags";
|
||||
public static final String URL_WATCHED_EX = HOST_EX + "watched";
|
||||
public static final String HOST_E = "https://" + DOMAIN_E + "/";
|
||||
public static final String API_E = HOST_E + "api.php";
|
||||
public static final String URL_FAVORITES_E = HOST_E + "favorites.php";
|
||||
public static final String URL_UCONFIG_E = HOST_E + "uconfig.php";
|
||||
public static final String URL_MY_TAGS_E = HOST_E + "mytags";
|
||||
public static final String URL_WATCHED_E = HOST_E + "watched";
|
||||
public static final String API_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login&CODE=01";
|
||||
public static final String URL_POPULAR_E = "https://e-hentai.org/popular";
|
||||
public static final String URL_POPULAR_EX = "https://exhentai.org/popular";
|
||||
public static final String URL_IMAGE_SEARCH_E = "https://upload.e-hentai.org/image_lookup.php";
|
||||
public static final String URL_IMAGE_SEARCH_EX = "https://exhentai.org/upload/image_lookup.php";
|
||||
public static final String URL_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login";
|
||||
public static final String URL_REGISTER = "https://forums.e-hentai.org/index.php?act=Reg&CODE=00";
|
||||
public static final String URL_FORUMS = "https://forums.e-hentai.org/";
|
||||
public static final String REFERER_EX = "https://" + DOMAIN_EX;
|
||||
public static final String ORIGIN_EX = REFERER_EX;
|
||||
public static final String REFERER_E = "https://" + DOMAIN_E;
|
||||
public static final String ORIGIN_E = REFERER_E;
|
||||
private static final String URL_PREFIX_THUMB_E = "https://ehgt.org/";
|
||||
private static final String URL_PREFIX_THUMB_EX = "https://exhentai.org/t/";
|
||||
|
||||
public static String getGalleryDetailUrl(long gid, String token) {
|
||||
return getGalleryDetailUrl(gid, token, 0, false);
|
||||
}
|
||||
|
||||
public static String getHost() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return HOST_E;
|
||||
case SITE_EX:
|
||||
return HOST_EX;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getFavoritesUrl() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return URL_FAVORITES_E;
|
||||
case SITE_EX:
|
||||
return URL_FAVORITES_EX;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getApiUrl() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return API_E;
|
||||
case SITE_EX:
|
||||
return API_EX;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getReferer() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return REFERER_E;
|
||||
case SITE_EX:
|
||||
return REFERER_EX;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getOrigin() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return ORIGIN_E;
|
||||
case SITE_EX:
|
||||
return ORIGIN_EX;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getUConfigUrl() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return URL_UCONFIG_E;
|
||||
case SITE_EX:
|
||||
return URL_UCONFIG_EX;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getMyTagsUrl() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return URL_MY_TAGS_E;
|
||||
case SITE_EX:
|
||||
return URL_MY_TAGS_EX;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getGalleryDetailUrl(long gid, String token, int index, boolean allComment) {
|
||||
UrlBuilder builder = new UrlBuilder(getHost() + "g/" + gid + '/' + token + '/');
|
||||
if (index != 0) {
|
||||
builder.addQuery("p", index);
|
||||
}
|
||||
if (allComment) {
|
||||
builder.addQuery("hc", 1);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static String getGalleryMultiPageViewerUrl(long gid, String token) {
|
||||
UrlBuilder builder = new UrlBuilder(getHost() + "mpv/" + gid + '/' + token + '/');
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static String getPageUrl(long gid, int index, String pToken) {
|
||||
return getHost() + "s/" + pToken + '/' + gid + '-' + (index + 1);
|
||||
}
|
||||
|
||||
public static String getAddFavorites(long gid, String token) {
|
||||
return getHost() + "gallerypopups.php?gid=" + gid + "&t=" + token + "&act=addfav";
|
||||
}
|
||||
|
||||
public static String getDownloadArchive(long gid, String token, String or) {
|
||||
return getHost() + "archiver.php?gid=" + gid + "&token=" + token + "&or=" + or;
|
||||
}
|
||||
|
||||
public static String getTagDefinitionUrl(String tag) {
|
||||
return "https://ehwiki.org/wiki/" + tag.replace(' ', '_');
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getPopularUrl() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return URL_POPULAR_E;
|
||||
case SITE_EX:
|
||||
return URL_POPULAR_EX;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getImageSearchUrl() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return URL_IMAGE_SEARCH_E;
|
||||
case SITE_EX:
|
||||
return URL_IMAGE_SEARCH_EX;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getWatchedUrl() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return URL_WATCHED_E;
|
||||
case SITE_EX:
|
||||
return URL_WATCHED_EX;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getThumbUrlPrefix() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
//case SITE_E:
|
||||
return URL_PREFIX_THUMB_E;
|
||||
//case SITE_EX:
|
||||
// return URL_PREFIX_THUMB_EX;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getFixedPreviewThumbUrl(String originUrl) {
|
||||
HttpUrl url = HttpUrl.parse(originUrl);
|
||||
if (url == null) return originUrl;
|
||||
List<String> pathSegments = url.pathSegments();
|
||||
if (pathSegments.size() < 3) return originUrl;
|
||||
|
||||
ListIterator<String> iterator = pathSegments.listIterator(pathSegments.size());
|
||||
// The last segments, like
|
||||
// 317a1a254cd9c3269e71b2aa2671fe8d28c91097-260198-640-480-png_250.jpg
|
||||
if (!iterator.hasPrevious()) return originUrl;
|
||||
String lastSegment = iterator.previous();
|
||||
// The second last segments, like
|
||||
// 7a
|
||||
if (!iterator.hasPrevious()) return originUrl;
|
||||
String secondLastSegment = iterator.previous();
|
||||
// The third last segments, like
|
||||
// 31
|
||||
if (!iterator.hasPrevious()) return originUrl;
|
||||
String thirdLastSegment = iterator.previous();
|
||||
// Check path segments
|
||||
if (lastSegment != null && secondLastSegment != null
|
||||
&& thirdLastSegment != null
|
||||
&& lastSegment.startsWith(thirdLastSegment)
|
||||
&& lastSegment.startsWith(secondLastSegment, thirdLastSegment.length())) {
|
||||
return getThumbUrlPrefix() + thirdLastSegment + "/" + secondLastSegment + "/" + lastSegment;
|
||||
} else {
|
||||
return originUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.client
|
||||
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.network.UrlBuilder
|
||||
|
||||
object EhUrl {
|
||||
const val SITE_E = 0
|
||||
const val SITE_EX = 1
|
||||
const val DOMAIN_EX = "exhentai.org"
|
||||
const val DOMAIN_E = "e-hentai.org"
|
||||
const val DOMAIN_LOFI = "lofi.e-hentai.org"
|
||||
const val HOST_EX = "https://$DOMAIN_EX/"
|
||||
const val API_EX = HOST_EX + "api.php"
|
||||
const val URL_FAVORITES_EX = HOST_EX + "favorites.php"
|
||||
const val URL_UCONFIG_EX = HOST_EX + "uconfig.php"
|
||||
const val URL_MY_TAGS_EX = HOST_EX + "mytags"
|
||||
const val URL_WATCHED_EX = HOST_EX + "watched"
|
||||
const val HOST_E = "https://$DOMAIN_E/"
|
||||
const val API_E = HOST_E + "api.php"
|
||||
const val URL_FAVORITES_E = HOST_E + "favorites.php"
|
||||
const val URL_UCONFIG_E = HOST_E + "uconfig.php"
|
||||
const val URL_MY_TAGS_E = HOST_E + "mytags"
|
||||
const val URL_WATCHED_E = HOST_E + "watched"
|
||||
const val API_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login&CODE=01"
|
||||
const val URL_POPULAR_E = "https://e-hentai.org/popular"
|
||||
const val URL_POPULAR_EX = "https://exhentai.org/popular"
|
||||
const val URL_IMAGE_SEARCH_E = "https://upload.e-hentai.org/image_lookup.php"
|
||||
const val URL_IMAGE_SEARCH_EX = "https://exhentai.org/upload/image_lookup.php"
|
||||
const val URL_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login"
|
||||
const val URL_REGISTER = "https://forums.e-hentai.org/index.php?act=Reg&CODE=00"
|
||||
const val URL_FORUMS = "https://forums.e-hentai.org/"
|
||||
const val URL_FUNDS = HOST_E + "exchange.php?t=gp"
|
||||
const val URL_HOME = HOST_E + "home.php"
|
||||
const val REFERER_EX = "https://$DOMAIN_EX"
|
||||
const val ORIGIN_EX = REFERER_EX
|
||||
const val REFERER_E = "https://$DOMAIN_E"
|
||||
const val ORIGIN_E = REFERER_E
|
||||
const val URL_PREFIX_THUMB_E = "https://ehgt.org/"
|
||||
const val URL_PREFIX_THUMB_EX = "https://exhentai.org/t/"
|
||||
|
||||
val host: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> HOST_E
|
||||
SITE_EX -> HOST_EX
|
||||
else -> HOST_E
|
||||
}
|
||||
|
||||
val favoritesUrl: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> URL_FAVORITES_E
|
||||
SITE_EX -> URL_FAVORITES_EX
|
||||
else -> URL_FAVORITES_E
|
||||
}
|
||||
|
||||
val apiUrl: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> API_E
|
||||
SITE_EX -> API_EX
|
||||
else -> API_E
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val referer: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> REFERER_E
|
||||
SITE_EX -> REFERER_EX
|
||||
else -> REFERER_E
|
||||
}
|
||||
|
||||
val origin: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> ORIGIN_E
|
||||
SITE_EX -> ORIGIN_EX
|
||||
else -> ORIGIN_E
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val uConfigUrl: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> URL_UCONFIG_E
|
||||
SITE_EX -> URL_UCONFIG_EX
|
||||
else -> URL_UCONFIG_E
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val myTagsUrl: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> URL_MY_TAGS_E
|
||||
SITE_EX -> URL_MY_TAGS_EX
|
||||
else -> URL_MY_TAGS_E
|
||||
}
|
||||
|
||||
val popularUrl: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> URL_POPULAR_E
|
||||
SITE_EX -> URL_POPULAR_EX
|
||||
else -> URL_POPULAR_E
|
||||
}
|
||||
|
||||
val imageSearchUrl: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> URL_IMAGE_SEARCH_E
|
||||
SITE_EX -> URL_IMAGE_SEARCH_EX
|
||||
else -> URL_IMAGE_SEARCH_E
|
||||
}
|
||||
|
||||
val watchedUrl: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> URL_WATCHED_E
|
||||
SITE_EX -> URL_WATCHED_EX
|
||||
else -> URL_WATCHED_E
|
||||
}
|
||||
|
||||
fun getGalleryDetailUrl(gid: Long, token: String?): String {
|
||||
return getGalleryDetailUrl(gid, token, 0, false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getGalleryDetailUrl(gid: Long, token: String?, index: Int, allComment: Boolean): String {
|
||||
val builder = UrlBuilder(host + "g/" + gid + '/' + token + '/')
|
||||
if (index != 0) {
|
||||
builder.addQuery("p", index)
|
||||
}
|
||||
if (allComment) {
|
||||
builder.addQuery("hc", 1)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getGalleryMultiPageViewerUrl(gid: Long, token: String): String {
|
||||
val builder = UrlBuilder(host + "mpv/" + gid + '/' + token + '/')
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getPageUrl(gid: Long, index: Int, pToken: String): String {
|
||||
return host + "s/" + pToken + '/' + gid + '-' + (index + 1)
|
||||
}
|
||||
|
||||
fun getAddFavorites(gid: Long, token: String?): String {
|
||||
return host + "gallerypopups.php?gid=" + gid + "&t=" + token + "&act=addfav"
|
||||
}
|
||||
|
||||
fun getDownloadArchive(gid: Long, token: String?, or: String): String {
|
||||
return host + "archiver.php?gid=" + gid + "&token=" + token + "&or=" + or
|
||||
}
|
||||
|
||||
fun getTagDefinitionUrl(tag: String): String {
|
||||
return "https://ehwiki.org/wiki/" + tag.replace(' ', '_')
|
||||
}
|
||||
|
||||
fun getThumbUrlPrefix(): String {
|
||||
return when (Settings.getGallerySite()) {
|
||||
SITE_E -> URL_PREFIX_THUMB_E
|
||||
SITE_EX -> URL_PREFIX_THUMB_EX
|
||||
else -> URL_PREFIX_THUMB_E
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder;
|
||||
import com.hippo.ehviewer.client.parser.GalleryDetailUrlParser;
|
||||
import com.hippo.ehviewer.client.parser.GalleryListUrlParser;
|
||||
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser;
|
||||
import com.hippo.ehviewer.ui.scene.GalleryDetailScene;
|
||||
import com.hippo.ehviewer.ui.scene.GalleryListScene;
|
||||
import com.hippo.ehviewer.ui.scene.ProgressScene;
|
||||
import com.hippo.scene.Announcer;
|
||||
|
||||
public class EhUrlOpener {
|
||||
|
||||
private static final String TAG = EhUrlOpener.class.getSimpleName();
|
||||
|
||||
@Nullable
|
||||
public static Announcer parseUrl(String url) {
|
||||
if (TextUtils.isEmpty(url)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ListUrlBuilder listUrlBuilder = GalleryListUrlParser.parse(url);
|
||||
if (listUrlBuilder != null) {
|
||||
Bundle args = new Bundle();
|
||||
args.putString(GalleryListScene.KEY_ACTION, GalleryListScene.ACTION_LIST_URL_BUILDER);
|
||||
args.putParcelable(GalleryListScene.KEY_LIST_URL_BUILDER, listUrlBuilder);
|
||||
return new Announcer(GalleryListScene.class).setArgs(args);
|
||||
}
|
||||
|
||||
GalleryDetailUrlParser.Result result1 = GalleryDetailUrlParser.parse(url);
|
||||
if (result1 != null) {
|
||||
Bundle args = new Bundle();
|
||||
args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GID_TOKEN);
|
||||
args.putLong(GalleryDetailScene.KEY_GID, result1.gid);
|
||||
args.putString(GalleryDetailScene.KEY_TOKEN, result1.token);
|
||||
return new Announcer(GalleryDetailScene.class).setArgs(args);
|
||||
}
|
||||
|
||||
GalleryPageUrlParser.Result result2 = GalleryPageUrlParser.parse(url);
|
||||
if (result2 != null) {
|
||||
Bundle args = new Bundle();
|
||||
args.putString(ProgressScene.KEY_ACTION, ProgressScene.ACTION_GALLERY_TOKEN);
|
||||
args.putLong(ProgressScene.KEY_GID, result2.gid);
|
||||
args.putString(ProgressScene.KEY_PTOKEN, result2.pToken);
|
||||
args.putInt(ProgressScene.KEY_PAGE, result2.page);
|
||||
return new Announcer(ProgressScene.class).setArgs(args);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Can't parse url: " + url);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class EhUtils {
|
||||
|
||||
public static final int NONE = -1; // Use it for homepage
|
||||
public static final int UNKNOWN = 0x400;
|
||||
|
||||
public static final int ALL_CATEGORY = EhUtils.UNKNOWN - 1;
|
||||
//DOUJINSHI|MANGA|ARTIST_CG|GAME_CG|WESTERN|NON_H|IMAGE_SET|COSPLAY|ASIAN_PORN|MISC;
|
||||
|
||||
public static final int BG_COLOR_DOUJINSHI = 0xfff44336;
|
||||
public static final int BG_COLOR_MANGA = 0xffff9800;
|
||||
public static final int BG_COLOR_ARTIST_CG = 0xfffbc02d;
|
||||
public static final int BG_COLOR_GAME_CG = 0xff4caf50;
|
||||
public static final int BG_COLOR_WESTERN = 0xff8bc34a;
|
||||
public static final int BG_COLOR_NON_H = 0xff2196f3;
|
||||
public static final int BG_COLOR_IMAGE_SET = 0xff3f51b5;
|
||||
public static final int BG_COLOR_COSPLAY = 0xff9c27b0;
|
||||
public static final int BG_COLOR_ASIAN_PORN = 0xff9575cd;
|
||||
public static final int BG_COLOR_MISC = 0xfff06292;
|
||||
public static final int BG_COLOR_UNKNOWN = Color.BLACK;
|
||||
|
||||
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff
|
||||
public static final Pattern PATTERN_TITLE_PREFIX = Pattern.compile(
|
||||
"^(?:(?:\\([^\\)]*\\))|(?:\\[[^\\]]*\\])|(?:\\{[^\\}]*\\})|(?:~[^~]*~)|\\s+)*");
|
||||
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff and something like ch. 1-23
|
||||
public static final Pattern PATTERN_TITLE_SUFFIX = Pattern.compile(
|
||||
"(?:\\s+ch.[\\s\\d-]+)?(?:(?:\\([^\\)]*\\))|(?:\\[[^\\]]*\\])|(?:\\{[^\\}]*\\})|(?:~[^~]*~)|\\s+)*$",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private static final int[] CATEGORY_VALUES = {
|
||||
EhConfig.MISC,
|
||||
EhConfig.DOUJINSHI,
|
||||
EhConfig.MANGA,
|
||||
EhConfig.ARTIST_CG,
|
||||
EhConfig.GAME_CG,
|
||||
EhConfig.IMAGE_SET,
|
||||
EhConfig.COSPLAY,
|
||||
EhConfig.ASIAN_PORN,
|
||||
EhConfig.NON_H,
|
||||
EhConfig.WESTERN,
|
||||
UNKNOWN};
|
||||
|
||||
private static final String[][] CATEGORY_STRINGS = {
|
||||
new String[]{"misc"},
|
||||
new String[]{"doujinshi"},
|
||||
new String[]{"manga"},
|
||||
new String[]{"artistcg", "Artist CG Sets", "Artist CG"},
|
||||
new String[]{"gamecg", "Game CG Sets", "Game CG"},
|
||||
new String[]{"imageset", "Image Sets", "Image Set"},
|
||||
new String[]{"cosplay"},
|
||||
new String[]{"asianporn", "Asian Porn"},
|
||||
new String[]{"non-h"},
|
||||
new String[]{"western"},
|
||||
new String[]{"unknown"}
|
||||
};
|
||||
|
||||
public static int getCategory(String type) {
|
||||
int i;
|
||||
for (i = 0; i < CATEGORY_STRINGS.length - 1; i++) {
|
||||
for (String str : CATEGORY_STRINGS[i])
|
||||
if (str.equalsIgnoreCase(type))
|
||||
return CATEGORY_VALUES[i];
|
||||
}
|
||||
|
||||
return CATEGORY_VALUES[i];
|
||||
}
|
||||
|
||||
public static String getCategory(int type) {
|
||||
int i;
|
||||
for (i = 0; i < CATEGORY_VALUES.length - 1; i++) {
|
||||
if (CATEGORY_VALUES[i] == type)
|
||||
break;
|
||||
}
|
||||
return CATEGORY_STRINGS[i][0];
|
||||
}
|
||||
|
||||
public static int getCategoryColor(int category) {
|
||||
switch (category) {
|
||||
case EhConfig.DOUJINSHI:
|
||||
return BG_COLOR_DOUJINSHI;
|
||||
case EhConfig.MANGA:
|
||||
return BG_COLOR_MANGA;
|
||||
case EhConfig.ARTIST_CG:
|
||||
return BG_COLOR_ARTIST_CG;
|
||||
case EhConfig.GAME_CG:
|
||||
return BG_COLOR_GAME_CG;
|
||||
case EhConfig.WESTERN:
|
||||
return BG_COLOR_WESTERN;
|
||||
case EhConfig.NON_H:
|
||||
return BG_COLOR_NON_H;
|
||||
case EhConfig.IMAGE_SET:
|
||||
return BG_COLOR_IMAGE_SET;
|
||||
case EhConfig.COSPLAY:
|
||||
return BG_COLOR_COSPLAY;
|
||||
case EhConfig.ASIAN_PORN:
|
||||
return BG_COLOR_ASIAN_PORN;
|
||||
case EhConfig.MISC:
|
||||
return BG_COLOR_MISC;
|
||||
default:
|
||||
return BG_COLOR_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
public static void signOut(Context context) {
|
||||
EhApplication.getEhCookieStore(context).signOut();
|
||||
Settings.putAvatar(null);
|
||||
Settings.putDisplayName(null);
|
||||
Settings.putNeedSignIn(true);
|
||||
}
|
||||
|
||||
public static boolean needSignedIn(Context context) {
|
||||
return Settings.getNeedSignIn() && !EhApplication.getEhCookieStore(context).hasSignedIn();
|
||||
}
|
||||
|
||||
public static String getSuitableTitle(GalleryInfo gi) {
|
||||
if (Settings.getShowJpnTitle()) {
|
||||
return TextUtils.isEmpty(gi.titleJpn) ? gi.title : gi.titleJpn;
|
||||
} else {
|
||||
return TextUtils.isEmpty(gi.title) ? gi.titleJpn : gi.title;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String extractTitle(String title) {
|
||||
if (null == title) {
|
||||
return null;
|
||||
}
|
||||
title = PATTERN_TITLE_PREFIX.matcher(title).replaceFirst("");
|
||||
title = PATTERN_TITLE_SUFFIX.matcher(title).replaceFirst("");
|
||||
// Sometimes title is combined by romaji and english translation.
|
||||
// Only need romaji.
|
||||
// TODO But not sure every '|' means that
|
||||
int index = title.indexOf('|');
|
||||
if (index >= 0) {
|
||||
title = title.substring(0, index);
|
||||
}
|
||||
if (title.isEmpty()) {
|
||||
return null;
|
||||
} else {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
public static String handleThumbUrlResolution(String url) {
|
||||
if (null == url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String resolution;
|
||||
switch (Settings.getThumbResolution()) {
|
||||
default:
|
||||
case 0: // Auto
|
||||
return url;
|
||||
case 1: // 250
|
||||
resolution = "250";
|
||||
break;
|
||||
case 2: // 300
|
||||
resolution = "300";
|
||||
break;
|
||||
}
|
||||
|
||||
int index1 = url.lastIndexOf('_');
|
||||
int index2 = url.lastIndexOf('.');
|
||||
if (index1 >= 0 && index2 >= 0 && index1 < index2) {
|
||||
return url.substring(0, index1 + 1) + resolution + url.substring(index2);
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 754 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user