mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-01-14 02:32:40 +00:00
Compare commits
823 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4674431829 | ||
|
|
2b9c7fee20 | ||
|
|
fbab80145e | ||
|
|
95d8b12065 | ||
|
|
9f8b7a180f | ||
|
|
7f62f56661 | ||
|
|
28ecf98fa6 | ||
|
|
68f55e6639 | ||
|
|
83e7af4503 | ||
|
|
fbfaa8d25f | ||
|
|
b46d199086 | ||
|
|
64c6aac0cf | ||
|
|
f3c64edf6e | ||
|
|
61673cda70 | ||
|
|
d71e6b18c0 | ||
|
|
19fe47f71e | ||
|
|
720bcbf8ac | ||
|
|
bb3b7d68c1 | ||
|
|
eb4b6d2a7f | ||
|
|
abc3e8d59c | ||
|
|
9f05f360f9 | ||
|
|
9b2dc1b263 | ||
|
|
50777d8d2c | ||
|
|
63e85fe4be | ||
|
|
b1eaf5616a | ||
|
|
22aa6d16a2 | ||
|
|
dfaa5675b6 | ||
|
|
ab4e1819c1 | ||
|
|
91aa65e717 | ||
|
|
ec684434dc | ||
|
|
3b5b9d7dab | ||
|
|
e7082baaff | ||
|
|
1caafac89a | ||
|
|
26e2fc6d91 | ||
|
|
740fa67a4e | ||
|
|
d468423db3 | ||
|
|
84664ebcdc | ||
|
|
987078fab5 | ||
|
|
7da28f28e5 | ||
|
|
3c9af84ea2 | ||
|
|
286fd19ba2 | ||
|
|
39dce71c28 | ||
|
|
b3b1d6d706 | ||
|
|
c04040468e | ||
|
|
aee7777478 | ||
|
|
09c1e21560 | ||
|
|
60e9f56b0f | ||
|
|
302e4ab664 | ||
|
|
484c3aa320 | ||
|
|
0bc769b971 | ||
|
|
5b98d41637 | ||
|
|
e14b7851b1 | ||
|
|
13d2334a45 | ||
|
|
9864e04aae | ||
|
|
4d6bbbf004 | ||
|
|
44305b4ccd | ||
|
|
7f86b13d93 | ||
|
|
d65b8d7d18 | ||
|
|
6968dd266a | ||
|
|
8754cbb38f | ||
|
|
c7b4705538 | ||
|
|
065faf31b6 | ||
|
|
da4b27f606 | ||
|
|
e1cc84ab5f | ||
|
|
533aede80f | ||
|
|
e84d5311f9 | ||
|
|
476b3f804b | ||
|
|
9445e8e8a0 | ||
|
|
e539753279 | ||
|
|
f5f81be6fe | ||
|
|
3b8a55f0d3 | ||
|
|
b1dd6cbb6e | ||
|
|
431724f637 | ||
|
|
6e5851aea8 | ||
|
|
30e5e58178 | ||
|
|
40207b515d | ||
|
|
95a6aaac76 | ||
|
|
da43f47487 | ||
|
|
ae5360fb27 | ||
|
|
47d2ae5c5e | ||
|
|
e1101dd6f1 | ||
|
|
d460351da2 | ||
|
|
fd076f5a58 | ||
|
|
f7d73fc21b | ||
|
|
5680b7c477 | ||
|
|
9f9b53c067 | ||
|
|
18d986a57d | ||
|
|
61632b3d9d | ||
|
|
f1688fb8b1 | ||
|
|
f0e85b31aa | ||
|
|
316871714a | ||
|
|
12c72842ff | ||
|
|
4230e11c4d | ||
|
|
c76ff8d367 | ||
|
|
831e9985e2 | ||
|
|
9912ee8199 | ||
|
|
55d17b556a | ||
|
|
5495be749b | ||
|
|
54f71c623a | ||
|
|
e3a891688b | ||
|
|
09d36a5dbc | ||
|
|
ff493406cf | ||
|
|
fbcee61e04 | ||
|
|
e62e34fd5c | ||
|
|
3b57135a6e | ||
|
|
69934dee52 | ||
|
|
51f2efd48c | ||
|
|
4de2cfdcc6 | ||
|
|
7845b7678d | ||
|
|
6f9543b9cf | ||
|
|
5b541cc9fb | ||
|
|
ea54520e0b | ||
|
|
00b6bd517a | ||
|
|
9fb5aa4b46 | ||
|
|
f089cd027e | ||
|
|
5151c6cb54 | ||
|
|
9407ac8c24 | ||
|
|
6a91a3a947 | ||
|
|
ac0bcea371 | ||
|
|
302a6ff4e8 | ||
|
|
0d89667428 | ||
|
|
dbb6848a9b | ||
|
|
a843e808d1 | ||
|
|
67af05e504 | ||
|
|
c995c6fda5 | ||
|
|
3b5cf0e37c | ||
|
|
c7a9847e66 | ||
|
|
3624f1b9a2 | ||
|
|
aecc908152 | ||
|
|
9f9c6eff00 | ||
|
|
99400fa570 | ||
|
|
17d00837bd | ||
|
|
0882d9d66b | ||
|
|
0df81409bf | ||
|
|
a75deb6ba2 | ||
|
|
4b8474b0ac | ||
|
|
7478e96a15 | ||
|
|
c1d9a253b0 | ||
|
|
eb7d9f76e5 | ||
|
|
ec45d4a729 | ||
|
|
07544cd198 | ||
|
|
3f3d1bfccf | ||
|
|
d3cb887ff0 | ||
|
|
88c68315f6 | ||
|
|
2d62fa401d | ||
|
|
3ff85c2ab7 | ||
|
|
13f5d3b5ac | ||
|
|
9eb55e1be5 | ||
|
|
6adbfade2b | ||
|
|
7568616f8e | ||
|
|
c93be13dfe | ||
|
|
e9dc96944b | ||
|
|
06e536eb45 | ||
|
|
72e90b4d57 | ||
|
|
83d43f845f | ||
|
|
5ed5a81708 | ||
|
|
ce003d2683 | ||
|
|
75248d7a12 | ||
|
|
06eff256f3 | ||
|
|
a476f332f7 | ||
|
|
6d49148c32 | ||
|
|
85acc53d40 | ||
|
|
f319e3e75a | ||
|
|
3abc660eb3 | ||
|
|
4c13dda1f9 | ||
|
|
294c35b2fb | ||
|
|
badaff8ebc | ||
|
|
7045f9711c | ||
|
|
aaf5d7b89c | ||
|
|
487952f52e | ||
|
|
34e31807fc | ||
|
|
21184f8755 | ||
|
|
2bac66b5fe | ||
|
|
b5f069d080 | ||
|
|
bc393e6bcd | ||
|
|
af411a61ae | ||
|
|
2805850711 | ||
|
|
0f0a367174 | ||
|
|
7009dc574f | ||
|
|
6941917c75 | ||
|
|
9560cf59be | ||
|
|
0a2374892c | ||
|
|
5d5c2ae2ed | ||
|
|
2cceb048e3 | ||
|
|
ed9c85b25a | ||
|
|
bb8bcf3c33 | ||
|
|
b5684ee7df | ||
|
|
2c27f784f7 | ||
|
|
46c1155c64 | ||
|
|
471ce4a24b | ||
|
|
b6841158df | ||
|
|
3372bacc62 | ||
|
|
6da9096176 | ||
|
|
244009a1cd | ||
|
|
0f22833ad5 | ||
|
|
b589ee6c26 | ||
|
|
9f0efdd544 | ||
|
|
07c7398a96 | ||
|
|
a117e459b0 | ||
|
|
7568af408a | ||
|
|
d1801e1dbc | ||
|
|
38d193899c | ||
|
|
f95d51b307 | ||
|
|
1bf55c2139 | ||
|
|
9b09028440 | ||
|
|
0cc890a1d1 | ||
|
|
fe138f6d61 | ||
|
|
4e1638f86e | ||
|
|
daa4fd5103 | ||
|
|
a3d8848825 | ||
|
|
61d102dc75 | ||
|
|
c0519d8313 | ||
|
|
48a2d2d24b | ||
|
|
fb0d626cb2 | ||
|
|
766326ad8c | ||
|
|
2a903f66dd | ||
|
|
8712310ad9 | ||
|
|
e8c3ab87c4 | ||
|
|
90c20f124b | ||
|
|
cd225eb5fe | ||
|
|
14e852237f | ||
|
|
d36ac7a5de | ||
|
|
55a138e8da | ||
|
|
c5e6bb58bc | ||
|
|
2642d6f5f0 | ||
|
|
bcb3cb9125 | ||
|
|
f7203d4ac9 | ||
|
|
6be23a0a6f | ||
|
|
089a9f1a9c | ||
|
|
2977de1df2 | ||
|
|
eab3f8b3ff | ||
|
|
d686a2c9dc | ||
|
|
d34e5f78a9 | ||
|
|
edc9d47da7 | ||
|
|
0c5608506e | ||
|
|
bda6139f42 | ||
|
|
1ae8ca1e21 | ||
|
|
342377e69a | ||
|
|
0447e4e664 | ||
|
|
fd3d61c6a0 | ||
|
|
69bf1c5d81 | ||
|
|
fbf6351b99 | ||
|
|
a78762756a | ||
|
|
1f24c18614 | ||
|
|
153790d80a | ||
|
|
e98f27cb66 | ||
|
|
c17d80948c | ||
|
|
c486368b9b | ||
|
|
42bb96af23 | ||
|
|
e082976914 | ||
|
|
156a2eb4ff | ||
|
|
e94981e6f7 | ||
|
|
be92921034 | ||
|
|
94403a9c3c | ||
|
|
b5ea61a079 | ||
|
|
af9e2420a6 | ||
|
|
14b3cf7ccd | ||
|
|
9c58a07a72 | ||
|
|
609855f774 | ||
|
|
37409e7d90 | ||
|
|
cc83991d8d | ||
|
|
bf5e94fc1a | ||
|
|
52420d4bf1 | ||
|
|
7f7bf8474e | ||
|
|
e1145f16f2 | ||
|
|
b430a23df1 | ||
|
|
fa7173b3d5 | ||
|
|
b2d78786c2 | ||
|
|
489420e855 | ||
|
|
5bc0d2c31b | ||
|
|
e9fda96aa1 | ||
|
|
64b0ccd574 | ||
|
|
d3aadc71b1 | ||
|
|
c21ccef7bc | ||
|
|
2152375227 | ||
|
|
10d57afaac | ||
|
|
e224f8ca28 | ||
|
|
1f975c0a3a | ||
|
|
ce075395a1 | ||
|
|
167653ac60 | ||
|
|
ee0f94c232 | ||
|
|
0ed3354cee | ||
|
|
b8f726153f | ||
|
|
afc362d2b6 | ||
|
|
776d8a4406 | ||
|
|
7718581882 | ||
|
|
ba245c49da | ||
|
|
cf60033424 | ||
|
|
0e39071b5e | ||
|
|
b6028cef5b | ||
|
|
62906fb84a | ||
|
|
4797cd9184 | ||
|
|
366c55c8f4 | ||
|
|
7e93456805 | ||
|
|
de1a92539a | ||
|
|
84dd1a688e | ||
|
|
36c4063db6 | ||
|
|
9c9a432ea0 | ||
|
|
1c53b22239 | ||
|
|
5dbab85505 | ||
|
|
2873f723e8 | ||
|
|
6b7043fb9d | ||
|
|
9d5612d104 | ||
|
|
e58088d290 | ||
|
|
77aa12dd81 | ||
|
|
8c3be2c9df | ||
|
|
266c3d03fc | ||
|
|
42ff60ce85 | ||
|
|
e08e724573 | ||
|
|
b155f23d27 | ||
|
|
a2d3e2c7e0 | ||
|
|
ed18466c3b | ||
|
|
845767e2f8 | ||
|
|
a0548fdbf8 | ||
|
|
b837912e75 | ||
|
|
0cd9fb32a8 | ||
|
|
fd62411b35 | ||
|
|
134850aa04 | ||
|
|
0795135f2f | ||
|
|
85eb1dc436 | ||
|
|
2f2b8784f9 | ||
|
|
181658e5a4 | ||
|
|
7dbb2b206c | ||
|
|
ef90493c27 | ||
|
|
d949894511 | ||
|
|
570dded8d6 | ||
|
|
235ead9222 | ||
|
|
3ee6788753 | ||
|
|
e3dfab5078 | ||
|
|
5f232a059d | ||
|
|
1b708d261d | ||
|
|
3341742f66 | ||
|
|
b731c79339 | ||
|
|
29b12c2f84 | ||
|
|
ba53f6611c | ||
|
|
6eeed50418 | ||
|
|
8caf9f87a1 | ||
|
|
2e6089088b | ||
|
|
40eaa166ae | ||
|
|
41e2e5f951 | ||
|
|
ac3938d529 | ||
|
|
515ec4d66d | ||
|
|
5adc27ea2b | ||
|
|
ab089a5f93 | ||
|
|
9d8fcbbffe | ||
|
|
590722d929 | ||
|
|
a0ee1b1653 | ||
|
|
b965f88eb2 | ||
|
|
a228e702da | ||
|
|
105981b2eb | ||
|
|
055365a449 | ||
|
|
f8a7aac40d | ||
|
|
6712ea5e6f | ||
|
|
382e69273e | ||
|
|
9b71828b97 | ||
|
|
00eddcb237 | ||
|
|
49cc643dcc | ||
|
|
42ec6f0810 | ||
|
|
9f47a274a8 | ||
|
|
1f8c0a9e5e | ||
|
|
cef9ccd937 | ||
|
|
9d773d6e8a | ||
|
|
3d93ecd6ec | ||
|
|
36e38e50e9 | ||
|
|
1a8be2bbf5 | ||
|
|
92b1fa5743 | ||
|
|
335e5c05db | ||
|
|
bfead79c07 | ||
|
|
bd8014bcbd | ||
|
|
948d57d3d1 | ||
|
|
31b830d6d0 | ||
|
|
2038df976c | ||
|
|
46cc215120 | ||
|
|
3afce82aa7 | ||
|
|
88e5be237e | ||
|
|
7c4b9d8843 | ||
|
|
b83e1716fe | ||
|
|
c3e41e2427 | ||
|
|
3f67b3b73c | ||
|
|
78c9e4e1ad | ||
|
|
69c090b5a1 | ||
|
|
2c8222fd55 | ||
|
|
d071891b2a | ||
|
|
986acc5fc5 | ||
|
|
e4295fb3fa | ||
|
|
cfad3fb5de | ||
|
|
b18236a27e | ||
|
|
f6bbc69cf9 | ||
|
|
707e4f7167 | ||
|
|
11d06dc86d | ||
|
|
8f46432391 | ||
|
|
99cdaec40e | ||
|
|
b32935a1b0 | ||
|
|
ed9a3517c6 | ||
|
|
e0a39efa2b | ||
|
|
3ad0e313ca | ||
|
|
bca547ce44 | ||
|
|
6bc697f926 | ||
|
|
bff5371e41 | ||
|
|
694013c9df | ||
|
|
a76398efd0 | ||
|
|
99bcd8d043 | ||
|
|
1602ecbaf9 | ||
|
|
7e17bdf369 | ||
|
|
d316bbad44 | ||
|
|
72151c8c0c | ||
|
|
e2e0a9bfa2 | ||
|
|
8d53b07167 | ||
|
|
1df852171d | ||
|
|
399e2626fb | ||
|
|
752a76eb44 | ||
|
|
8feee05eec | ||
|
|
e9a4caaf0b | ||
|
|
8de367e03f | ||
|
|
dad88b83fb | ||
|
|
846f7f2f05 | ||
|
|
41e18ae694 | ||
|
|
deeac118a1 | ||
|
|
594d77e713 | ||
|
|
2ea404659b | ||
|
|
e2ec95e6ff | ||
|
|
db87df743d | ||
|
|
23f9ffdab7 | ||
|
|
28063c35c2 | ||
|
|
21a39b06e7 | ||
|
|
8fb29ae6c2 | ||
|
|
21895caa3a | ||
|
|
f17b92512c | ||
|
|
014682664d | ||
|
|
5bf1df9f14 | ||
|
|
dea1e0dcb9 | ||
|
|
eb5fb42da9 | ||
|
|
c46a0f7b2e | ||
|
|
835476870b | ||
|
|
718acb5059 | ||
|
|
1aa763e86c | ||
|
|
0395dc6e9e | ||
|
|
96de70b71e | ||
|
|
f44883e79f | ||
|
|
c56fb8cec2 | ||
|
|
3625a38a23 | ||
|
|
1393d3ad7f | ||
|
|
b674cfec24 | ||
|
|
f0f0c43b72 | ||
|
|
33caad4690 | ||
|
|
0afc8005d0 | ||
|
|
f04d2e76fa | ||
|
|
8e1d7f162d | ||
|
|
ee65e89230 | ||
|
|
a8e26238a8 | ||
|
|
a3dc95bef1 | ||
|
|
a29df9a2dd | ||
|
|
cc17d268fc | ||
|
|
22a9a06b87 | ||
|
|
d063d39dbc | ||
|
|
87e29dbd84 | ||
|
|
2227a7a6bd | ||
|
|
867f633d16 | ||
|
|
b9de3c202a | ||
|
|
56364c4a2c | ||
|
|
b1fd2c007d | ||
|
|
741a872c39 | ||
|
|
e1e2add616 | ||
|
|
0c664e346a | ||
|
|
7ddb856ccd | ||
|
|
17c0b981d1 | ||
|
|
7f0a9904ff | ||
|
|
2b4190d85d | ||
|
|
57e89babf1 | ||
|
|
209dc5ace9 | ||
|
|
ab7f3c7399 | ||
|
|
1e7e8d4121 | ||
|
|
04b75ef05f | ||
|
|
de19421de1 | ||
|
|
61f64a7349 | ||
|
|
6fb16bad85 | ||
|
|
694813ac90 | ||
|
|
6f3fd50ed8 | ||
|
|
c1e1c191d0 | ||
|
|
f4c8fdaf07 | ||
|
|
457ebe3aa2 | ||
|
|
8da8ce0a0a | ||
|
|
cc869b98a3 | ||
|
|
f9e7873e54 | ||
|
|
c4cba8aa37 | ||
|
|
22e4ef4034 | ||
|
|
708cdc4c62 | ||
|
|
94931df60b | ||
|
|
b3605fe6d4 | ||
|
|
11e0ed7c4f | ||
|
|
34e89448b1 | ||
|
|
9a6e936996 | ||
|
|
5ed58b8609 | ||
|
|
0d7d610127 | ||
|
|
9f789167da | ||
|
|
92a42235a1 | ||
|
|
3f52938f08 | ||
|
|
a08cd4ce6a | ||
|
|
46b12ed819 | ||
|
|
e7ef193da6 | ||
|
|
2ebe5aa878 | ||
|
|
f58f6639f8 | ||
|
|
f995ba115c | ||
|
|
755dff343d | ||
|
|
c8c7d23971 | ||
|
|
9309159c38 | ||
|
|
8e45296826 | ||
|
|
559bcfc6a5 | ||
|
|
23c2f748d6 | ||
|
|
3e409b9cc1 | ||
|
|
c0453065e4 | ||
|
|
88a6e5981f | ||
|
|
8970a663ec | ||
|
|
334437137e | ||
|
|
949c01b37f | ||
|
|
17146c2c13 | ||
|
|
dcd35b038e | ||
|
|
550364906d | ||
|
|
fa8483bbb6 | ||
|
|
b976e40439 | ||
|
|
6fcae39fe2 | ||
|
|
ec1de9824a | ||
|
|
8f83c210ad | ||
|
|
d68009ff7e | ||
|
|
bac2045111 | ||
|
|
bbf3c37978 | ||
|
|
901c63d1f2 | ||
|
|
13306b5c1b | ||
|
|
b883ad1657 | ||
|
|
6029e18980 | ||
|
|
a81b791ee3 | ||
|
|
7e5aaabadf | ||
|
|
e01e0d5aed | ||
|
|
db07b81c3c | ||
|
|
d660e4ac9d | ||
|
|
d90588ce6b | ||
|
|
7e78197b37 | ||
|
|
06af26f1f2 | ||
|
|
d0e07276dd | ||
|
|
59c19614ea | ||
|
|
0f04dd6dae | ||
|
|
b365973ac6 | ||
|
|
19fb8cfbfe | ||
|
|
d8e6a5cb33 | ||
|
|
f4c6a49339 | ||
|
|
23ee22566d | ||
|
|
ea70a1f334 | ||
|
|
68a1407314 | ||
|
|
881efb9be8 | ||
|
|
7844547e4f | ||
|
|
054279d553 | ||
|
|
b2bbdc656a | ||
|
|
a0151f2a68 | ||
|
|
fd5f4d9840 | ||
|
|
98d7e6bcc6 | ||
|
|
e92ca5e572 | ||
|
|
27ca9ed8b8 | ||
|
|
c2b1d45fc4 | ||
|
|
03939555ac | ||
|
|
5a2cd93d13 | ||
|
|
ef69625cd2 | ||
|
|
ae88b4c697 | ||
|
|
693756bdd6 | ||
|
|
c05633979c | ||
|
|
7d80d04f34 | ||
|
|
3ff2da3b20 | ||
|
|
eb15bc97a7 | ||
|
|
c6cd2dd854 | ||
|
|
aae8865bdd | ||
|
|
c15cead9e2 | ||
|
|
0ccd30b12e | ||
|
|
d2a59ecc62 | ||
|
|
7a67d192c3 | ||
|
|
d32ad36f3d | ||
|
|
f587d79cd8 | ||
|
|
9e290ce91a | ||
|
|
0c40a45075 | ||
|
|
17c5e73994 | ||
|
|
890d1cb50b | ||
|
|
4c89d1a6e5 | ||
|
|
77d3a1ef45 | ||
|
|
6e289b44c7 | ||
|
|
8039055a87 | ||
|
|
6c0f5bef21 | ||
|
|
7dd7ea1a32 | ||
|
|
3bce9a8ead | ||
|
|
da82e3f5d1 | ||
|
|
0df5d7a934 | ||
|
|
27f38f329f | ||
|
|
c4707978c4 | ||
|
|
2ad0792581 | ||
|
|
8741856234 | ||
|
|
e37a86efc2 | ||
|
|
baee238a2c | ||
|
|
e8437052d8 | ||
|
|
cf13f5ca56 | ||
|
|
52f82ed228 | ||
|
|
84ec320df4 | ||
|
|
0033843bc2 | ||
|
|
3ca461413e | ||
|
|
e6d9d8e26d | ||
|
|
763995d4c9 | ||
|
|
8a992d4c47 | ||
|
|
da052df106 | ||
|
|
60d4c8a55d | ||
|
|
4292ca94ff | ||
|
|
570738190d | ||
|
|
86dafdd92b | ||
|
|
dab53450c1 | ||
|
|
773aa1eff0 | ||
|
|
14de2f289f | ||
|
|
eeeeeef3a7 | ||
|
|
ea1be11a80 | ||
|
|
309fd3fb7d | ||
|
|
6a24dcec73 | ||
|
|
3e2dba2fd5 | ||
|
|
527c38adf9 | ||
|
|
f62a7919a5 | ||
|
|
844f80a5f1 | ||
|
|
94e23142a5 | ||
|
|
9339fc80b4 | ||
|
|
d092e39c56 | ||
|
|
160a33e8c8 | ||
|
|
429ee7eb93 | ||
|
|
c891f2f1ed | ||
|
|
3108c903dd | ||
|
|
0cdfa6e377 | ||
|
|
52a21e4a24 | ||
|
|
8e152df46d | ||
|
|
a68c8ceebe | ||
|
|
1ce44b31e2 | ||
|
|
8f52f918db | ||
|
|
55f5f76275 | ||
|
|
f122d73754 | ||
|
|
8227d85feb | ||
|
|
46e2f4e579 | ||
|
|
0c65f73180 | ||
|
|
0fb7eab2f9 | ||
|
|
2b2de8811e | ||
|
|
5e87463125 | ||
|
|
424d3fdcd7 | ||
|
|
6452c7e08c | ||
|
|
e21257b786 | ||
|
|
0f70aeb910 | ||
|
|
cedfbf5f67 | ||
|
|
31fab60701 | ||
|
|
afef8d8d0b | ||
|
|
ac2543d0a1 | ||
|
|
81658de08f | ||
|
|
2ad0d47f61 | ||
|
|
b9d6d55aa4 | ||
|
|
719d8651b3 | ||
|
|
f3988c37b6 | ||
|
|
912f09c83e | ||
|
|
27330951aa | ||
|
|
84089453e7 | ||
|
|
59f76ef304 | ||
|
|
4061145933 | ||
|
|
e900a69a26 | ||
|
|
9cdec5de50 | ||
|
|
ceabfd1a8b | ||
|
|
bc283bce4e | ||
|
|
544cae4fb4 | ||
|
|
38a0395d45 | ||
|
|
a5b7666188 | ||
|
|
58a626dedb | ||
|
|
7e311e5567 | ||
|
|
596005c69e | ||
|
|
14ee7d53d7 | ||
|
|
2f575c13f1 | ||
|
|
82738e23ce | ||
|
|
93e99d096a | ||
|
|
784b9cf207 | ||
|
|
7b56244c8b | ||
|
|
44192d6e49 | ||
|
|
4ded3adadb | ||
|
|
3798d5228c | ||
|
|
0491c4af9c | ||
|
|
69799613aa | ||
|
|
c0cbec700c | ||
|
|
e9c9dfcd8c | ||
|
|
c9cbb1e6f1 | ||
|
|
3ea8841e3a | ||
|
|
8b26e5b106 | ||
|
|
495fa170a9 | ||
|
|
406d02fd28 | ||
|
|
3ad8883999 | ||
|
|
dcd7055a9d | ||
|
|
a8e326ceea | ||
|
|
b125ff702a | ||
|
|
6e546703a9 | ||
|
|
71f1bbdcc1 | ||
|
|
bdb86a7fde | ||
|
|
8ac8258400 | ||
|
|
0338ff8d51 | ||
|
|
d694e61006 | ||
|
|
b6be586766 | ||
|
|
9915287a5a | ||
|
|
cb19f792da | ||
|
|
f449aee901 | ||
|
|
b665122c3c | ||
|
|
f4dbd5c9bd | ||
|
|
cb6d7e6dd7 | ||
|
|
4d4a86f889 | ||
|
|
e3829303f9 | ||
|
|
86d4cf3dfc | ||
|
|
ed4196f732 | ||
|
|
af70fdd7a6 | ||
|
|
0ee6c1e47e | ||
|
|
0886c6b216 | ||
|
|
754fe3adfb | ||
|
|
b5058f99ce | ||
|
|
f7be693470 | ||
|
|
b772afeeab | ||
|
|
f0c7cb9f94 | ||
|
|
1561c210fb | ||
|
|
727fb27ad0 | ||
|
|
ceb1d70551 | ||
|
|
720bff02aa | ||
|
|
f82dd3e152 | ||
|
|
a3dff2c608 | ||
|
|
28f87dcb2b | ||
|
|
4daae95979 | ||
|
|
577bfab366 | ||
|
|
655993d8f2 | ||
|
|
3e079a6858 | ||
|
|
e5afffdced | ||
|
|
a43c434376 | ||
|
|
225647cf65 | ||
|
|
fdab92c92b | ||
|
|
72adb7a53b | ||
|
|
f84907e2c9 | ||
|
|
ab3b8d0a14 | ||
|
|
5e7b635006 | ||
|
|
58181ee37f | ||
|
|
4db4b3af86 | ||
|
|
504f45efa6 | ||
|
|
114a7ccdd4 | ||
|
|
1a9b3c9d7c | ||
|
|
137fabee0e | ||
|
|
2d41022ee4 | ||
|
|
2ce0caa943 | ||
|
|
2839154b68 | ||
|
|
e25c5544ba | ||
|
|
7c70033f19 | ||
|
|
59f90fcdfe | ||
|
|
e05def491f | ||
|
|
19738c0c1b | ||
|
|
0790a43aa2 | ||
|
|
fd4a4d979a | ||
|
|
6011dec272 | ||
|
|
98ae78eb06 | ||
|
|
0a66f54487 | ||
|
|
5e2f373a57 | ||
|
|
ae8f47500f | ||
|
|
36c1e19ef0 | ||
|
|
dc933a5e66 | ||
|
|
c7699a5a3b | ||
|
|
0f656aa7a9 | ||
|
|
1af8481fff | ||
|
|
3787c6ec70 | ||
|
|
3e7ff96445 | ||
|
|
558e3b1032 | ||
|
|
65279b3364 | ||
|
|
d3ca184570 | ||
|
|
40a2322e09 | ||
|
|
0b593b343e | ||
|
|
83dc62239f | ||
|
|
34cb44f2be | ||
|
|
55a82b631e | ||
|
|
b358231b20 | ||
|
|
4579fa52ac | ||
|
|
b4449e4998 | ||
|
|
d5b1dceb66 | ||
|
|
8f46757c0d | ||
|
|
87378fc79c | ||
|
|
95f44cbe9a | ||
|
|
8849ddab81 | ||
|
|
9a47714645 | ||
|
|
b1396b98a3 | ||
|
|
c3433baf0c | ||
|
|
fab692c386 | ||
|
|
834f599fb1 | ||
|
|
701207dcc5 | ||
|
|
19ba37dfad | ||
|
|
284228ef16 | ||
|
|
ad2de3a828 | ||
|
|
797e1a105d | ||
|
|
9c00e7f45c | ||
|
|
26a1b1377d | ||
|
|
df2bb228c5 | ||
|
|
49db47c12c | ||
|
|
cc1e5edaec | ||
|
|
f6060261a1 | ||
|
|
8c73253a52 | ||
|
|
4106645d6e | ||
|
|
c68c35e084 | ||
|
|
fd34b1a291 | ||
|
|
bfc987f81b | ||
|
|
c93c52a58c | ||
|
|
e72c6eed24 | ||
|
|
646e327ed2 | ||
|
|
cb5c219ffe | ||
|
|
3794002c7b | ||
|
|
bd2b32bfbc | ||
|
|
884528927e | ||
|
|
4688b1fe23 | ||
|
|
dfd558f848 | ||
|
|
d2e065d273 | ||
|
|
84a52342ab | ||
|
|
6f3d5e9fb8 | ||
|
|
7d18eb8b02 | ||
|
|
7e3eb5e14d | ||
|
|
039a8e0b87 | ||
|
|
a8b5534838 | ||
|
|
2581fa4176 | ||
|
|
d90b1ca5be | ||
|
|
845663f80f | ||
|
|
9530af95f4 | ||
|
|
c1efa78820 | ||
|
|
d61797fa3a | ||
|
|
cc6989b4f9 | ||
|
|
fc31458cc4 |
@@ -13,7 +13,7 @@
|
||||
</p>
|
||||
<hr>
|
||||
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#updates">Updates</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
||||
<p align="center"><a href="https://newpipe.schabi.org">Website</a> • <a href="https://newpipe.schabi.org/blog/">Blog</a> • <a href="https://newpipe.schabi.org/press/">Press</a></p>
|
||||
<p align="center"><a href="https://newpipe.schabi.org">Website</a> • <a href="https://newpipe.schabi.org/blog/">Blog</a> • <a href="https://newpipe.schabi.org/FAQ/">FAQ</a> • <a href="https://newpipe.schabi.org/press/">Press</a></p>
|
||||
<hr>
|
||||
|
||||
<b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b>
|
||||
@@ -81,6 +81,7 @@ NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/doc
|
||||
* YouTube
|
||||
* SoundCloud \[beta\]
|
||||
* media.ccc.de \[beta\]
|
||||
* PeerTube instances \[beta\]
|
||||
|
||||
## Updates
|
||||
When a change to the NewPipe code occurs (due to either adding features or bug fixing), eventually a release will occur. These are in the format x.xx.x . In order to get this new version, you can:
|
||||
|
||||
2
app/.gitignore
vendored
2
app/.gitignore
vendored
@@ -1,3 +1,3 @@
|
||||
.gitignore
|
||||
/build
|
||||
app.iml
|
||||
*.iml
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
@@ -8,8 +11,8 @@ android {
|
||||
applicationId "org.schabi.newpipe"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 28
|
||||
versionCode 790
|
||||
versionName "0.17.4"
|
||||
versionCode 860
|
||||
versionName "0.18.6"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
@@ -44,20 +47,23 @@ android {
|
||||
|
||||
ext {
|
||||
androidxLibVersion = '1.0.0'
|
||||
exoPlayerLibVersion = '2.10.6'
|
||||
exoPlayerLibVersion = '2.10.8'
|
||||
roomDbLibVersion = '2.1.0'
|
||||
leakCanaryLibVersion = '1.5.4' //1.6.1
|
||||
okHttpLibVersion = '3.12.6'
|
||||
icepickLibVersion = '3.2.0'
|
||||
stethoLibVersion = '1.5.0'
|
||||
markwonVersion = '4.2.1'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
|
||||
androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', {
|
||||
exclude module: 'support-annotations'
|
||||
})
|
||||
|
||||
implementation 'com.github.teamnewpipe:NewPipeExtractor:v0.17.4'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:6446abc6d'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.mockito:mockito-core:2.23.0'
|
||||
|
||||
@@ -89,17 +95,21 @@ dependencies {
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.2.2'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
|
||||
implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1'
|
||||
implementation 'org.ocpsoft.prettytime:prettytime:4.0.3.Final'
|
||||
|
||||
implementation "androidx.room:room-runtime:${roomDbLibVersion}"
|
||||
implementation "androidx.room:room-rxjava2:${roomDbLibVersion}"
|
||||
annotationProcessor "androidx.room:room-compiler:${roomDbLibVersion}"
|
||||
kapt "androidx.room:room-compiler:${roomDbLibVersion}"
|
||||
|
||||
implementation "frankiesardo:icepick:${icepickLibVersion}"
|
||||
annotationProcessor "frankiesardo:icepick-processor:${icepickLibVersion}"
|
||||
kapt "frankiesardo:icepick-processor:${icepickLibVersion}"
|
||||
|
||||
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryLibVersion}"
|
||||
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:${leakCanaryLibVersion}"
|
||||
|
||||
implementation "com.squareup.okhttp3:okhttp:${okHttpLibVersion}"
|
||||
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoLibVersion}"
|
||||
|
||||
implementation "io.noties.markwon:core:${markwonVersion}"
|
||||
implementation "io.noties.markwon:linkify:${markwonVersion}"
|
||||
}
|
||||
|
||||
3
app/proguard-rules.pro
vendored
3
app/proguard-rules.pro
vendored
@@ -17,6 +17,9 @@
|
||||
#}
|
||||
|
||||
-dontobfuscate
|
||||
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
|
||||
-keep class org.ocpsoft.prettytime.i18n.** { *; }
|
||||
|
||||
-keep class org.mozilla.javascript.** { *; }
|
||||
|
||||
-keep class org.mozilla.classfile.ClassFileWriter
|
||||
|
||||
@@ -15,7 +15,7 @@ import com.squareup.leakcanary.LeakCanary;
|
||||
import com.squareup.leakcanary.LeakDirectoryProvider;
|
||||
import com.squareup.leakcanary.RefWatcher;
|
||||
|
||||
import org.schabi.newpipe.extractor.Downloader;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -39,7 +39,7 @@ public class DebugApp extends App {
|
||||
|
||||
@Override
|
||||
protected Downloader getDownloader() {
|
||||
return org.schabi.newpipe.Downloader.init(new OkHttpClient.Builder()
|
||||
return DownloaderImpl.init(new OkHttpClient.Builder()
|
||||
.addNetworkInterceptor(new StethoInterceptor()));
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,8 @@
|
||||
android:name=".player.MainVideoPlayer"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"/>
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/VideoPlayerTheme"/>
|
||||
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
@@ -112,7 +113,7 @@
|
||||
|
||||
<activity
|
||||
android:name=".ReCaptchaActivity"
|
||||
android:label="@string/reCaptchaActivity"/>
|
||||
android:label="@string/recaptcha"/>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
@@ -153,6 +154,7 @@
|
||||
<!-- channel prefix -->
|
||||
<data android:pathPrefix="/channel/"/>
|
||||
<data android:pathPrefix="/user/"/>
|
||||
<data android:pathPrefix="/c/"/>
|
||||
<!-- playlist prefix -->
|
||||
<data android:pathPrefix="/playlist"/>
|
||||
</intent-filter>
|
||||
|
||||
@@ -6,9 +6,9 @@ import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
@@ -21,13 +21,15 @@ import org.acra.config.ACRAConfiguration;
|
||||
import org.acra.config.ACRAConfigurationException;
|
||||
import org.acra.config.ConfigurationBuilder;
|
||||
import org.acra.sender.ReportSenderFactory;
|
||||
import org.schabi.newpipe.extractor.Downloader;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.report.AcraReportSenderFactory;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -95,10 +97,15 @@ public class App extends Application {
|
||||
SettingsActivity.initSettings(this);
|
||||
|
||||
NewPipe.init(getDownloader(),
|
||||
org.schabi.newpipe.util.Localization.getPreferredExtractorLocal(this));
|
||||
Localization.getPreferredLocalization(this),
|
||||
Localization.getPreferredContentCountry(this));
|
||||
Localization.init(getApplicationContext());
|
||||
|
||||
StateSaver.init(this);
|
||||
initNotificationChannel();
|
||||
|
||||
ServiceHelper.initServices(this);
|
||||
|
||||
// Initialize image loader
|
||||
ImageLoader.getInstance().init(getImageLoaderConfigurations(10, 50));
|
||||
|
||||
@@ -109,7 +116,7 @@ public class App extends Application {
|
||||
}
|
||||
|
||||
protected Downloader getDownloader() {
|
||||
return org.schabi.newpipe.Downloader.init(null);
|
||||
return DownloaderImpl.init(null);
|
||||
}
|
||||
|
||||
private void configureRxJavaErrorHandler() {
|
||||
|
||||
@@ -107,6 +107,7 @@ public abstract class BaseFragment extends Fragment {
|
||||
if (DEBUG) Log.d(TAG, "setTitle() called with: title = [" + title + "]");
|
||||
if((!useAsFrontPage || mIsVisibleToUser)
|
||||
&& (activity != null && activity.getSupportActionBar() != null)) {
|
||||
activity.getSupportActionBar().setDisplayShowTitleEnabled(true);
|
||||
activity.getSupportActionBar().setTitle(title);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,296 +0,0 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.schabi.newpipe.extractor.DownloadRequest;
|
||||
import org.schabi.newpipe.extractor.DownloadResponse;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.extractor.utils.Localization;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 28.01.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* Downloader.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe 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.
|
||||
*
|
||||
* NewPipe 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
||||
public static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0";
|
||||
|
||||
private static Downloader instance;
|
||||
private String mCookies;
|
||||
private final OkHttpClient client;
|
||||
|
||||
private Downloader(OkHttpClient.Builder builder) {
|
||||
this.client = builder
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
//.cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"), 16 * 1024 * 1024))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* It's recommended to call exactly once in the entire lifetime of the application.
|
||||
*
|
||||
* @param builder if null, default builder will be used
|
||||
*/
|
||||
public static Downloader init(@Nullable OkHttpClient.Builder builder) {
|
||||
return instance = new Downloader(builder != null ? builder : new OkHttpClient.Builder());
|
||||
}
|
||||
|
||||
public static Downloader getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public String getCookies() {
|
||||
return mCookies;
|
||||
}
|
||||
|
||||
public void setCookies(String cookies) {
|
||||
mCookies = cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of the content that the url is pointing by firing a HEAD request.
|
||||
*
|
||||
* @param url an url pointing to the content
|
||||
* @return the size of the content, in bytes
|
||||
*/
|
||||
public long getContentLength(String url) throws IOException {
|
||||
Response response = null;
|
||||
try {
|
||||
final Request request = new Request.Builder()
|
||||
.head().url(url)
|
||||
.addHeader("User-Agent", USER_AGENT)
|
||||
.build();
|
||||
response = client.newCall(request).execute();
|
||||
|
||||
String contentLength = response.header("Content-Length");
|
||||
return contentLength == null ? -1 : Long.parseLong(contentLength);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IOException("Invalid content length", e);
|
||||
} finally {
|
||||
if (response != null) {
|
||||
response.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the text file at the supplied URL as in download(String),
|
||||
* but set the HTTP header field "Accept-Language" to the supplied string.
|
||||
*
|
||||
* @param siteUrl the URL of the text file to return the contents of
|
||||
* @param localization the language and country (usually a 2-character code) to set
|
||||
* @return the contents of the specified text file
|
||||
*/
|
||||
@Override
|
||||
public String download(String siteUrl, Localization localization) throws IOException, ReCaptchaException {
|
||||
Map<String, String> requestProperties = new HashMap<>();
|
||||
requestProperties.put("Accept-Language", localization.getLanguage());
|
||||
return download(siteUrl, requestProperties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the text file at the supplied URL as in download(String),
|
||||
* but set the HTTP headers included in the customProperties map.
|
||||
*
|
||||
* @param siteUrl the URL of the text file to return the contents of
|
||||
* @param customProperties set request header properties
|
||||
* @return the contents of the specified text file
|
||||
* @throws IOException
|
||||
*/
|
||||
@Override
|
||||
public String download(String siteUrl, Map<String, String> customProperties) throws IOException, ReCaptchaException {
|
||||
return getBody(siteUrl, customProperties).string();
|
||||
}
|
||||
|
||||
public InputStream stream(String siteUrl) throws IOException {
|
||||
try {
|
||||
return getBody(siteUrl, Collections.emptyMap()).byteStream();
|
||||
} catch (ReCaptchaException e) {
|
||||
throw new IOException(e.getMessage(), e.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
private ResponseBody getBody(String siteUrl, Map<String, String> customProperties) throws IOException, ReCaptchaException {
|
||||
final Request.Builder requestBuilder = new Request.Builder()
|
||||
.method("GET", null).url(siteUrl);
|
||||
|
||||
for (Map.Entry<String, String> header : customProperties.entrySet()) {
|
||||
requestBuilder.addHeader(header.getKey(), header.getValue());
|
||||
}
|
||||
|
||||
if (!customProperties.containsKey("User-Agent")) {
|
||||
requestBuilder.header("User-Agent", USER_AGENT);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(mCookies)) {
|
||||
requestBuilder.addHeader("Cookie", mCookies);
|
||||
}
|
||||
|
||||
final Request request = requestBuilder.build();
|
||||
final Response response = client.newCall(request).execute();
|
||||
final ResponseBody body = response.body();
|
||||
|
||||
if (response.code() == 429) {
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested", siteUrl);
|
||||
}
|
||||
|
||||
if (body == null) {
|
||||
response.close();
|
||||
return null;
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download (via HTTP) the text file located at the supplied URL, and return its contents.
|
||||
* Primarily intended for downloading web pages.
|
||||
*
|
||||
* @param siteUrl the URL of the text file to download
|
||||
* @return the contents of the specified text file
|
||||
*/
|
||||
@Override
|
||||
public String download(String siteUrl) throws IOException, ReCaptchaException {
|
||||
return download(siteUrl, Collections.emptyMap());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public DownloadResponse get(String siteUrl, DownloadRequest request) throws IOException, ReCaptchaException {
|
||||
final Request.Builder requestBuilder = new Request.Builder()
|
||||
.method("GET", null).url(siteUrl);
|
||||
|
||||
Map<String, List<String>> requestHeaders = request.getRequestHeaders();
|
||||
// set custom headers in request
|
||||
for (Map.Entry<String, List<String>> pair : requestHeaders.entrySet()) {
|
||||
for(String value : pair.getValue()){
|
||||
requestBuilder.addHeader(pair.getKey(), value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!requestHeaders.containsKey("User-Agent")) {
|
||||
requestBuilder.header("User-Agent", USER_AGENT);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(mCookies)) {
|
||||
requestBuilder.addHeader("Cookie", mCookies);
|
||||
}
|
||||
|
||||
final Request okRequest = requestBuilder.build();
|
||||
final Response response = client.newCall(okRequest).execute();
|
||||
final ResponseBody body = response.body();
|
||||
|
||||
if (response.code() == 429) {
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested", siteUrl);
|
||||
}
|
||||
|
||||
if (body == null) {
|
||||
response.close();
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DownloadResponse(response.code(), body.string(), response.headers().toMultimap());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownloadResponse get(String siteUrl) throws IOException, ReCaptchaException {
|
||||
return get(siteUrl, DownloadRequest.emptyRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownloadResponse post(String siteUrl, DownloadRequest request) throws IOException, ReCaptchaException {
|
||||
|
||||
Map<String, List<String>> requestHeaders = request.getRequestHeaders();
|
||||
if(null == requestHeaders.get("Content-Type") || requestHeaders.get("Content-Type").isEmpty()){
|
||||
// content type header is required. maybe throw an exception here
|
||||
return null;
|
||||
}
|
||||
|
||||
String contentType = requestHeaders.get("Content-Type").get(0);
|
||||
|
||||
RequestBody okRequestBody = null;
|
||||
if (null != request.getRequestBody()) {
|
||||
okRequestBody = RequestBody.create(MediaType.parse(contentType), request.getRequestBody());
|
||||
}
|
||||
final Request.Builder requestBuilder = new Request.Builder()
|
||||
.method("POST", okRequestBody).url(siteUrl);
|
||||
|
||||
// set custom headers in request
|
||||
for (Map.Entry<String, List<String>> pair : requestHeaders.entrySet()) {
|
||||
for(String value : pair.getValue()){
|
||||
requestBuilder.addHeader(pair.getKey(), value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!requestHeaders.containsKey("User-Agent")) {
|
||||
requestBuilder.header("User-Agent", USER_AGENT);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(mCookies)) {
|
||||
requestBuilder.addHeader("Cookie", mCookies);
|
||||
}
|
||||
|
||||
final Request okRequest = requestBuilder.build();
|
||||
final Response response = client.newCall(okRequest).execute();
|
||||
final ResponseBody body = response.body();
|
||||
|
||||
if (response.code() == 429) {
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested", siteUrl);
|
||||
}
|
||||
|
||||
if (body == null) {
|
||||
response.close();
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DownloadResponse(response.code(), body.string(), response.headers().toMultimap());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownloadResponse head(String siteUrl) throws IOException, ReCaptchaException {
|
||||
final Request request = new Request.Builder()
|
||||
.head().url(siteUrl)
|
||||
.addHeader("User-Agent", USER_AGENT)
|
||||
.build();
|
||||
final Response response = client.newCall(request).execute();
|
||||
|
||||
if (response.code() == 429) {
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested", siteUrl);
|
||||
}
|
||||
|
||||
return new DownloadResponse(response.code(), null, response.headers().toMultimap());
|
||||
}
|
||||
|
||||
}
|
||||
220
app/src/main/java/org/schabi/newpipe/DownloaderImpl.java
Normal file
220
app/src/main/java/org/schabi/newpipe/DownloaderImpl.java
Normal file
@@ -0,0 +1,220 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.extractor.downloader.Request;
|
||||
import org.schabi.newpipe.extractor.downloader.Response;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import okhttp3.CipherSuite;
|
||||
import okhttp3.ConnectionSpec;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
|
||||
public class DownloaderImpl extends Downloader {
|
||||
public static final String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:68.0) Gecko/20100101 Firefox/68.0";
|
||||
|
||||
private static DownloaderImpl instance;
|
||||
private String mCookies;
|
||||
private OkHttpClient client;
|
||||
|
||||
private DownloaderImpl(OkHttpClient.Builder builder) {
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
|
||||
enableModernTLS(builder);
|
||||
}
|
||||
this.client = builder
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
//.cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"), 16 * 1024 * 1024))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* It's recommended to call exactly once in the entire lifetime of the application.
|
||||
*
|
||||
* @param builder if null, default builder will be used
|
||||
*/
|
||||
public static DownloaderImpl init(@Nullable OkHttpClient.Builder builder) {
|
||||
return instance = new DownloaderImpl(builder != null ? builder : new OkHttpClient.Builder());
|
||||
}
|
||||
|
||||
public static DownloaderImpl getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public String getCookies() {
|
||||
return mCookies;
|
||||
}
|
||||
|
||||
public void setCookies(String cookies) {
|
||||
mCookies = cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of the content that the url is pointing by firing a HEAD request.
|
||||
*
|
||||
* @param url an url pointing to the content
|
||||
* @return the size of the content, in bytes
|
||||
*/
|
||||
public long getContentLength(String url) throws IOException {
|
||||
try {
|
||||
final Response response = head(url);
|
||||
return Long.parseLong(response.getHeader("Content-Length"));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IOException("Invalid content length", e);
|
||||
} catch (ReCaptchaException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public InputStream stream(String siteUrl) throws IOException {
|
||||
try {
|
||||
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
|
||||
.method("GET", null).url(siteUrl)
|
||||
.addHeader("User-Agent", USER_AGENT);
|
||||
|
||||
if (!TextUtils.isEmpty(mCookies)) {
|
||||
requestBuilder.addHeader("Cookie", mCookies);
|
||||
}
|
||||
|
||||
final okhttp3.Request request = requestBuilder.build();
|
||||
final okhttp3.Response response = client.newCall(request).execute();
|
||||
final ResponseBody body = response.body();
|
||||
|
||||
if (response.code() == 429) {
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested", siteUrl);
|
||||
}
|
||||
|
||||
if (body == null) {
|
||||
response.close();
|
||||
return null;
|
||||
}
|
||||
|
||||
return body.byteStream();
|
||||
} catch (ReCaptchaException e) {
|
||||
throw new IOException(e.getMessage(), e.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response execute(@NonNull Request request) throws IOException, ReCaptchaException {
|
||||
final String httpMethod = request.httpMethod();
|
||||
final String url = request.url();
|
||||
final Map<String, List<String>> headers = request.headers();
|
||||
final byte[] dataToSend = request.dataToSend();
|
||||
|
||||
RequestBody requestBody = null;
|
||||
if (dataToSend != null) {
|
||||
requestBody = RequestBody.create(null, dataToSend);
|
||||
}
|
||||
|
||||
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
|
||||
.method(httpMethod, requestBody).url(url)
|
||||
.addHeader("User-Agent", USER_AGENT);
|
||||
|
||||
if (!TextUtils.isEmpty(mCookies)) {
|
||||
requestBuilder.addHeader("Cookie", mCookies);
|
||||
}
|
||||
|
||||
for (Map.Entry<String, List<String>> pair : headers.entrySet()) {
|
||||
final String headerName = pair.getKey();
|
||||
final List<String> headerValueList = pair.getValue();
|
||||
|
||||
if (headerValueList.size() > 1) {
|
||||
requestBuilder.removeHeader(headerName);
|
||||
for (String headerValue : headerValueList) {
|
||||
requestBuilder.addHeader(headerName, headerValue);
|
||||
}
|
||||
} else if (headerValueList.size() == 1) {
|
||||
requestBuilder.header(headerName, headerValueList.get(0));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final okhttp3.Response response = client.newCall(requestBuilder.build()).execute();
|
||||
|
||||
if (response.code() == 429) {
|
||||
response.close();
|
||||
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested", url);
|
||||
}
|
||||
|
||||
final ResponseBody body = response.body();
|
||||
String responseBodyToReturn = null;
|
||||
|
||||
if (body != null) {
|
||||
responseBodyToReturn = body.string();
|
||||
}
|
||||
|
||||
final String latestUrl = response.request().url().toString();
|
||||
return new Response(response.code(), response.message(), response.headers().toMultimap(), responseBodyToReturn, latestUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable TLS 1.2 and 1.1 on Android Kitkat. This function is mostly taken from the documentation of
|
||||
* OkHttpClient.Builder.sslSocketFactory(_,_)
|
||||
* <p>
|
||||
* If there is an error, the function will safely fall back to doing nothing and printing the error to the console.
|
||||
*
|
||||
* @param builder The HTTPClient Builder on which TLS is enabled on (will be modified in-place)
|
||||
*/
|
||||
private static void enableModernTLS(OkHttpClient.Builder builder) {
|
||||
try {
|
||||
// get the default TrustManager
|
||||
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];
|
||||
|
||||
// insert our own TLSSocketFactory
|
||||
SSLSocketFactory sslSocketFactory = TLSSocketFactoryCompat.getInstance();
|
||||
|
||||
builder.sslSocketFactory(sslSocketFactory, trustManager);
|
||||
|
||||
// This will try to enable all modern CipherSuites(+2 more) that are supported on the device.
|
||||
// Necessary because some servers (e.g. Framatube.org) don't support the old cipher suites.
|
||||
// https://github.com/square/okhttp/issues/4053#issuecomment-402579554
|
||||
List<CipherSuite> cipherSuites = new ArrayList<>();
|
||||
cipherSuites.addAll(ConnectionSpec.MODERN_TLS.cipherSuites());
|
||||
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA);
|
||||
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA);
|
||||
ConnectionSpec legacyTLS = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
|
||||
.cipherSuites(cipherSuites.toArray(new CipherSuite[0]))
|
||||
.build();
|
||||
|
||||
builder.connectionSpecs(Arrays.asList(legacyTLS, ConnectionSpec.CLEARTEXT));
|
||||
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
|
||||
if (DEBUG) e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ public class ImageDownloader extends BaseImageDownloader {
|
||||
}
|
||||
|
||||
protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
|
||||
final Downloader downloader = (Downloader) NewPipe.getDownloader();
|
||||
final DownloaderImpl downloader = (DownloaderImpl) NewPipe.getDownloader();
|
||||
return downloader.stream(imageUri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,14 +29,17 @@ import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -47,12 +50,14 @@ import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.view.GravityCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.google.android.material.navigation.NavigationView;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
||||
import org.schabi.newpipe.fragments.BackPressable;
|
||||
import org.schabi.newpipe.fragments.MainFragment;
|
||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||
@@ -60,12 +65,20 @@ import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PeertubeHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
private static final String TAG = "MainActivity";
|
||||
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
|
||||
@@ -97,8 +110,13 @@ public class MainActivity extends AppCompatActivity {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
|
||||
// enable TLS1.1/1.2 for kitkat devices, to fix download and play for mediaCCC sources
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
|
||||
TLSSocketFactoryCompat.setAsDefault();
|
||||
}
|
||||
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
|
||||
|
||||
assureCorrectAppLanguage(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
@@ -300,13 +318,57 @@ public class MainActivity extends AppCompatActivity {
|
||||
final String title = s.getServiceInfo().getName() +
|
||||
(ServiceHelper.isBeta(s) ? " (beta)" : "");
|
||||
|
||||
drawerItems.getMenu()
|
||||
MenuItem menuItem = drawerItems.getMenu()
|
||||
.add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
|
||||
.setIcon(ServiceHelper.getIcon(s.getServiceId()));
|
||||
|
||||
// peertube specifics
|
||||
if(s.getServiceId() == 3){
|
||||
enhancePeertubeMenu(s, menuItem);
|
||||
}
|
||||
}
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
|
||||
}
|
||||
|
||||
private void enhancePeertubeMenu(StreamingService s, MenuItem menuItem) {
|
||||
PeertubeInstance currentInstace = PeertubeHelper.getCurrentInstance();
|
||||
menuItem.setTitle(currentInstace.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : ""));
|
||||
Spinner spinner = (Spinner) LayoutInflater.from(this).inflate(R.layout.instance_spinner_layout, null);
|
||||
List<PeertubeInstance> instances = PeertubeHelper.getInstanceList(this);
|
||||
List<String> items = new ArrayList<>();
|
||||
int defaultSelect = 0;
|
||||
for(PeertubeInstance instance: instances){
|
||||
items.add(instance.getName());
|
||||
if(instance.getUrl().equals(currentInstace.getUrl())){
|
||||
defaultSelect = items.size()-1;
|
||||
}
|
||||
}
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, R.layout.instance_spinner_item, items);
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
spinner.setAdapter(adapter);
|
||||
spinner.setSelection(defaultSelect, false);
|
||||
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
PeertubeInstance newInstance = instances.get(position);
|
||||
if(newInstance.getUrl().equals(PeertubeHelper.getCurrentInstance().getUrl())) return;
|
||||
PeertubeHelper.selectInstance(newInstance, getApplicationContext());
|
||||
changeService(menuItem);
|
||||
drawer.closeDrawers();
|
||||
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||
getSupportFragmentManager().popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
||||
recreate();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
|
||||
}
|
||||
});
|
||||
menuItem.setActionView(spinner);
|
||||
}
|
||||
|
||||
private void showTabs() throws ExtractionException {
|
||||
serviceArrow.setImageResource(R.drawable.ic_arrow_down_white);
|
||||
|
||||
@@ -358,6 +420,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
assureCorrectAppLanguage(this);
|
||||
Localization.init(getApplicationContext()); //change the date format to match the selected language on resume
|
||||
super.onResume();
|
||||
|
||||
// close drawer on return, and don't show animation, so its looks like the drawer isn't open
|
||||
@@ -367,6 +431,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
String selectedServiceName = NewPipe.getService(
|
||||
ServiceHelper.getSelectedServiceId(this)).getServiceInfo().getName();
|
||||
headerServiceView.setText(selectedServiceName);
|
||||
headerServiceView.post(() -> headerServiceView.setSelected(true));
|
||||
toggleServiceButton.setContentDescription(
|
||||
getString(R.string.drawer_header_description) + selectedServiceName);
|
||||
} catch (Exception e) {
|
||||
@@ -387,6 +452,10 @@ public class MainActivity extends AppCompatActivity {
|
||||
sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false).apply();
|
||||
NavigationHelper.openMainActivity(this);
|
||||
}
|
||||
|
||||
final boolean isHistoryEnabled = sharedPreferences.getBoolean(
|
||||
getString(R.string.enable_watch_history_key), true);
|
||||
drawerItems.getMenu().findItem(ITEM_ID_HISTORY).setVisible(isHistoryEnabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -489,8 +558,6 @@ public class MainActivity extends AppCompatActivity {
|
||||
if (!(fragment instanceof SearchFragment)) {
|
||||
findViewById(R.id.toolbar).findViewById(R.id.toolbar_search_container).setVisibility(View.GONE);
|
||||
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.main_menu, menu);
|
||||
}
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
@@ -512,14 +579,6 @@ public class MainActivity extends AppCompatActivity {
|
||||
case android.R.id.home:
|
||||
onHomeButtonPressed();
|
||||
return true;
|
||||
case R.id.action_show_downloads:
|
||||
return NavigationHelper.openDownloads(this);
|
||||
case R.id.action_history:
|
||||
NavigationHelper.openStatisticFragment(getSupportFragmentManager());
|
||||
return true;
|
||||
case R.id.action_settings:
|
||||
NavigationHelper.openSettings(this);
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import androidx.core.app.NavUtils;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.webkit.CookieManager;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/*
|
||||
* Created by beneth <bmauduit@beneth.fr> on 06.12.16.
|
||||
*
|
||||
@@ -37,48 +42,46 @@ import android.webkit.WebViewClient;
|
||||
public class ReCaptchaActivity extends AppCompatActivity {
|
||||
public static final int RECAPTCHA_REQUEST = 10;
|
||||
public static final String RECAPTCHA_URL_EXTRA = "recaptcha_url_extra";
|
||||
|
||||
public static final String TAG = ReCaptchaActivity.class.toString();
|
||||
public static final String YT_URL = "https://www.youtube.com";
|
||||
|
||||
private String url;
|
||||
private WebView webView;
|
||||
private String foundCookies = "";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
ThemeHelper.setTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_recaptcha);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
url = getIntent().getStringExtra(RECAPTCHA_URL_EXTRA);
|
||||
String url = getIntent().getStringExtra(RECAPTCHA_URL_EXTRA);
|
||||
if (url == null || url.isEmpty()) {
|
||||
url = YT_URL;
|
||||
}
|
||||
|
||||
|
||||
// Set return to Cancel by default
|
||||
// set return to Cancel by default
|
||||
setResult(RESULT_CANCELED);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setTitle(R.string.reCaptcha_title);
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
}
|
||||
webView = findViewById(R.id.reCaptchaWebView);
|
||||
|
||||
WebView myWebView = findViewById(R.id.reCaptchaWebView);
|
||||
|
||||
// Enable Javascript
|
||||
WebSettings webSettings = myWebView.getSettings();
|
||||
// enable Javascript
|
||||
WebSettings webSettings = webView.getSettings();
|
||||
webSettings.setJavaScriptEnabled(true);
|
||||
|
||||
ReCaptchaWebViewClient webClient = new ReCaptchaWebViewClient(this);
|
||||
myWebView.setWebViewClient(webClient);
|
||||
webView.setWebViewClient(new WebViewClient() {
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
super.onPageFinished(view, url);
|
||||
handleCookies(url);
|
||||
}
|
||||
});
|
||||
|
||||
// Cleaning cache, history and cookies from webView
|
||||
myWebView.clearCache(true);
|
||||
myWebView.clearHistory();
|
||||
// cleaning cache, history and cookies from webView
|
||||
webView.clearCache(true);
|
||||
webView.clearHistory();
|
||||
android.webkit.CookieManager cookieManager = CookieManager.getInstance();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
cookieManager.removeAllCookies(aBoolean -> {});
|
||||
@@ -86,77 +89,82 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
cookieManager.removeAllCookie();
|
||||
}
|
||||
|
||||
myWebView.loadUrl(url);
|
||||
webView.loadUrl(url);
|
||||
}
|
||||
|
||||
private class ReCaptchaWebViewClient extends WebViewClient {
|
||||
private final Activity context;
|
||||
private String mCookies;
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.menu_recaptcha, menu);
|
||||
|
||||
ReCaptchaWebViewClient(Activity ctx) {
|
||||
context = ctx;
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(false);
|
||||
actionBar.setTitle(R.string.title_activity_recaptcha);
|
||||
actionBar.setSubtitle(R.string.subtitle_activity_recaptcha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageStarted(WebView view, String url, Bitmap favicon) {
|
||||
// TODO: Start Loader
|
||||
super.onPageStarted(view, url, favicon);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
String cookies = CookieManager.getInstance().getCookie(url);
|
||||
|
||||
// TODO: Stop Loader
|
||||
|
||||
// find cookies : s_gl & goojf and Add cookies to Downloader
|
||||
if (find_access_cookies(cookies)) {
|
||||
// Give cookies to Downloader class
|
||||
Downloader.getInstance().setCookies(mCookies);
|
||||
|
||||
// Closing activity and return to parent
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean find_access_cookies(String cookies) {
|
||||
boolean ret = false;
|
||||
String c_s_gl = "";
|
||||
String c_goojf = "";
|
||||
|
||||
String[] parts = cookies.split("; ");
|
||||
for (String part : parts) {
|
||||
if (part.trim().startsWith("s_gl")) {
|
||||
c_s_gl = part.trim();
|
||||
}
|
||||
if (part.trim().startsWith("goojf")) {
|
||||
c_goojf = part.trim();
|
||||
}
|
||||
}
|
||||
if (c_s_gl.length() > 0 && c_goojf.length() > 0) {
|
||||
ret = true;
|
||||
//mCookies = c_s_gl + "; " + c_goojf;
|
||||
// Youtube seems to also need the other cookies:
|
||||
mCookies = cookies;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
saveCookiesAndFinish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
switch (id) {
|
||||
case android.R.id.home: {
|
||||
Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
NavUtils.navigateUpTo(this, intent);
|
||||
case R.id.menu_item_done:
|
||||
saveCookiesAndFinish();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void saveCookiesAndFinish() {
|
||||
handleCookies(webView.getUrl()); // try to get cookies of unclosed page
|
||||
if (!foundCookies.isEmpty()) {
|
||||
// give cookies to Downloader class
|
||||
DownloaderImpl.getInstance().setCookies(foundCookies);
|
||||
setResult(RESULT_OK);
|
||||
}
|
||||
|
||||
Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
NavUtils.navigateUpTo(this, intent);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void handleCookies(String url) {
|
||||
String cookies = CookieManager.getInstance().getCookie(url);
|
||||
if (MainActivity.DEBUG) Log.d(TAG, "handleCookies: url=" + url + "; cookies=" + (cookies == null ? "null" : cookies));
|
||||
if (cookies == null) return;
|
||||
|
||||
addYoutubeCookies(cookies);
|
||||
// add other methods to extract cookies here
|
||||
}
|
||||
|
||||
private void addYoutubeCookies(@NonNull String cookies) {
|
||||
if (cookies.contains("s_gl=") || cookies.contains("goojf=") || cookies.contains("VISITOR_INFO1_LIVE=")) {
|
||||
// youtube seems to also need the other cookies:
|
||||
addCookie(cookies);
|
||||
}
|
||||
}
|
||||
|
||||
private void addCookie(String cookie) {
|
||||
if (foundCookies.contains(cookie)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (foundCookies.isEmpty() || foundCookies.endsWith("; ")) {
|
||||
foundCookies += cookie;
|
||||
} else if (foundCookies.endsWith(";")) {
|
||||
foundCookies += " " + cookie;
|
||||
} else {
|
||||
foundCookies += "; " + cookie;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,27 +22,30 @@ import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class AboutActivity extends AppCompatActivity {
|
||||
|
||||
/**
|
||||
* List of all software components
|
||||
*/
|
||||
private static final SoftwareComponent[] SOFTWARE_COMPONENTS = new SoftwareComponent[]{
|
||||
new SoftwareComponent("Giga Get", "2014", "Peter Cai", "https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL2),
|
||||
new SoftwareComponent("NewPipe Extractor", "2017", "Christian Schabesberger", "https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3),
|
||||
new SoftwareComponent("Giga Get", "2014 - 2015", "Peter Cai", "https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL2),
|
||||
new SoftwareComponent("NewPipe Extractor", "2017 - 2020", "Christian Schabesberger", "https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3),
|
||||
new SoftwareComponent("Jsoup", "2017", "Jonathan Hedley", "https://github.com/jhy/jsoup", StandardLicenses.MIT),
|
||||
new SoftwareComponent("Rhino", "2015", "Mozilla", "https://www.mozilla.org/rhino/", StandardLicenses.MPL2),
|
||||
new SoftwareComponent("ACRA", "2013", "Kevin Gaudin", "http://www.acra.ch", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("Universal Image Loader", "2011 - 2015", "Sergey Tarasevich", "https://github.com/nostra13/Android-Universal-Image-Loader", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("CircleImageView", "2014 - 2017", "Henning Dodenhof", "https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("CircleImageView", "2014 - 2020", "Henning Dodenhof", "https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam", "https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2),
|
||||
new SoftwareComponent("ExoPlayer", "2014-2017", "Google Inc", "https://github.com/google/ExoPlayer", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("RxAndroid", "2015", "The RxAndroid authors", "https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("RxJava", "2016-present", "RxJava Contributors", "https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("RxBinding", "2015", "Jake Wharton", "https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2)
|
||||
new SoftwareComponent("ExoPlayer", "2014 - 2020", "Google Inc", "https://github.com/google/ExoPlayer", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("RxAndroid", "2015 - 2018", "The RxAndroid authors", "https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("RxJava", "2016 - 2020", "RxJava Contributors", "https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("RxBinding", "2015 - 2018", "Jake Wharton", "https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("PrettyTime", "2012 - 2020", "Lincoln Baxter, III", "https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("Markwon", "2017 - 2020", "Noties", "https://github.com/noties/Markwon", StandardLicenses.APACHE2)
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -62,8 +65,10 @@ public class AboutActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
assureCorrectAppLanguage(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.setTheme(this);
|
||||
this.setTitle(getString(R.string.title_activity_about));
|
||||
|
||||
setContentView(R.layout.activity_about);
|
||||
|
||||
@@ -83,13 +88,6 @@ public class AboutActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// Inflate the menu; this adds items to the action bar if it is present.
|
||||
getMenuInflater().inflate(R.menu.menu_about, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
|
||||
@@ -99,11 +97,6 @@ public class AboutActivity extends AppCompatActivity {
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
return true;
|
||||
case R.id.action_settings:
|
||||
NavigationHelper.openSettings(this);
|
||||
return true;
|
||||
case R.id.action_show_downloads:
|
||||
return NavigationHelper.openDownloads(this);
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.schabi.newpipe.about;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.res.Resources;
|
||||
import android.os.AsyncTask;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
@@ -14,6 +15,8 @@ import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class LicenseFragmentHelper extends AsyncTask<Object, Void, Integer> {
|
||||
|
||||
final WeakReference<Activity> weakReference;
|
||||
@@ -55,15 +58,15 @@ public class LicenseFragmentHelper extends AsyncTask<Object, Void, Integer> {
|
||||
wv.loadData(webViewData, "text/html; charset=UTF-8", null);
|
||||
|
||||
alert.setView(wv);
|
||||
alert.setNegativeButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
assureCorrectAppLanguage(activity.getApplicationContext());
|
||||
alert.setNegativeButton(getFinishString(activity), (dialog, which) -> dialog.dismiss());
|
||||
alert.show();
|
||||
}
|
||||
|
||||
private static String getFinishString(Activity activity) {
|
||||
return activity.getApplicationContext().getResources().getString(R.string.finish);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context the context to use
|
||||
* @param license the license
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.schabi.newpipe.database.playlist.model;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Ignore;
|
||||
@@ -72,10 +74,16 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||
|
||||
@Ignore
|
||||
public boolean isIdenticalTo(final PlaylistInfo info) {
|
||||
return getServiceId() == info.getServiceId() && getName().equals(info.getName()) &&
|
||||
getStreamCount() == info.getStreamCount() && getUrl().equals(info.getUrl()) &&
|
||||
getThumbnailUrl().equals(info.getThumbnailUrl()) &&
|
||||
getUploader().equals(info.getUploaderName());
|
||||
/*
|
||||
* Returns boolean comparing the online playlist and the local copy.
|
||||
* (False if info changed such as playlist name or track count)
|
||||
*/
|
||||
return getServiceId() == info.getServiceId()
|
||||
&& getStreamCount() == info.getStreamCount()
|
||||
&& TextUtils.equals(getName(), info.getName())
|
||||
&& TextUtils.equals(getUrl(), info.getUrl())
|
||||
&& TextUtils.equals(getThumbnailUrl(), info.getThumbnailUrl())
|
||||
&& TextUtils.equals(getUploader(), info.getUploaderName());
|
||||
}
|
||||
|
||||
public long getUid() {
|
||||
|
||||
@@ -12,12 +12,14 @@ import android.view.MenuItem;
|
||||
import android.view.ViewTreeObserver;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
import us.shandian.giga.ui.fragment.MissionsFragment;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class DownloadActivity extends AppCompatActivity {
|
||||
|
||||
private static final String MISSIONS_FRAGMENT_TAG = "fragment_tag";
|
||||
@@ -29,6 +31,7 @@ public class DownloadActivity extends AppCompatActivity {
|
||||
i.setClass(this, DownloadManagerService.class);
|
||||
startService(i);
|
||||
|
||||
assureCorrectAppLanguage(this);
|
||||
ThemeHelper.setTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_downloader);
|
||||
@@ -74,15 +77,9 @@ public class DownloadActivity extends AppCompatActivity {
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home: {
|
||||
case android.R.id.home:
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
case R.id.action_settings: {
|
||||
Intent intent = new Intent(this, SettingsActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@@ -11,15 +11,6 @@ import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.view.menu.ActionMenuItemView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -34,18 +25,29 @@ import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.view.menu.ActionMenuItemView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import com.nononsenseapps.filepicker.Utils;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.RouterActivity;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.localization.Localization;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.Stream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.extractor.utils.Localization;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
@@ -68,6 +70,7 @@ import java.util.Locale;
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import us.shandian.giga.get.MissionRecoveryInfo;
|
||||
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
import us.shandian.giga.postprocessing.Postprocessing;
|
||||
@@ -76,6 +79,8 @@ import us.shandian.giga.service.DownloadManagerService;
|
||||
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
|
||||
import us.shandian.giga.service.MissionState;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
|
||||
private static final String TAG = "DialogFragment";
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
@@ -367,6 +372,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
toolbar.setOnMenuItemClickListener(item -> {
|
||||
if (item.getItemId() == R.id.okay) {
|
||||
prepareSelectedDownload();
|
||||
if (getActivity() instanceof RouterActivity) {
|
||||
getActivity().finish();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -488,35 +496,24 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
}
|
||||
|
||||
private int getSubtitleIndexBy(List<SubtitlesStream> streams) {
|
||||
Localization loc = NewPipe.getPreferredLocalization();
|
||||
final Localization preferredLocalization = NewPipe.getPreferredLocalization();
|
||||
|
||||
int candidate = 0;
|
||||
for (int i = 0; i < streams.size(); i++) {
|
||||
Locale streamLocale = streams.get(i).getLocale();
|
||||
String tag = streamLocale.getLanguage().concat("-").concat(streamLocale.getCountry());
|
||||
if (tag.equalsIgnoreCase(loc.getLanguage())) {
|
||||
return i;
|
||||
final Locale streamLocale = streams.get(i).getLocale();
|
||||
|
||||
final boolean languageEquals = streamLocale.getLanguage() != null && preferredLocalization.getLanguageCode() != null &&
|
||||
streamLocale.getLanguage().equals(new Locale(preferredLocalization.getLanguageCode()).getLanguage());
|
||||
final boolean countryEquals = streamLocale.getCountry() != null && streamLocale.getCountry().equals(preferredLocalization.getCountryCode());
|
||||
|
||||
if (languageEquals) {
|
||||
if (countryEquals) return i;
|
||||
|
||||
candidate = i;
|
||||
}
|
||||
}
|
||||
|
||||
// fallback
|
||||
// 1st loop match country & language
|
||||
// 2nd loop match language only
|
||||
int index = loc.getLanguage().indexOf("-");
|
||||
String lang = index > 0 ? loc.getLanguage().substring(0, index) : loc.getLanguage();
|
||||
|
||||
for (int j = 0; j < 2; j++) {
|
||||
for (int i = 0; i < streams.size(); i++) {
|
||||
Locale streamLocale = streams.get(i).getLocale();
|
||||
|
||||
if (streamLocale.getLanguage().equalsIgnoreCase(lang)) {
|
||||
if (j > 0 || streamLocale.getCountry().equalsIgnoreCase(loc.getCountry())) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
return candidate;
|
||||
}
|
||||
|
||||
StoredDirectoryHelper mainStorageAudio = null;
|
||||
@@ -533,10 +530,11 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
}
|
||||
|
||||
private void showFailedDialog(@StringRes int msg) {
|
||||
assureCorrectAppLanguage(getContext());
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.general_error)
|
||||
.setMessage(msg)
|
||||
.setNegativeButton(android.R.string.ok, null)
|
||||
.setNegativeButton(getString(R.string.finish), null)
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
@@ -565,8 +563,16 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
case R.id.audio_button:
|
||||
mainStorage = mainStorageAudio;
|
||||
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
|
||||
mime = format.mimeType;
|
||||
filename += format.suffix;
|
||||
switch(format) {
|
||||
case WEBMA_OPUS:
|
||||
mime = "audio/ogg";
|
||||
filename += "opus";
|
||||
break;
|
||||
default:
|
||||
mime = format.mimeType;
|
||||
filename += format.suffix;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case R.id.video_button:
|
||||
mainStorage = mainStorageVideo;
|
||||
@@ -773,12 +779,13 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
}
|
||||
|
||||
Stream selectedStream;
|
||||
Stream secondaryStream = null;
|
||||
char kind;
|
||||
int threads = threadsSeekBar.getProgress() + 1;
|
||||
String[] urls;
|
||||
MissionRecoveryInfo[] recoveryInfo;
|
||||
String psName = null;
|
||||
String[] psArgs = null;
|
||||
String secondaryStreamUrl = null;
|
||||
long nearLength = 0;
|
||||
|
||||
// more download logic: select muxer, subtitle converter, etc.
|
||||
@@ -789,18 +796,20 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
|
||||
if (selectedStream.getFormat() == MediaFormat.M4A) {
|
||||
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
|
||||
} else if (selectedStream.getFormat() == MediaFormat.WEBMA_OPUS) {
|
||||
psName = Postprocessing.ALGORITHM_OGG_FROM_WEBM_DEMUXER;
|
||||
}
|
||||
break;
|
||||
case R.id.video_button:
|
||||
kind = 'v';
|
||||
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
|
||||
|
||||
SecondaryStreamHelper<AudioStream> secondaryStream = videoStreamsAdapter
|
||||
SecondaryStreamHelper<AudioStream> secondary = videoStreamsAdapter
|
||||
.getAllSecondary()
|
||||
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
|
||||
|
||||
if (secondaryStream != null) {
|
||||
secondaryStreamUrl = secondaryStream.getStream().getUrl();
|
||||
if (secondary != null) {
|
||||
secondaryStream = secondary.getStream();
|
||||
|
||||
if (selectedStream.getFormat() == MediaFormat.MPEG_4)
|
||||
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
|
||||
@@ -812,8 +821,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
|
||||
// set nearLength, only, if both sizes are fetched or known. This probably
|
||||
// does not work on slow networks but is later updated in the downloader
|
||||
if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) {
|
||||
nearLength = secondaryStream.getSizeInBytes() + videoSize;
|
||||
if (secondary.getSizeInBytes() > 0 && videoSize > 0) {
|
||||
nearLength = secondary.getSizeInBytes() + videoSize;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -827,7 +836,6 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
psArgs = new String[]{
|
||||
selectedStream.getFormat().getSuffix(),
|
||||
"false",// ignore empty frames
|
||||
"false",// detect youtube duplicate lines
|
||||
};
|
||||
}
|
||||
break;
|
||||
@@ -835,13 +843,25 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
return;
|
||||
}
|
||||
|
||||
if (secondaryStreamUrl == null) {
|
||||
urls = new String[]{selectedStream.getUrl()};
|
||||
if (secondaryStream == null) {
|
||||
urls = new String[]{
|
||||
selectedStream.getUrl()
|
||||
};
|
||||
recoveryInfo = new MissionRecoveryInfo[]{
|
||||
new MissionRecoveryInfo(selectedStream)
|
||||
};
|
||||
} else {
|
||||
urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl};
|
||||
urls = new String[]{
|
||||
selectedStream.getUrl(), secondaryStream.getUrl()
|
||||
};
|
||||
recoveryInfo = new MissionRecoveryInfo[]{
|
||||
new MissionRecoveryInfo(selectedStream), new MissionRecoveryInfo(secondaryStream)
|
||||
};
|
||||
}
|
||||
|
||||
DownloadManagerService.startMission(context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength);
|
||||
DownloadManagerService.startMission(
|
||||
context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo
|
||||
);
|
||||
|
||||
dismiss();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -15,7 +16,7 @@ import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
@@ -29,6 +30,7 @@ import org.schabi.newpipe.settings.tabs.Tab;
|
||||
import org.schabi.newpipe.settings.tabs.TabsManager;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.views.ScrollableTabLayout;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -36,7 +38,7 @@ import java.util.List;
|
||||
public class MainFragment extends BaseFragment implements TabLayout.OnTabSelectedListener {
|
||||
private ViewPager viewPager;
|
||||
private SelectedTabsPagerAdapter pagerAdapter;
|
||||
private TabLayout tabLayout;
|
||||
private ScrollableTabLayout tabLayout;
|
||||
|
||||
private List<Tab> tabsList = new ArrayList<>();
|
||||
private TabsManager tabsManager;
|
||||
@@ -52,32 +54,19 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
destroyOldFragments();
|
||||
|
||||
tabsManager = TabsManager.getManager(activity);
|
||||
tabsManager.setSavedTabsListener(() -> {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "TabsManager.SavedTabsChangeListener: onTabsChanged called, isResumed = " + isResumed());
|
||||
}
|
||||
if (isResumed()) {
|
||||
updateTabs();
|
||||
setupTabs();
|
||||
} else {
|
||||
hasTabsChanged = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void destroyOldFragments() {
|
||||
for (Fragment fragment : getChildFragmentManager().getFragments()) {
|
||||
if (fragment != null) {
|
||||
getChildFragmentManager()
|
||||
.beginTransaction()
|
||||
.remove(fragment)
|
||||
.commitNowAllowingStateLoss();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_main, container, false);
|
||||
@@ -90,23 +79,17 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
tabLayout = rootView.findViewById(R.id.main_tab_layout);
|
||||
viewPager = rootView.findViewById(R.id.pager);
|
||||
|
||||
/* Nested fragment, use child fragment here to maintain backstack in view pager. */
|
||||
pagerAdapter = new SelectedTabsPagerAdapter(getChildFragmentManager());
|
||||
viewPager.setAdapter(pagerAdapter);
|
||||
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
tabLayout.addOnTabSelectedListener(this);
|
||||
updateTabs();
|
||||
|
||||
setupTabs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (hasTabsChanged) {
|
||||
hasTabsChanged = false;
|
||||
updateTabs();
|
||||
}
|
||||
if (hasTabsChanged) setupTabs();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -153,45 +136,42 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
// Tabs
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void updateTabs() {
|
||||
public void setupTabs() {
|
||||
tabsList.clear();
|
||||
tabsList.addAll(tabsManager.getTabs());
|
||||
pagerAdapter.notifyDataSetChanged();
|
||||
|
||||
viewPager.setOffscreenPageLimit(pagerAdapter.getCount());
|
||||
updateTabsIcon();
|
||||
updateTabsContentDescription();
|
||||
updateCurrentTitle();
|
||||
if (pagerAdapter == null || !pagerAdapter.sameTabs(tabsList)) {
|
||||
pagerAdapter = new SelectedTabsPagerAdapter(requireContext(), getChildFragmentManager(), tabsList);
|
||||
}
|
||||
// Clear previous tabs/fragments and set new adapter
|
||||
viewPager.setAdapter(pagerAdapter);
|
||||
viewPager.setOffscreenPageLimit(tabsList.size());
|
||||
|
||||
updateTabsIconAndDescription();
|
||||
updateTitleForTab(viewPager.getCurrentItem());
|
||||
|
||||
hasTabsChanged = false;
|
||||
}
|
||||
|
||||
private void updateTabsIcon() {
|
||||
private void updateTabsIconAndDescription() {
|
||||
for (int i = 0; i < tabsList.size(); i++) {
|
||||
final TabLayout.Tab tabToSet = tabLayout.getTabAt(i);
|
||||
if (tabToSet != null) {
|
||||
tabToSet.setIcon(tabsList.get(i).getTabIconRes(activity));
|
||||
final Tab tab = tabsList.get(i);
|
||||
tabToSet.setIcon(tab.getTabIconRes(requireContext()));
|
||||
tabToSet.setContentDescription(tab.getTabName(requireContext()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTabsContentDescription() {
|
||||
for (int i = 0; i < tabsList.size(); i++) {
|
||||
final TabLayout.Tab tabToSet = tabLayout.getTabAt(i);
|
||||
if (tabToSet != null) {
|
||||
final Tab t = tabsList.get(i);
|
||||
tabToSet.setIcon(t.getTabIconRes(activity));
|
||||
tabToSet.setContentDescription(t.getTabName(activity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateCurrentTitle() {
|
||||
setTitle(tabsList.get(viewPager.getCurrentItem()).getTabName(requireContext()));
|
||||
private void updateTitleForTab(int tabPosition) {
|
||||
setTitle(tabsList.get(tabPosition).getTabName(requireContext()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab selectedTab) {
|
||||
if (DEBUG) Log.d(TAG, "onTabSelected() called with: selectedTab = [" + selectedTab + "]");
|
||||
updateCurrentTitle();
|
||||
updateTitleForTab(selectedTab.getPosition());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -201,29 +181,33 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab) {
|
||||
if (DEBUG) Log.d(TAG, "onTabReselected() called with: tab = [" + tab + "]");
|
||||
updateCurrentTitle();
|
||||
updateTitleForTab(tab.getPosition());
|
||||
}
|
||||
|
||||
private class SelectedTabsPagerAdapter extends FragmentPagerAdapter {
|
||||
private static class SelectedTabsPagerAdapter extends FragmentStatePagerAdapter {
|
||||
private final Context context;
|
||||
private final List<Tab> internalTabsList;
|
||||
|
||||
private SelectedTabsPagerAdapter(FragmentManager fragmentManager) {
|
||||
super(fragmentManager);
|
||||
private SelectedTabsPagerAdapter(Context context, FragmentManager fragmentManager, List<Tab> tabsList) {
|
||||
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
||||
this.context = context;
|
||||
this.internalTabsList = new ArrayList<>(tabsList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
final Tab tab = tabsList.get(position);
|
||||
final Tab tab = internalTabsList.get(position);
|
||||
|
||||
Throwable throwable = null;
|
||||
Fragment fragment = null;
|
||||
try {
|
||||
fragment = tab.getFragment();
|
||||
fragment = tab.getFragment(context);
|
||||
} catch (ExtractionException e) {
|
||||
throwable = e;
|
||||
}
|
||||
|
||||
if (throwable != null) {
|
||||
ErrorActivity.reportError(activity, throwable, activity.getClass(), null,
|
||||
ErrorActivity.reportError(context, throwable, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
|
||||
return new BlankFragment();
|
||||
}
|
||||
@@ -244,15 +228,11 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return tabsList.size();
|
||||
return internalTabsList.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(ViewGroup container, int position, Object object) {
|
||||
getChildFragmentManager()
|
||||
.beginTransaction()
|
||||
.remove((Fragment) object)
|
||||
.commitNowAllowingStateLoss();
|
||||
public boolean sameTabs(List<Tab> tabsToCompare) {
|
||||
return internalTabsList.equals(tabsToCompare);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
@@ -18,7 +17,6 @@ import androidx.fragment.app.Fragment;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import android.text.Html;
|
||||
import android.text.Spanned;
|
||||
@@ -58,6 +56,7 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.Description;
|
||||
import org.schabi.newpipe.extractor.stream.Stream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
@@ -79,6 +78,7 @@ import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.InfoCache;
|
||||
import org.schabi.newpipe.util.KoreUtil;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
@@ -95,6 +95,8 @@ import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import icepick.State;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.linkify.LinkifyPlugin;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
@@ -482,7 +484,6 @@ public class VideoDetailFragment
|
||||
videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view);
|
||||
videoDescriptionView = rootView.findViewById(R.id.detail_description_view);
|
||||
videoDescriptionView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
videoDescriptionView.setAutoLinkMask(Linkify.WEB_URLS);
|
||||
|
||||
thumbsUpTextView = rootView.findViewById(R.id.detail_thumbs_up_count_view);
|
||||
thumbsUpImageView = rootView.findViewById(R.id.detail_thumbs_up_img_view);
|
||||
@@ -599,22 +600,27 @@ public class VideoDetailFragment
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (isLoading.get()) {
|
||||
// if is still loading block menu
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.action_settings) {
|
||||
NavigationHelper.openSettings(requireContext());
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isLoading.get()) {
|
||||
// if still loading, block menu buttons related to video info
|
||||
return true;
|
||||
}
|
||||
|
||||
int id = item.getItemId();
|
||||
switch (id) {
|
||||
case R.id.menu_item_share: {
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.shareUrl(this.getContext(), currentInfo.getName(), currentInfo.getOriginalUrl());
|
||||
ShareUtils.shareUrl(requireContext(), currentInfo.getName(), currentInfo.getOriginalUrl());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case R.id.menu_item_openInBrowser: {
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.openUrlInBrowser(this.getContext(), currentInfo.getOriginalUrl());
|
||||
ShareUtils.openUrlInBrowser(requireContext(), currentInfo.getOriginalUrl());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -624,7 +630,7 @@ public class VideoDetailFragment
|
||||
url.replace("https", "http")));
|
||||
} catch (Exception e) {
|
||||
if (DEBUG) Log.i(TAG, "Failed to start kore", e);
|
||||
showInstallKoreDialog(activity);
|
||||
KoreUtil.showInstallKoreDialog(activity);
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
@@ -632,16 +638,6 @@ public class VideoDetailFragment
|
||||
}
|
||||
}
|
||||
|
||||
private static void showInstallKoreDialog(final Context context) {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setMessage(R.string.kore_not_found)
|
||||
.setPositiveButton(R.string.install, (DialogInterface dialog, int which) ->
|
||||
NavigationHelper.installKore(context))
|
||||
.setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> {
|
||||
});
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
private void setupActionBarOnError(final String url) {
|
||||
if (DEBUG) Log.d(TAG, "setupActionBarHandlerOnError() called with: url = [" + url + "]");
|
||||
Log.e("-----", "missing code");
|
||||
@@ -928,28 +924,41 @@ public class VideoDetailFragment
|
||||
return sortedVideoStreams != null ? sortedVideoStreams.get(selectedVideoStreamIndex) : null;
|
||||
}
|
||||
|
||||
private void prepareDescription(final String descriptionHtml) {
|
||||
if (TextUtils.isEmpty(descriptionHtml)) {
|
||||
private void prepareDescription(Description description) {
|
||||
if (TextUtils.isEmpty(description.getContent()) || description == Description.emptyDescription) {
|
||||
return;
|
||||
}
|
||||
|
||||
disposables.add(Single.just(descriptionHtml)
|
||||
.map((@io.reactivex.annotations.NonNull String description) -> {
|
||||
Spanned parsedDescription;
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
parsedDescription = Html.fromHtml(description, 0);
|
||||
} else {
|
||||
//noinspection deprecation
|
||||
parsedDescription = Html.fromHtml(description);
|
||||
}
|
||||
return parsedDescription;
|
||||
})
|
||||
.subscribeOn(Schedulers.computation())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe((@io.reactivex.annotations.NonNull Spanned spanned) -> {
|
||||
videoDescriptionView.setText(spanned);
|
||||
videoDescriptionView.setVisibility(View.VISIBLE);
|
||||
}));
|
||||
if (description.getType() == Description.HTML) {
|
||||
disposables.add(Single.just(description.getContent())
|
||||
.map((@io.reactivex.annotations.NonNull String descriptionText) -> {
|
||||
Spanned parsedDescription;
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
parsedDescription = Html.fromHtml(descriptionText, 0);
|
||||
} else {
|
||||
//noinspection deprecation
|
||||
parsedDescription = Html.fromHtml(descriptionText);
|
||||
}
|
||||
return parsedDescription;
|
||||
})
|
||||
.subscribeOn(Schedulers.computation())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe((@io.reactivex.annotations.NonNull Spanned spanned) -> {
|
||||
videoDescriptionView.setText(spanned);
|
||||
videoDescriptionView.setVisibility(View.VISIBLE);
|
||||
}));
|
||||
} else if (description.getType() == Description.MARKDOWN) {
|
||||
final Markwon markwon = Markwon.builder(getContext())
|
||||
.usePlugin(LinkifyPlugin.create())
|
||||
.build();
|
||||
markwon.setMarkdown(videoDescriptionView, description.getContent());
|
||||
videoDescriptionView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
//== Description.PLAIN_TEXT
|
||||
videoDescriptionView.setAutoLinkMask(Linkify.WEB_URLS);
|
||||
videoDescriptionView.setText(description.getContent(), TextView.BufferType.SPANNABLE);
|
||||
videoDescriptionView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void setHeightThumbnail() {
|
||||
@@ -1067,7 +1076,13 @@ public class VideoDetailFragment
|
||||
uploaderThumb.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.buddy));
|
||||
|
||||
if (info.getViewCount() >= 0) {
|
||||
videoCountView.setText(Localization.localizeViewCount(activity, info.getViewCount()));
|
||||
if (info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
|
||||
videoCountView.setText(Localization.listeningCount(activity, info.getViewCount()));
|
||||
} else if (info.getStreamType().equals(StreamType.LIVE_STREAM)) {
|
||||
videoCountView.setText(Localization.localizeWatchingCount(activity, info.getViewCount()));
|
||||
} else {
|
||||
videoCountView.setText(Localization.localizeViewCount(activity, info.getViewCount()));
|
||||
}
|
||||
videoCountView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
videoCountView.setVisibility(View.GONE);
|
||||
@@ -1120,9 +1135,15 @@ public class VideoDetailFragment
|
||||
videoTitleToggleArrow.setVisibility(View.VISIBLE);
|
||||
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
|
||||
videoDescriptionRootLayout.setVisibility(View.GONE);
|
||||
if (!TextUtils.isEmpty(info.getUploadDate())) {
|
||||
videoUploadDateView.setText(Localization.localizeDate(activity, info.getUploadDate()));
|
||||
|
||||
if (info.getUploadDate() != null) {
|
||||
videoUploadDateView.setText(Localization.localizeUploadDate(activity, info.getUploadDate().date().getTime()));
|
||||
videoUploadDateView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
videoUploadDateView.setText(null);
|
||||
videoUploadDateView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
prepareDescription(info.getDescription());
|
||||
updateProgressInfo(info);
|
||||
|
||||
|
||||
@@ -111,6 +111,8 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
super.startLoading(forceLoad);
|
||||
|
||||
showListFooter(false);
|
||||
infoListAdapter.clearStreamItemList();
|
||||
|
||||
currentInfo = null;
|
||||
if (currentWorker != null) currentWorker.dispose();
|
||||
currentWorker = loadResult(forceLoad)
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
@@ -98,7 +99,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if(activity != null
|
||||
if (activity != null
|
||||
&& useAsFrontPage
|
||||
&& isVisibleToUser) {
|
||||
setTitle(currentInfo != null ? currentInfo.getName() : name);
|
||||
@@ -152,7 +153,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if(useAsFrontPage && supportActionBar != null) {
|
||||
if (useAsFrontPage && supportActionBar != null) {
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(false);
|
||||
} else {
|
||||
inflater.inflate(R.menu.menu_channel, menu);
|
||||
@@ -165,7 +166,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
|
||||
private void openRssFeed() {
|
||||
final ChannelInfo info = currentInfo;
|
||||
if(info != null) {
|
||||
if (info != null) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(info.getFeedUrl()));
|
||||
startActivity(intent);
|
||||
}
|
||||
@@ -174,14 +175,21 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_settings:
|
||||
NavigationHelper.openSettings(requireContext());
|
||||
break;
|
||||
case R.id.menu_item_rss:
|
||||
openRssFeed();
|
||||
break;
|
||||
case R.id.menu_item_openInBrowser:
|
||||
ShareUtils.openUrlInBrowser(this.getContext(), currentInfo.getOriginalUrl());
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.openUrlInBrowser(requireContext(), currentInfo.getOriginalUrl());
|
||||
}
|
||||
break;
|
||||
case R.id.menu_item_share:
|
||||
ShareUtils.shareUrl(this.getContext(), name, currentInfo.getOriginalUrl());
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.shareUrl(requireContext(), name, currentInfo.getOriginalUrl());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
@@ -218,7 +226,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
.debounce(100, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe((List<SubscriptionEntity> subscriptionEntities) ->
|
||||
updateSubscribeButton(!subscriptionEntities.isEmpty())
|
||||
updateSubscribeButton(!subscriptionEntities.isEmpty())
|
||||
, onError));
|
||||
|
||||
}
|
||||
@@ -359,13 +367,13 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
|
||||
headerRootLayout.setVisibility(View.VISIBLE);
|
||||
imageLoader.displayImage(result.getBannerUrl(), headerChannelBanner,
|
||||
ImageDisplayConstants.DISPLAY_BANNER_OPTIONS);
|
||||
ImageDisplayConstants.DISPLAY_BANNER_OPTIONS);
|
||||
imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
|
||||
headerSubscribersTextView.setVisibility(View.VISIBLE);
|
||||
if (result.getSubscriberCount() >= 0) {
|
||||
headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.getSubscriberCount()));
|
||||
headerSubscribersTextView.setText(Localization.shortSubscriberCount(activity, result.getSubscriberCount()));
|
||||
} else {
|
||||
headerSubscribersTextView.setText(R.string.subscribers_count_not_available);
|
||||
}
|
||||
@@ -397,8 +405,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
|
||||
private PlayQueue getPlayQueue(final int index) {
|
||||
final List<StreamInfoItem> streamItems = new ArrayList<>();
|
||||
for(InfoItem i : infoListAdapter.getItemsList()) {
|
||||
if(i instanceof StreamInfoItem) {
|
||||
for (InfoItem i : infoListAdapter.getItemsList()) {
|
||||
if (i instanceof StreamInfoItem) {
|
||||
streamItems.add((StreamInfoItem) i);
|
||||
}
|
||||
}
|
||||
@@ -432,12 +440,16 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
protected boolean onError(Throwable exception) {
|
||||
if (super.onError(exception)) return true;
|
||||
|
||||
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error;
|
||||
onUnrecoverableError(exception,
|
||||
UserAction.REQUESTED_CHANNEL,
|
||||
NewPipe.getNameOfService(serviceId),
|
||||
url,
|
||||
errorId);
|
||||
if (exception instanceof ContentNotAvailableException) {
|
||||
showError(getString(R.string.content_not_available), false);
|
||||
} else {
|
||||
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error;
|
||||
onUnrecoverableError(exception,
|
||||
UserAction.REQUESTED_CHANNEL,
|
||||
NewPipe.getNameOfService(serviceId),
|
||||
url,
|
||||
errorId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.schabi.newpipe.fragments.list.kiosk;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.kiosk.KioskList;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
|
||||
public class DefaultKioskFragment extends KioskFragment {
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (serviceId < 0) {
|
||||
updateSelectedDefaultKiosk();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (serviceId != ServiceHelper.getSelectedServiceId(requireContext())) {
|
||||
if (currentWorker != null) currentWorker.dispose();
|
||||
updateSelectedDefaultKiosk();
|
||||
reloadContent();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSelectedDefaultKiosk() {
|
||||
try {
|
||||
serviceId = ServiceHelper.getSelectedServiceId(requireContext());
|
||||
|
||||
final KioskList kioskList = NewPipe.getService(serviceId).getKioskList();
|
||||
kioskId = kioskList.getDefaultKioskId();
|
||||
url = kioskList.getListLinkHandlerFactoryByType(kioskId).fromId(kioskId).getUrl();
|
||||
|
||||
kioskTranslatedName = KioskTranslator.getTranslatedKioskName(kioskId, requireContext());
|
||||
name = kioskTranslatedName;
|
||||
|
||||
currentInfo = null;
|
||||
currentNextPageUrl = null;
|
||||
} catch (ExtractionException e) {
|
||||
onUnrecoverableError(e, UserAction.REQUESTED_KIOSK, "none", "Loading default kiosk from selected service", 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
|
||||
import android.preference.PreferenceManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@@ -17,10 +19,12 @@ import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Single;
|
||||
@@ -52,6 +56,8 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
@State
|
||||
protected String kioskId = "";
|
||||
protected String kioskTranslatedName;
|
||||
@State
|
||||
protected ContentCountry contentCountry;
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -87,6 +93,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
|
||||
kioskTranslatedName = KioskTranslator.getTranslatedKioskName(kioskId, activity);
|
||||
name = kioskTranslatedName;
|
||||
contentCountry = Localization.getPreferredContentCountry(requireContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -108,6 +115,15 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
return inflater.inflate(R.layout.fragment_kiosk, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (!Localization.getPreferredContentCountry(requireContext()).equals(contentCountry)) {
|
||||
reloadContent();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -127,6 +143,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
|
||||
@Override
|
||||
public Single<KioskInfo> loadResult(boolean forceReload) {
|
||||
contentCountry = Localization.getPreferredContentCountry(requireContext());
|
||||
return ExtractorHelper.getKioskInfo(serviceId,
|
||||
url,
|
||||
forceReload);
|
||||
|
||||
@@ -222,11 +222,14 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_settings:
|
||||
NavigationHelper.openSettings(requireContext());
|
||||
break;
|
||||
case R.id.menu_item_openInBrowser:
|
||||
ShareUtils.openUrlInBrowser(this.getContext(), url);
|
||||
ShareUtils.openUrlInBrowser(requireContext(), url);
|
||||
break;
|
||||
case R.id.menu_item_share:
|
||||
ShareUtils.shareUrl(this.getContext(), name, url);
|
||||
ShareUtils.shareUrl(requireContext(), name, url);
|
||||
break;
|
||||
case R.id.menu_item_bookmark:
|
||||
onBookmarkClicked();
|
||||
@@ -259,7 +262,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
animateView(headerRootLayout, true, 100);
|
||||
animateView(headerUploaderLayout, true, 300);
|
||||
headerUploaderLayout.setOnClickListener(null);
|
||||
if (!TextUtils.isEmpty(result.getUploaderName())) {
|
||||
if (!TextUtils.isEmpty(result.getUploaderName())) { // If we have an uploader : Put them into the ui
|
||||
headerUploaderName.setText(result.getUploaderName());
|
||||
if (!TextUtils.isEmpty(result.getUploaderUrl())) {
|
||||
headerUploaderLayout.setOnClickListener(v -> {
|
||||
@@ -273,6 +276,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
}
|
||||
});
|
||||
}
|
||||
} else { // Else : say we have no uploader
|
||||
headerUploaderName.setText(R.string.playlist_no_uploader);
|
||||
}
|
||||
|
||||
playlistCtrl.setVisibility(View.VISIBLE);
|
||||
@@ -444,4 +449,4 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
playlistBookmarkButton.setIcon(ThemeHelper.resolveResourceIdFromAttr(activity, iconAttr));
|
||||
playlistBookmarkButton.setTitle(titleRes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ public class SearchFragment
|
||||
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
isSuggestionsEnabled = preferences.getBoolean(getString(R.string.show_search_suggestions_key), true);
|
||||
contentCountry = preferences.getString(getString(R.string.content_country_key), getString(R.string.default_country_value));
|
||||
contentCountry = preferences.getString(getString(R.string.content_country_key), getString(R.string.default_localization_key));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -723,7 +723,7 @@ public class SearchFragment
|
||||
showError(getString(R.string.url_not_supported_toast), false)));
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
} catch (Exception ignored) {
|
||||
// Exception occurred, it's not a url
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.text.util.Linkify;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import org.jsoup.helper.StringUtil;
|
||||
import org.schabi.newpipe.R;
|
||||
@@ -14,6 +20,7 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.util.CommentTextOnTouchListener;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
@@ -101,10 +108,17 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
ellipsize();
|
||||
}
|
||||
|
||||
if (null != item.getLikeCount()) {
|
||||
if (item.getLikeCount() >= 0) {
|
||||
itemLikesCountView.setText(String.valueOf(item.getLikeCount()));
|
||||
} else {
|
||||
itemLikesCountView.setText("-");
|
||||
}
|
||||
|
||||
if (item.getPublishedTime() != null) {
|
||||
itemPublishedTime.setText(Localization.relativeTime(item.getPublishedTime().date()));
|
||||
} else {
|
||||
itemPublishedTime.setText(item.getTextualPublishedTime());
|
||||
}
|
||||
itemPublishedTime.setText(item.getPublishedTime());
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
toggleEllipsize();
|
||||
@@ -112,6 +126,21 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
itemBuilder.getOnCommentsSelectedListener().selected(item);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View view) {
|
||||
|
||||
ClipboardManager clipboardManager = (ClipboardManager) itemBuilder.getContext()
|
||||
.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText(null,commentText));
|
||||
Toast.makeText(itemBuilder.getContext(), R.string.msg_copied, Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private void ellipsize() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.TextUtils;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
@@ -7,10 +8,13 @@ import android.widget.TextView;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 01.08.16.
|
||||
* <p>
|
||||
@@ -53,15 +57,38 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
|
||||
private String getStreamInfoDetailLine(final StreamInfoItem infoItem) {
|
||||
String viewsAndDate = "";
|
||||
if (infoItem.getViewCount() >= 0) {
|
||||
viewsAndDate = Localization.shortViewCount(itemBuilder.getContext(), infoItem.getViewCount());
|
||||
}
|
||||
if (!TextUtils.isEmpty(infoItem.getUploadDate())) {
|
||||
if (viewsAndDate.isEmpty()) {
|
||||
viewsAndDate = infoItem.getUploadDate();
|
||||
if (infoItem.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
|
||||
viewsAndDate = Localization.listeningCount(itemBuilder.getContext(), infoItem.getViewCount());
|
||||
} else if (infoItem.getStreamType().equals(StreamType.LIVE_STREAM)) {
|
||||
viewsAndDate = Localization.shortWatchingCount(itemBuilder.getContext(), infoItem.getViewCount());
|
||||
} else {
|
||||
viewsAndDate += " • " + infoItem.getUploadDate();
|
||||
viewsAndDate = Localization.shortViewCount(itemBuilder.getContext(), infoItem.getViewCount());
|
||||
}
|
||||
}
|
||||
|
||||
final String uploadDate = getFormattedRelativeUploadDate(infoItem);
|
||||
if (!TextUtils.isEmpty(uploadDate)) {
|
||||
if (viewsAndDate.isEmpty()) {
|
||||
return uploadDate;
|
||||
}
|
||||
|
||||
return Localization.concatenateStrings(viewsAndDate, uploadDate);
|
||||
}
|
||||
|
||||
return viewsAndDate;
|
||||
}
|
||||
|
||||
private String getFormattedRelativeUploadDate(final StreamInfoItem infoItem) {
|
||||
if (infoItem.getUploadDate() != null) {
|
||||
String formattedRelativeTime = Localization.relativeTime(infoItem.getUploadDate().date());
|
||||
|
||||
if (DEBUG && PreferenceManager.getDefaultSharedPreferences(itemBuilder.getContext())
|
||||
.getBoolean(itemBuilder.getContext().getString(R.string.show_original_time_ago_key), false)) {
|
||||
formattedRelativeTime += " (" + infoItem.getTextualUploadDate() + ")";
|
||||
}
|
||||
return formattedRelativeTime;
|
||||
} else {
|
||||
return infoItem.getTextualUploadDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package org.schabi.newpipe.local.bookmark;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.AlertDialog.Builder;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.util.Log;
|
||||
import android.widget.EditText;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
@@ -10,6 +13,7 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
@@ -118,8 +122,7 @@ public final class BookmarkFragment
|
||||
@Override
|
||||
public void held(LocalItem selectedItem) {
|
||||
if (selectedItem instanceof PlaylistMetadataEntry) {
|
||||
showLocalDeleteDialog((PlaylistMetadataEntry) selectedItem);
|
||||
|
||||
showLocalDialog((PlaylistMetadataEntry) selectedItem);
|
||||
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
||||
showRemoteDeleteDialog((PlaylistRemoteEntity) selectedItem);
|
||||
}
|
||||
@@ -247,14 +250,30 @@ public final class BookmarkFragment
|
||||
// Utils
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void showLocalDeleteDialog(final PlaylistMetadataEntry item) {
|
||||
showDeleteDialog(item.name, localPlaylistManager.deletePlaylist(item.uid));
|
||||
}
|
||||
|
||||
private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) {
|
||||
showDeleteDialog(item.getName(), remotePlaylistManager.deletePlaylist(item.getUid()));
|
||||
}
|
||||
|
||||
private void showLocalDialog(PlaylistMetadataEntry selectedItem) {
|
||||
View dialogView = View.inflate(getContext(), R.layout.dialog_bookmark, null);
|
||||
EditText editText = dialogView.findViewById(R.id.playlist_name_edit_text);
|
||||
editText.setText(selectedItem.name);
|
||||
|
||||
Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setView(dialogView)
|
||||
.setPositiveButton(R.string.rename_playlist, (dialog, which) -> {
|
||||
changeLocalPlaylistName(selectedItem.uid, editText.getText().toString());
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setNeutralButton(R.string.delete, (dialog, which) -> {
|
||||
showDeleteDialog(selectedItem.name,
|
||||
localPlaylistManager.deletePlaylist(selectedItem.uid));
|
||||
dialog.dismiss();
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showDeleteDialog(final String name, final Single<Integer> deleteReactor) {
|
||||
if (activity == null || disposables == null) return;
|
||||
|
||||
@@ -271,6 +290,23 @@ public final class BookmarkFragment
|
||||
.show();
|
||||
}
|
||||
|
||||
private void changeLocalPlaylistName(long id, String name) {
|
||||
if (localPlaylistManager == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Updating playlist id=[" + id +
|
||||
"] with new name=[" + name + "] items");
|
||||
}
|
||||
|
||||
localPlaylistManager.renamePlaylist(id, name);
|
||||
final Disposable disposable = localPlaylistManager.renamePlaylist(id, name)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(longs -> {/*Do nothing on success*/}, this::onError);
|
||||
disposables.add(disposable);
|
||||
}
|
||||
|
||||
private static List<PlaylistLocalItem> merge(final List<PlaylistMetadataEntry> localPlaylists,
|
||||
final List<PlaylistRemoteEntity> remotePlaylists) {
|
||||
List<PlaylistLocalItem> items = new ArrayList<>(
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
package org.schabi.newpipe.local.dialog;
|
||||
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
@@ -152,6 +153,12 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
final Toast successToast = Toast.makeText(getContext(),
|
||||
R.string.playlist_add_stream_success, Toast.LENGTH_SHORT);
|
||||
|
||||
if (playlist.thumbnailUrl.equals("drawable://" + R.drawable.dummy_thumbnail_playlist)) {
|
||||
playlistDisposables.add(manager.changePlaylistThumbnail(playlist.uid, streams.get(0).getThumbnailUrl())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(ignored -> successToast.show()));
|
||||
}
|
||||
|
||||
playlistDisposables.add(manager.appendToPlaylist(playlist.uid, streams)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(ignored -> successToast.show()));
|
||||
|
||||
@@ -10,6 +10,8 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.text.DateFormat;
|
||||
|
||||
public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
||||
@@ -28,8 +30,14 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
||||
|
||||
itemTitleView.setText(item.getName());
|
||||
itemStreamCountView.setText(String.valueOf(item.getStreamCount()));
|
||||
itemUploaderView.setText(Localization.concatenateStrings(item.getUploader(),
|
||||
// Here is where the uploader name is set in the bookmarked playlists library
|
||||
if (!TextUtils.isEmpty(item.getUploader())) {
|
||||
itemUploaderView.setText(Localization.concatenateStrings(item.getUploader(),
|
||||
NewPipe.getNameOfService(item.getServiceId())));
|
||||
} else {
|
||||
itemUploaderView.setText(NewPipe.getNameOfService(item.getServiceId()));
|
||||
}
|
||||
|
||||
|
||||
itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
|
||||
|
||||
@@ -4,11 +4,6 @@ import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -18,6 +13,12 @@ import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
@@ -325,6 +326,16 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
headerBackgroundButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
|
||||
headerPopupButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
|
||||
return true;
|
||||
});
|
||||
|
||||
headerBackgroundButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
|
||||
return true;
|
||||
});
|
||||
|
||||
hideLoading();
|
||||
}
|
||||
|
||||
@@ -377,8 +388,10 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
this.name = name;
|
||||
setTitle(name);
|
||||
|
||||
Log.d(TAG, "Updating playlist id=[" + playlistId +
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Updating playlist id=[" + playlistId +
|
||||
"] with new name=[" + name + "] items");
|
||||
}
|
||||
|
||||
final Disposable disposable = playlistManager.renamePlaylist(playlistId, name)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
@@ -393,8 +406,10 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
R.string.playlist_thumbnail_change_success,
|
||||
Toast.LENGTH_SHORT);
|
||||
|
||||
Log.d(TAG, "Updating playlist id=[" + playlistId +
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Updating playlist id=[" + playlistId +
|
||||
"] with new thumbnail url=[" + thumbnailUrl + "]");
|
||||
}
|
||||
|
||||
final Disposable disposable = playlistManager
|
||||
.changePlaylistThumbnail(playlistId, thumbnailUrl)
|
||||
@@ -403,10 +418,25 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
disposables.add(disposable);
|
||||
}
|
||||
|
||||
private void updateThumbnailUrl() {
|
||||
String newThumbnailUrl;
|
||||
|
||||
if (!itemListAdapter.getItemsList().isEmpty()) {
|
||||
newThumbnailUrl = ((PlaylistStreamEntry) itemListAdapter.getItemsList().get(0)).thumbnailUrl;
|
||||
} else {
|
||||
newThumbnailUrl = "drawable://" + R.drawable.dummy_thumbnail_playlist;
|
||||
}
|
||||
|
||||
changeThumbnailUrl(newThumbnailUrl);
|
||||
}
|
||||
|
||||
private void deleteItem(final PlaylistStreamEntry item) {
|
||||
if (itemListAdapter == null) return;
|
||||
|
||||
itemListAdapter.removeItem(item);
|
||||
if (playlistManager.getPlaylistThumbnail(playlistId).equals(item.thumbnailUrl))
|
||||
updateThumbnailUrl();
|
||||
|
||||
setVideoCount(itemListAdapter.getItemsList().size());
|
||||
saveChanges();
|
||||
}
|
||||
@@ -446,8 +476,10 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "Updating playlist id=[" + playlistId +
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Updating playlist id=[" + playlistId +
|
||||
"] with [" + streamIds.size() + "] items");
|
||||
}
|
||||
|
||||
final Disposable disposable = playlistManager.updateJoin(playlistId, streamIds)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
@@ -103,6 +103,10 @@ public class LocalPlaylistManager {
|
||||
return modifyPlaylist(playlistId, null, thumbnailUrl);
|
||||
}
|
||||
|
||||
public String getPlaylistThumbnail(final long playlistId) {
|
||||
return playlistTable.getPlaylist(playlistId).blockingFirst().get(0).getThumbnailUrl();
|
||||
}
|
||||
|
||||
private Maybe<Integer> modifyPlaylist(final long playlistId,
|
||||
@Nullable final String name,
|
||||
@Nullable final String thumbnailUrl) {
|
||||
|
||||
@@ -15,6 +15,8 @@ import org.schabi.newpipe.util.ThemeHelper;
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class ImportConfirmationDialog extends DialogFragment {
|
||||
@State
|
||||
protected Intent resultServiceIntent;
|
||||
@@ -34,11 +36,12 @@ public class ImportConfirmationDialog extends DialogFragment {
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
assureCorrectAppLanguage(getContext());
|
||||
return new AlertDialog.Builder(getContext(), ThemeHelper.getDialogTheme(getContext()))
|
||||
.setMessage(R.string.import_network_expensive_warning)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
|
||||
.setPositiveButton(R.string.finish, (dialogInterface, i) -> {
|
||||
if (resultServiceIntent != null && getContext() != null) {
|
||||
getContext().startService(resultServiceIntent);
|
||||
}
|
||||
|
||||
@@ -25,12 +25,17 @@ import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.RemoteViews;
|
||||
@@ -48,11 +53,12 @@ import org.schabi.newpipe.player.helper.LockManager;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
|
||||
import org.schabi.newpipe.player.resolver.MediaSourceTag;
|
||||
import org.schabi.newpipe.util.BitmapUtils;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
/**
|
||||
* Base players joining the common properties
|
||||
@@ -75,6 +81,7 @@ public final class BackgroundPlayer extends Service {
|
||||
|
||||
private BasePlayerImpl basePlayerImpl;
|
||||
private LockManager lockManager;
|
||||
private SharedPreferences sharedPreferences;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Service-Activity Binder
|
||||
@@ -95,6 +102,9 @@ public final class BackgroundPlayer extends Service {
|
||||
|
||||
private boolean shouldUpdateOnProgress;
|
||||
|
||||
private static final int NOTIFICATION_UPDATES_BEFORE_RESET = 60;
|
||||
private int timesNotificationUpdated;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Service's LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -104,7 +114,8 @@ public final class BackgroundPlayer extends Service {
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called");
|
||||
notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE));
|
||||
lockManager = new LockManager(this);
|
||||
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
|
||||
assureCorrectAppLanguage(this);
|
||||
ThemeHelper.setTheme(this);
|
||||
basePlayerImpl = new BasePlayerImpl(this);
|
||||
basePlayerImpl.setup();
|
||||
@@ -180,6 +191,7 @@ public final class BackgroundPlayer extends Service {
|
||||
|
||||
private void resetNotification() {
|
||||
notBuilder = createNotification();
|
||||
timesNotificationUpdated = 0;
|
||||
}
|
||||
|
||||
private NotificationCompat.Builder createNotification() {
|
||||
@@ -195,12 +207,45 @@ public final class BackgroundPlayer extends Service {
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setCustomContentView(notRemoteView)
|
||||
.setCustomBigContentView(bigNotRemoteView);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
setLockScreenThumbnail(builder);
|
||||
}
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
|
||||
builder.setPriority(NotificationCompat.PRIORITY_MAX);
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
private void setLockScreenThumbnail(NotificationCompat.Builder builder) {
|
||||
boolean isLockScreenThumbnailEnabled = sharedPreferences.getBoolean(
|
||||
getString(R.string.enable_lock_screen_video_thumbnail_key),
|
||||
true
|
||||
);
|
||||
|
||||
if (isLockScreenThumbnailEnabled) {
|
||||
basePlayerImpl.mediaSessionManager.setLockScreenArt(
|
||||
builder,
|
||||
getCenteredThumbnailBitmap()
|
||||
);
|
||||
} else {
|
||||
basePlayerImpl.mediaSessionManager.clearLockScreenArt(builder);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Bitmap getCenteredThumbnailBitmap() {
|
||||
int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
|
||||
int screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels;
|
||||
|
||||
return BitmapUtils.centerCrop(
|
||||
basePlayerImpl.getThumbnail(),
|
||||
screenWidth,
|
||||
screenHeight);
|
||||
}
|
||||
|
||||
private void setupNotification(RemoteViews remoteViews) {
|
||||
if (basePlayerImpl == null) return;
|
||||
|
||||
@@ -248,10 +293,13 @@ public final class BackgroundPlayer extends Service {
|
||||
//if (DEBUG) Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]");
|
||||
if (notBuilder == null) return;
|
||||
if (drawableId != -1) {
|
||||
if (notRemoteView != null) notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
|
||||
if (notRemoteView != null)
|
||||
notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
|
||||
if (bigNotRemoteView != null)
|
||||
bigNotRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
|
||||
}
|
||||
notificationManager.notify(NOTIFICATION_ID, notBuilder.build());
|
||||
timesNotificationUpdated++;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -275,7 +323,8 @@ public final class BackgroundPlayer extends Service {
|
||||
|
||||
protected class BasePlayerImpl extends BasePlayer {
|
||||
|
||||
@NonNull final private AudioPlaybackResolver resolver;
|
||||
@NonNull
|
||||
final private AudioPlaybackResolver resolver;
|
||||
private int cachedDuration;
|
||||
private String cachedDurationString;
|
||||
|
||||
@@ -294,8 +343,10 @@ public final class BackgroundPlayer extends Service {
|
||||
super.handleIntent(intent);
|
||||
|
||||
resetNotification();
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false);
|
||||
if (notRemoteView != null) notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false);
|
||||
if (bigNotRemoteView != null)
|
||||
bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false);
|
||||
if (notRemoteView != null)
|
||||
notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false);
|
||||
startForeground(NOTIFICATION_ID, notBuilder.build());
|
||||
}
|
||||
|
||||
@@ -330,6 +381,7 @@ public final class BackgroundPlayer extends Service {
|
||||
updateNotificationThumbnail();
|
||||
updateNotification(-1);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// States Implementation
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -351,10 +403,15 @@ public final class BackgroundPlayer extends Service {
|
||||
updateProgress(currentProgress, duration, bufferPercent);
|
||||
|
||||
if (!shouldUpdateOnProgress) return;
|
||||
resetNotification();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O /*Oreo*/) updateNotificationThumbnail();
|
||||
if (timesNotificationUpdated > NOTIFICATION_UPDATES_BEFORE_RESET) {
|
||||
resetNotification();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O /*Oreo*/) {
|
||||
updateNotificationThumbnail();
|
||||
}
|
||||
}
|
||||
if (bigNotRemoteView != null) {
|
||||
if(cachedDuration != duration) {
|
||||
if (cachedDuration != duration) {
|
||||
cachedDuration = duration;
|
||||
cachedDurationString = getTimeString(duration);
|
||||
}
|
||||
@@ -382,8 +439,10 @@ public final class BackgroundPlayer extends Service {
|
||||
@Override
|
||||
public void destroy() {
|
||||
super.destroy();
|
||||
if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, null);
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, null);
|
||||
if (notRemoteView != null)
|
||||
notRemoteView.setImageViewBitmap(R.id.notificationCover, null);
|
||||
if (bigNotRemoteView != null)
|
||||
bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, null);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -57,7 +57,10 @@ public final class BackgroundPlayerActivity extends ServicePlayerActivity {
|
||||
|
||||
this.player.setRecovery();
|
||||
getApplicationContext().sendBroadcast(getPlayerShutdownIntent());
|
||||
getApplicationContext().startService(getSwitchIntent(PopupVideoPlayer.class));
|
||||
getApplicationContext().startService(
|
||||
getSwitchIntent(PopupVideoPlayer.class)
|
||||
.putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying())
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -55,7 +55,7 @@ import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.Downloader;
|
||||
import org.schabi.newpipe.DownloaderImpl;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
@@ -150,6 +150,8 @@ public abstract class BasePlayer implements
|
||||
@NonNull
|
||||
public static final String RESUME_PLAYBACK = "resume_playback";
|
||||
@NonNull
|
||||
public static final String START_PAUSED = "start_paused";
|
||||
@NonNull
|
||||
public static final String SELECT_ON_APPEND = "select_on_append";
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -178,7 +180,6 @@ public abstract class BasePlayer implements
|
||||
// Player
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected final static int FAST_FORWARD_REWIND_AMOUNT_MILLIS = 10000; // 10 Seconds
|
||||
protected final static int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds
|
||||
protected final static int PROGRESS_LOOP_INTERVAL_MILLIS = 500;
|
||||
protected final static int RECOVERY_SKIP_THRESHOLD_MILLIS = 3000; // 3 seconds
|
||||
@@ -209,7 +210,7 @@ public abstract class BasePlayer implements
|
||||
this.progressUpdateReactor = new SerialDisposable();
|
||||
this.databaseUpdateReactor = new CompositeDisposable();
|
||||
|
||||
final String userAgent = Downloader.USER_AGENT;
|
||||
final String userAgent = DownloaderImpl.USER_AGENT;
|
||||
final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter.Builder(context).build();
|
||||
this.dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter);
|
||||
|
||||
@@ -305,7 +306,7 @@ public abstract class BasePlayer implements
|
||||
}
|
||||
// Good to go...
|
||||
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence,
|
||||
/*playOnInit=*/true);
|
||||
/*playOnInit=*/!intent.getBooleanExtra(START_PAUSED, false));
|
||||
}
|
||||
|
||||
protected void initPlayback(@NonNull final PlayQueue queue,
|
||||
@@ -945,21 +946,28 @@ public abstract class BasePlayer implements
|
||||
public void onPlayPause() {
|
||||
if (DEBUG) Log.d(TAG, "onPlayPause() called");
|
||||
|
||||
if (!isPlaying()) {
|
||||
onPlay();
|
||||
} else {
|
||||
if (isPlaying()) {
|
||||
onPause();
|
||||
} else {
|
||||
onPlay();
|
||||
}
|
||||
}
|
||||
|
||||
public void onFastRewind() {
|
||||
if (DEBUG) Log.d(TAG, "onFastRewind() called");
|
||||
seekBy(-FAST_FORWARD_REWIND_AMOUNT_MILLIS);
|
||||
seekBy(-getSeekDuration());
|
||||
}
|
||||
|
||||
public void onFastForward() {
|
||||
if (DEBUG) Log.d(TAG, "onFastForward() called");
|
||||
seekBy(FAST_FORWARD_REWIND_AMOUNT_MILLIS);
|
||||
seekBy(getSeekDuration());
|
||||
}
|
||||
|
||||
private int getSeekDuration() {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final String key = context.getString(R.string.seek_duration_key);
|
||||
final String value = prefs.getString(key, context.getString(R.string.seek_duration_default_value));
|
||||
return Integer.parseInt(value);
|
||||
}
|
||||
|
||||
public void onPlayPrevious() {
|
||||
|
||||
@@ -28,6 +28,7 @@ import android.database.ContentObserver;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.media.AudioManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
@@ -44,9 +45,11 @@ import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.DisplayCutout;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
@@ -75,6 +78,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback;
|
||||
import org.schabi.newpipe.player.resolver.MediaSourceTag;
|
||||
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.KoreUtil;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
@@ -93,6 +97,7 @@ import static org.schabi.newpipe.util.AnimationUtils.Type.SCALE_AND_ALPHA;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateRotation;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE;
|
||||
|
||||
/**
|
||||
@@ -123,6 +128,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
assureCorrectAppLanguage(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
@@ -190,6 +196,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
@Override
|
||||
protected void onResume() {
|
||||
if (DEBUG) Log.d(TAG, "onResume() called");
|
||||
assureCorrectAppLanguage(this);
|
||||
super.onResume();
|
||||
|
||||
if (globalScreenOrientationLocked()) {
|
||||
@@ -220,6 +227,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
assureCorrectAppLanguage(this);
|
||||
|
||||
if (playerImpl.isSomePopupMenuVisible()) {
|
||||
playerImpl.getQualityPopupMenu().dismiss();
|
||||
@@ -364,8 +372,8 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
}
|
||||
|
||||
private boolean globalScreenOrientationLocked() {
|
||||
// 1: Screen orientation changes using acelerometer
|
||||
// 0: Screen orientatino is locked
|
||||
// 1: Screen orientation changes using accelerometer
|
||||
// 0: Screen orientation is locked
|
||||
return !(android.provider.Settings.System.getInt(getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) == 1);
|
||||
}
|
||||
|
||||
@@ -435,6 +443,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
private boolean queueVisible;
|
||||
|
||||
private ImageButton moreOptionsButton;
|
||||
private ImageButton kodiButton;
|
||||
private ImageButton shareButton;
|
||||
private ImageButton toggleOrientationButton;
|
||||
private ImageButton switchPopupButton;
|
||||
@@ -471,6 +480,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
|
||||
this.moreOptionsButton = rootView.findViewById(R.id.moreOptionsButton);
|
||||
this.secondaryControls = rootView.findViewById(R.id.secondaryControls);
|
||||
this.kodiButton = rootView.findViewById(R.id.kodi);
|
||||
this.shareButton = rootView.findViewById(R.id.share);
|
||||
this.toggleOrientationButton = rootView.findViewById(R.id.toggleOrientation);
|
||||
this.switchBackgroundButton = rootView.findViewById(R.id.switchBackground);
|
||||
@@ -482,6 +492,9 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
|
||||
titleTextView.setSelected(true);
|
||||
channelTextView.setSelected(true);
|
||||
boolean showKodiButton = PreferenceManager.getDefaultSharedPreferences(this.context).getBoolean(
|
||||
this.context.getString(R.string.show_play_with_kodi_key), false);
|
||||
kodiButton.setVisibility(showKodiButton ? View.VISIBLE : View.GONE);
|
||||
|
||||
getRootView().setKeepScreenOn(true);
|
||||
}
|
||||
@@ -518,6 +531,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
closeButton.setOnClickListener(this);
|
||||
|
||||
moreOptionsButton.setOnClickListener(this);
|
||||
kodiButton.setOnClickListener(this);
|
||||
shareButton.setOnClickListener(this);
|
||||
toggleOrientationButton.setOnClickListener(this);
|
||||
switchBackgroundButton.setOnClickListener(this);
|
||||
@@ -538,6 +552,19 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
setInitialGestureValues();
|
||||
}
|
||||
});
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
queueLayout.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
|
||||
@Override
|
||||
public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
|
||||
final DisplayCutout cutout = windowInsets.getDisplayCutout();
|
||||
if (cutout != null)
|
||||
view.setPadding(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(),
|
||||
cutout.getSafeInsetRight(), cutout.getSafeInsetBottom());
|
||||
return windowInsets;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void minimize() {
|
||||
@@ -588,6 +615,17 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
finish();
|
||||
}
|
||||
|
||||
public void onKodiShare() {
|
||||
onPause();
|
||||
try {
|
||||
NavigationHelper.playWithKore(this.context, Uri.parse(
|
||||
playerImpl.getVideoUrl().replace("https", "http")));
|
||||
} catch (Exception e) {
|
||||
if (DEBUG) Log.i(TAG, "Failed to start kore", e);
|
||||
KoreUtil.showInstallKoreDialog(this.context);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Player Overrides
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -614,7 +652,8 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
this.getPlaybackPitch(),
|
||||
this.getPlaybackSkipSilence(),
|
||||
this.getPlaybackQuality(),
|
||||
false
|
||||
false,
|
||||
!isPlaying()
|
||||
);
|
||||
context.startService(intent);
|
||||
|
||||
@@ -637,7 +676,8 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
this.getPlaybackPitch(),
|
||||
this.getPlaybackSkipSilence(),
|
||||
this.getPlaybackQuality(),
|
||||
false
|
||||
false,
|
||||
!isPlaying()
|
||||
);
|
||||
context.startService(intent);
|
||||
|
||||
@@ -686,6 +726,8 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
} else if (v.getId() == closeButton.getId()) {
|
||||
onPlaybackShutdown();
|
||||
return;
|
||||
} else if (v.getId() == kodiButton.getId()) {
|
||||
onKodiShare();
|
||||
}
|
||||
|
||||
if (getCurrentState() != STATE_COMPLETED) {
|
||||
@@ -882,6 +924,18 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
final float currentVolumeNormalized = (float) getAudioReactor().getVolume() / getAudioReactor().getMaxVolume();
|
||||
volumeProgressBar.setProgress((int) (volumeProgressBar.getMax() * currentVolumeNormalized));
|
||||
}
|
||||
|
||||
float screenBrightness = getWindow().getAttributes().screenBrightness;
|
||||
if (screenBrightness < 0)
|
||||
screenBrightness = Settings.System.getInt(getContentResolver(),
|
||||
Settings.System.SCREEN_BRIGHTNESS, 0) / 255.0f;
|
||||
|
||||
brightnessProgressBar.setProgress((int) (brightnessProgressBar.getMax() * screenBrightness));
|
||||
|
||||
if (DEBUG) Log.d(TAG, "setInitialGestureValues: volumeProgressBar.getProgress() ["
|
||||
+ volumeProgressBar.getProgress() + "] "
|
||||
+ "brightnessProgressBar.getProgress() ["
|
||||
+ brightnessProgressBar.getProgress() + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -80,6 +80,7 @@ import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING;
|
||||
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION;
|
||||
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
/**
|
||||
* Service Popup Player implementing VideoPlayer
|
||||
@@ -142,6 +143,7 @@ public final class PopupVideoPlayer extends Service {
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
assureCorrectAppLanguage(this);
|
||||
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
|
||||
notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE));
|
||||
|
||||
@@ -169,6 +171,7 @@ public final class PopupVideoPlayer extends Service {
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
assureCorrectAppLanguage(this);
|
||||
if (DEBUG) Log.d(TAG, "onConfigurationChanged() called with: newConfig = [" + newConfig + "]");
|
||||
updateScreenSize();
|
||||
updatePopupSize(popupLayoutParams.width, -1);
|
||||
@@ -567,7 +570,8 @@ public final class PopupVideoPlayer extends Service {
|
||||
this.getPlaybackPitch(),
|
||||
this.getPlaybackSkipSilence(),
|
||||
this.getPlaybackQuality(),
|
||||
false
|
||||
false,
|
||||
!isPlaying()
|
||||
);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
@@ -1036,7 +1040,7 @@ public final class PopupVideoPlayer extends Service {
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
popupGestureDetector.onTouchEvent(event);
|
||||
if (playerImpl == null) return false;
|
||||
if (event.getPointerCount() == 2 && !isResizing) {
|
||||
if (event.getPointerCount() == 2 && !isMoving && !isResizing) {
|
||||
if (DEBUG) Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.");
|
||||
playerImpl.showAndAnimateControl(-1, true);
|
||||
playerImpl.getLoadingPanel().setVisibility(View.GONE);
|
||||
@@ -1123,4 +1127,4 @@ public final class PopupVideoPlayer extends Service {
|
||||
return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,10 @@ public final class PopupVideoPlayerActivity extends ServicePlayerActivity {
|
||||
if (item.getItemId() == R.id.action_switch_background) {
|
||||
this.player.setRecovery();
|
||||
getApplicationContext().sendBroadcast(getPlayerShutdownIntent());
|
||||
getApplicationContext().startService(getSwitchIntent(BackgroundPlayer.class));
|
||||
getApplicationContext().startService(
|
||||
getSwitchIntent(BackgroundPlayer.class)
|
||||
.putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying())
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -46,6 +46,7 @@ import java.util.List;
|
||||
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.formatPitch;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
|
||||
@@ -116,6 +117,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
assureCorrectAppLanguage(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.setTheme(this);
|
||||
setContentView(R.layout.activity_player_queue_control);
|
||||
@@ -154,12 +156,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
return true;
|
||||
case R.id.action_append_playlist:
|
||||
appendAllToPlaylist();
|
||||
return true;
|
||||
case R.id.action_settings:
|
||||
NavigationHelper.openSettings(this);
|
||||
redraw = true;
|
||||
return true;
|
||||
case R.id.action_append_playlist:
|
||||
appendAllToPlaylist();
|
||||
return true;
|
||||
case R.id.action_system_audio:
|
||||
startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
|
||||
@@ -167,7 +168,10 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
case R.id.action_switch_main:
|
||||
this.player.setRecovery();
|
||||
getApplicationContext().sendBroadcast(getPlayerShutdownIntent());
|
||||
getApplicationContext().startActivity(getSwitchIntent(MainVideoPlayer.class));
|
||||
getApplicationContext().startActivity(
|
||||
getSwitchIntent(MainVideoPlayer.class)
|
||||
.putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying())
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return onPlayerOptionSelected(item) || super.onOptionsItemSelected(item);
|
||||
@@ -189,8 +193,10 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
this.player.getPlaybackPitch(),
|
||||
this.player.getPlaybackSkipSilence(),
|
||||
null,
|
||||
false,
|
||||
false
|
||||
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying());
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -26,14 +26,13 @@ import android.animation.PropertyValuesHolder;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
@@ -45,6 +44,10 @@ import android.widget.ProgressBar;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
@@ -285,6 +288,17 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
if (captionPopupMenu == null) return;
|
||||
captionPopupMenu.getMenu().removeGroup(captionPopupMenuGroupId);
|
||||
|
||||
String userPreferredLanguage = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getString(context.getString(R.string.caption_user_set_key), null);
|
||||
/*
|
||||
* only search for autogenerated cc as fallback
|
||||
* if "(auto-generated)" was not already selected
|
||||
* we are only looking for "(" instead of "(auto-generated)" to hopefully get all
|
||||
* internationalized variants such as "(automatisch-erzeugt)" and so on
|
||||
*/
|
||||
boolean searchForAutogenerated = userPreferredLanguage != null &&
|
||||
!userPreferredLanguage.contains("(");
|
||||
|
||||
// Add option for turning off caption
|
||||
MenuItem captionOffItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId,
|
||||
0, Menu.NONE, R.string.caption_none);
|
||||
@@ -294,6 +308,8 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
trackSelector.setParameters(trackSelector.buildUponParameters()
|
||||
.setRendererDisabled(textRendererIndex, true));
|
||||
}
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
prefs.edit().remove(context.getString(R.string.caption_user_set_key)).commit();
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -308,9 +324,26 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
trackSelector.setPreferredTextLanguage(captionLanguage);
|
||||
trackSelector.setParameters(trackSelector.buildUponParameters()
|
||||
.setRendererDisabled(textRendererIndex, false));
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
prefs.edit().putString(context.getString(R.string.caption_user_set_key),
|
||||
captionLanguage).commit();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// apply caption language from previous user preference
|
||||
if (userPreferredLanguage != null && (captionLanguage.equals(userPreferredLanguage) ||
|
||||
searchForAutogenerated && captionLanguage.startsWith(userPreferredLanguage) ||
|
||||
userPreferredLanguage.contains("(") &&
|
||||
captionLanguage.startsWith(userPreferredLanguage.substring(0,
|
||||
userPreferredLanguage.indexOf('('))))) {
|
||||
final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT);
|
||||
if (textRendererIndex != RENDERER_UNAVAILABLE) {
|
||||
trackSelector.setPreferredTextLanguage(captionLanguage);
|
||||
trackSelector.setParameters(trackSelector.buildUponParameters()
|
||||
.setRendererDisabled(textRendererIndex, false));
|
||||
}
|
||||
searchForAutogenerated = false;
|
||||
}
|
||||
}
|
||||
captionPopupMenu.setOnDismissListener(this);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,19 @@ package org.schabi.newpipe.player.helper;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.media.MediaMetadata;
|
||||
import android.os.Build;
|
||||
import android.support.v4.media.MediaMetadataCompat;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
import android.view.KeyEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.media.session.MediaButtonReceiver;
|
||||
import androidx.media.app.NotificationCompat.MediaStyle;
|
||||
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
||||
@@ -19,8 +26,10 @@ import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController;
|
||||
public class MediaSessionManager {
|
||||
private static final String TAG = "MediaSessionManager";
|
||||
|
||||
@NonNull private final MediaSessionCompat mediaSession;
|
||||
@NonNull private final MediaSessionConnector sessionConnector;
|
||||
@NonNull
|
||||
private final MediaSessionCompat mediaSession;
|
||||
@NonNull
|
||||
private final MediaSessionConnector sessionConnector;
|
||||
|
||||
public MediaSessionManager(@NonNull final Context context,
|
||||
@NonNull final Player player,
|
||||
@@ -40,13 +49,45 @@ public class MediaSessionManager {
|
||||
return MediaButtonReceiver.handleIntent(mediaSession, intent);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public void setLockScreenArt(NotificationCompat.Builder builder, @Nullable Bitmap thumbnailBitmap) {
|
||||
if (thumbnailBitmap == null || !mediaSession.isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
mediaSession.setMetadata(
|
||||
new MediaMetadataCompat.Builder()
|
||||
.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, thumbnailBitmap)
|
||||
.build()
|
||||
);
|
||||
|
||||
MediaStyle mediaStyle = new MediaStyle()
|
||||
.setMediaSession(mediaSession.getSessionToken());
|
||||
|
||||
builder.setStyle(mediaStyle);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public void clearLockScreenArt(NotificationCompat.Builder builder) {
|
||||
mediaSession.setMetadata(
|
||||
new MediaMetadataCompat.Builder()
|
||||
.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, null)
|
||||
.build()
|
||||
);
|
||||
|
||||
MediaStyle mediaStyle = new MediaStyle()
|
||||
.setMediaSession(mediaSession.getSessionToken());
|
||||
|
||||
builder.setStyle(mediaStyle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called on player destruction to prevent leakage.
|
||||
* */
|
||||
*/
|
||||
public void dispose() {
|
||||
this.sessionConnector.setPlayer(null);
|
||||
this.sessionConnector.setQueueNavigator(null);
|
||||
this.mediaSession.setActive(false);
|
||||
this.mediaSession.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
@@ -17,6 +19,7 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.SliderStrategy;
|
||||
|
||||
import static org.schabi.newpipe.player.BasePlayer.DEBUG;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class PlaybackParameterDialog extends DialogFragment {
|
||||
@NonNull private static final String TAG = "PlaybackParameterDialog";
|
||||
@@ -108,6 +111,7 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
assureCorrectAppLanguage(getContext());
|
||||
super.onCreate(savedInstanceState);
|
||||
if (savedInstanceState != null) {
|
||||
initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO);
|
||||
@@ -137,6 +141,7 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
assureCorrectAppLanguage(getContext());
|
||||
final View view = View.inflate(getContext(), R.layout.dialog_playback_parameter, null);
|
||||
setupControlViews(view);
|
||||
|
||||
@@ -216,13 +221,23 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||
private void setupHookingControl(@NonNull View rootView) {
|
||||
unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox);
|
||||
if (unhookingCheckbox != null) {
|
||||
unhookingCheckbox.setChecked(pitch != tempo);
|
||||
// restore whether pitch and tempo are unhooked or not
|
||||
unhookingCheckbox.setChecked(PreferenceManager.getDefaultSharedPreferences(getContext())
|
||||
.getBoolean(getString(R.string.playback_unhook_key), true));
|
||||
|
||||
unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
|
||||
if (isChecked) return;
|
||||
// When unchecked, slide back to the minimum of current tempo or pitch
|
||||
final double minimum = Math.min(getCurrentPitch(), getCurrentTempo());
|
||||
setSliders(minimum);
|
||||
setCurrentPlaybackParameters();
|
||||
// save whether pitch and tempo are unhooked or not
|
||||
PreferenceManager.getDefaultSharedPreferences(getContext())
|
||||
.edit()
|
||||
.putBoolean(getString(R.string.playback_unhook_key), isChecked)
|
||||
.apply();
|
||||
|
||||
if (!isChecked) {
|
||||
// when unchecked, slide back to the minimum of current tempo or pitch
|
||||
final double minimum = Math.min(getCurrentPitch(), getCurrentTempo());
|
||||
setSliders(minimum);
|
||||
setCurrentPlaybackParameters();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,6 +319,7 @@ public class MediaSourceManager {
|
||||
|
||||
private Observable<Long> getEdgeIntervalSignal() {
|
||||
return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.filter(ignored ->
|
||||
playbackListener.isApproachingPlaybackEdge(playbackNearEndGapMillis));
|
||||
}
|
||||
|
||||
@@ -43,9 +43,12 @@ import java.io.StringWriter;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
import java.util.Vector;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 24.10.15.
|
||||
*
|
||||
@@ -91,7 +94,7 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
if (rootView != null) {
|
||||
Snackbar.make(rootView, R.string.error_snackbar_message, 3 * 1000)
|
||||
.setActionTextColor(Color.YELLOW)
|
||||
.setAction(R.string.error_snackbar_action, v ->
|
||||
.setAction(context.getString(R.string.error_snackbar_action).toUpperCase(), v ->
|
||||
startErrorActivity(returnActivity, context, errorInfo, el)).show();
|
||||
} else {
|
||||
startErrorActivity(returnActivity, context, errorInfo, el);
|
||||
@@ -171,6 +174,7 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
assureCorrectAppLanguage(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.setTheme(this);
|
||||
setContentView(R.layout.activity_error);
|
||||
@@ -374,8 +378,12 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private String getContentLangString() {
|
||||
return PreferenceManager.getDefaultSharedPreferences(this)
|
||||
String contentLanguage = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getString(this.getString(R.string.content_country_key), "none");
|
||||
if (contentLanguage.equals(getString(R.string.default_localization_key))) {
|
||||
contentLanguage = Locale.getDefault().toString();
|
||||
}
|
||||
return contentLanguage;
|
||||
}
|
||||
|
||||
private String getOsString() {
|
||||
|
||||
@@ -7,18 +7,20 @@ import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.Preference;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.nononsenseapps.filepicker.Utils;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.utils.Localization;
|
||||
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||
import org.schabi.newpipe.extractor.localization.Localization;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||
@@ -39,6 +41,8 @@ import java.util.Map;
|
||||
import java.util.zip.ZipFile;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
|
||||
private static final int REQUEST_IMPORT_PATH = 8945;
|
||||
@@ -53,10 +57,18 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
|
||||
private String thumbnailLoadToggleKey;
|
||||
|
||||
private Localization initialSelectedLocalization;
|
||||
private ContentCountry initialSelectedContentCountry;
|
||||
private String initialLanguage;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
thumbnailLoadToggleKey = getString(R.string.download_thumbnail_key);
|
||||
|
||||
initialSelectedLocalization = org.schabi.newpipe.util.Localization.getPreferredLocalization(requireContext());
|
||||
initialSelectedContentCountry = org.schabi.newpipe.util.Localization.getPreferredContentCountry(requireContext());
|
||||
initialLanguage = PreferenceManager.getDefaultSharedPreferences(getContext()).getString("app_language_key", "en");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -108,24 +120,29 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
startActivityForResult(i, REQUEST_EXPORT_PATH);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
Preference setPreferredLanguage = findPreference(getString(R.string.content_language_key));
|
||||
setPreferredLanguage.setOnPreferenceChangeListener((Preference p, Object newLanguage) -> {
|
||||
Localization oldLocal = org.schabi.newpipe.util.Localization.getPreferredExtractorLocal(getActivity());
|
||||
NewPipe.setLocalization(new Localization(oldLocal.getCountry(), (String) newLanguage));
|
||||
return true;
|
||||
});
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
Preference setPreferredCountry = findPreference(getString(R.string.content_country_key));
|
||||
setPreferredCountry.setOnPreferenceChangeListener((Preference p, Object newCountry) -> {
|
||||
Localization oldLocal = org.schabi.newpipe.util.Localization.getPreferredExtractorLocal(getActivity());
|
||||
NewPipe.setLocalization(new Localization((String) newCountry, oldLocal.getLanguage()));
|
||||
return true;
|
||||
});
|
||||
final Localization selectedLocalization = org.schabi.newpipe.util.Localization
|
||||
.getPreferredLocalization(requireContext());
|
||||
final ContentCountry selectedContentCountry = org.schabi.newpipe.util.Localization
|
||||
.getPreferredContentCountry(requireContext());
|
||||
final String selectedLanguage = PreferenceManager.getDefaultSharedPreferences(getContext()).getString("app_language_key", "en");
|
||||
|
||||
if (!selectedLocalization.equals(initialSelectedLocalization)
|
||||
|| !selectedContentCountry.equals(initialSelectedContentCountry) || !selectedLanguage.equals(initialLanguage)) {
|
||||
Toast.makeText(requireContext(), R.string.localization_changes_requires_app_restart, Toast.LENGTH_LONG).show();
|
||||
|
||||
NewPipe.setupLocalization(selectedLocalization, selectedContentCountry);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @NonNull Intent data) {
|
||||
assureCorrectAppLanguage(getContext());
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], resultCode = [" + resultCode + "], data = [" + data + "]");
|
||||
@@ -140,7 +157,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
} else {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder.setMessage(R.string.override_current_data)
|
||||
.setPositiveButton(android.R.string.ok,
|
||||
.setPositiveButton(getString(R.string.finish),
|
||||
(DialogInterface d, int id) -> importDatabase(path))
|
||||
.setNegativeButton(android.R.string.cancel,
|
||||
(DialogInterface d, int id) -> d.cancel());
|
||||
@@ -179,7 +196,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}finally {
|
||||
} finally {
|
||||
try {
|
||||
if (output != null) {
|
||||
output.flush();
|
||||
@@ -226,7 +243,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
}
|
||||
|
||||
//If settings file exist, ask if it should be imported.
|
||||
if(ZipHelper.extractFileFromZip(filePath, newpipe_settings.getPath(), "newpipe.settings")) {
|
||||
if (ZipHelper.extractFileFromZip(filePath, newpipe_settings.getPath(), "newpipe.settings")) {
|
||||
AlertDialog.Builder alert = new AlertDialog.Builder(getContext());
|
||||
alert.setTitle(R.string.import_settings);
|
||||
|
||||
@@ -235,7 +252,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
// restart app to properly load db
|
||||
System.exit(0);
|
||||
});
|
||||
alert.setPositiveButton(android.R.string.yes, (dialog, which) -> {
|
||||
alert.setPositiveButton(getString(R.string.finish), (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
loadSharedPreferences(newpipe_settings);
|
||||
// restart app to properly load db
|
||||
@@ -281,7 +298,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
e.printStackTrace();
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}finally {
|
||||
} finally {
|
||||
try {
|
||||
if (input != null) {
|
||||
input.close();
|
||||
|
||||
@@ -8,11 +8,12 @@ import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.preference.Preference;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.nononsenseapps.filepicker.Utils;
|
||||
|
||||
@@ -28,6 +29,8 @@ import java.nio.charset.StandardCharsets;
|
||||
|
||||
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class DownloadSettingsFragment extends BasePreferenceFragment {
|
||||
private static final int REQUEST_DOWNLOAD_VIDEO_PATH = 0x1235;
|
||||
private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236;
|
||||
@@ -159,7 +162,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
|
||||
AlertDialog.Builder msg = new AlertDialog.Builder(ctx);
|
||||
msg.setTitle(title);
|
||||
msg.setMessage(message);
|
||||
msg.setPositiveButton(android.R.string.ok, null);
|
||||
msg.setPositiveButton(getString(R.string.finish), null);
|
||||
msg.show();
|
||||
}
|
||||
|
||||
@@ -202,6 +205,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
assureCorrectAppLanguage(getContext());
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], " +
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.InputType;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.appcompat.widget.AppCompatImageView;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.grack.nanojson.JsonStringWriter;
|
||||
import com.grack.nanojson.JsonWriter;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.PeertubeHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
public class PeertubeInstanceListFragment extends Fragment {
|
||||
|
||||
private List<PeertubeInstance> instanceList = new ArrayList<>();
|
||||
private PeertubeInstance selectedInstance;
|
||||
private String savedInstanceListKey;
|
||||
public InstanceListAdapter instanceListAdapter;
|
||||
|
||||
private ProgressBar progressBar;
|
||||
private SharedPreferences sharedPreferences;
|
||||
|
||||
private CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Lifecycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext());
|
||||
savedInstanceListKey = getString(R.string.peertube_instance_list_key);
|
||||
selectedInstance = PeertubeHelper.getCurrentInstance();
|
||||
updateInstanceList();
|
||||
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_instance_list, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View rootView, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
|
||||
initViews(rootView);
|
||||
}
|
||||
|
||||
private void initViews(@NonNull View rootView) {
|
||||
TextView instanceHelpTV = rootView.findViewById(R.id.instanceHelpTV);
|
||||
instanceHelpTV.setText(getString(R.string.peertube_instance_url_help, getString(R.string.peertube_instance_list_url)));
|
||||
|
||||
initButton(rootView);
|
||||
|
||||
RecyclerView listInstances = rootView.findViewById(R.id.instances);
|
||||
listInstances.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
|
||||
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
|
||||
itemTouchHelper.attachToRecyclerView(listInstances);
|
||||
|
||||
instanceListAdapter = new InstanceListAdapter(requireContext(), itemTouchHelper);
|
||||
listInstances.setAdapter(instanceListAdapter);
|
||||
|
||||
progressBar = rootView.findViewById(R.id.loading_progress_bar);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
updateTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
saveChanges();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (disposables != null) disposables.clear();
|
||||
disposables = null;
|
||||
}
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private final int MENU_ITEM_RESTORE_ID = 123456;
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
|
||||
final MenuItem restoreItem = menu.add(Menu.NONE, MENU_ITEM_RESTORE_ID, Menu.NONE, R.string.restore_defaults);
|
||||
restoreItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
|
||||
final int restoreIcon = ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_restore_defaults);
|
||||
restoreItem.setIcon(AppCompatResources.getDrawable(requireContext(), restoreIcon));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == MENU_ITEM_RESTORE_ID) {
|
||||
restoreDefaults();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void updateInstanceList() {
|
||||
instanceList.clear();
|
||||
instanceList.addAll(PeertubeHelper.getInstanceList(requireContext()));
|
||||
}
|
||||
|
||||
private void selectInstance(PeertubeInstance instance) {
|
||||
selectedInstance = PeertubeHelper.selectInstance(instance, requireContext());
|
||||
sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, true).apply();
|
||||
}
|
||||
|
||||
private void updateTitle() {
|
||||
if (getActivity() instanceof AppCompatActivity) {
|
||||
ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
|
||||
if (actionBar != null) actionBar.setTitle(R.string.peertube_instance_url_title);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveChanges() {
|
||||
JsonStringWriter jsonWriter = JsonWriter.string().object().array("instances");
|
||||
for (PeertubeInstance instance : instanceList) {
|
||||
jsonWriter.object();
|
||||
jsonWriter.value("name", instance.getName());
|
||||
jsonWriter.value("url", instance.getUrl());
|
||||
jsonWriter.end();
|
||||
}
|
||||
String jsonToSave = jsonWriter.end().end().done();
|
||||
sharedPreferences.edit().putString(savedInstanceListKey, jsonToSave).apply();
|
||||
}
|
||||
|
||||
private void restoreDefaults() {
|
||||
new AlertDialog.Builder(requireContext(), ThemeHelper.getDialogTheme(requireContext()))
|
||||
.setTitle(R.string.restore_defaults)
|
||||
.setMessage(R.string.restore_defaults_confirmation)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.yes, (dialog, which) -> {
|
||||
sharedPreferences.edit().remove(savedInstanceListKey).apply();
|
||||
selectInstance(PeertubeInstance.defaultInstance);
|
||||
updateInstanceList();
|
||||
instanceListAdapter.notifyDataSetChanged();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private void initButton(View rootView) {
|
||||
final FloatingActionButton fab = rootView.findViewById(R.id.addInstanceButton);
|
||||
fab.setOnClickListener(v -> {
|
||||
showAddItemDialog(requireContext());
|
||||
});
|
||||
}
|
||||
|
||||
private void showAddItemDialog(Context c) {
|
||||
final EditText urlET = new EditText(c);
|
||||
urlET.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
|
||||
urlET.setHint(R.string.peertube_instance_add_help);
|
||||
AlertDialog dialog = new AlertDialog.Builder(c)
|
||||
.setTitle(R.string.peertube_instance_add_title)
|
||||
.setIcon(R.drawable.place_holder_peertube)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.finish, (dialog1, which) -> {
|
||||
String url = urlET.getText().toString();
|
||||
addInstance(url);
|
||||
})
|
||||
.create();
|
||||
dialog.setView(urlET, 50, 0, 50, 0);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void addInstance(String url) {
|
||||
String cleanUrl = cleanUrl(url);
|
||||
if(null == cleanUrl) return;
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
Disposable disposable = Single.fromCallable(() -> {
|
||||
PeertubeInstance instance = new PeertubeInstance(cleanUrl);
|
||||
instance.fetchInstanceMetaData();
|
||||
return instance;
|
||||
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe((instance) -> {
|
||||
progressBar.setVisibility(View.GONE);
|
||||
add(instance);
|
||||
}, e -> {
|
||||
progressBar.setVisibility(View.GONE);
|
||||
Toast.makeText(getActivity(), R.string.peertube_instance_add_fail, Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
disposables.add(disposable);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String cleanUrl(String url){
|
||||
url = url.trim();
|
||||
// if protocol not present, add https
|
||||
if(!url.startsWith("http")){
|
||||
url = "https://" + url;
|
||||
}
|
||||
// remove trailing slash
|
||||
url = url.replaceAll("/$", "");
|
||||
// only allow https
|
||||
if (!url.startsWith("https://")) {
|
||||
Toast.makeText(getActivity(), R.string.peertube_instance_add_https_only, Toast.LENGTH_SHORT).show();
|
||||
return null;
|
||||
}
|
||||
// only allow if not already exists
|
||||
for (PeertubeInstance instance : instanceList) {
|
||||
if (instance.getUrl().equals(url)) {
|
||||
Toast.makeText(getActivity(), R.string.peertube_instance_add_exists, Toast.LENGTH_SHORT).show();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
private void add(final PeertubeInstance instance) {
|
||||
instanceList.add(instance);
|
||||
instanceListAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// List Handling
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private class InstanceListAdapter extends RecyclerView.Adapter<InstanceListAdapter.TabViewHolder> {
|
||||
private ItemTouchHelper itemTouchHelper;
|
||||
private final LayoutInflater inflater;
|
||||
private RadioButton lastChecked;
|
||||
|
||||
InstanceListAdapter(Context context, ItemTouchHelper itemTouchHelper) {
|
||||
this.itemTouchHelper = itemTouchHelper;
|
||||
this.inflater = LayoutInflater.from(context);
|
||||
}
|
||||
|
||||
public void swapItems(int fromPosition, int toPosition) {
|
||||
Collections.swap(instanceList, fromPosition, toPosition);
|
||||
notifyItemMoved(fromPosition, toPosition);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public InstanceListAdapter.TabViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = inflater.inflate(R.layout.item_instance, parent, false);
|
||||
return new InstanceListAdapter.TabViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull InstanceListAdapter.TabViewHolder holder, int position) {
|
||||
holder.bind(position, holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return instanceList.size();
|
||||
}
|
||||
|
||||
class TabViewHolder extends RecyclerView.ViewHolder {
|
||||
private AppCompatImageView instanceIconView;
|
||||
private TextView instanceNameView;
|
||||
private TextView instanceUrlView;
|
||||
private RadioButton instanceRB;
|
||||
private ImageView handle;
|
||||
|
||||
TabViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
|
||||
instanceIconView = itemView.findViewById(R.id.instanceIcon);
|
||||
instanceNameView = itemView.findViewById(R.id.instanceName);
|
||||
instanceUrlView = itemView.findViewById(R.id.instanceUrl);
|
||||
instanceRB = itemView.findViewById(R.id.selectInstanceRB);
|
||||
handle = itemView.findViewById(R.id.handle);
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
void bind(int position, TabViewHolder holder) {
|
||||
handle.setOnTouchListener(getOnTouchListener(holder));
|
||||
|
||||
final PeertubeInstance instance = instanceList.get(position);
|
||||
instanceNameView.setText(instance.getName());
|
||||
instanceUrlView.setText(instance.getUrl());
|
||||
instanceRB.setOnCheckedChangeListener(null);
|
||||
if (selectedInstance.getUrl().equals(instance.getUrl())) {
|
||||
if (lastChecked != null && lastChecked != instanceRB) {
|
||||
lastChecked.setChecked(false);
|
||||
}
|
||||
instanceRB.setChecked(true);
|
||||
lastChecked = instanceRB;
|
||||
}
|
||||
instanceRB.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
if (isChecked) {
|
||||
selectInstance(instance);
|
||||
if (lastChecked != null && lastChecked != instanceRB) {
|
||||
lastChecked.setChecked(false);
|
||||
}
|
||||
lastChecked = instanceRB;
|
||||
}
|
||||
});
|
||||
instanceIconView.setImageResource(R.drawable.place_holder_peertube);
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private View.OnTouchListener getOnTouchListener(final RecyclerView.ViewHolder item) {
|
||||
return (view, motionEvent) -> {
|
||||
if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
||||
if (itemTouchHelper != null && getItemCount() > 1) {
|
||||
itemTouchHelper.startDrag(item);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
|
||||
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
|
||||
ItemTouchHelper.START | ItemTouchHelper.END) {
|
||||
@Override
|
||||
public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize,
|
||||
int viewSizeOutOfBounds, int totalSize,
|
||||
long msSinceStartScroll) {
|
||||
final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize,
|
||||
viewSizeOutOfBounds, totalSize, msSinceStartScroll);
|
||||
final int minimumAbsVelocity = Math.max(12,
|
||||
Math.abs(standardSpeed));
|
||||
return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source,
|
||||
RecyclerView.ViewHolder target) {
|
||||
if (source.getItemViewType() != target.getItemViewType() ||
|
||||
instanceListAdapter == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int sourceIndex = source.getAdapterPosition();
|
||||
final int targetIndex = target.getAdapterPosition();
|
||||
instanceListAdapter.swapItems(sourceIndex, targetIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLongPressDragEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isItemViewSwipeEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
|
||||
int position = viewHolder.getAdapterPosition();
|
||||
// do not allow swiping the selected instance
|
||||
if(instanceList.get(position).getUrl().equals(selectedInstance.getUrl())) {
|
||||
instanceListAdapter.notifyItemChanged(position);
|
||||
return;
|
||||
}
|
||||
instanceList.remove(position);
|
||||
instanceListAdapter.notifyItemRemoved(position);
|
||||
|
||||
if (instanceList.isEmpty()) {
|
||||
instanceList.add(selectedInstance);
|
||||
instanceListAdapter.notifyItemInserted(0);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import android.view.MenuItem;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 31.08.15.
|
||||
@@ -44,7 +45,7 @@ public class SettingsActivity extends AppCompatActivity implements BasePreferenc
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceBundle) {
|
||||
setTheme(ThemeHelper.getSettingsThemeStyle(this));
|
||||
|
||||
assureCorrectAppLanguage(this);
|
||||
super.onCreate(savedInstanceBundle);
|
||||
setContentView(R.layout.settings_layout);
|
||||
|
||||
|
||||
@@ -1,12 +1,83 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.ListPreference;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
|
||||
public class VideoAudioSettingsFragment extends BasePreferenceFragment {
|
||||
|
||||
private SharedPreferences.OnSharedPreferenceChangeListener listener;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
//initializing R.array.seek_duration_description to display the translation of seconds
|
||||
Resources res = getResources();
|
||||
String[] durationsValues = res.getStringArray(R.array.seek_duration_value);
|
||||
String[] durationsDescriptions = res.getStringArray(R.array.seek_duration_description);
|
||||
int currentDurationValue;
|
||||
for (int i = 0; i < durationsDescriptions.length; i++) {
|
||||
currentDurationValue = Integer.parseInt(durationsValues[i]) / 1000;
|
||||
try {
|
||||
durationsDescriptions[i] = String.format(
|
||||
res.getQuantityString(R.plurals.dynamic_seek_duration_description, currentDurationValue),
|
||||
currentDurationValue);
|
||||
} catch (Resources.NotFoundException ignored) {
|
||||
//if this happens, the translation is missing, and the english string will be displayed instead
|
||||
}
|
||||
}
|
||||
ListPreference durations = (ListPreference) findPreference(getString(R.string.seek_duration_key));
|
||||
durations.setEntries(durationsDescriptions);
|
||||
|
||||
listener = (sharedPreferences, s) -> {
|
||||
|
||||
// on M and above, if user chooses to minimise to popup player on exit and the app doesn't have
|
||||
// display over other apps permission, show a snackbar to let the user give permission
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
||||
s.equals(getString(R.string.minimize_on_exit_key))) {
|
||||
|
||||
String newSetting = sharedPreferences.getString(s, null);
|
||||
if (newSetting != null
|
||||
&& newSetting.equals(getString(R.string.minimize_on_exit_popup_key))
|
||||
&& !Settings.canDrawOverlays(getContext())) {
|
||||
|
||||
Snackbar.make(getListView(), R.string.permission_display_over_apps, Snackbar.LENGTH_INDEFINITE)
|
||||
.setAction(R.string.settings,
|
||||
view -> PermissionHelper.checkSystemAlertWindowPermission(getContext()))
|
||||
.show();
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.video_audio_settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(listener);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ public class ChooseTabsFragment extends Fragment {
|
||||
break;
|
||||
case DEFAULT_KIOSK:
|
||||
if (!tabList.contains(tab)) {
|
||||
returnList.add(new ChooseTabListItem(tab.getTabId(), "Default Kiosk",
|
||||
returnList.add(new ChooseTabListItem(tab.getTabId(), getString(R.string.default_kiosk_page_summary),
|
||||
ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_hot)));
|
||||
}
|
||||
break;
|
||||
@@ -305,23 +305,25 @@ public class ChooseTabsFragment extends Fragment {
|
||||
return;
|
||||
}
|
||||
|
||||
String tabName = tab.getTabName(requireContext());
|
||||
final String tabName;
|
||||
switch (type) {
|
||||
case BLANK:
|
||||
tabName = requireContext().getString(R.string.blank_page_summary);
|
||||
break;
|
||||
case KIOSK:
|
||||
tabName = NewPipe.getNameOfService(((Tab.KioskTab) tab).getKioskServiceId()) + "/" + tabName;
|
||||
break;
|
||||
case CHANNEL:
|
||||
tabName = NewPipe.getNameOfService(((Tab.ChannelTab) tab).getChannelServiceId()) + "/" + tabName;
|
||||
tabName = getString(R.string.blank_page_summary);
|
||||
break;
|
||||
case DEFAULT_KIOSK:
|
||||
tabName = "Default Kiosk";
|
||||
tabName = getString(R.string.default_kiosk_page_summary);
|
||||
break;
|
||||
case KIOSK:
|
||||
tabName = NewPipe.getNameOfService(((Tab.KioskTab) tab).getKioskServiceId()) + "/" + tab.getTabName(requireContext());
|
||||
break;
|
||||
case CHANNEL:
|
||||
tabName = NewPipe.getNameOfService(((Tab.ChannelTab) tab).getChannelServiceId()) + "/" + tab.getTabName(requireContext());
|
||||
break;
|
||||
default:
|
||||
tabName = tab.getTabName(requireContext());
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
tabNameView.setText(tabName);
|
||||
tabIconView.setImageResource(tab.getTabIconRes(requireContext()));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.schabi.newpipe.settings.tabs;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -9,22 +10,26 @@ import androidx.fragment.app.Fragment;
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonSink;
|
||||
|
||||
import org.jsoup.helper.StringUtil;
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.fragments.BlankFragment;
|
||||
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
|
||||
import org.schabi.newpipe.fragments.list.kiosk.DefaultKioskFragment;
|
||||
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
|
||||
import org.schabi.newpipe.local.bookmark.BookmarkFragment;
|
||||
import org.schabi.newpipe.local.feed.FeedFragment;
|
||||
import org.schabi.newpipe.local.history.StatisticsPlaylistFragment;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionFragment;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public abstract class Tab {
|
||||
Tab() {
|
||||
}
|
||||
@@ -40,10 +45,12 @@ public abstract class Tab {
|
||||
/**
|
||||
* Return a instance of the fragment that this tab represent.
|
||||
*/
|
||||
public abstract Fragment getFragment() throws ExtractionException;
|
||||
public abstract Fragment getFragment(Context context) throws ExtractionException;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == this) return true;
|
||||
|
||||
return obj instanceof Tab && obj.getClass().equals(this.getClass())
|
||||
&& ((Tab) obj).getTabId() == this.getTabId();
|
||||
}
|
||||
@@ -115,12 +122,6 @@ public abstract class Tab {
|
||||
return new KioskTab(jsonObject);
|
||||
case CHANNEL:
|
||||
return new ChannelTab(jsonObject);
|
||||
case DEFAULT_KIOSK:
|
||||
DefaultKioskTab tab = new DefaultKioskTab();
|
||||
if(!StringUtil.isBlank(tab.getKioskId())){
|
||||
return tab;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,13 +134,13 @@ public abstract class Tab {
|
||||
|
||||
public enum Type {
|
||||
BLANK(new BlankTab()),
|
||||
DEFAULT_KIOSK(new DefaultKioskTab()),
|
||||
SUBSCRIPTIONS(new SubscriptionsTab()),
|
||||
FEED(new FeedTab()),
|
||||
BOOKMARKS(new BookmarksTab()),
|
||||
HISTORY(new HistoryTab()),
|
||||
KIOSK(new KioskTab()),
|
||||
CHANNEL(new ChannelTab()),
|
||||
DEFAULT_KIOSK(new DefaultKioskTab());
|
||||
CHANNEL(new ChannelTab());
|
||||
|
||||
private Tab tab;
|
||||
|
||||
@@ -176,7 +177,7 @@ public abstract class Tab {
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlankFragment getFragment() {
|
||||
public BlankFragment getFragment(Context context) {
|
||||
return new BlankFragment();
|
||||
}
|
||||
}
|
||||
@@ -201,7 +202,7 @@ public abstract class Tab {
|
||||
}
|
||||
|
||||
@Override
|
||||
public SubscriptionFragment getFragment() {
|
||||
public SubscriptionFragment getFragment(Context context) {
|
||||
return new SubscriptionFragment();
|
||||
}
|
||||
|
||||
@@ -227,7 +228,7 @@ public abstract class Tab {
|
||||
}
|
||||
|
||||
@Override
|
||||
public FeedFragment getFragment() {
|
||||
public FeedFragment getFragment(Context context) {
|
||||
return new FeedFragment();
|
||||
}
|
||||
}
|
||||
@@ -252,7 +253,7 @@ public abstract class Tab {
|
||||
}
|
||||
|
||||
@Override
|
||||
public BookmarkFragment getFragment() {
|
||||
public BookmarkFragment getFragment(Context context) {
|
||||
return new BookmarkFragment();
|
||||
}
|
||||
}
|
||||
@@ -277,7 +278,7 @@ public abstract class Tab {
|
||||
}
|
||||
|
||||
@Override
|
||||
public StatisticsPlaylistFragment getFragment() {
|
||||
public StatisticsPlaylistFragment getFragment(Context context) {
|
||||
return new StatisticsPlaylistFragment();
|
||||
}
|
||||
}
|
||||
@@ -327,7 +328,7 @@ public abstract class Tab {
|
||||
}
|
||||
|
||||
@Override
|
||||
public KioskFragment getFragment() throws ExtractionException {
|
||||
public KioskFragment getFragment(Context context) throws ExtractionException {
|
||||
return KioskFragment.getInstance(kioskServiceId, kioskId);
|
||||
}
|
||||
|
||||
@@ -343,6 +344,13 @@ public abstract class Tab {
|
||||
kioskId = jsonObject.getString(JSON_KIOSK_ID_KEY, "<no-id>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return super.equals(obj) &&
|
||||
kioskServiceId == ((KioskTab) obj).kioskServiceId
|
||||
&& Objects.equals(kioskId, ((KioskTab) obj).kioskId);
|
||||
}
|
||||
|
||||
public int getKioskServiceId() {
|
||||
return kioskServiceId;
|
||||
}
|
||||
@@ -394,7 +402,7 @@ public abstract class Tab {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFragment getFragment() {
|
||||
public ChannelFragment getFragment(Context context) {
|
||||
return ChannelFragment.getInstance(channelServiceId, channelUrl, channelName);
|
||||
}
|
||||
|
||||
@@ -412,6 +420,14 @@ public abstract class Tab {
|
||||
channelName = jsonObject.getString(JSON_CHANNEL_NAME_KEY, "<no-name>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return super.equals(obj) &&
|
||||
channelServiceId == ((ChannelTab) obj).channelServiceId
|
||||
&& Objects.equals(channelUrl, ((ChannelTab) obj).channelUrl)
|
||||
&& Objects.equals(channelName, ((ChannelTab) obj).channelName);
|
||||
}
|
||||
|
||||
public int getChannelServiceId() {
|
||||
return channelServiceId;
|
||||
}
|
||||
@@ -428,22 +444,6 @@ public abstract class Tab {
|
||||
public static class DefaultKioskTab extends Tab {
|
||||
public static final int ID = 7;
|
||||
|
||||
private int kioskServiceId;
|
||||
private String kioskId;
|
||||
|
||||
protected DefaultKioskTab() {
|
||||
initKiosk();
|
||||
}
|
||||
|
||||
public void initKiosk() {
|
||||
this.kioskServiceId = ServiceHelper.getSelectedServiceId(App.getApp());
|
||||
try {
|
||||
this.kioskId = NewPipe.getService(this.kioskServiceId).getKioskList().getDefaultKioskId();
|
||||
} catch (ExtractionException e) {
|
||||
this.kioskId = "";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTabId() {
|
||||
return ID;
|
||||
@@ -451,27 +451,31 @@ public abstract class Tab {
|
||||
|
||||
@Override
|
||||
public String getTabName(Context context) {
|
||||
return KioskTranslator.getTranslatedKioskName(kioskId, context);
|
||||
return KioskTranslator.getTranslatedKioskName(getDefaultKioskId(context), context);
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
@Override
|
||||
public int getTabIconRes(Context context) {
|
||||
final int kioskIcon = KioskTranslator.getKioskIcons(kioskId, context);
|
||||
|
||||
if (kioskIcon <= 0) {
|
||||
throw new IllegalStateException("Kiosk ID is not valid: \"" + kioskId + "\"");
|
||||
}
|
||||
|
||||
return kioskIcon;
|
||||
return KioskTranslator.getKioskIcons(getDefaultKioskId(context), context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KioskFragment getFragment() throws ExtractionException {
|
||||
return KioskFragment.getInstance(kioskServiceId, kioskId);
|
||||
public DefaultKioskFragment getFragment(Context context) throws ExtractionException {
|
||||
return new DefaultKioskFragment();
|
||||
}
|
||||
|
||||
public String getKioskId() {
|
||||
private String getDefaultKioskId(Context context) {
|
||||
final int kioskServiceId = ServiceHelper.getSelectedServiceId(context);
|
||||
|
||||
String kioskId = "";
|
||||
try {
|
||||
final StreamingService service = NewPipe.getService(kioskServiceId);
|
||||
kioskId = service.getKioskList().getDefaultKioskId();
|
||||
} catch (ExtractionException e) {
|
||||
ErrorActivity.reportError(context, e, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.REQUESTED_KIOSK, "none", "Loading default kiosk from selected service", 0));
|
||||
}
|
||||
return kioskId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.schabi.newpipe.settings.tabs;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.grack.nanojson.JsonArray;
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParser;
|
||||
@@ -9,18 +7,25 @@ import com.grack.nanojson.JsonParserException;
|
||||
import com.grack.nanojson.JsonStringWriter;
|
||||
import com.grack.nanojson.JsonWriter;
|
||||
|
||||
import org.jsoup.helper.StringUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Class to get a JSON representation of a list of tabs, and the other way around.
|
||||
*/
|
||||
public class TabsJsonHelper {
|
||||
private static final String JSON_TABS_ARRAY_KEY = "tabs";
|
||||
|
||||
private static final List<Tab> FALLBACK_INITIAL_TABS_LIST = Collections.unmodifiableList(Arrays.asList(
|
||||
Tab.Type.DEFAULT_KIOSK.getTab(),
|
||||
Tab.Type.SUBSCRIPTIONS.getTab(),
|
||||
Tab.Type.BOOKMARKS.getTab()
|
||||
));
|
||||
|
||||
public static class InvalidJsonException extends Exception {
|
||||
private InvalidJsonException() {
|
||||
super();
|
||||
@@ -83,16 +88,6 @@ public class TabsJsonHelper {
|
||||
return returnTabs;
|
||||
}
|
||||
|
||||
public static List<Tab> getDefaultTabs(){
|
||||
List<Tab> tabs = new ArrayList<>();
|
||||
Tab.DefaultKioskTab tab = new Tab.DefaultKioskTab();
|
||||
if(!StringUtil.isBlank(tab.getKioskId())){
|
||||
tabs.add(tab);
|
||||
}
|
||||
tabs.add(Tab.Type.SUBSCRIPTIONS.getTab());
|
||||
tabs.add(Tab.Type.BOOKMARKS.getTab());
|
||||
return Collections.unmodifiableList(tabs);
|
||||
}
|
||||
/**
|
||||
* Get a JSON representation from a list of tabs.
|
||||
*
|
||||
@@ -112,4 +107,8 @@ public class TabsJsonHelper {
|
||||
jsonWriter.end();
|
||||
return jsonWriter.done();
|
||||
}
|
||||
|
||||
public static List<Tab> getDefaultTabs(){
|
||||
return FALLBACK_INITIAL_TABS_LIST;
|
||||
}
|
||||
}
|
||||
@@ -137,6 +137,7 @@ public class DataReader {
|
||||
|
||||
position = 0;
|
||||
readOffset = readBuffer.length;
|
||||
readCount = 0;
|
||||
}
|
||||
|
||||
public boolean canRewind() {
|
||||
|
||||
@@ -15,7 +15,6 @@ import java.util.NoSuchElementException;
|
||||
*/
|
||||
public class Mp4DashReader {
|
||||
|
||||
// <editor-fold defaultState="collapsed" desc="Constants">
|
||||
private static final int ATOM_MOOF = 0x6D6F6F66;
|
||||
private static final int ATOM_MFHD = 0x6D666864;
|
||||
private static final int ATOM_TRAF = 0x74726166;
|
||||
@@ -50,7 +49,7 @@ public class Mp4DashReader {
|
||||
private static final int HANDLER_VIDE = 0x76696465;
|
||||
private static final int HANDLER_SOUN = 0x736F756E;
|
||||
private static final int HANDLER_SUBT = 0x73756274;
|
||||
// </editor-fold>
|
||||
|
||||
|
||||
private final DataReader stream;
|
||||
|
||||
@@ -293,7 +292,8 @@ public class Mp4DashReader {
|
||||
return null;
|
||||
}
|
||||
|
||||
// <editor-fold defaultState="collapsed" desc="Utils">
|
||||
|
||||
|
||||
private long readUint() throws IOException {
|
||||
return stream.readInt() & 0xffffffffL;
|
||||
}
|
||||
@@ -392,9 +392,7 @@ public class Mp4DashReader {
|
||||
return readBox();
|
||||
}
|
||||
|
||||
// </editor-fold>
|
||||
|
||||
// <editor-fold defaultState="collapsed" desc="Box readers">
|
||||
|
||||
private Moof parse_moof(Box ref, int trackId) throws IOException {
|
||||
Moof obj = new Moof();
|
||||
@@ -795,9 +793,8 @@ public class Mp4DashReader {
|
||||
return readFullBox(b);
|
||||
}
|
||||
|
||||
// </editor-fold>
|
||||
|
||||
// <editor-fold defaultState="collapsed" desc="Helper classes">
|
||||
|
||||
class Box {
|
||||
|
||||
int type;
|
||||
@@ -1013,5 +1010,5 @@ public class Mp4DashReader {
|
||||
public TrunEntry info;
|
||||
public byte[] data;
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
}
|
||||
|
||||
@@ -6,10 +6,12 @@ import org.schabi.newpipe.streams.Mp4DashReader.Mp4DashChunk;
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.Mp4DashSample;
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.Mp4Track;
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.TrunEntry;
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.TrackKind;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
@@ -44,6 +46,8 @@ public class Mp4FromDashWriter {
|
||||
|
||||
private int overrideMainBrand = 0x00;
|
||||
|
||||
private ArrayList<Integer> compatibleBrands = new ArrayList<>(5);
|
||||
|
||||
public Mp4FromDashWriter(SharpStream... sources) throws IOException {
|
||||
for (SharpStream src : sources) {
|
||||
if (!src.canRewind() && !src.canRead()) {
|
||||
@@ -55,6 +59,10 @@ public class Mp4FromDashWriter {
|
||||
readers = new Mp4DashReader[sourceTracks.length];
|
||||
readersChunks = new Mp4DashChunk[readers.length];
|
||||
time = (System.currentTimeMillis() / 1000L) + EPOCH_OFFSET;
|
||||
|
||||
compatibleBrands.add(0x6D703431);// mp41
|
||||
compatibleBrands.add(0x69736F6D);// isom
|
||||
compatibleBrands.add(0x69736F32);// iso2
|
||||
}
|
||||
|
||||
public Mp4Track[] getTracksFromSource(int sourceIndex) throws IllegalStateException {
|
||||
@@ -102,8 +110,8 @@ public class Mp4FromDashWriter {
|
||||
}
|
||||
}
|
||||
|
||||
public void setMainBrand(int brandId) {
|
||||
overrideMainBrand = brandId;
|
||||
public void setMainBrand(int brand) {
|
||||
overrideMainBrand = brand;
|
||||
}
|
||||
|
||||
public boolean isDone() {
|
||||
@@ -145,7 +153,7 @@ public class Mp4FromDashWriter {
|
||||
// not allowed for very short tracks (less than 0.5 seconds)
|
||||
//
|
||||
outStream = output;
|
||||
int read = 8;// mdat box header size
|
||||
long read = 8;// mdat box header size
|
||||
long totalSampleSize = 0;
|
||||
int[] sampleExtra = new int[readers.length];
|
||||
int[] defaultMediaTime = new int[readers.length];
|
||||
@@ -157,7 +165,15 @@ public class Mp4FromDashWriter {
|
||||
tablesInfo[i] = new TablesInfo();
|
||||
}
|
||||
|
||||
//<editor-fold defaultstate="expanded" desc="calculate stbl sample tables size and required moov values">
|
||||
int single_sample_buffer;
|
||||
if (tracks.length == 1 && tracks[0].kind == TrackKind.Audio) {
|
||||
// near 1 second of audio data per chunk, avoid split the audio stream in large chunks
|
||||
single_sample_buffer = tracks[0].trak.mdia.mdhd_timeScale / 1000;
|
||||
} else {
|
||||
single_sample_buffer = -1;
|
||||
}
|
||||
|
||||
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
int samplesSize = 0;
|
||||
int sampleSizeChanges = 0;
|
||||
@@ -206,24 +222,10 @@ public class Mp4FromDashWriter {
|
||||
|
||||
readers[i].rewind();
|
||||
|
||||
int tmp = tablesInfo[i].stsz - SAMPLES_PER_CHUNK_INIT;
|
||||
tablesInfo[i].stco = (tmp / SAMPLES_PER_CHUNK) + 1;// +1 for samples in first chunk
|
||||
|
||||
tmp = tmp % SAMPLES_PER_CHUNK;
|
||||
if (tmp == 0) {
|
||||
tablesInfo[i].stsc = 2;// first chunk (init) and succesive chunks
|
||||
tablesInfo[i].stsc_bEntries = new int[]{
|
||||
1, SAMPLES_PER_CHUNK_INIT, 1,
|
||||
2, SAMPLES_PER_CHUNK, 1
|
||||
};
|
||||
if (single_sample_buffer > 0) {
|
||||
initChunkTables(tablesInfo[i], single_sample_buffer, single_sample_buffer);
|
||||
} else {
|
||||
tablesInfo[i].stsc = 3;// first chunk (init) and succesive chunks and remain chunk
|
||||
tablesInfo[i].stsc_bEntries = new int[]{
|
||||
1, SAMPLES_PER_CHUNK_INIT, 1,
|
||||
2, SAMPLES_PER_CHUNK, 1,
|
||||
tablesInfo[i].stco + 1, tmp, 1
|
||||
};
|
||||
tablesInfo[i].stco++;
|
||||
initChunkTables(tablesInfo[i], SAMPLES_PER_CHUNK_INIT, SAMPLES_PER_CHUNK);
|
||||
}
|
||||
|
||||
sampleCount[i] = tablesInfo[i].stsz;
|
||||
@@ -244,11 +246,11 @@ public class Mp4FromDashWriter {
|
||||
tracks[i].trak.tkhd.duration = sampleExtra[i];// this never should happen
|
||||
}
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
|
||||
boolean is64 = read > THRESHOLD_FOR_CO64;
|
||||
|
||||
// calculate the moov size;
|
||||
// calculate the moov size
|
||||
int auxSize = make_moov(defaultMediaTime, tablesInfo, is64);
|
||||
|
||||
if (auxSize < THRESHOLD_MOOV_LENGTH) {
|
||||
@@ -261,17 +263,12 @@ public class Mp4FromDashWriter {
|
||||
final int ftyp_size = make_ftyp();
|
||||
|
||||
// reserve moov space in the output stream
|
||||
/*if (outStream.canSetLength()) {
|
||||
long length = writeOffset + auxSize;
|
||||
outStream.setLength(length);
|
||||
outSeek(length);
|
||||
} else {*/
|
||||
if (auxSize > 0) {
|
||||
int length = auxSize;
|
||||
byte[] buffer = new byte[8 * 1024];// 8 KiB
|
||||
byte[] buffer = new byte[64 * 1024];// 64 KiB
|
||||
while (length > 0) {
|
||||
int count = Math.min(length, buffer.length);
|
||||
outWrite(buffer, 0, count);
|
||||
outWrite(buffer, count);
|
||||
length -= count;
|
||||
}
|
||||
}
|
||||
@@ -280,20 +277,23 @@ public class Mp4FromDashWriter {
|
||||
outSeek(ftyp_size);
|
||||
}
|
||||
|
||||
// tablesInfo contais row counts
|
||||
// and after returning from make_moov() will contain table offsets
|
||||
// tablesInfo contains row counts
|
||||
// and after returning from make_moov() will contain those table offsets
|
||||
make_moov(defaultMediaTime, tablesInfo, is64);
|
||||
|
||||
// write tables: stts stsc
|
||||
// write tables: stts stsc sbgp
|
||||
// reset for ctts table: sampleCount sampleExtra
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
writeEntryArray(tablesInfo[i].stts, 2, sampleCount[i], defaultSampleDuration[i]);
|
||||
writeEntryArray(tablesInfo[i].stsc, tablesInfo[i].stsc_bEntries.length, tablesInfo[i].stsc_bEntries);
|
||||
tablesInfo[i].stsc_bEntries = null;
|
||||
if (tablesInfo[i].ctts > 0) {
|
||||
sampleCount[i] = 1;// index is not base zero
|
||||
sampleCount[i] = 1;// the index is not base zero
|
||||
sampleExtra[i] = -1;
|
||||
}
|
||||
if (tablesInfo[i].sbgp > 0) {
|
||||
writeEntryArray(tablesInfo[i].sbgp, 1, sampleCount[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (auxBuffer == null) {
|
||||
@@ -303,8 +303,8 @@ public class Mp4FromDashWriter {
|
||||
outWrite(make_mdat(totalSampleSize, is64));
|
||||
|
||||
int[] sampleIndex = new int[readers.length];
|
||||
int[] sizes = new int[SAMPLES_PER_CHUNK];
|
||||
int[] sync = new int[SAMPLES_PER_CHUNK];
|
||||
int[] sizes = new int[single_sample_buffer > 0 ? single_sample_buffer : SAMPLES_PER_CHUNK];
|
||||
int[] sync = new int[single_sample_buffer > 0 ? single_sample_buffer : SAMPLES_PER_CHUNK];
|
||||
|
||||
int written = readers.length;
|
||||
while (written > 0) {
|
||||
@@ -317,7 +317,12 @@ public class Mp4FromDashWriter {
|
||||
|
||||
long chunkOffset = writeOffset;
|
||||
int syncCount = 0;
|
||||
int limit = sampleIndex[i] == 0 ? SAMPLES_PER_CHUNK_INIT : SAMPLES_PER_CHUNK;
|
||||
int limit;
|
||||
if (single_sample_buffer > 0) {
|
||||
limit = single_sample_buffer;
|
||||
} else {
|
||||
limit = sampleIndex[i] == 0 ? SAMPLES_PER_CHUNK_INIT : SAMPLES_PER_CHUNK;
|
||||
}
|
||||
|
||||
int j = 0;
|
||||
for (; j < limit; j++) {
|
||||
@@ -326,6 +331,7 @@ public class Mp4FromDashWriter {
|
||||
if (sample == null) {
|
||||
if (tablesInfo[i].ctts > 0 && sampleExtra[i] >= 0) {
|
||||
writeEntryArray(tablesInfo[i].ctts, 1, sampleCount[i], sampleExtra[i]);// flush last entries
|
||||
outRestore();
|
||||
}
|
||||
sampleIndex[i] = -1;
|
||||
break;
|
||||
@@ -354,7 +360,7 @@ public class Mp4FromDashWriter {
|
||||
sizes[j] = sample.data.length;
|
||||
}
|
||||
|
||||
outWrite(sample.data, 0, sample.data.length);
|
||||
outWrite(sample.data, sample.data.length);
|
||||
}
|
||||
|
||||
if (j > 0) {
|
||||
@@ -368,10 +374,12 @@ public class Mp4FromDashWriter {
|
||||
tablesInfo[i].stss = writeEntryArray(tablesInfo[i].stss, syncCount, sync);
|
||||
}
|
||||
|
||||
if (is64) {
|
||||
tablesInfo[i].stco = writeEntry64(tablesInfo[i].stco, chunkOffset);
|
||||
} else {
|
||||
tablesInfo[i].stco = writeEntryArray(tablesInfo[i].stco, 1, (int) chunkOffset);
|
||||
if (tablesInfo[i].stco > 0) {
|
||||
if (is64) {
|
||||
tablesInfo[i].stco = writeEntry64(tablesInfo[i].stco, chunkOffset);
|
||||
} else {
|
||||
tablesInfo[i].stco = writeEntryArray(tablesInfo[i].stco, 1, (int) chunkOffset);
|
||||
}
|
||||
}
|
||||
|
||||
outRestore();
|
||||
@@ -404,7 +412,7 @@ public class Mp4FromDashWriter {
|
||||
}
|
||||
}
|
||||
|
||||
// <editor-fold defaultstate="expanded" desc="Stbl handling">
|
||||
|
||||
private int writeEntry64(int offset, long value) throws IOException {
|
||||
outBackup();
|
||||
|
||||
@@ -447,16 +455,51 @@ public class Mp4FromDashWriter {
|
||||
lastWriteOffset = -1;
|
||||
}
|
||||
}
|
||||
// </editor-fold>
|
||||
|
||||
// <editor-fold defaultstate="expanded" desc="Utils">
|
||||
private void outWrite(byte[] buffer) throws IOException {
|
||||
outWrite(buffer, 0, buffer.length);
|
||||
private void initChunkTables(TablesInfo tables, int firstCount, int succesiveCount) {
|
||||
// tables.stsz holds amount of samples of the track (total)
|
||||
int totalSamples = (tables.stsz - firstCount);
|
||||
float chunkAmount = totalSamples / (float) succesiveCount;
|
||||
int remainChunkOffset = (int) Math.ceil(chunkAmount);
|
||||
boolean remain = remainChunkOffset != (int) chunkAmount;
|
||||
int index = 0;
|
||||
|
||||
tables.stsc = 1;
|
||||
if (firstCount != succesiveCount) {
|
||||
tables.stsc++;
|
||||
}
|
||||
if (remain) {
|
||||
tables.stsc++;
|
||||
}
|
||||
|
||||
// stsc_table_entry = [first_chunk, samples_per_chunk, sample_description_index]
|
||||
tables.stsc_bEntries = new int[tables.stsc * 3];
|
||||
tables.stco = remainChunkOffset + 1;// total entrys in chunk offset box
|
||||
|
||||
tables.stsc_bEntries[index++] = 1;
|
||||
tables.stsc_bEntries[index++] = firstCount;
|
||||
tables.stsc_bEntries[index++] = 1;
|
||||
|
||||
if (firstCount != succesiveCount) {
|
||||
tables.stsc_bEntries[index++] = 2;
|
||||
tables.stsc_bEntries[index++] = succesiveCount;
|
||||
tables.stsc_bEntries[index++] = 1;
|
||||
}
|
||||
|
||||
if (remain) {
|
||||
tables.stsc_bEntries[index++] = remainChunkOffset + 1;
|
||||
tables.stsc_bEntries[index++] = totalSamples % succesiveCount;
|
||||
tables.stsc_bEntries[index] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
private void outWrite(byte[] buffer, int offset, int count) throws IOException {
|
||||
private void outWrite(byte[] buffer) throws IOException {
|
||||
outWrite(buffer, buffer.length);
|
||||
}
|
||||
|
||||
private void outWrite(byte[] buffer, int count) throws IOException {
|
||||
writeOffset += count;
|
||||
outStream.write(buffer, offset, count);
|
||||
outStream.write(buffer, 0, count);
|
||||
}
|
||||
|
||||
private void outSeek(long offset) throws IOException {
|
||||
@@ -509,7 +552,6 @@ public class Mp4FromDashWriter {
|
||||
);
|
||||
|
||||
if (extra >= 0) {
|
||||
//size += 4;// commented for auxiliar buffer !!!
|
||||
offset += 4;
|
||||
auxWrite(extra);
|
||||
}
|
||||
@@ -531,7 +573,7 @@ public class Mp4FromDashWriter {
|
||||
if (moovSimulation) {
|
||||
writeOffset += buffer.length;
|
||||
} else if (auxBuffer == null) {
|
||||
outWrite(buffer, 0, buffer.length);
|
||||
outWrite(buffer, buffer.length);
|
||||
} else {
|
||||
auxBuffer.put(buffer);
|
||||
}
|
||||
@@ -560,23 +602,33 @@ public class Mp4FromDashWriter {
|
||||
private int auxOffset() {
|
||||
return auxBuffer == null ? (int) writeOffset : auxBuffer.position();
|
||||
}
|
||||
// </editor-fold>
|
||||
|
||||
// <editor-fold defaultstate="expanded" desc="Box makers">
|
||||
|
||||
|
||||
private int make_ftyp() throws IOException {
|
||||
byte[] buffer = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70,// ftyp
|
||||
0x6D, 0x70, 0x34, 0x32,// mayor brand (mp42)
|
||||
0x00, 0x00, 0x02, 0x00,// default minor version (512)
|
||||
0x6D, 0x70, 0x34, 0x31, 0x69, 0x73, 0x6F, 0x6D, 0x69, 0x73, 0x6F, 0x32// compatible brands: mp41 isom iso2
|
||||
};
|
||||
int size = 16 + (compatibleBrands.size() * 4);
|
||||
if (overrideMainBrand != 0) size += 4;
|
||||
|
||||
if (overrideMainBrand != 0)
|
||||
ByteBuffer.wrap(buffer).putInt(8, overrideMainBrand);
|
||||
ByteBuffer buffer = ByteBuffer.allocate(size);
|
||||
buffer.putInt(size);
|
||||
buffer.putInt(0x66747970);// "ftyp"
|
||||
|
||||
outWrite(buffer);
|
||||
if (overrideMainBrand == 0) {
|
||||
buffer.putInt(0x6D703432);// mayor brand "mp42"
|
||||
buffer.putInt(512);// default minor version
|
||||
} else {
|
||||
buffer.putInt(overrideMainBrand);
|
||||
buffer.putInt(0);
|
||||
buffer.putInt(0x6D703432);// "mp42" compatible brand
|
||||
}
|
||||
|
||||
return buffer.length;
|
||||
for (Integer brand : compatibleBrands) {
|
||||
buffer.putInt(brand);// compatible brand
|
||||
}
|
||||
|
||||
outWrite(buffer.array());
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
private byte[] make_mdat(long refSize, boolean is64) {
|
||||
@@ -703,7 +755,7 @@ public class Mp4FromDashWriter {
|
||||
int mediaTime;
|
||||
|
||||
if (tracks[index].trak.edst_elst == null) {
|
||||
// is a audio track ¿is edst/elst opcional for audio tracks?
|
||||
// is a audio track ¿is edst/elst optional for audio tracks?
|
||||
mediaTime = 0x00;// ffmpeg set this value as zero, instead of defaultMediaTime
|
||||
bMediaRate = 0x00010000;
|
||||
} else {
|
||||
@@ -719,13 +771,12 @@ public class Mp4FromDashWriter {
|
||||
.array()
|
||||
);
|
||||
|
||||
make_mdia(tracks[index].trak.mdia, tables, is64);
|
||||
make_mdia(tracks[index].trak.mdia, tables, is64, tracks[index].kind == TrackKind.Audio);
|
||||
|
||||
lengthFor(start);
|
||||
}
|
||||
|
||||
private void make_mdia(Mdia mdia, TablesInfo tablesInfo, boolean is64) throws IOException {
|
||||
|
||||
private void make_mdia(Mdia mdia, TablesInfo tablesInfo, boolean is64, boolean isAudio) throws IOException {
|
||||
int start_mdia = auxOffset();
|
||||
auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x61});// mdia
|
||||
auxWrite(mdia.mdhd);
|
||||
@@ -745,7 +796,7 @@ public class Mp4FromDashWriter {
|
||||
// And stsz can be empty if has a default sample size
|
||||
//
|
||||
if (moovSimulation) {
|
||||
make(0x73747473, -1, 2, 1);
|
||||
make(0x73747473, -1, 2, 1);// stts
|
||||
if (tablesInfo.stss > 0) {
|
||||
make(0x73747373, -1, 1, tablesInfo.stss);
|
||||
}
|
||||
@@ -768,6 +819,11 @@ public class Mp4FromDashWriter {
|
||||
tablesInfo.stco = make(is64 ? 0x636F3634 : 0x7374636F, -1, is64 ? 2 : 1, tablesInfo.stco);
|
||||
}
|
||||
|
||||
if (isAudio) {
|
||||
auxWrite(make_sgpd());
|
||||
tablesInfo.sbgp = make_sbgp();// during simulation the returned offset is ignored
|
||||
}
|
||||
|
||||
lengthFor(start_stbl);
|
||||
lengthFor(start_minf);
|
||||
lengthFor(start_mdia);
|
||||
@@ -794,17 +850,60 @@ public class Mp4FromDashWriter {
|
||||
|
||||
return buffer.array();
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
private int make_sbgp() throws IOException {
|
||||
int offset = auxOffset();
|
||||
|
||||
auxWrite(new byte[] {
|
||||
0x00, 0x00, 0x00, 0x1C,// box size
|
||||
0x73, 0x62, 0x67, 0x70,// "sbpg"
|
||||
0x00, 0x00, 0x00, 0x00,// default box flags
|
||||
0x72, 0x6F, 0x6C, 0x6C,// group type "roll"
|
||||
0x00, 0x00, 0x00, 0x01,// group table size
|
||||
0x00, 0x00, 0x00, 0x00,// group[0] total samples (to be set later)
|
||||
0x00, 0x00, 0x00, 0x01// group[0] description index
|
||||
});
|
||||
|
||||
return offset + 0x14;
|
||||
}
|
||||
|
||||
private byte[] make_sgpd() {
|
||||
/*
|
||||
* Sample Group Description Box
|
||||
*
|
||||
* ¿whats does?
|
||||
* the table inside of this box gives information about the
|
||||
* characteristics of sample groups. The descriptive information is any other
|
||||
* information needed to define or characterize the sample group.
|
||||
*
|
||||
* ¿is replicabled this box?
|
||||
* NO due lacks of documentation about this box but...
|
||||
* most of m4a encoders and ffmpeg uses this box with dummy values (same values)
|
||||
*/
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.wrap(new byte[] {
|
||||
0x00, 0x00, 0x00, 0x1A,// box size
|
||||
0x73, 0x67, 0x70, 0x64,// "sgpd"
|
||||
0x01, 0x00, 0x00, 0x00,// box flags (unknown flag sets)
|
||||
0x72, 0x6F, 0x6C, 0x6C, // ¿¿group type??
|
||||
0x00, 0x00, 0x00, 0x02,// ¿¿??
|
||||
0x00, 0x00, 0x00, 0x01,// ¿¿??
|
||||
(byte)0xFF, (byte)0xFF// ¿¿??
|
||||
});
|
||||
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
class TablesInfo {
|
||||
|
||||
public int stts;
|
||||
public int stsc;
|
||||
public int[] stsc_bEntries;
|
||||
public int ctts;
|
||||
public int stsz;
|
||||
public int stsz_default;
|
||||
public int stss;
|
||||
public int stco;
|
||||
int stts;
|
||||
int stsc;
|
||||
int[] stsc_bEntries;
|
||||
int ctts;
|
||||
int stsz;
|
||||
int stsz_default;
|
||||
int stss;
|
||||
int stco;
|
||||
int sbgp;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,431 @@
|
||||
package org.schabi.newpipe.streams;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.WebMReader.Cluster;
|
||||
import org.schabi.newpipe.streams.WebMReader.Segment;
|
||||
import org.schabi.newpipe.streams.WebMReader.SimpleBlock;
|
||||
import org.schabi.newpipe.streams.WebMReader.WebMTrack;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
*/
|
||||
public class OggFromWebMWriter implements Closeable {
|
||||
|
||||
private static final byte FLAG_UNSET = 0x00;
|
||||
//private static final byte FLAG_CONTINUED = 0x01;
|
||||
private static final byte FLAG_FIRST = 0x02;
|
||||
private static final byte FLAG_LAST = 0x04;
|
||||
|
||||
private final static byte HEADER_CHECKSUM_OFFSET = 22;
|
||||
private final static byte HEADER_SIZE = 27;
|
||||
|
||||
private final static int TIME_SCALE_NS = 1000000000;
|
||||
|
||||
private boolean done = false;
|
||||
private boolean parsed = false;
|
||||
|
||||
private SharpStream source;
|
||||
private SharpStream output;
|
||||
|
||||
private int sequence_count = 0;
|
||||
private final int STREAM_ID;
|
||||
private byte packet_flag = FLAG_FIRST;
|
||||
|
||||
private WebMReader webm = null;
|
||||
private WebMTrack webm_track = null;
|
||||
private Segment webm_segment = null;
|
||||
private Cluster webm_cluster = null;
|
||||
private SimpleBlock webm_block = null;
|
||||
|
||||
private long webm_block_last_timecode = 0;
|
||||
private long webm_block_near_duration = 0;
|
||||
|
||||
private short segment_table_size = 0;
|
||||
private final byte[] segment_table = new byte[255];
|
||||
private long segment_table_next_timestamp = TIME_SCALE_NS;
|
||||
|
||||
private final int[] crc32_table = new int[256];
|
||||
|
||||
public OggFromWebMWriter(@NonNull SharpStream source, @NonNull SharpStream target) {
|
||||
if (!source.canRead() || !source.canRewind()) {
|
||||
throw new IllegalArgumentException("source stream must be readable and allows seeking");
|
||||
}
|
||||
if (!target.canWrite() || !target.canRewind()) {
|
||||
throw new IllegalArgumentException("output stream must be writable and allows seeking");
|
||||
}
|
||||
|
||||
this.source = source;
|
||||
this.output = target;
|
||||
|
||||
this.STREAM_ID = (int) System.currentTimeMillis();
|
||||
|
||||
populate_crc32_table();
|
||||
}
|
||||
|
||||
public boolean isDone() {
|
||||
return done;
|
||||
}
|
||||
|
||||
public boolean isParsed() {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
public WebMTrack[] getTracksFromSource() throws IllegalStateException {
|
||||
if (!parsed) {
|
||||
throw new IllegalStateException("source must be parsed first");
|
||||
}
|
||||
|
||||
return webm.getAvailableTracks();
|
||||
}
|
||||
|
||||
public void parseSource() throws IOException, IllegalStateException {
|
||||
if (done) {
|
||||
throw new IllegalStateException("already done");
|
||||
}
|
||||
if (parsed) {
|
||||
throw new IllegalStateException("already parsed");
|
||||
}
|
||||
|
||||
try {
|
||||
webm = new WebMReader(source);
|
||||
webm.parse();
|
||||
webm_segment = webm.getNextSegment();
|
||||
} finally {
|
||||
parsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void selectTrack(int trackIndex) throws IOException {
|
||||
if (!parsed) {
|
||||
throw new IllegalStateException("source must be parsed first");
|
||||
}
|
||||
if (done) {
|
||||
throw new IOException("already done");
|
||||
}
|
||||
if (webm_track != null) {
|
||||
throw new IOException("tracks already selected");
|
||||
}
|
||||
|
||||
switch (webm.getAvailableTracks()[trackIndex].kind) {
|
||||
case Audio:
|
||||
case Video:
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException("the track must an audio or video stream");
|
||||
}
|
||||
|
||||
try {
|
||||
webm_track = webm.selectTrack(trackIndex);
|
||||
} finally {
|
||||
parsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
done = true;
|
||||
parsed = true;
|
||||
|
||||
webm_track = null;
|
||||
webm = null;
|
||||
|
||||
if (!output.isClosed()) {
|
||||
output.flush();
|
||||
}
|
||||
|
||||
source.close();
|
||||
output.close();
|
||||
}
|
||||
|
||||
public void build() throws IOException {
|
||||
float resolution;
|
||||
SimpleBlock bloq;
|
||||
ByteBuffer header = ByteBuffer.allocate(27 + (255 * 255));
|
||||
ByteBuffer page = ByteBuffer.allocate(64 * 1024);
|
||||
|
||||
header.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
/* step 1: get the amount of frames per seconds */
|
||||
switch (webm_track.kind) {
|
||||
case Audio:
|
||||
resolution = getSampleFrequencyFromTrack(webm_track.bMetadata);
|
||||
if (resolution == 0f) {
|
||||
throw new RuntimeException("cannot get the audio sample rate");
|
||||
}
|
||||
break;
|
||||
case Video:
|
||||
// WARNING: untested
|
||||
if (webm_track.defaultDuration == 0) {
|
||||
throw new RuntimeException("missing default frame time");
|
||||
}
|
||||
resolution = 1000f / ((float) webm_track.defaultDuration / webm_segment.info.timecodeScale);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("not implemented");
|
||||
}
|
||||
|
||||
/* step 2: create packet with code init data */
|
||||
if (webm_track.codecPrivate != null) {
|
||||
addPacketSegment(webm_track.codecPrivate.length);
|
||||
make_packetHeader(0x00, header, webm_track.codecPrivate);
|
||||
write(header);
|
||||
output.write(webm_track.codecPrivate);
|
||||
}
|
||||
|
||||
/* step 3: create packet with metadata */
|
||||
byte[] buffer = make_metadata();
|
||||
if (buffer != null) {
|
||||
addPacketSegment(buffer.length);
|
||||
make_packetHeader(0x00, header, buffer);
|
||||
write(header);
|
||||
output.write(buffer);
|
||||
}
|
||||
|
||||
/* step 4: calculate amount of packets */
|
||||
while (webm_segment != null) {
|
||||
bloq = getNextBlock();
|
||||
|
||||
if (bloq != null && addPacketSegment(bloq)) {
|
||||
int pos = page.position();
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
bloq.data.read(page.array(), pos, bloq.dataSize);
|
||||
page.position(pos + bloq.dataSize);
|
||||
continue;
|
||||
}
|
||||
|
||||
// calculate the current packet duration using the next block
|
||||
double elapsed_ns = webm_track.codecDelay;
|
||||
|
||||
if (bloq == null) {
|
||||
packet_flag = FLAG_LAST;// note: if the flag is FLAG_CONTINUED, is changed
|
||||
elapsed_ns += webm_block_last_timecode;
|
||||
|
||||
if (webm_track.defaultDuration > 0) {
|
||||
elapsed_ns += webm_track.defaultDuration;
|
||||
} else {
|
||||
// hardcoded way, guess the sample duration
|
||||
elapsed_ns += webm_block_near_duration;
|
||||
}
|
||||
} else {
|
||||
elapsed_ns += bloq.absoluteTimeCodeNs;
|
||||
}
|
||||
|
||||
// get the sample count in the page
|
||||
elapsed_ns = elapsed_ns / TIME_SCALE_NS;
|
||||
elapsed_ns = Math.ceil(elapsed_ns * resolution);
|
||||
|
||||
// create header and calculate page checksum
|
||||
int checksum = make_packetHeader((long) elapsed_ns, header, null);
|
||||
checksum = calc_crc32(checksum, page.array(), page.position());
|
||||
|
||||
header.putInt(HEADER_CHECKSUM_OFFSET, checksum);
|
||||
|
||||
// dump data
|
||||
write(header);
|
||||
write(page);
|
||||
|
||||
webm_block = bloq;
|
||||
}
|
||||
}
|
||||
|
||||
private int make_packetHeader(long gran_pos, @NonNull ByteBuffer buffer, byte[] immediate_page) {
|
||||
short length = HEADER_SIZE;
|
||||
|
||||
buffer.putInt(0x5367674f);// "OggS" binary string in little-endian
|
||||
buffer.put((byte) 0x00);// version
|
||||
buffer.put(packet_flag);// type
|
||||
|
||||
buffer.putLong(gran_pos);// granulate position
|
||||
|
||||
buffer.putInt(STREAM_ID);// bitstream serial number
|
||||
buffer.putInt(sequence_count++);// page sequence number
|
||||
|
||||
buffer.putInt(0x00);// page checksum
|
||||
|
||||
buffer.put((byte) segment_table_size);// segment table
|
||||
buffer.put(segment_table, 0, segment_table_size);// segment size
|
||||
|
||||
length += segment_table_size;
|
||||
|
||||
clearSegmentTable();// clear segment table for next header
|
||||
|
||||
int checksum_crc32 = calc_crc32(0x00, buffer.array(), length);
|
||||
|
||||
if (immediate_page != null) {
|
||||
checksum_crc32 = calc_crc32(checksum_crc32, immediate_page, immediate_page.length);
|
||||
buffer.putInt(HEADER_CHECKSUM_OFFSET, checksum_crc32);
|
||||
segment_table_next_timestamp -= TIME_SCALE_NS;
|
||||
}
|
||||
|
||||
return checksum_crc32;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private byte[] make_metadata() {
|
||||
if ("A_OPUS".equals(webm_track.codecId)) {
|
||||
return new byte[]{
|
||||
0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73,// "OpusTags" binary string
|
||||
0x07, 0x00, 0x00, 0x00,// writting application string size
|
||||
0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65,// "NewPipe" binary string
|
||||
0x00, 0x00, 0x00, 0x00// additional tags count (zero means no tags)
|
||||
};
|
||||
} else if ("A_VORBIS".equals(webm_track.codecId)) {
|
||||
return new byte[]{
|
||||
0x03,// ????????
|
||||
0x76, 0x6f, 0x72, 0x62, 0x69, 0x73,// "vorbis" binary string
|
||||
0x07, 0x00, 0x00, 0x00,// writting application string size
|
||||
0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65,// "NewPipe" binary string
|
||||
0x01, 0x00, 0x00, 0x00,// additional tags count (zero means no tags)
|
||||
|
||||
/*
|
||||
// whole file duration (not implemented)
|
||||
0x44,// tag string size
|
||||
0x55, 0x52, 0x41, 0x54, 0x49, 0x4F, 0x4E, 0x3D, 0x30, 0x30, 0x3A, 0x30, 0x30, 0x3A, 0x30,
|
||||
0x30, 0x2E, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30
|
||||
*/
|
||||
0x0F,// tag string size
|
||||
0x00, 0x00, 0x00, 0x45, 0x4E, 0x43, 0x4F, 0x44, 0x45, 0x52, 0x3D,// "ENCODER=" binary string
|
||||
0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65,// "NewPipe" binary string
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// ????????
|
||||
};
|
||||
}
|
||||
|
||||
// not implemented for the desired codec
|
||||
return null;
|
||||
}
|
||||
|
||||
private void write(ByteBuffer buffer) throws IOException {
|
||||
output.write(buffer.array(), 0, buffer.position());
|
||||
buffer.position(0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Nullable
|
||||
private SimpleBlock getNextBlock() throws IOException {
|
||||
SimpleBlock res;
|
||||
|
||||
if (webm_block != null) {
|
||||
res = webm_block;
|
||||
webm_block = null;
|
||||
return res;
|
||||
}
|
||||
|
||||
if (webm_segment == null) {
|
||||
webm_segment = webm.getNextSegment();
|
||||
if (webm_segment == null) {
|
||||
return null;// no more blocks in the selected track
|
||||
}
|
||||
}
|
||||
|
||||
if (webm_cluster == null) {
|
||||
webm_cluster = webm_segment.getNextCluster();
|
||||
if (webm_cluster == null) {
|
||||
webm_segment = null;
|
||||
return getNextBlock();
|
||||
}
|
||||
}
|
||||
|
||||
res = webm_cluster.getNextSimpleBlock();
|
||||
if (res == null) {
|
||||
webm_cluster = null;
|
||||
return getNextBlock();
|
||||
}
|
||||
|
||||
webm_block_near_duration = res.absoluteTimeCodeNs - webm_block_last_timecode;
|
||||
webm_block_last_timecode = res.absoluteTimeCodeNs;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private float getSampleFrequencyFromTrack(byte[] bMetadata) {
|
||||
// hardcoded way
|
||||
ByteBuffer buffer = ByteBuffer.wrap(bMetadata);
|
||||
|
||||
while (buffer.remaining() >= 6) {
|
||||
int id = buffer.getShort() & 0xFFFF;
|
||||
if (id == 0x0000B584) {
|
||||
return buffer.getFloat();
|
||||
}
|
||||
}
|
||||
|
||||
return 0f;
|
||||
}
|
||||
|
||||
private void clearSegmentTable() {
|
||||
segment_table_next_timestamp += TIME_SCALE_NS;
|
||||
packet_flag = FLAG_UNSET;
|
||||
segment_table_size = 0;
|
||||
}
|
||||
|
||||
private boolean addPacketSegment(SimpleBlock block) {
|
||||
long timestamp = block.absoluteTimeCodeNs + webm_track.codecDelay;
|
||||
|
||||
if (timestamp >= segment_table_next_timestamp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return addPacketSegment(block.dataSize);
|
||||
}
|
||||
|
||||
private boolean addPacketSegment(int size) {
|
||||
if (size > 65025) {
|
||||
throw new UnsupportedOperationException("page size cannot be larger than 65025");
|
||||
}
|
||||
|
||||
int available = (segment_table.length - segment_table_size) * 255;
|
||||
boolean extra = (size % 255) == 0;
|
||||
|
||||
if (extra) {
|
||||
// add a zero byte entry in the table
|
||||
// required to indicate the sample size is multiple of 255
|
||||
available -= 255;
|
||||
}
|
||||
|
||||
// check if possible add the segment, without overflow the table
|
||||
if (available < size) {
|
||||
return false;// not enough space on the page
|
||||
}
|
||||
|
||||
for (; size > 0; size -= 255) {
|
||||
segment_table[segment_table_size++] = (byte) Math.min(size, 255);
|
||||
}
|
||||
|
||||
if (extra) {
|
||||
segment_table[segment_table_size++] = 0x00;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void populate_crc32_table() {
|
||||
for (int i = 0; i < 0x100; i++) {
|
||||
int crc = i << 24;
|
||||
for (int j = 0; j < 8; j++) {
|
||||
long b = crc >>> 31;
|
||||
crc <<= 1;
|
||||
crc ^= (int) (0x100000000L - b) & 0x04c11db7;
|
||||
}
|
||||
crc32_table[i] = crc;
|
||||
}
|
||||
}
|
||||
|
||||
private int calc_crc32(int initial_crc, byte[] buffer, int size) {
|
||||
for (int i = 0; i < size; i++) {
|
||||
int reg = (initial_crc >>> 24) & 0xff;
|
||||
initial_crc = (initial_crc << 8) ^ crc32_table[reg ^ (buffer[i] & 0xff)];
|
||||
}
|
||||
|
||||
return initial_crc;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package org.schabi.newpipe.streams;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.nodes.Node;
|
||||
import org.jsoup.nodes.TextNode;
|
||||
import org.jsoup.parser.Parser;
|
||||
import org.jsoup.select.Elements;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
*/
|
||||
public class SrtFromTtmlWriter {
|
||||
private static final String NEW_LINE = "\r\n";
|
||||
|
||||
private SharpStream out;
|
||||
private boolean ignoreEmptyFrames;
|
||||
private final Charset charset = StandardCharsets.UTF_8;
|
||||
|
||||
private int frameIndex = 0;
|
||||
|
||||
public SrtFromTtmlWriter(SharpStream out, boolean ignoreEmptyFrames) {
|
||||
this.out = out;
|
||||
this.ignoreEmptyFrames = ignoreEmptyFrames;
|
||||
}
|
||||
|
||||
private static String getTimestamp(Element frame, String attr) {
|
||||
return frame
|
||||
.attr(attr)
|
||||
.replace('.', ',');// SRT subtitles uses comma as decimal separator
|
||||
}
|
||||
|
||||
private void writeFrame(String begin, String end, StringBuilder text) throws IOException {
|
||||
writeString(String.valueOf(frameIndex++));
|
||||
writeString(NEW_LINE);
|
||||
writeString(begin);
|
||||
writeString(" --> ");
|
||||
writeString(end);
|
||||
writeString(NEW_LINE);
|
||||
writeString(text.toString());
|
||||
writeString(NEW_LINE);
|
||||
writeString(NEW_LINE);
|
||||
}
|
||||
|
||||
private void writeString(String text) throws IOException {
|
||||
out.write(text.getBytes(charset));
|
||||
}
|
||||
|
||||
public void build(SharpStream ttml) throws IOException {
|
||||
/*
|
||||
* TTML parser with BASIC support
|
||||
* multiple CUE is not supported
|
||||
* styling is not supported
|
||||
* tag timestamps (in auto-generated subtitles) are not supported, maybe in the future
|
||||
* also TimestampTagOption enum is not applicable
|
||||
* Language parsing is not supported
|
||||
*/
|
||||
|
||||
// parse XML
|
||||
byte[] buffer = new byte[(int) ttml.available()];
|
||||
ttml.read(buffer);
|
||||
Document doc = Jsoup.parse(new ByteArrayInputStream(buffer), "UTF-8", "", Parser.xmlParser());
|
||||
|
||||
StringBuilder text = new StringBuilder(128);
|
||||
Elements paragraph_list = doc.select("body > div > p");
|
||||
|
||||
// check if has frames
|
||||
if (paragraph_list.size() < 1) return;
|
||||
|
||||
for (Element paragraph : paragraph_list) {
|
||||
text.setLength(0);
|
||||
|
||||
for (Node children : paragraph.childNodes()) {
|
||||
if (children instanceof TextNode)
|
||||
text.append(((TextNode) children).text());
|
||||
else if (children instanceof Element && ((Element) children).tagName().equalsIgnoreCase("br"))
|
||||
text.append(NEW_LINE);
|
||||
}
|
||||
|
||||
if (ignoreEmptyFrames && text.length() < 1) continue;
|
||||
|
||||
String begin = getTimestamp(paragraph, "begin");
|
||||
String end = getTimestamp(paragraph, "end");
|
||||
|
||||
writeFrame(begin, end, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,369 +0,0 @@
|
||||
package org.schabi.newpipe.streams;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.text.ParseException;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.xpath.XPathExpressionException;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
*/
|
||||
public class SubtitleConverter {
|
||||
private static final String NEW_LINE = "\r\n";
|
||||
|
||||
public void dumpTTML(SharpStream in, final SharpStream out, final boolean ignoreEmptyFrames, final boolean detectYoutubeDuplicateLines
|
||||
) throws IOException, ParseException, SAXException, ParserConfigurationException, XPathExpressionException {
|
||||
|
||||
final FrameWriter callback = new FrameWriter() {
|
||||
int frameIndex = 0;
|
||||
final Charset charset = Charset.forName("utf-8");
|
||||
|
||||
@Override
|
||||
public void yield(SubtitleFrame frame) throws IOException {
|
||||
if (ignoreEmptyFrames && frame.isEmptyText()) {
|
||||
return;
|
||||
}
|
||||
out.write(String.valueOf(frameIndex++).getBytes(charset));
|
||||
out.write(NEW_LINE.getBytes(charset));
|
||||
out.write(getTime(frame.start, true).getBytes(charset));
|
||||
out.write(" --> ".getBytes(charset));
|
||||
out.write(getTime(frame.end, true).getBytes(charset));
|
||||
out.write(NEW_LINE.getBytes(charset));
|
||||
out.write(frame.text.getBytes(charset));
|
||||
out.write(NEW_LINE.getBytes(charset));
|
||||
out.write(NEW_LINE.getBytes(charset));
|
||||
}
|
||||
};
|
||||
|
||||
read_xml_based(in, callback, detectYoutubeDuplicateLines,
|
||||
"tt", "xmlns", "http://www.w3.org/ns/ttml",
|
||||
new String[]{"timedtext", "head", "wp"},
|
||||
new String[]{"body", "div", "p"},
|
||||
"begin", "end", true
|
||||
);
|
||||
}
|
||||
|
||||
private void read_xml_based(SharpStream source, FrameWriter callback, boolean detectYoutubeDuplicateLines,
|
||||
String root, String formatAttr, String formatVersion, String[] cuePath, String[] framePath,
|
||||
String timeAttr, String durationAttr, boolean hasTimestamp
|
||||
) throws IOException, ParseException, SAXException, ParserConfigurationException, XPathExpressionException {
|
||||
/*
|
||||
* XML based subtitles parser with BASIC support
|
||||
* multiple CUE is not supported
|
||||
* styling is not supported
|
||||
* tag timestamps (in auto-generated subtitles) are not supported, maybe in the future
|
||||
* also TimestampTagOption enum is not applicable
|
||||
* Language parsing is not supported
|
||||
*/
|
||||
|
||||
byte[] buffer = new byte[(int) source.available()];
|
||||
source.read(buffer);
|
||||
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
factory.setNamespaceAware(true);
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
Document xml = builder.parse(new ByteArrayInputStream(buffer));
|
||||
|
||||
String attr;
|
||||
|
||||
// get the format version or namespace
|
||||
Element node = xml.getDocumentElement();
|
||||
|
||||
if (node == null) {
|
||||
throw new ParseException("Can't get the format version. ¿wrong namespace?", -1);
|
||||
} else if (!node.getNodeName().equals(root)) {
|
||||
throw new ParseException("Invalid root", -1);
|
||||
}
|
||||
|
||||
if (formatAttr.equals("xmlns")) {
|
||||
if (!node.getNamespaceURI().equals(formatVersion)) {
|
||||
throw new UnsupportedOperationException("Expected xml namespace: " + formatVersion);
|
||||
}
|
||||
} else {
|
||||
attr = node.getAttributeNS(formatVersion, formatAttr);
|
||||
if (attr == null) {
|
||||
throw new ParseException("Can't get the format attribute", -1);
|
||||
}
|
||||
if (!attr.equals(formatVersion)) {
|
||||
throw new ParseException("Invalid format version : " + attr, -1);
|
||||
}
|
||||
}
|
||||
|
||||
NodeList node_list;
|
||||
|
||||
int line_break = 0;// Maximum characters per line if present (valid for TranScript v3)
|
||||
|
||||
if (!hasTimestamp) {
|
||||
node_list = selectNodes(xml, cuePath, formatVersion);
|
||||
|
||||
if (node_list != null) {
|
||||
// if the subtitle has multiple CUEs, use the highest value
|
||||
for (int i = 0; i < node_list.getLength(); i++) {
|
||||
try {
|
||||
int tmp = Integer.parseInt(((Element) node_list.item(i)).getAttributeNS(formatVersion, "ah"));
|
||||
if (tmp > line_break) {
|
||||
line_break = tmp;
|
||||
}
|
||||
} catch (Exception err) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse every frame
|
||||
node_list = selectNodes(xml, framePath, formatVersion);
|
||||
|
||||
if (node_list == null) {
|
||||
return;// no frames detected
|
||||
}
|
||||
|
||||
int fs_ff = -1;// first timestamp of first frame
|
||||
boolean limit_lines = false;
|
||||
|
||||
for (int i = 0; i < node_list.getLength(); i++) {
|
||||
Element elem = (Element) node_list.item(i);
|
||||
SubtitleFrame obj = new SubtitleFrame();
|
||||
obj.text = elem.getTextContent();
|
||||
|
||||
attr = elem.getAttribute(timeAttr);// ¡this cant be null!
|
||||
obj.start = hasTimestamp ? parseTimestamp(attr) : Integer.parseInt(attr);
|
||||
|
||||
attr = elem.getAttribute(durationAttr);
|
||||
if (obj.text == null || attr == null) {
|
||||
continue;// normally is a blank line (on auto-generated subtitles) ignore
|
||||
}
|
||||
|
||||
if (hasTimestamp) {
|
||||
obj.end = parseTimestamp(attr);
|
||||
|
||||
if (detectYoutubeDuplicateLines) {
|
||||
if (limit_lines) {
|
||||
int swap = obj.end;
|
||||
obj.end = fs_ff;
|
||||
fs_ff = swap;
|
||||
} else {
|
||||
if (fs_ff < 0) {
|
||||
fs_ff = obj.end;
|
||||
} else {
|
||||
if (fs_ff < obj.start) {
|
||||
limit_lines = true;// the subtitles has duplicated lines
|
||||
} else {
|
||||
detectYoutubeDuplicateLines = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
obj.end = obj.start + Integer.parseInt(attr);
|
||||
}
|
||||
|
||||
if (/*node.getAttribute("w").equals("1") &&*/line_break > 1 && obj.text.length() > line_break) {
|
||||
|
||||
// implement auto line breaking (once)
|
||||
StringBuilder text = new StringBuilder(obj.text);
|
||||
obj.text = null;
|
||||
|
||||
switch (text.charAt(line_break)) {
|
||||
case ' ':
|
||||
case '\t':
|
||||
putBreakAt(line_break, text);
|
||||
break;
|
||||
default:// find the word start position
|
||||
for (int j = line_break - 1; j > 0; j--) {
|
||||
switch (text.charAt(j)) {
|
||||
case ' ':
|
||||
case '\t':
|
||||
putBreakAt(j, text);
|
||||
j = -1;
|
||||
break;
|
||||
case '\r':
|
||||
case '\n':
|
||||
j = -1;// long word, just ignore
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
obj.text = text.toString();// set the processed text
|
||||
}
|
||||
|
||||
callback.yield(obj);
|
||||
}
|
||||
}
|
||||
|
||||
private static NodeList selectNodes(Document xml, String[] path, String namespaceUri) {
|
||||
Element ref = xml.getDocumentElement();
|
||||
|
||||
for (int i = 0; i < path.length - 1; i++) {
|
||||
NodeList nodes = ref.getChildNodes();
|
||||
if (nodes.getLength() < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Element elem;
|
||||
for (int j = 0; j < nodes.getLength(); j++) {
|
||||
if (nodes.item(j).getNodeType() == Node.ELEMENT_NODE) {
|
||||
elem = (Element) nodes.item(j);
|
||||
if (elem.getNodeName().equals(path[i]) && elem.getNamespaceURI().equals(namespaceUri)) {
|
||||
ref = elem;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ref.getElementsByTagNameNS(namespaceUri, path[path.length - 1]);
|
||||
}
|
||||
|
||||
private static int parseTimestamp(String multiImpl) throws NumberFormatException, ParseException {
|
||||
if (multiImpl.length() < 1) {
|
||||
return 0;
|
||||
} else if (multiImpl.length() == 1) {
|
||||
return Integer.parseInt(multiImpl) * 1000;// ¡this must be a number in seconds!
|
||||
}
|
||||
|
||||
// detect wallclock-time
|
||||
if (multiImpl.startsWith("wallclock(")) {
|
||||
throw new UnsupportedOperationException("Parsing wallclock timestamp is not implemented");
|
||||
}
|
||||
|
||||
// detect offset-time
|
||||
if (multiImpl.indexOf(':') < 0) {
|
||||
int multiplier = 1000;
|
||||
char metric = multiImpl.charAt(multiImpl.length() - 1);
|
||||
switch (metric) {
|
||||
case 'h':
|
||||
multiplier *= 3600000;
|
||||
break;
|
||||
case 'm':
|
||||
multiplier *= 60000;
|
||||
break;
|
||||
case 's':
|
||||
if (multiImpl.charAt(multiImpl.length() - 2) == 'm') {
|
||||
multiplier = 1;// ms
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (!Character.isDigit(metric)) {
|
||||
throw new NumberFormatException("Invalid metric suffix found on : " + multiImpl);
|
||||
}
|
||||
metric = '\0';
|
||||
break;
|
||||
}
|
||||
try {
|
||||
String offset_time = multiImpl;
|
||||
|
||||
if (multiplier == 1) {
|
||||
offset_time = offset_time.substring(0, offset_time.length() - 2);
|
||||
} else if (metric != '\0') {
|
||||
offset_time = offset_time.substring(0, offset_time.length() - 1);
|
||||
}
|
||||
|
||||
double time_metric_based = Double.parseDouble(offset_time);
|
||||
if (Math.abs(time_metric_based) <= Double.MAX_VALUE) {
|
||||
return (int) (time_metric_based * multiplier);
|
||||
}
|
||||
} catch (Exception err) {
|
||||
throw new UnsupportedOperationException("Invalid or not implemented timestamp on: " + multiImpl);
|
||||
}
|
||||
}
|
||||
|
||||
// detect clock-time
|
||||
int time = 0;
|
||||
String[] units = multiImpl.split(":");
|
||||
|
||||
if (units.length < 3) {
|
||||
throw new ParseException("Invalid clock-time timestamp", -1);
|
||||
}
|
||||
|
||||
time += Integer.parseInt(units[0]) * 3600000;// hours
|
||||
time += Integer.parseInt(units[1]) * 60000;//minutes
|
||||
time += Float.parseFloat(units[2]) * 1000f;// seconds and milliseconds (if present)
|
||||
|
||||
// frames and sub-frames are ignored (not implemented)
|
||||
// time += units[3] * fps;
|
||||
return time;
|
||||
}
|
||||
|
||||
private static void putBreakAt(int idx, StringBuilder str) {
|
||||
// this should be optimized at compile time
|
||||
|
||||
if (NEW_LINE.length() > 1) {
|
||||
str.delete(idx, idx + 1);// remove after replace
|
||||
str.insert(idx, NEW_LINE);
|
||||
} else {
|
||||
str.setCharAt(idx, NEW_LINE.charAt(0));
|
||||
}
|
||||
}
|
||||
|
||||
private static String getTime(int time, boolean comma) {
|
||||
// cast every value to integer to avoid auto-round in ToString("00").
|
||||
StringBuilder str = new StringBuilder(12);
|
||||
str.append(numberToString(time / 1000 / 3600, 2));// hours
|
||||
str.append(':');
|
||||
str.append(numberToString(time / 1000 / 60 % 60, 2));// minutes
|
||||
str.append(':');
|
||||
str.append(numberToString(time / 1000 % 60, 2));// seconds
|
||||
str.append(comma ? ',' : '.');
|
||||
str.append(numberToString(time % 1000, 3));// miliseconds
|
||||
|
||||
return str.toString();
|
||||
}
|
||||
|
||||
private static String numberToString(int nro, int pad) {
|
||||
return String.format(Locale.ENGLISH, "%0".concat(String.valueOf(pad)).concat("d"), nro);
|
||||
}
|
||||
|
||||
|
||||
/******************
|
||||
* helper classes *
|
||||
******************/
|
||||
|
||||
private interface FrameWriter {
|
||||
|
||||
void yield(SubtitleFrame frame) throws IOException;
|
||||
}
|
||||
|
||||
private static class SubtitleFrame {
|
||||
//Java no support unsigned int
|
||||
|
||||
public int end;
|
||||
public int start;
|
||||
public String text = "";
|
||||
|
||||
private boolean isEmptyText() {
|
||||
if (text == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
switch (text.charAt(i)) {
|
||||
case ' ':
|
||||
case '\t':
|
||||
case '\r':
|
||||
case '\n':
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,7 +15,6 @@ import java.util.NoSuchElementException;
|
||||
*/
|
||||
public class WebMReader {
|
||||
|
||||
//<editor-fold defaultState="collapsed" desc="constants">
|
||||
private final static int ID_EMBL = 0x0A45DFA3;
|
||||
private final static int ID_EMBLReadVersion = 0x02F7;
|
||||
private final static int ID_EMBLDocType = 0x0282;
|
||||
@@ -37,11 +36,14 @@ public class WebMReader {
|
||||
private final static int ID_Audio = 0x61;
|
||||
private final static int ID_DefaultDuration = 0x3E383;
|
||||
private final static int ID_FlagLacing = 0x1C;
|
||||
private final static int ID_CodecDelay = 0x16AA;
|
||||
|
||||
private final static int ID_Cluster = 0x0F43B675;
|
||||
private final static int ID_Timecode = 0x67;
|
||||
private final static int ID_SimpleBlock = 0x23;
|
||||
//</editor-fold>
|
||||
private final static int ID_Block = 0x21;
|
||||
private final static int ID_GroupBlock = 0x20;
|
||||
|
||||
|
||||
public enum TrackKind {
|
||||
Audio/*2*/, Video/*1*/, Other
|
||||
@@ -96,7 +98,7 @@ public class WebMReader {
|
||||
}
|
||||
|
||||
ensure(segment.ref);
|
||||
|
||||
// WARNING: track cannot be the same or have different index in new segments
|
||||
Element elem = untilElement(null, ID_Segment);
|
||||
if (elem == null) {
|
||||
done = true;
|
||||
@@ -107,7 +109,8 @@ public class WebMReader {
|
||||
return segment;
|
||||
}
|
||||
|
||||
//<editor-fold defaultstate="collapsed" desc="utils">
|
||||
|
||||
|
||||
private long readNumber(Element parent) throws IOException {
|
||||
int length = (int) parent.contentSize;
|
||||
long value = 0;
|
||||
@@ -189,6 +192,9 @@ public class WebMReader {
|
||||
Element elem;
|
||||
while (ref == null ? stream.available() : (stream.position() < (ref.offset + ref.size))) {
|
||||
elem = readElement();
|
||||
if (expected.length < 1) {
|
||||
return elem;
|
||||
}
|
||||
for (int type : expected) {
|
||||
if (elem.type == type) {
|
||||
return elem;
|
||||
@@ -219,9 +225,9 @@ public class WebMReader {
|
||||
|
||||
stream.skipBytes(skip);
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
//<editor-fold defaultState="collapsed" desc="elements readers">
|
||||
|
||||
|
||||
private boolean readEbml(Element ref, int minReadVersion, int minDocTypeVersion) throws IOException {
|
||||
Element elem = untilElement(ref, ID_EMBLReadVersion);
|
||||
if (elem == null) {
|
||||
@@ -300,9 +306,7 @@ public class WebMReader {
|
||||
WebMTrack entry = new WebMTrack();
|
||||
boolean drop = false;
|
||||
Element elem;
|
||||
while ((elem = untilElement(elem_trackEntry,
|
||||
ID_TrackNumber, ID_TrackType, ID_CodecID, ID_CodecPrivate, ID_FlagLacing, ID_DefaultDuration, ID_Audio, ID_Video
|
||||
)) != null) {
|
||||
while ((elem = untilElement(elem_trackEntry)) != null) {
|
||||
switch (elem.type) {
|
||||
case ID_TrackNumber:
|
||||
entry.trackNumber = readNumber(elem);
|
||||
@@ -326,8 +330,9 @@ public class WebMReader {
|
||||
case ID_FlagLacing:
|
||||
drop = readNumber(elem) != lacingExpected;
|
||||
break;
|
||||
case ID_CodecDelay:
|
||||
entry.codecDelay = readNumber(elem);
|
||||
default:
|
||||
System.out.println();
|
||||
break;
|
||||
}
|
||||
ensure(elem);
|
||||
@@ -360,12 +365,13 @@ public class WebMReader {
|
||||
|
||||
private SimpleBlock readSimpleBlock(Element ref) throws IOException {
|
||||
SimpleBlock obj = new SimpleBlock(ref);
|
||||
obj.dataSize = stream.position();
|
||||
obj.trackNumber = readEncodedNumber();
|
||||
obj.relativeTimeCode = stream.readShort();
|
||||
obj.flags = (byte) stream.read();
|
||||
obj.dataSize = (ref.offset + ref.size) - stream.position();
|
||||
obj.dataSize = (int) ((ref.offset + ref.size) - stream.position());
|
||||
obj.createdFromBlock = ref.type == ID_Block;
|
||||
|
||||
// NOTE: lacing is not implemented, and will be mixed with the stream data
|
||||
if (obj.dataSize < 0) {
|
||||
throw new IOException(String.format("Unexpected SimpleBlock element size, missing %s bytes", -obj.dataSize));
|
||||
}
|
||||
@@ -383,9 +389,9 @@ public class WebMReader {
|
||||
|
||||
return obj;
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
//<editor-fold defaultstate="collapsed" desc="class helpers">
|
||||
|
||||
|
||||
class Element {
|
||||
|
||||
int type;
|
||||
@@ -409,6 +415,7 @@ public class WebMReader {
|
||||
public byte[] bMetadata;
|
||||
public TrackKind kind;
|
||||
public long defaultDuration;
|
||||
public long codecDelay;
|
||||
}
|
||||
|
||||
public class Segment {
|
||||
@@ -448,6 +455,7 @@ public class WebMReader {
|
||||
public class SimpleBlock {
|
||||
|
||||
public InputStream data;
|
||||
public boolean createdFromBlock;
|
||||
|
||||
SimpleBlock(Element ref) {
|
||||
this.ref = ref;
|
||||
@@ -455,8 +463,9 @@ public class WebMReader {
|
||||
|
||||
public long trackNumber;
|
||||
public short relativeTimeCode;
|
||||
public long absoluteTimeCodeNs;
|
||||
public byte flags;
|
||||
public long dataSize;
|
||||
public int dataSize;
|
||||
private final Element ref;
|
||||
|
||||
public boolean isKeyframe() {
|
||||
@@ -468,33 +477,55 @@ public class WebMReader {
|
||||
|
||||
Element ref;
|
||||
SimpleBlock currentSimpleBlock = null;
|
||||
Element currentBlockGroup = null;
|
||||
public long timecode;
|
||||
|
||||
Cluster(Element ref) {
|
||||
this.ref = ref;
|
||||
}
|
||||
|
||||
boolean check() {
|
||||
boolean insideClusterBounds() {
|
||||
return stream.position() >= (ref.offset + ref.size);
|
||||
}
|
||||
|
||||
public SimpleBlock getNextSimpleBlock() throws IOException {
|
||||
if (check()) {
|
||||
if (insideClusterBounds()) {
|
||||
return null;
|
||||
}
|
||||
if (currentSimpleBlock != null) {
|
||||
|
||||
if (currentBlockGroup != null) {
|
||||
ensure(currentBlockGroup);
|
||||
currentBlockGroup = null;
|
||||
currentSimpleBlock = null;
|
||||
} else if (currentSimpleBlock != null) {
|
||||
ensure(currentSimpleBlock.ref);
|
||||
}
|
||||
|
||||
while (!check()) {
|
||||
Element elem = untilElement(ref, ID_SimpleBlock);
|
||||
while (!insideClusterBounds()) {
|
||||
Element elem = untilElement(ref, ID_SimpleBlock, ID_GroupBlock);
|
||||
if (elem == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (elem.type == ID_GroupBlock) {
|
||||
currentBlockGroup = elem;
|
||||
elem = untilElement(currentBlockGroup, ID_Block);
|
||||
|
||||
if (elem == null) {
|
||||
ensure(currentBlockGroup);
|
||||
currentBlockGroup = null;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
currentSimpleBlock = readSimpleBlock(elem);
|
||||
if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) {
|
||||
currentSimpleBlock.data = stream.getView((int) currentSimpleBlock.dataSize);
|
||||
|
||||
// calculate the timestamp in nanoseconds
|
||||
currentSimpleBlock.absoluteTimeCodeNs = currentSimpleBlock.relativeTimeCode + this.timecode;
|
||||
currentSimpleBlock.absoluteTimeCodeNs *= segment.info.timecodeScale;
|
||||
|
||||
return currentSimpleBlock;
|
||||
}
|
||||
|
||||
@@ -505,5 +536,5 @@ public class WebMReader {
|
||||
}
|
||||
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.schabi.newpipe.streams.WebMReader.SimpleBlock;
|
||||
import org.schabi.newpipe.streams.WebMReader.WebMTrack;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
@@ -17,7 +18,7 @@ import java.util.ArrayList;
|
||||
/**
|
||||
* @author kapodamy
|
||||
*/
|
||||
public class WebMWriter {
|
||||
public class WebMWriter implements Closeable {
|
||||
|
||||
private final static int BUFFER_SIZE = 8 * 1024;
|
||||
private final static int DEFAULT_TIMECODE_SCALE = 1000000;
|
||||
@@ -35,7 +36,7 @@ public class WebMWriter {
|
||||
private long written = 0;
|
||||
|
||||
private Segment[] readersSegment;
|
||||
private Cluster[] readersCluter;
|
||||
private Cluster[] readersCluster;
|
||||
|
||||
private int[] predefinedDurations;
|
||||
|
||||
@@ -81,7 +82,7 @@ public class WebMWriter {
|
||||
public void selectTracks(int... trackIndex) throws IOException {
|
||||
try {
|
||||
readersSegment = new Segment[readers.length];
|
||||
readersCluter = new Cluster[readers.length];
|
||||
readersCluster = new Cluster[readers.length];
|
||||
predefinedDurations = new int[readers.length];
|
||||
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
@@ -102,6 +103,7 @@ public class WebMWriter {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
done = true;
|
||||
parsed = true;
|
||||
@@ -114,7 +116,7 @@ public class WebMWriter {
|
||||
readers = null;
|
||||
infoTracks = null;
|
||||
readersSegment = null;
|
||||
readersCluter = null;
|
||||
readersCluster = null;
|
||||
outBuffer = null;
|
||||
}
|
||||
|
||||
@@ -247,7 +249,7 @@ public class WebMWriter {
|
||||
nextCueTime += DEFAULT_CUES_EACH_MS;
|
||||
}
|
||||
keyFrames.add(
|
||||
new KeyFrame(baseSegmentOffset, currentClusterOffset - 7, written, bTimecode.length, bloq.absoluteTimecode)
|
||||
new KeyFrame(baseSegmentOffset, currentClusterOffset - 8, written, bTimecode.length, bloq.absoluteTimecode)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -334,17 +336,17 @@ public class WebMWriter {
|
||||
}
|
||||
}
|
||||
|
||||
if (readersCluter[internalTrackId] == null) {
|
||||
readersCluter[internalTrackId] = readersSegment[internalTrackId].getNextCluster();
|
||||
if (readersCluter[internalTrackId] == null) {
|
||||
if (readersCluster[internalTrackId] == null) {
|
||||
readersCluster[internalTrackId] = readersSegment[internalTrackId].getNextCluster();
|
||||
if (readersCluster[internalTrackId] == null) {
|
||||
readersSegment[internalTrackId] = null;
|
||||
return getNextBlockFrom(internalTrackId);
|
||||
}
|
||||
}
|
||||
|
||||
SimpleBlock res = readersCluter[internalTrackId].getNextSimpleBlock();
|
||||
SimpleBlock res = readersCluster[internalTrackId].getNextSimpleBlock();
|
||||
if (res == null) {
|
||||
readersCluter[internalTrackId] = null;
|
||||
readersCluster[internalTrackId] = null;
|
||||
return new Block();// fake block to indicate the end of the cluster
|
||||
}
|
||||
|
||||
@@ -353,16 +355,11 @@ public class WebMWriter {
|
||||
bloq.dataSize = (int) res.dataSize;
|
||||
bloq.trackNumber = internalTrackId;
|
||||
bloq.flags = res.flags;
|
||||
bloq.absoluteTimecode = convertTimecode(res.relativeTimeCode, readersSegment[internalTrackId].info.timecodeScale);
|
||||
bloq.absoluteTimecode += readersCluter[internalTrackId].timecode;
|
||||
bloq.absoluteTimecode = res.absoluteTimeCodeNs / DEFAULT_TIMECODE_SCALE;
|
||||
|
||||
return bloq;
|
||||
}
|
||||
|
||||
private short convertTimecode(int time, long oldTimeScale) {
|
||||
return (short) (time * (DEFAULT_TIMECODE_SCALE / oldTimeScale));
|
||||
}
|
||||
|
||||
private void seekTo(SharpStream stream, long offset) throws IOException {
|
||||
if (stream.canSeek()) {
|
||||
stream.seek(offset);
|
||||
|
||||
43
app/src/main/java/org/schabi/newpipe/util/BitmapUtils.java
Normal file
43
app/src/main/java/org/schabi/newpipe/util/BitmapUtils.java
Normal file
@@ -0,0 +1,43 @@
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class BitmapUtils {
|
||||
|
||||
@Nullable
|
||||
public static Bitmap centerCrop(Bitmap inputBitmap, int newWidth, int newHeight) {
|
||||
if (inputBitmap == null || inputBitmap.isRecycled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
float sourceWidth = inputBitmap.getWidth();
|
||||
float sourceHeight = inputBitmap.getHeight();
|
||||
|
||||
float xScale = newWidth / sourceWidth;
|
||||
float yScale = newHeight / sourceHeight;
|
||||
|
||||
float newXScale;
|
||||
float newYScale;
|
||||
|
||||
if (yScale > xScale) {
|
||||
newXScale = xScale / yScale;
|
||||
newYScale = 1.0f;
|
||||
} else {
|
||||
newXScale = 1.0f;
|
||||
newYScale = yScale / xScale;
|
||||
}
|
||||
|
||||
float scaledWidth = newXScale * sourceWidth;
|
||||
float scaledHeight = newYScale * sourceHeight;
|
||||
|
||||
int left = (int) ((sourceWidth - scaledWidth) / 2);
|
||||
int top = (int) ((sourceHeight - scaledHeight) / 2);
|
||||
int width = (int) scaledWidth;
|
||||
int height = (int) scaledHeight;
|
||||
|
||||
return Bitmap.createBitmap(inputBitmap, left, top, width, height);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -32,7 +32,7 @@ import org.schabi.newpipe.extractor.Info;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.SuggestionExtractor;
|
||||
import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||
|
||||
@@ -31,6 +31,12 @@ public class KioskTranslator {
|
||||
return c.getString(R.string.top_50);
|
||||
case "New & hot":
|
||||
return c.getString(R.string.new_and_hot);
|
||||
case "Local":
|
||||
return c.getString(R.string.local);
|
||||
case "Recently added":
|
||||
return c.getString(R.string.recently_added);
|
||||
case "Most liked":
|
||||
return c.getString(R.string.most_liked);
|
||||
case "conferences":
|
||||
return c.getString(R.string.conferences);
|
||||
default:
|
||||
@@ -46,6 +52,12 @@ public class KioskTranslator {
|
||||
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
|
||||
case "New & hot":
|
||||
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
|
||||
case "Local":
|
||||
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_local);
|
||||
case "Recently added":
|
||||
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_recent);
|
||||
case "Most liked":
|
||||
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.thumbs_up);
|
||||
case "conferences":
|
||||
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
|
||||
default:
|
||||
|
||||
23
app/src/main/java/org/schabi/newpipe/util/KoreUtil.java
Normal file
23
app/src/main/java/org/schabi/newpipe/util/KoreUtil.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
|
||||
public class KoreUtil {
|
||||
private KoreUtil() { }
|
||||
|
||||
public static void showInstallKoreDialog(final Context context) {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setMessage(R.string.kore_not_found)
|
||||
.setPositiveButton(R.string.install,
|
||||
(DialogInterface dialog, int which) -> NavigationHelper.installKore(context))
|
||||
.setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> {
|
||||
});
|
||||
builder.create().show();
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,34 @@
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.PluralsRes;
|
||||
import androidx.annotation.StringRes;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.ocpsoft.prettytime.PrettyTime;
|
||||
import org.ocpsoft.prettytime.units.Decade;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.text.DateFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
|
||||
/*
|
||||
* Created by chschtsch on 12/29/15.
|
||||
*
|
||||
@@ -42,11 +51,16 @@ import java.util.Locale;
|
||||
|
||||
public class Localization {
|
||||
|
||||
public final static String DOT_SEPARATOR = " • ";
|
||||
private static final String DOT_SEPARATOR = " • ";
|
||||
private static PrettyTime prettyTime;
|
||||
|
||||
private Localization() {
|
||||
}
|
||||
|
||||
public static void init(Context context) {
|
||||
initPrettyTime(context);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String concatenateStrings(final String... strings) {
|
||||
return concatenateStrings(Arrays.asList(strings));
|
||||
@@ -69,29 +83,37 @@ public class Localization {
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
public static org.schabi.newpipe.extractor.utils.Localization getPreferredExtractorLocal(Context context) {
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
public static org.schabi.newpipe.extractor.localization.Localization getPreferredLocalization(final Context context) {
|
||||
final String contentLanguage = PreferenceManager
|
||||
.getDefaultSharedPreferences(context)
|
||||
.getString(context.getString(R.string.content_language_key), context.getString(R.string.default_localization_key));
|
||||
if (contentLanguage.equals(context.getString(R.string.default_localization_key))) {
|
||||
return org.schabi.newpipe.extractor.localization.Localization.fromLocale(Locale.getDefault());
|
||||
}
|
||||
return org.schabi.newpipe.extractor.localization.Localization.fromLocalizationCode(contentLanguage);
|
||||
}
|
||||
|
||||
String languageCode = sp.getString(context.getString(R.string.content_language_key),
|
||||
context.getString(R.string.default_language_value));
|
||||
|
||||
String countryCode = sp.getString(context.getString(R.string.content_country_key),
|
||||
context.getString(R.string.default_country_value));
|
||||
|
||||
return new org.schabi.newpipe.extractor.utils.Localization(countryCode, languageCode);
|
||||
public static ContentCountry getPreferredContentCountry(final Context context) {
|
||||
final String contentCountry = PreferenceManager
|
||||
.getDefaultSharedPreferences(context)
|
||||
.getString(context.getString(R.string.content_country_key), context.getString(R.string.default_localization_key));
|
||||
if (contentCountry.equals(context.getString(R.string.default_localization_key))) {
|
||||
return new ContentCountry(Locale.getDefault().getCountry());
|
||||
}
|
||||
return new ContentCountry(contentCountry);
|
||||
}
|
||||
|
||||
public static Locale getPreferredLocale(Context context) {
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
String languageCode = sp.getString(context.getString(R.string.content_language_key),
|
||||
context.getString(R.string.default_language_value));
|
||||
context.getString(R.string.default_localization_key));
|
||||
|
||||
try {
|
||||
if (languageCode.length() == 2) {
|
||||
return new Locale(languageCode);
|
||||
} else if (languageCode.contains("_")) {
|
||||
String country = languageCode.substring(languageCode.indexOf("_"), languageCode.length());
|
||||
String country = languageCode.substring(languageCode.indexOf("_"));
|
||||
return new Locale(languageCode.substring(0, 2), country);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
@@ -101,58 +123,56 @@ public class Localization {
|
||||
}
|
||||
|
||||
public static String localizeNumber(Context context, long number) {
|
||||
Locale locale = getPreferredLocale(context);
|
||||
NumberFormat nf = NumberFormat.getInstance(locale);
|
||||
return localizeNumber(context, (double) number);
|
||||
}
|
||||
|
||||
public static String localizeNumber(Context context, double number) {
|
||||
NumberFormat nf = NumberFormat.getInstance(getAppLocale(context));
|
||||
return nf.format(number);
|
||||
}
|
||||
|
||||
private static String formatDate(Context context, String date) {
|
||||
Locale locale = getPreferredLocale(context);
|
||||
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
|
||||
Date datum = null;
|
||||
try {
|
||||
datum = formatter.parse(date);
|
||||
} catch (ParseException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM, locale);
|
||||
|
||||
return df.format(datum);
|
||||
public static String formatDate(Date date, Context context) {
|
||||
return DateFormat.getDateInstance(DateFormat.MEDIUM, getAppLocale(context)).format(date);
|
||||
}
|
||||
|
||||
public static String localizeDate(Context context, String date) {
|
||||
Resources res = context.getResources();
|
||||
String dateString = res.getString(R.string.upload_date_text);
|
||||
|
||||
String formattedDate = formatDate(context, date);
|
||||
return String.format(dateString, formattedDate);
|
||||
@SuppressLint("StringFormatInvalid")
|
||||
public static String localizeUploadDate(Context context, Date date) {
|
||||
return context.getString(R.string.upload_date_text, formatDate(date, context));
|
||||
}
|
||||
|
||||
public static String localizeViewCount(Context context, long viewCount) {
|
||||
return getQuantity(context, R.plurals.views, R.string.no_views, viewCount, localizeNumber(context, viewCount));
|
||||
}
|
||||
|
||||
public static String localizeSubscribersCount(Context context, long subscriberCount) {
|
||||
return getQuantity(context, R.plurals.subscribers, R.string.no_subscribers, subscriberCount, localizeNumber(context, subscriberCount));
|
||||
}
|
||||
|
||||
public static String localizeStreamCount(Context context, long streamCount) {
|
||||
return getQuantity(context, R.plurals.videos, R.string.no_videos, streamCount, localizeNumber(context, streamCount));
|
||||
}
|
||||
|
||||
public static String localizeWatchingCount(Context context, long watchingCount) {
|
||||
return getQuantity(context, R.plurals.watching, R.string.no_one_watching, watchingCount, localizeNumber(context, watchingCount));
|
||||
}
|
||||
|
||||
public static String shortCount(Context context, long count) {
|
||||
double value = (double) count;
|
||||
if (count >= 1000000000) {
|
||||
return Long.toString(count / 1000000000) + context.getString(R.string.short_billion);
|
||||
return localizeNumber(context, round(value / 1000000000, 1)) + context.getString(R.string.short_billion);
|
||||
} else if (count >= 1000000) {
|
||||
return Long.toString(count / 1000000) + context.getString(R.string.short_million);
|
||||
return localizeNumber(context, round(value / 1000000, 1)) + context.getString(R.string.short_million);
|
||||
} else if (count >= 1000) {
|
||||
return Long.toString(count / 1000) + context.getString(R.string.short_thousand);
|
||||
return localizeNumber(context, round(value / 1000, 1)) + context.getString(R.string.short_thousand);
|
||||
} else {
|
||||
return Long.toString(count);
|
||||
return localizeNumber(context, value);
|
||||
}
|
||||
}
|
||||
|
||||
public static String listeningCount(Context context, long listeningCount) {
|
||||
return getQuantity(context, R.plurals.listening, R.string.no_one_listening, listeningCount, shortCount(context, listeningCount));
|
||||
}
|
||||
|
||||
public static String shortWatchingCount(Context context, long watchingCount) {
|
||||
return getQuantity(context, R.plurals.watching, R.string.no_one_watching, watchingCount, shortCount(context, watchingCount));
|
||||
}
|
||||
|
||||
public static String shortViewCount(Context context, long viewCount) {
|
||||
return getQuantity(context, R.plurals.views, R.string.no_views, viewCount, shortCount(context, viewCount));
|
||||
}
|
||||
@@ -192,4 +212,58 @@ public class Localization {
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Pretty Time
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private static void initPrettyTime(Context context) {
|
||||
prettyTime = new PrettyTime(getAppLocale(context));
|
||||
// Do not use decades as YouTube doesn't either.
|
||||
prettyTime.removeUnit(Decade.class);
|
||||
}
|
||||
|
||||
private static PrettyTime getPrettyTime() {
|
||||
return prettyTime;
|
||||
}
|
||||
|
||||
public static String relativeTime(Calendar calendarTime) {
|
||||
String time = getPrettyTime().formatUnrounded(calendarTime);
|
||||
return time.startsWith("-") ? time.substring(1) : time;
|
||||
//workaround fix for russian showing -1 day ago, -19hrs ago…
|
||||
}
|
||||
|
||||
private static void changeAppLanguage(Locale loc, Resources res) {
|
||||
DisplayMetrics dm = res.getDisplayMetrics();
|
||||
Configuration conf = res.getConfiguration();
|
||||
conf.setLocale(loc);
|
||||
res.updateConfiguration(conf, dm);
|
||||
}
|
||||
|
||||
public static Locale getAppLocale(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
String lang = prefs.getString(context.getString(R.string.app_language_key), "en");
|
||||
Locale loc;
|
||||
if (lang.equals(context.getString(R.string.default_localization_key))) {
|
||||
loc = Locale.getDefault();
|
||||
} else if (lang.matches(".*-.*")) {
|
||||
//to differentiate different versions of the language
|
||||
//for example, pt (portuguese in Portugal) and pt-br (portuguese in Brazil)
|
||||
String[] localisation = lang.split("-");
|
||||
lang = localisation[0];
|
||||
String country = localisation[1];
|
||||
loc = new Locale(lang, country);
|
||||
} else {
|
||||
loc = new Locale(lang);
|
||||
}
|
||||
return loc;
|
||||
}
|
||||
|
||||
public static void assureCorrectAppLanguage(Context c) {
|
||||
changeAppLanguage(getAppLocale(c), c.getResources());
|
||||
}
|
||||
|
||||
private static double round(double value, int places) {
|
||||
return new BigDecimal(value).setScale(places, RoundingMode.HALF_UP).doubleValue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,12 +109,14 @@ public class NavigationHelper {
|
||||
final float playbackPitch,
|
||||
final boolean playbackSkipSilence,
|
||||
@Nullable final String playbackQuality,
|
||||
final boolean resumePlayback) {
|
||||
final boolean resumePlayback,
|
||||
final boolean startPaused) {
|
||||
return getPlayerIntent(context, targetClazz, playQueue, playbackQuality, resumePlayback)
|
||||
.putExtra(BasePlayer.REPEAT_MODE, repeatMode)
|
||||
.putExtra(BasePlayer.PLAYBACK_SPEED, playbackSpeed)
|
||||
.putExtra(BasePlayer.PLAYBACK_PITCH, playbackPitch)
|
||||
.putExtra(BasePlayer.PLAYBACK_SKIP_SILENCE, playbackSkipSilence);
|
||||
.putExtra(BasePlayer.PLAYBACK_SKIP_SILENCE, playbackSkipSilence)
|
||||
.putExtra(BasePlayer.START_PAUSED, startPaused);
|
||||
}
|
||||
|
||||
public static void playOnMainPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import com.grack.nanojson.JsonArray;
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParser;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
import com.grack.nanojson.JsonStringWriter;
|
||||
import com.grack.nanojson.JsonWriter;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class PeertubeHelper {
|
||||
|
||||
public static List<PeertubeInstance> getInstanceList(Context context) {
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
String savedInstanceListKey = context.getString(R.string.peertube_instance_list_key);
|
||||
final String savedJson = sharedPreferences.getString(savedInstanceListKey, null);
|
||||
if (null == savedJson) {
|
||||
return Collections.singletonList(getCurrentInstance());
|
||||
}
|
||||
|
||||
try {
|
||||
JsonArray array = JsonParser.object().from(savedJson).getArray("instances");
|
||||
List<PeertubeInstance> result = new ArrayList<>();
|
||||
for (Object o : array) {
|
||||
if (o instanceof JsonObject) {
|
||||
JsonObject instance = (JsonObject) o;
|
||||
String name = instance.getString("name");
|
||||
String url = instance.getString("url");
|
||||
result.add(new PeertubeInstance(url, name));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (JsonParserException e) {
|
||||
return Collections.singletonList(getCurrentInstance());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static PeertubeInstance selectInstance(PeertubeInstance instance, Context context) {
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
String selectedInstanceKey = context.getString(R.string.peertube_selected_instance_key);
|
||||
JsonStringWriter jsonWriter = JsonWriter.string().object();
|
||||
jsonWriter.value("name", instance.getName());
|
||||
jsonWriter.value("url", instance.getUrl());
|
||||
String jsonToSave = jsonWriter.end().done();
|
||||
sharedPreferences.edit().putString(selectedInstanceKey, jsonToSave).apply();
|
||||
ServiceList.PeerTube.setInstance(instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static PeertubeInstance getCurrentInstance(){
|
||||
return ServiceList.PeerTube.getInstance();
|
||||
}
|
||||
}
|
||||
@@ -52,10 +52,12 @@ public class SecondaryStreamHelper<T extends Stream> {
|
||||
}
|
||||
}
|
||||
|
||||
if (m4v) return null;
|
||||
|
||||
// retry, but this time in reverse order
|
||||
for (int i = audioStreams.size() - 1; i >= 0; i--) {
|
||||
AudioStream audio = audioStreams.get(i);
|
||||
if (audio.getFormat() == (m4v ? MediaFormat.MP3 : MediaFormat.OPUS)) {
|
||||
if (audio.getFormat() == MediaFormat.WEBMA_OPUS) {
|
||||
return audio;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParser;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -27,15 +34,17 @@ public class ServiceHelper {
|
||||
return R.drawable.place_holder_cloud;
|
||||
case 2:
|
||||
return R.drawable.place_holder_gadse;
|
||||
case 3:
|
||||
return R.drawable.place_holder_peertube;
|
||||
default:
|
||||
return R.drawable.place_holder_circle;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getTranslatedFilterString(String filter, Context c) {
|
||||
switch(filter) {
|
||||
switch (filter) {
|
||||
case "all": return c.getString(R.string.all);
|
||||
case "videos": return c.getString(R.string.videos);
|
||||
case "videos": return c.getString(R.string.videos_string);
|
||||
case "channels": return c.getString(R.string.channels);
|
||||
case "playlists": return c.getString(R.string.playlists);
|
||||
case "tracks": return c.getString(R.string.tracks);
|
||||
@@ -126,9 +135,36 @@ public class ServiceHelper {
|
||||
}
|
||||
|
||||
public static boolean isBeta(final StreamingService s) {
|
||||
switch(s.getServiceInfo().getName()) {
|
||||
switch (s.getServiceInfo().getName()) {
|
||||
case "YouTube": return false;
|
||||
default: return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static void initService(Context context, int serviceId) {
|
||||
if (serviceId == ServiceList.PeerTube.getServiceId()) {
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
String json = sharedPreferences.getString(context.getString(R.string.peertube_selected_instance_key), null);
|
||||
if (null == json) {
|
||||
return;
|
||||
}
|
||||
|
||||
JsonObject jsonObject = null;
|
||||
try {
|
||||
jsonObject = JsonParser.object().from(json);
|
||||
} catch (JsonParserException e) {
|
||||
return;
|
||||
}
|
||||
String name = jsonObject.getString("name");
|
||||
String url = jsonObject.getString("url");
|
||||
PeertubeInstance instance = new PeertubeInstance(url, name);
|
||||
ServiceList.PeerTube.setInstance(instance);
|
||||
}
|
||||
}
|
||||
|
||||
public static void initServices(Context context) {
|
||||
for (StreamingService s : ServiceList.all()) {
|
||||
initService(context, s.getServiceId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import android.widget.ImageView;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.Downloader;
|
||||
import org.schabi.newpipe.DownloaderImpl;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.Stream;
|
||||
@@ -140,7 +140,15 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
||||
if (stream instanceof SubtitlesStream) {
|
||||
formatNameView.setText(((SubtitlesStream) stream).getLanguageTag());
|
||||
} else {
|
||||
formatNameView.setText(stream.getFormat().getName());
|
||||
switch (stream.getFormat()) {
|
||||
case WEBMA_OPUS:
|
||||
// noinspection AndroidLintSetTextI18n
|
||||
formatNameView.setText("opus");
|
||||
break;
|
||||
default:
|
||||
formatNameView.setText(stream.getFormat().getName());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
qualityView.setText(qualityString);
|
||||
@@ -182,7 +190,7 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
||||
continue;
|
||||
}
|
||||
|
||||
final long contentLength = Downloader.getInstance().getContentLength(stream.getUrl());
|
||||
final long contentLength = DownloaderImpl.getInstance().getContentLength(stream.getUrl());
|
||||
streamsWrapper.setSize(stream, contentLength);
|
||||
hasChanged = true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.UnknownHostException;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
|
||||
|
||||
/**
|
||||
* This is an extension of the SSLSocketFactory which enables TLS 1.2 and 1.1.
|
||||
* Created for usage on Android 4.1-4.4 devices, which haven't enabled those by default.
|
||||
*/
|
||||
public class TLSSocketFactoryCompat extends SSLSocketFactory {
|
||||
|
||||
|
||||
private static TLSSocketFactoryCompat instance = null;
|
||||
|
||||
private SSLSocketFactory internalSSLSocketFactory;
|
||||
|
||||
public static TLSSocketFactoryCompat getInstance() throws NoSuchAlgorithmException, KeyManagementException {
|
||||
if (instance != null) {
|
||||
return instance;
|
||||
}
|
||||
return instance = new TLSSocketFactoryCompat();
|
||||
}
|
||||
|
||||
|
||||
public TLSSocketFactoryCompat() throws KeyManagementException, NoSuchAlgorithmException {
|
||||
SSLContext context = SSLContext.getInstance("TLS");
|
||||
context.init(null, null, null);
|
||||
internalSSLSocketFactory = context.getSocketFactory();
|
||||
}
|
||||
|
||||
public TLSSocketFactoryCompat(TrustManager[] tm) throws KeyManagementException, NoSuchAlgorithmException {
|
||||
SSLContext context = SSLContext.getInstance("TLS");
|
||||
context.init(null, tm, new java.security.SecureRandom());
|
||||
internalSSLSocketFactory = context.getSocketFactory();
|
||||
}
|
||||
|
||||
public static void setAsDefault() {
|
||||
try {
|
||||
HttpsURLConnection.setDefaultSSLSocketFactory(getInstance());
|
||||
} catch (NoSuchAlgorithmException | KeyManagementException e) {
|
||||
if (DEBUG) e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getDefaultCipherSuites() {
|
||||
return internalSSLSocketFactory.getDefaultCipherSuites();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedCipherSuites() {
|
||||
return internalSSLSocketFactory.getSupportedCipherSuites();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket() throws IOException {
|
||||
return enableTLSOnSocket(internalSSLSocketFactory.createSocket());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
|
||||
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
|
||||
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
|
||||
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(InetAddress host, int port) throws IOException {
|
||||
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
|
||||
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort));
|
||||
}
|
||||
|
||||
private Socket enableTLSOnSocket(Socket socket) {
|
||||
if (socket != null && (socket instanceof SSLSocket)) {
|
||||
((SSLSocket) socket).setEnabledProtocols(new String[]{"TLSv1.1", "TLSv1.2"});
|
||||
}
|
||||
return socket;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package org.schabi.newpipe.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayout.Tab;
|
||||
|
||||
/**
|
||||
* A TabLayout that is scrollable when tabs exceed its width.
|
||||
* Hides when there are less than 2 tabs.
|
||||
*/
|
||||
public class ScrollableTabLayout extends TabLayout {
|
||||
private static final String TAG = ScrollableTabLayout.class.getSimpleName();
|
||||
|
||||
private int layoutWidth = 0;
|
||||
private int prevVisibility = View.GONE;
|
||||
|
||||
public ScrollableTabLayout(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public ScrollableTabLayout(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public ScrollableTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||
super.onLayout(changed, l, t, r, b);
|
||||
|
||||
remeasureTabs();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
|
||||
layoutWidth = w;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTab(@NonNull Tab tab, int position, boolean setSelected) {
|
||||
super.addTab(tab, position, setSelected);
|
||||
|
||||
hasMultipleTabs();
|
||||
|
||||
// Adding a tab won't decrease total tabs' width so tabMode won't have to change to FIXED
|
||||
if (getTabMode() != MODE_SCROLLABLE) {
|
||||
remeasureTabs();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeTabAt(int position) {
|
||||
super.removeTabAt(position);
|
||||
|
||||
hasMultipleTabs();
|
||||
|
||||
// Removing a tab won't increase total tabs' width so tabMode won't have to change to SCROLLABLE
|
||||
if (getTabMode() != MODE_FIXED) {
|
||||
remeasureTabs();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onVisibilityChanged(View changedView, int visibility) {
|
||||
super.onVisibilityChanged(changedView, visibility);
|
||||
|
||||
// Recheck content width in case some tabs have been added or removed while ScrollableTabLayout was invisible
|
||||
// We don't have to check if it was GONE because then requestLayout() will be called
|
||||
if (changedView == this) {
|
||||
if (prevVisibility == View.INVISIBLE) {
|
||||
remeasureTabs();
|
||||
}
|
||||
prevVisibility = visibility;
|
||||
}
|
||||
}
|
||||
|
||||
private void setMode(int mode) {
|
||||
if (mode == getTabMode()) return;
|
||||
|
||||
setTabMode(mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make ScrollableTabLayout not visible if there are less than two tabs
|
||||
*/
|
||||
private void hasMultipleTabs() {
|
||||
if (getTabCount() > 1) {
|
||||
setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate minimal width required by tabs and set tabMode accordingly
|
||||
*/
|
||||
private void remeasureTabs() {
|
||||
if (prevVisibility != View.VISIBLE) return;
|
||||
if (layoutWidth == 0) return;
|
||||
|
||||
final int count = getTabCount();
|
||||
int contentWidth = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
View child = getTabAt(i).view;
|
||||
if (child.getVisibility() == View.VISIBLE) {
|
||||
// Use tab's minimum requested width should actual content be too small
|
||||
contentWidth += Math.max(child.getMinimumWidth(), child.getMeasuredWidth());
|
||||
}
|
||||
}
|
||||
|
||||
if (contentWidth > layoutWidth) {
|
||||
setMode(TabLayout.MODE_SCROLLABLE);
|
||||
} else {
|
||||
setMode(TabLayout.MODE_FIXED);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
package us.shandian.giga.get;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -13,6 +15,7 @@ import java.nio.channels.ClosedByInterruptException;
|
||||
import us.shandian.giga.util.Utility;
|
||||
|
||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_FORBIDDEN;
|
||||
|
||||
public class DownloadInitializer extends Thread {
|
||||
private final static String TAG = "DownloadInitializer";
|
||||
@@ -28,9 +31,9 @@ public class DownloadInitializer extends Thread {
|
||||
mConn = null;
|
||||
}
|
||||
|
||||
private static void safeClose(HttpURLConnection con) {
|
||||
private void dispose() {
|
||||
try {
|
||||
con.getInputStream().close();
|
||||
mConn.getInputStream().close();
|
||||
} catch (Exception e) {
|
||||
// nothing to do
|
||||
}
|
||||
@@ -51,9 +54,9 @@ public class DownloadInitializer extends Thread {
|
||||
long lowestSize = Long.MAX_VALUE;
|
||||
|
||||
for (int i = 0; i < mMission.urls.length && mMission.running; i++) {
|
||||
mConn = mMission.openConnection(mMission.urls[i], mId, -1, -1);
|
||||
mConn = mMission.openConnection(mMission.urls[i], true, -1, -1);
|
||||
mMission.establishConnection(mId, mConn);
|
||||
safeClose(mConn);
|
||||
dispose();
|
||||
|
||||
if (Thread.interrupted()) return;
|
||||
long length = Utility.getContentLength(mConn);
|
||||
@@ -81,9 +84,9 @@ public class DownloadInitializer extends Thread {
|
||||
}
|
||||
} else {
|
||||
// ask for the current resource length
|
||||
mConn = mMission.openConnection(mId, -1, -1);
|
||||
mConn = mMission.openConnection(true, -1, -1);
|
||||
mMission.establishConnection(mId, mConn);
|
||||
safeClose(mConn);
|
||||
dispose();
|
||||
|
||||
if (!mMission.running || Thread.interrupted()) return;
|
||||
|
||||
@@ -107,9 +110,9 @@ public class DownloadInitializer extends Thread {
|
||||
}
|
||||
} else {
|
||||
// Open again
|
||||
mConn = mMission.openConnection(mId, mMission.length - 10, mMission.length);
|
||||
mConn = mMission.openConnection(true, mMission.length - 10, mMission.length);
|
||||
mMission.establishConnection(mId, mConn);
|
||||
safeClose(mConn);
|
||||
dispose();
|
||||
|
||||
if (!mMission.running || Thread.interrupted()) return;
|
||||
|
||||
@@ -151,12 +154,33 @@ public class DownloadInitializer extends Thread {
|
||||
|
||||
if (!mMission.running || Thread.interrupted()) return;
|
||||
|
||||
if (!mMission.unknownLength && mMission.recoveryInfo != null) {
|
||||
String entityTag = mConn.getHeaderField("ETAG");
|
||||
String lastModified = mConn.getHeaderField("Last-Modified");
|
||||
MissionRecoveryInfo recovery = mMission.recoveryInfo[mMission.current];
|
||||
|
||||
if (!TextUtils.isEmpty(entityTag)) {
|
||||
recovery.validateCondition = entityTag;
|
||||
} else if (!TextUtils.isEmpty(lastModified)) {
|
||||
recovery.validateCondition = lastModified;// Note: this is less precise
|
||||
} else {
|
||||
recovery.validateCondition = null;
|
||||
}
|
||||
}
|
||||
|
||||
mMission.running = false;
|
||||
break;
|
||||
} catch (InterruptedIOException | ClosedByInterruptException e) {
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
if (!mMission.running) return;
|
||||
if (!mMission.running || super.isInterrupted()) return;
|
||||
|
||||
if (e instanceof DownloadMission.HttpError && ((DownloadMission.HttpError) e).statusCode == ERROR_HTTP_FORBIDDEN) {
|
||||
// for youtube streams. The url has expired
|
||||
interrupt();
|
||||
mMission.doRecover(ERROR_HTTP_FORBIDDEN);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e instanceof IOException && e.getMessage().contains("Permission denied")) {
|
||||
mMission.notifyError(DownloadMission.ERROR_PERMISSION_DENIED, e);
|
||||
@@ -179,13 +203,6 @@ public class DownloadInitializer extends Thread {
|
||||
@Override
|
||||
public void interrupt() {
|
||||
super.interrupt();
|
||||
|
||||
if (mConn != null) {
|
||||
try {
|
||||
mConn.disconnect();
|
||||
} catch (Exception e) {
|
||||
// nothing to do
|
||||
}
|
||||
}
|
||||
if (mConn != null) dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
package us.shandian.giga.get;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.OsConstants;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.Downloader;
|
||||
import org.schabi.newpipe.DownloaderImpl;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.Serializable;
|
||||
import java.net.ConnectException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
|
||||
@@ -27,14 +33,11 @@ import us.shandian.giga.util.Utility;
|
||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
|
||||
public class DownloadMission extends Mission {
|
||||
private static final long serialVersionUID = 5L;// last bump: 30 june 2019
|
||||
private static final long serialVersionUID = 6L;// last bump: 07 october 2019
|
||||
|
||||
static final int BUFFER_SIZE = 64 * 1024;
|
||||
static final int BLOCK_SIZE = 512 * 1024;
|
||||
|
||||
@SuppressWarnings("SpellCheckingInspection")
|
||||
private static final String INSUFFICIENT_STORAGE = "ENOSPC";
|
||||
|
||||
private static final String TAG = "DownloadMission";
|
||||
|
||||
public static final int ERROR_NOTHING = -1;
|
||||
@@ -51,8 +54,9 @@ public class DownloadMission extends Mission {
|
||||
public static final int ERROR_INSUFFICIENT_STORAGE = 1010;
|
||||
public static final int ERROR_PROGRESS_LOST = 1011;
|
||||
public static final int ERROR_TIMEOUT = 1012;
|
||||
public static final int ERROR_RESOURCE_GONE = 1013;
|
||||
public static final int ERROR_HTTP_NO_CONTENT = 204;
|
||||
public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206;
|
||||
static final int ERROR_HTTP_FORBIDDEN = 403;
|
||||
|
||||
/**
|
||||
* The urls of the file to download
|
||||
@@ -60,9 +64,9 @@ public class DownloadMission extends Mission {
|
||||
public String[] urls;
|
||||
|
||||
/**
|
||||
* Number of bytes downloaded
|
||||
* Number of bytes downloaded and written
|
||||
*/
|
||||
public long done;
|
||||
public volatile long done;
|
||||
|
||||
/**
|
||||
* Indicates a file generated dynamically on the web server
|
||||
@@ -118,31 +122,36 @@ public class DownloadMission extends Mission {
|
||||
/**
|
||||
* Download/File resume offset in fallback mode (if applicable) {@link DownloadRunnableFallback}
|
||||
*/
|
||||
long fallbackResumeOffset;
|
||||
volatile long fallbackResumeOffset;
|
||||
|
||||
/**
|
||||
* Maximum of download threads running, chosen by the user
|
||||
*/
|
||||
public int threadCount = 3;
|
||||
|
||||
/**
|
||||
* information required to recover a download
|
||||
*/
|
||||
public MissionRecoveryInfo[] recoveryInfo;
|
||||
|
||||
private transient int finishCount;
|
||||
public transient boolean running;
|
||||
public transient volatile boolean running;
|
||||
public boolean enqueued;
|
||||
|
||||
public int errCode = ERROR_NOTHING;
|
||||
public Exception errObject = null;
|
||||
|
||||
public transient boolean recovered;
|
||||
public transient Handler mHandler;
|
||||
private transient boolean mWritingToFile;
|
||||
private transient boolean[] blockAcquired;
|
||||
|
||||
private transient long writingToFileNext;
|
||||
private transient volatile boolean writingToFile;
|
||||
|
||||
final Object LOCK = new Lock();
|
||||
|
||||
private transient boolean deleted;
|
||||
|
||||
public transient volatile Thread[] threads = new Thread[0];
|
||||
private transient Thread init = null;
|
||||
@NonNull
|
||||
public transient Thread[] threads = new Thread[0];
|
||||
public transient Thread init = null;
|
||||
|
||||
public DownloadMission(String[] urls, StoredFileHelper storage, char kind, Postprocessing psInstance) {
|
||||
if (urls == null) throw new NullPointerException("urls is null");
|
||||
@@ -197,37 +206,35 @@ public class DownloadMission extends Mission {
|
||||
}
|
||||
|
||||
/**
|
||||
* Open connection
|
||||
* Opens a connection
|
||||
*
|
||||
* @param threadId id of the calling thread, used only for debug
|
||||
* @param rangeStart range start
|
||||
* @param rangeEnd range end
|
||||
* @param headRequest {@code true} for use {@code HEAD} request method, otherwise, {@code GET} is used
|
||||
* @param rangeStart range start
|
||||
* @param rangeEnd range end
|
||||
* @return a {@link java.net.URLConnection URLConnection} linking to the URL.
|
||||
* @throws IOException if an I/O exception occurs.
|
||||
*/
|
||||
HttpURLConnection openConnection(int threadId, long rangeStart, long rangeEnd) throws IOException {
|
||||
return openConnection(urls[current], threadId, rangeStart, rangeEnd);
|
||||
HttpURLConnection openConnection(boolean headRequest, long rangeStart, long rangeEnd) throws IOException {
|
||||
return openConnection(urls[current], headRequest, rangeStart, rangeEnd);
|
||||
}
|
||||
|
||||
HttpURLConnection openConnection(String url, int threadId, long rangeStart, long rangeEnd) throws IOException {
|
||||
HttpURLConnection openConnection(String url, boolean headRequest, long rangeStart, long rangeEnd) throws IOException {
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
|
||||
conn.setInstanceFollowRedirects(true);
|
||||
conn.setRequestProperty("User-Agent", Downloader.USER_AGENT);
|
||||
conn.setRequestProperty("User-Agent", DownloaderImpl.USER_AGENT);
|
||||
conn.setRequestProperty("Accept", "*/*");
|
||||
conn.setRequestProperty("Accept-Encoding", "*");
|
||||
|
||||
if (headRequest) conn.setRequestMethod("HEAD");
|
||||
|
||||
// BUG workaround: switching between networks can freeze the download forever
|
||||
conn.setConnectTimeout(30000);
|
||||
conn.setReadTimeout(10000);
|
||||
|
||||
if (rangeStart >= 0) {
|
||||
String req = "bytes=" + rangeStart + "-";
|
||||
if (rangeEnd > 0) req += rangeEnd;
|
||||
|
||||
conn.setRequestProperty("Range", req);
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, threadId + ":" + conn.getRequestProperty("Range"));
|
||||
}
|
||||
}
|
||||
|
||||
return conn;
|
||||
@@ -240,18 +247,21 @@ public class DownloadMission extends Mission {
|
||||
* @throws HttpError if the HTTP Status-Code is not satisfiable
|
||||
*/
|
||||
void establishConnection(int threadId, HttpURLConnection conn) throws IOException, HttpError {
|
||||
conn.connect();
|
||||
int statusCode = conn.getResponseCode();
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, threadId + ":Content-Length=" + conn.getContentLength() + " Code:" + statusCode);
|
||||
Log.d(TAG, threadId + ":[request] Range=" + conn.getRequestProperty("Range"));
|
||||
Log.d(TAG, threadId + ":[response] Code=" + statusCode);
|
||||
Log.d(TAG, threadId + ":[response] Content-Length=" + conn.getContentLength());
|
||||
Log.d(TAG, threadId + ":[response] Content-Range=" + conn.getHeaderField("Content-Range"));
|
||||
}
|
||||
|
||||
|
||||
switch (statusCode) {
|
||||
case 204:
|
||||
case 205:
|
||||
case 207:
|
||||
throw new HttpError(conn.getResponseCode());
|
||||
throw new HttpError(statusCode);
|
||||
case 416:
|
||||
return;// let the download thread handle this error
|
||||
default:
|
||||
@@ -268,28 +278,19 @@ public class DownloadMission extends Mission {
|
||||
}
|
||||
|
||||
synchronized void notifyProgress(long deltaLen) {
|
||||
if (!running) return;
|
||||
|
||||
if (recovered) {
|
||||
recovered = false;
|
||||
}
|
||||
|
||||
if (unknownLength) {
|
||||
length += deltaLen;// Update length before proceeding
|
||||
}
|
||||
|
||||
done += deltaLen;
|
||||
|
||||
if (done > length) {
|
||||
done = length;
|
||||
}
|
||||
if (metadata == null) return;
|
||||
|
||||
if (done != length && !deleted && !mWritingToFile) {
|
||||
mWritingToFile = true;
|
||||
runAsync(-2, this::writeThisToFile);
|
||||
if (!writingToFile && (done > writingToFileNext || deltaLen < 0)) {
|
||||
writingToFile = true;
|
||||
writingToFileNext = done + BLOCK_SIZE;
|
||||
writeThisToFileAsync();
|
||||
}
|
||||
|
||||
notify(DownloadManagerService.MESSAGE_PROGRESS);
|
||||
}
|
||||
|
||||
synchronized void notifyError(Exception err) {
|
||||
@@ -314,13 +315,29 @@ public class DownloadMission extends Mission {
|
||||
|
||||
public synchronized void notifyError(int code, Exception err) {
|
||||
Log.e(TAG, "notifyError() code = " + code, err);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (err != null && err.getCause() instanceof ErrnoException) {
|
||||
int errno = ((ErrnoException) err.getCause()).errno;
|
||||
if (errno == OsConstants.ENOSPC) {
|
||||
code = ERROR_INSUFFICIENT_STORAGE;
|
||||
err = null;
|
||||
} else if (errno == OsConstants.EACCES) {
|
||||
code = ERROR_PERMISSION_DENIED;
|
||||
err = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (err instanceof IOException) {
|
||||
if (!storage.canWrite() || err.getMessage().contains("Permission denied")) {
|
||||
if (err.getMessage().contains("Permission denied")) {
|
||||
code = ERROR_PERMISSION_DENIED;
|
||||
err = null;
|
||||
} else if (err.getMessage().contains(INSUFFICIENT_STORAGE)) {
|
||||
} else if (err.getMessage().contains("ENOSPC")) {
|
||||
code = ERROR_INSUFFICIENT_STORAGE;
|
||||
err = null;
|
||||
} else if (!storage.canWrite()) {
|
||||
code = ERROR_FILE_CREATION;
|
||||
err = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,44 +359,42 @@ public class DownloadMission extends Mission {
|
||||
|
||||
notify(DownloadManagerService.MESSAGE_ERROR);
|
||||
|
||||
if (running) {
|
||||
running = false;
|
||||
recovered = true;
|
||||
if (threads != null) selfPause();
|
||||
}
|
||||
if (running) pauseThreads();
|
||||
}
|
||||
|
||||
synchronized void notifyFinished() {
|
||||
if (errCode > ERROR_NOTHING) return;
|
||||
|
||||
finishCount++;
|
||||
|
||||
if (blocks.length < 1 || threads == null || finishCount == threads.length) {
|
||||
if (errCode != ERROR_NOTHING) return;
|
||||
if (current < urls.length) {
|
||||
if (++finishCount < threads.length) return;
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onFinish: " + (current + 1) + "/" + urls.length);
|
||||
}
|
||||
|
||||
if ((current + 1) < urls.length) {
|
||||
// prepare next sub-mission
|
||||
long current_offset = offsets[current++];
|
||||
offsets[current] = current_offset + length;
|
||||
initializer();
|
||||
return;
|
||||
Log.d(TAG, "onFinish: downloaded " + (current + 1) + "/" + urls.length);
|
||||
}
|
||||
|
||||
current++;
|
||||
unknownLength = false;
|
||||
|
||||
if (!doPostprocessing()) return;
|
||||
|
||||
enqueued = false;
|
||||
running = false;
|
||||
deleteThisFromFile();
|
||||
|
||||
notify(DownloadManagerService.MESSAGE_FINISHED);
|
||||
if (current < urls.length) {
|
||||
// prepare next sub-mission
|
||||
offsets[current] = offsets[current - 1] + length;
|
||||
initializer();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (psAlgorithm != null && psState == 0) {
|
||||
threads = new Thread[]{
|
||||
runAsync(1, this::doPostprocessing)
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// this mission is fully finished
|
||||
|
||||
unknownLength = false;
|
||||
enqueued = false;
|
||||
running = false;
|
||||
|
||||
deleteThisFromFile();
|
||||
notify(DownloadManagerService.MESSAGE_FINISHED);
|
||||
}
|
||||
|
||||
private void notifyPostProcessing(int state) {
|
||||
@@ -397,10 +412,15 @@ public class DownloadMission extends Mission {
|
||||
|
||||
Log.d(TAG, action + " postprocessing on " + storage.getName());
|
||||
|
||||
if (state == 2) {
|
||||
psState = state;
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (LOCK) {
|
||||
// don't return without fully write the current state
|
||||
psState = state;
|
||||
Utility.writeToFile(metadata, DownloadMission.this);
|
||||
writeThisToFile();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,14 +429,10 @@ public class DownloadMission extends Mission {
|
||||
* Start downloading with multiple threads.
|
||||
*/
|
||||
public void start() {
|
||||
if (running || isFinished()) return;
|
||||
if (running || isFinished() || urls.length < 1) return;
|
||||
|
||||
// ensure that the previous state is completely paused.
|
||||
joinForThread(init);
|
||||
if (threads != null) {
|
||||
for (Thread thread : threads) joinForThread(thread);
|
||||
threads = null;
|
||||
}
|
||||
joinForThreads(10000);
|
||||
|
||||
running = true;
|
||||
errCode = ERROR_NOTHING;
|
||||
@@ -427,7 +443,14 @@ public class DownloadMission extends Mission {
|
||||
}
|
||||
|
||||
if (current >= urls.length) {
|
||||
runAsync(1, this::notifyFinished);
|
||||
notifyFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
notify(DownloadManagerService.MESSAGE_RUNNING);
|
||||
|
||||
if (urls[current] == null) {
|
||||
doRecover(ERROR_RESOURCE_GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -441,18 +464,13 @@ public class DownloadMission extends Mission {
|
||||
blockAcquired = new boolean[blocks.length];
|
||||
|
||||
if (blocks.length < 1) {
|
||||
if (unknownLength) {
|
||||
done = 0;
|
||||
length = 0;
|
||||
}
|
||||
|
||||
threads = new Thread[]{runAsync(1, new DownloadRunnableFallback(this))};
|
||||
} else {
|
||||
int remainingBlocks = 0;
|
||||
for (int block : blocks) if (block >= 0) remainingBlocks++;
|
||||
|
||||
if (remainingBlocks < 1) {
|
||||
runAsync(1, this::notifyFinished);
|
||||
notifyFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -478,7 +496,7 @@ public class DownloadMission extends Mission {
|
||||
}
|
||||
|
||||
running = false;
|
||||
recovered = true;
|
||||
notify(DownloadManagerService.MESSAGE_PAUSED);
|
||||
|
||||
if (init != null && init.isAlive()) {
|
||||
// NOTE: if start() method is running ¡will no have effect!
|
||||
@@ -493,29 +511,14 @@ public class DownloadMission extends Mission {
|
||||
Log.w(TAG, "pausing a download that can not be resumed (range requests not allowed by the server).");
|
||||
}
|
||||
|
||||
// check if the calling thread (alias UI thread) is interrupted
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
writeThisToFile();
|
||||
return;
|
||||
}
|
||||
|
||||
// wait for all threads are suspended before save the state
|
||||
if (threads != null) runAsync(-1, this::selfPause);
|
||||
init = null;
|
||||
pauseThreads();
|
||||
}
|
||||
|
||||
private void selfPause() {
|
||||
try {
|
||||
for (Thread thread : threads) {
|
||||
if (thread.isAlive()) {
|
||||
thread.interrupt();
|
||||
thread.join(5000);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// nothing to do
|
||||
} finally {
|
||||
writeThisToFile();
|
||||
}
|
||||
private void pauseThreads() {
|
||||
running = false;
|
||||
joinForThreads(-1);
|
||||
writeThisToFile();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -523,9 +526,10 @@ public class DownloadMission extends Mission {
|
||||
*/
|
||||
@Override
|
||||
public boolean delete() {
|
||||
deleted = true;
|
||||
if (psAlgorithm != null) psAlgorithm.cleanupTemporalDir();
|
||||
|
||||
notify(DownloadManagerService.MESSAGE_DELETED);
|
||||
|
||||
boolean res = deleteThisFromFile();
|
||||
|
||||
if (!super.delete()) return false;
|
||||
@@ -540,35 +544,37 @@ public class DownloadMission extends Mission {
|
||||
* @param persistChanges {@code true} to commit changes to the metadata file, otherwise, {@code false}
|
||||
*/
|
||||
public void resetState(boolean rollback, boolean persistChanges, int errorCode) {
|
||||
done = 0;
|
||||
length = 0;
|
||||
errCode = errorCode;
|
||||
errObject = null;
|
||||
unknownLength = false;
|
||||
threads = null;
|
||||
threads = new Thread[0];
|
||||
fallbackResumeOffset = 0;
|
||||
blocks = null;
|
||||
blockAcquired = null;
|
||||
|
||||
if (rollback) current = 0;
|
||||
|
||||
if (persistChanges)
|
||||
Utility.writeToFile(metadata, DownloadMission.this);
|
||||
if (persistChanges) writeThisToFile();
|
||||
}
|
||||
|
||||
private void initializer() {
|
||||
init = runAsync(DownloadInitializer.mId, new DownloadInitializer(this));
|
||||
}
|
||||
|
||||
private void writeThisToFileAsync() {
|
||||
runAsync(-2, this::writeThisToFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write this {@link DownloadMission} to the meta file asynchronously
|
||||
* if no thread is already running.
|
||||
*/
|
||||
private void writeThisToFile() {
|
||||
void writeThisToFile() {
|
||||
synchronized (LOCK) {
|
||||
if (deleted) return;
|
||||
Utility.writeToFile(metadata, DownloadMission.this);
|
||||
if (metadata == null) return;
|
||||
Utility.writeToFile(metadata, this);
|
||||
writingToFile = false;
|
||||
}
|
||||
mWritingToFile = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -621,11 +627,10 @@ public class DownloadMission extends Mission {
|
||||
public long getLength() {
|
||||
long calculated;
|
||||
if (psState == 1 || psState == 3) {
|
||||
calculated = length;
|
||||
} else {
|
||||
calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length;
|
||||
return length;
|
||||
}
|
||||
|
||||
calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length;
|
||||
calculated -= offsets[0];// don't count reserved space
|
||||
|
||||
return calculated > nearLength ? calculated : nearLength;
|
||||
@@ -638,7 +643,7 @@ public class DownloadMission extends Mission {
|
||||
*/
|
||||
public void setEnqueued(boolean queue) {
|
||||
enqueued = queue;
|
||||
runAsync(-2, this::writeThisToFile);
|
||||
writeThisToFileAsync();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -667,24 +672,29 @@ public class DownloadMission extends Mission {
|
||||
* @return {@code true} is this mission its "healthy", otherwise, {@code false}
|
||||
*/
|
||||
public boolean isCorrupt() {
|
||||
if (urls.length < 1) return false;
|
||||
return (isPsFailed() || errCode == ERROR_POSTPROCESSING_HOLD) || isFinished();
|
||||
}
|
||||
|
||||
private boolean doPostprocessing() {
|
||||
if (psAlgorithm == null || psState == 2) return true;
|
||||
/**
|
||||
* Indicates if mission urls has expired and there an attempt to renovate them
|
||||
*
|
||||
* @return {@code true} if the mission is running a recovery procedure, otherwise, {@code false}
|
||||
*/
|
||||
public boolean isRecovering() {
|
||||
return threads.length > 0 && threads[0] instanceof DownloadMissionRecover && threads[0].isAlive();
|
||||
}
|
||||
|
||||
private void doPostprocessing() {
|
||||
errCode = ERROR_NOTHING;
|
||||
errObject = null;
|
||||
Thread thread = Thread.currentThread();
|
||||
|
||||
notifyPostProcessing(1);
|
||||
notifyProgress(0);
|
||||
|
||||
if (DEBUG)
|
||||
Thread.currentThread().setName("[" + TAG + "] ps = " +
|
||||
psAlgorithm.getClass().getSimpleName() +
|
||||
" filename = " + storage.getName()
|
||||
);
|
||||
|
||||
threads = new Thread[]{Thread.currentThread()};
|
||||
if (DEBUG) {
|
||||
thread.setName("[" + TAG + "] ps = " + psAlgorithm + " filename = " + storage.getName());
|
||||
}
|
||||
|
||||
Exception exception = null;
|
||||
|
||||
@@ -693,6 +703,11 @@ public class DownloadMission extends Mission {
|
||||
} catch (Exception err) {
|
||||
Log.e(TAG, "Post-processing failed. " + psAlgorithm.toString(), err);
|
||||
|
||||
if (err instanceof InterruptedIOException || err instanceof ClosedByInterruptException || thread.isInterrupted()) {
|
||||
notifyError(DownloadMission.ERROR_POSTPROCESSING_STOPPED, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (errCode == ERROR_NOTHING) errCode = ERROR_POSTPROCESSING;
|
||||
|
||||
exception = err;
|
||||
@@ -703,16 +718,38 @@ public class DownloadMission extends Mission {
|
||||
if (errCode != ERROR_NOTHING) {
|
||||
if (exception == null) exception = errObject;
|
||||
notifyError(ERROR_POSTPROCESSING, exception);
|
||||
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
return true;
|
||||
notifyFinished();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to recover the download
|
||||
*
|
||||
* @param errorCode error code which trigger the recovery procedure
|
||||
*/
|
||||
void doRecover(int errorCode) {
|
||||
Log.i(TAG, "Attempting to recover the mission: " + storage.getName());
|
||||
|
||||
if (recoveryInfo == null) {
|
||||
notifyError(errorCode, null);
|
||||
urls = new String[0];// mark this mission as dead
|
||||
return;
|
||||
}
|
||||
|
||||
joinForThreads(0);
|
||||
|
||||
threads = new Thread[]{
|
||||
runAsync(DownloadMissionRecover.mID, new DownloadMissionRecover(this, errorCode))
|
||||
};
|
||||
}
|
||||
|
||||
private boolean deleteThisFromFile() {
|
||||
synchronized (LOCK) {
|
||||
return metadata.delete();
|
||||
boolean res = metadata.delete();
|
||||
metadata = null;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -722,8 +759,8 @@ public class DownloadMission extends Mission {
|
||||
* @param id id of new thread (used for debugging only)
|
||||
* @param who the Runnable whose {@code run} method is invoked.
|
||||
*/
|
||||
private void runAsync(int id, Runnable who) {
|
||||
runAsync(id, new Thread(who));
|
||||
private Thread runAsync(int id, Runnable who) {
|
||||
return runAsync(id, new Thread(who));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -749,25 +786,47 @@ public class DownloadMission extends Mission {
|
||||
return who;
|
||||
}
|
||||
|
||||
private void joinForThread(Thread thread) {
|
||||
if (thread == null || !thread.isAlive()) return;
|
||||
if (thread == Thread.currentThread()) return;
|
||||
/**
|
||||
* Waits at most {@code millis} milliseconds for the thread to die
|
||||
*
|
||||
* @param millis the time to wait in milliseconds
|
||||
*/
|
||||
private void joinForThreads(int millis) {
|
||||
final Thread currentThread = Thread.currentThread();
|
||||
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "a thread is !still alive!: " + thread.getName());
|
||||
if (init != null && init != currentThread && init.isAlive()) {
|
||||
init.interrupt();
|
||||
|
||||
if (millis > 0) {
|
||||
try {
|
||||
init.join(millis);
|
||||
} catch (InterruptedException e) {
|
||||
Log.w(TAG, "Initializer thread is still running", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// still alive, this should not happen.
|
||||
// Possible reasons:
|
||||
// if a thread is still alive, possible reasons:
|
||||
// slow device
|
||||
// the user is spamming start/pause buttons
|
||||
// start() method called quickly after pause()
|
||||
|
||||
for (Thread thread : threads) {
|
||||
if (!thread.isAlive() || thread == Thread.currentThread()) continue;
|
||||
thread.interrupt();
|
||||
}
|
||||
|
||||
try {
|
||||
thread.join(10000);
|
||||
for (Thread thread : threads) {
|
||||
if (!thread.isAlive()) continue;
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "thread alive: " + thread.getName());
|
||||
}
|
||||
if (millis > 0) thread.join(millis);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Log.d(TAG, "timeout on join : " + thread.getName());
|
||||
throw new RuntimeException("A thread is still running:\n" + thread.getName());
|
||||
throw new RuntimeException("A download thread is still running", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -785,9 +844,9 @@ public class DownloadMission extends Mission {
|
||||
}
|
||||
}
|
||||
|
||||
static class Block {
|
||||
int position;
|
||||
int done;
|
||||
public static class Block {
|
||||
public int position;
|
||||
public int done;
|
||||
}
|
||||
|
||||
private static class Lock implements Serializable {
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
package us.shandian.giga.get;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
import java.util.List;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission.HttpError;
|
||||
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_RESOURCE_GONE;
|
||||
|
||||
public class DownloadMissionRecover extends Thread {
|
||||
private static final String TAG = "DownloadMissionRecover";
|
||||
static final int mID = -3;
|
||||
|
||||
private final DownloadMission mMission;
|
||||
private final boolean mNotInitialized;
|
||||
|
||||
private final int mErrCode;
|
||||
|
||||
private HttpURLConnection mConn;
|
||||
private MissionRecoveryInfo mRecovery;
|
||||
private StreamExtractor mExtractor;
|
||||
|
||||
DownloadMissionRecover(DownloadMission mission, int errCode) {
|
||||
mMission = mission;
|
||||
mNotInitialized = mission.blocks == null && mission.current == 0;
|
||||
mErrCode = errCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (mMission.source == null) {
|
||||
mMission.notifyError(mErrCode, null);
|
||||
return;
|
||||
}
|
||||
|
||||
Exception err = null;
|
||||
int attempt = 0;
|
||||
|
||||
while (attempt++ < mMission.maxRetry) {
|
||||
try {
|
||||
tryRecover();
|
||||
return;
|
||||
} catch (InterruptedIOException | ClosedByInterruptException e) {
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
if (!mMission.running || super.isInterrupted()) return;
|
||||
err = e;
|
||||
}
|
||||
}
|
||||
|
||||
// give up
|
||||
mMission.notifyError(mErrCode, err);
|
||||
}
|
||||
|
||||
private void tryRecover() throws ExtractionException, IOException, HttpError {
|
||||
if (mExtractor == null) {
|
||||
try {
|
||||
StreamingService svr = NewPipe.getServiceByUrl(mMission.source);
|
||||
mExtractor = svr.getStreamExtractor(mMission.source);
|
||||
mExtractor.fetchPage();
|
||||
} catch (ExtractionException e) {
|
||||
mExtractor = null;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// maybe the following check is redundant
|
||||
if (!mMission.running || super.isInterrupted()) return;
|
||||
|
||||
if (!mNotInitialized) {
|
||||
// set the current download url to null in case if the recovery
|
||||
// process is canceled. Next time start() method is called the
|
||||
// recovery will be executed, saving time
|
||||
mMission.urls[mMission.current] = null;
|
||||
|
||||
mRecovery = mMission.recoveryInfo[mMission.current];
|
||||
resolveStream();
|
||||
return;
|
||||
}
|
||||
|
||||
Log.w(TAG, "mission is not fully initialized, this will take a while");
|
||||
|
||||
try {
|
||||
for (; mMission.current < mMission.urls.length; mMission.current++) {
|
||||
mRecovery = mMission.recoveryInfo[mMission.current];
|
||||
|
||||
if (test()) continue;
|
||||
if (!mMission.running) return;
|
||||
|
||||
resolveStream();
|
||||
if (!mMission.running) return;
|
||||
|
||||
// before continue, check if the current stream was resolved
|
||||
if (mMission.urls[mMission.current] == null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
mMission.current = 0;
|
||||
}
|
||||
|
||||
mMission.writeThisToFile();
|
||||
|
||||
if (!mMission.running || super.isInterrupted()) return;
|
||||
|
||||
mMission.running = false;
|
||||
mMission.start();
|
||||
}
|
||||
|
||||
private void resolveStream() throws IOException, ExtractionException, HttpError {
|
||||
// FIXME: this getErrorMessage() always returns "video is unavailable"
|
||||
/*if (mExtractor.getErrorMessage() != null) {
|
||||
mMission.notifyError(mErrCode, new ExtractionException(mExtractor.getErrorMessage()));
|
||||
return;
|
||||
}*/
|
||||
|
||||
String url = null;
|
||||
|
||||
switch (mRecovery.kind) {
|
||||
case 'a':
|
||||
for (AudioStream audio : mExtractor.getAudioStreams()) {
|
||||
if (audio.average_bitrate == mRecovery.desiredBitrate && audio.getFormat() == mRecovery.format) {
|
||||
url = audio.getUrl();
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'v':
|
||||
List<VideoStream> videoStreams;
|
||||
if (mRecovery.desired2)
|
||||
videoStreams = mExtractor.getVideoOnlyStreams();
|
||||
else
|
||||
videoStreams = mExtractor.getVideoStreams();
|
||||
for (VideoStream video : videoStreams) {
|
||||
if (video.resolution.equals(mRecovery.desired) && video.getFormat() == mRecovery.format) {
|
||||
url = video.getUrl();
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 's':
|
||||
for (SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery.format)) {
|
||||
String tag = subtitles.getLanguageTag();
|
||||
if (tag.equals(mRecovery.desired) && subtitles.isAutoGenerated() == mRecovery.desired2) {
|
||||
url = subtitles.getURL();
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Unknown stream type");
|
||||
}
|
||||
|
||||
resolve(url);
|
||||
}
|
||||
|
||||
private void resolve(String url) throws IOException, HttpError {
|
||||
if (mRecovery.validateCondition == null) {
|
||||
Log.w(TAG, "validation condition not defined, the resource can be stale");
|
||||
}
|
||||
|
||||
if (mMission.unknownLength || mRecovery.validateCondition == null) {
|
||||
recover(url, false);
|
||||
return;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
////// Validate the http resource doing a range request
|
||||
/////////////////////
|
||||
try {
|
||||
mConn = mMission.openConnection(url, true, mMission.length - 10, mMission.length);
|
||||
mConn.setRequestProperty("If-Range", mRecovery.validateCondition);
|
||||
mMission.establishConnection(mID, mConn);
|
||||
|
||||
int code = mConn.getResponseCode();
|
||||
|
||||
switch (code) {
|
||||
case 200:
|
||||
case 413:
|
||||
// stale
|
||||
recover(url, true);
|
||||
return;
|
||||
case 206:
|
||||
// in case of validation using the Last-Modified date, check the resource length
|
||||
long[] contentRange = parseContentRange(mConn.getHeaderField("Content-Range"));
|
||||
boolean lengthMismatch = contentRange[2] != -1 && contentRange[2] != mMission.length;
|
||||
|
||||
recover(url, lengthMismatch);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new HttpError(code);
|
||||
} finally {
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private void recover(String url, boolean stale) {
|
||||
Log.i(TAG,
|
||||
String.format("recover() name=%s isStale=%s url=%s", mMission.storage.getName(), stale, url)
|
||||
);
|
||||
|
||||
mMission.urls[mMission.current] = url;
|
||||
|
||||
if (url == null) {
|
||||
mMission.urls = new String[0];
|
||||
mMission.notifyError(ERROR_RESOURCE_GONE, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mNotInitialized) return;
|
||||
|
||||
if (stale) {
|
||||
mMission.resetState(false, false, DownloadMission.ERROR_NOTHING);
|
||||
}
|
||||
|
||||
mMission.writeThisToFile();
|
||||
|
||||
if (!mMission.running || super.isInterrupted()) return;
|
||||
|
||||
mMission.running = false;
|
||||
mMission.start();
|
||||
}
|
||||
|
||||
private long[] parseContentRange(String value) {
|
||||
long[] range = new long[3];
|
||||
|
||||
if (value == null) {
|
||||
// this never should happen
|
||||
return range;
|
||||
}
|
||||
|
||||
try {
|
||||
value = value.trim();
|
||||
|
||||
if (!value.startsWith("bytes")) {
|
||||
return range;// unknown range type
|
||||
}
|
||||
|
||||
int space = value.lastIndexOf(' ') + 1;
|
||||
int dash = value.indexOf('-', space) + 1;
|
||||
int bar = value.indexOf('/', dash);
|
||||
|
||||
// start
|
||||
range[0] = Long.parseLong(value.substring(space, dash - 1));
|
||||
|
||||
// end
|
||||
range[1] = Long.parseLong(value.substring(dash, bar));
|
||||
|
||||
// resource length
|
||||
value = value.substring(bar + 1);
|
||||
if (value.equals("*")) {
|
||||
range[2] = -1;// unknown length received from the server but should be valid
|
||||
} else {
|
||||
range[2] = Long.parseLong(value);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
private boolean test() {
|
||||
if (mMission.urls[mMission.current] == null) return false;
|
||||
|
||||
try {
|
||||
mConn = mMission.openConnection(mMission.urls[mMission.current], true, -1, -1);
|
||||
mMission.establishConnection(mID, mConn);
|
||||
|
||||
if (mConn.getResponseCode() == 200) return true;
|
||||
} catch (Exception e) {
|
||||
// nothing to do
|
||||
} finally {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void disconnect() {
|
||||
try {
|
||||
try {
|
||||
mConn.getInputStream().close();
|
||||
} finally {
|
||||
mConn.disconnect();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// nothing to do
|
||||
} finally {
|
||||
mConn = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void interrupt() {
|
||||
super.interrupt();
|
||||
if (mConn != null) disconnect();
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,10 @@ import java.net.HttpURLConnection;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission.Block;
|
||||
import us.shandian.giga.get.DownloadMission.HttpError;
|
||||
|
||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_FORBIDDEN;
|
||||
|
||||
|
||||
/**
|
||||
@@ -19,7 +21,7 @@ import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
* an error occurs or the process is stopped.
|
||||
*/
|
||||
public class DownloadRunnable extends Thread {
|
||||
private static final String TAG = DownloadRunnable.class.getSimpleName();
|
||||
private static final String TAG = "DownloadRunnable";
|
||||
|
||||
private final DownloadMission mMission;
|
||||
private final int mId;
|
||||
@@ -41,13 +43,7 @@ public class DownloadRunnable extends Thread {
|
||||
public void run() {
|
||||
boolean retry = false;
|
||||
Block block = null;
|
||||
|
||||
int retryCount = 0;
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, mId + ":recovered: " + mMission.recovered);
|
||||
}
|
||||
|
||||
SharpStream f;
|
||||
|
||||
try {
|
||||
@@ -84,13 +80,14 @@ public class DownloadRunnable extends Thread {
|
||||
}
|
||||
|
||||
try {
|
||||
mConn = mMission.openConnection(mId, start, end);
|
||||
mConn = mMission.openConnection(false, start, end);
|
||||
mMission.establishConnection(mId, mConn);
|
||||
|
||||
// check if the download can be resumed
|
||||
if (mConn.getResponseCode() == 416) {
|
||||
if (block.done > 0) {
|
||||
// try again from the start (of the block)
|
||||
mMission.notifyProgress(-block.done);
|
||||
block.done = 0;
|
||||
retry = true;
|
||||
mConn.disconnect();
|
||||
@@ -118,7 +115,7 @@ public class DownloadRunnable extends Thread {
|
||||
int len;
|
||||
|
||||
// use always start <= end
|
||||
// fixes a deadlock in DownloadRunnable because youtube is sending one byte alone after downloading 26MiB exactly
|
||||
// fixes a deadlock because in some videos, youtube is sending one byte alone
|
||||
while (start <= end && mMission.running && (len = is.read(buf, 0, buf.length)) != -1) {
|
||||
f.write(buf, 0, len);
|
||||
start += len;
|
||||
@@ -133,6 +130,17 @@ public class DownloadRunnable extends Thread {
|
||||
} catch (Exception e) {
|
||||
if (!mMission.running || e instanceof ClosedByInterruptException) break;
|
||||
|
||||
if (e instanceof HttpError && ((HttpError) e).statusCode == ERROR_HTTP_FORBIDDEN) {
|
||||
// for youtube streams. The url has expired, recover
|
||||
f.close();
|
||||
|
||||
if (mId == 1) {
|
||||
// only the first thread will execute the recovery procedure
|
||||
mMission.doRecover(ERROR_HTTP_FORBIDDEN);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (retryCount++ >= mMission.maxRetry) {
|
||||
mMission.notifyError(e);
|
||||
break;
|
||||
@@ -144,11 +152,7 @@ public class DownloadRunnable extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
f.close();
|
||||
} catch (Exception err) {
|
||||
// ¿ejected media storage? ¿file deleted? ¿storage ran out of space?
|
||||
}
|
||||
f.close();
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "thread " + mId + " exited from main download loop");
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package us.shandian.giga.get;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -10,9 +11,11 @@ import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission.HttpError;
|
||||
import us.shandian.giga.util.Utility;
|
||||
|
||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_FORBIDDEN;
|
||||
|
||||
/**
|
||||
* Single-threaded fallback mode
|
||||
@@ -33,7 +36,11 @@ public class DownloadRunnableFallback extends Thread {
|
||||
|
||||
private void dispose() {
|
||||
try {
|
||||
if (mIs != null) mIs.close();
|
||||
try {
|
||||
if (mIs != null) mIs.close();
|
||||
} finally {
|
||||
mConn.disconnect();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// nothing to do
|
||||
}
|
||||
@@ -41,22 +48,10 @@ public class DownloadRunnableFallback extends Thread {
|
||||
if (mF != null) mF.close();
|
||||
}
|
||||
|
||||
private long loadPosition() {
|
||||
synchronized (mMission.LOCK) {
|
||||
return mMission.fallbackResumeOffset;
|
||||
}
|
||||
}
|
||||
|
||||
private void savePosition(long position) {
|
||||
synchronized (mMission.LOCK) {
|
||||
mMission.fallbackResumeOffset = position;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
boolean done;
|
||||
long start = loadPosition();
|
||||
long start = mMission.fallbackResumeOffset;
|
||||
|
||||
if (DEBUG && !mMission.unknownLength && start > 0) {
|
||||
Log.i(TAG, "Resuming a single-thread download at " + start);
|
||||
@@ -66,11 +61,18 @@ public class DownloadRunnableFallback extends Thread {
|
||||
long rangeStart = (mMission.unknownLength || start < 1) ? -1 : start;
|
||||
|
||||
int mId = 1;
|
||||
mConn = mMission.openConnection(mId, rangeStart, -1);
|
||||
mConn = mMission.openConnection(false, rangeStart, -1);
|
||||
|
||||
if (mRetryCount == 0 && rangeStart == -1) {
|
||||
// workaround: bypass android connection pool
|
||||
mConn.setRequestProperty("Range", "bytes=0-");
|
||||
}
|
||||
|
||||
mMission.establishConnection(mId, mConn);
|
||||
|
||||
// check if the download can be resumed
|
||||
if (mConn.getResponseCode() == 416 && start > 0) {
|
||||
mMission.notifyProgress(-start);
|
||||
start = 0;
|
||||
mRetryCount--;
|
||||
throw new DownloadMission.HttpError(416);
|
||||
@@ -80,12 +82,17 @@ public class DownloadRunnableFallback extends Thread {
|
||||
if (!mMission.unknownLength)
|
||||
mMission.unknownLength = Utility.getContentLength(mConn) == -1;
|
||||
|
||||
if (mMission.unknownLength || mConn.getResponseCode() == 200) {
|
||||
// restart amount of bytes downloaded
|
||||
mMission.done = mMission.offsets[mMission.current] - mMission.offsets[0];
|
||||
}
|
||||
|
||||
mF = mMission.storage.getStream();
|
||||
mF.seek(mMission.offsets[mMission.current] + start);
|
||||
|
||||
mIs = mConn.getInputStream();
|
||||
|
||||
byte[] buf = new byte[64 * 1024];
|
||||
byte[] buf = new byte[DownloadMission.BUFFER_SIZE];
|
||||
int len = 0;
|
||||
|
||||
while (mMission.running && (len = mIs.read(buf, 0, buf.length)) != -1) {
|
||||
@@ -94,15 +101,24 @@ public class DownloadRunnableFallback extends Thread {
|
||||
mMission.notifyProgress(len);
|
||||
}
|
||||
|
||||
dispose();
|
||||
|
||||
// if thread goes interrupted check if the last part is written. This avoid re-download the whole file
|
||||
done = len == -1;
|
||||
} catch (Exception e) {
|
||||
dispose();
|
||||
|
||||
savePosition(start);
|
||||
mMission.fallbackResumeOffset = start;
|
||||
|
||||
if (!mMission.running || e instanceof ClosedByInterruptException) return;
|
||||
|
||||
if (e instanceof HttpError && ((HttpError) e).statusCode == ERROR_HTTP_FORBIDDEN) {
|
||||
// for youtube streams. The url has expired, recover
|
||||
dispose();
|
||||
mMission.doRecover(ERROR_HTTP_FORBIDDEN);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mRetryCount++ >= mMission.maxRetry) {
|
||||
mMission.notifyError(e);
|
||||
return;
|
||||
@@ -116,12 +132,10 @@ public class DownloadRunnableFallback extends Thread {
|
||||
return;
|
||||
}
|
||||
|
||||
dispose();
|
||||
|
||||
if (done) {
|
||||
mMission.notifyFinished();
|
||||
} else {
|
||||
savePosition(start);
|
||||
mMission.fallbackResumeOffset = start;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,17 +2,17 @@ package us.shandian.giga.get;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class FinishedMission extends Mission {
|
||||
public class FinishedMission extends Mission {
|
||||
|
||||
public FinishedMission() {
|
||||
}
|
||||
|
||||
public FinishedMission(@NonNull DownloadMission mission) {
|
||||
source = mission.source;
|
||||
length = mission.length;// ¿or mission.done?
|
||||
length = mission.length;
|
||||
timestamp = mission.timestamp;
|
||||
kind = mission.kind;
|
||||
storage = mission.storage;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
115
app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.java
Normal file
115
app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.java
Normal file
@@ -0,0 +1,115 @@
|
||||
package us.shandian.giga.get;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.Stream;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class MissionRecoveryInfo implements Serializable, Parcelable {
|
||||
private static final long serialVersionUID = 0L;
|
||||
|
||||
MediaFormat format;
|
||||
String desired;
|
||||
boolean desired2;
|
||||
int desiredBitrate;
|
||||
byte kind;
|
||||
String validateCondition = null;
|
||||
|
||||
public MissionRecoveryInfo(@NonNull Stream stream) {
|
||||
if (stream instanceof AudioStream) {
|
||||
desiredBitrate = ((AudioStream) stream).average_bitrate;
|
||||
desired2 = false;
|
||||
kind = 'a';
|
||||
} else if (stream instanceof VideoStream) {
|
||||
desired = ((VideoStream) stream).getResolution();
|
||||
desired2 = ((VideoStream) stream).isVideoOnly();
|
||||
kind = 'v';
|
||||
} else if (stream instanceof SubtitlesStream) {
|
||||
desired = ((SubtitlesStream) stream).getLanguageTag();
|
||||
desired2 = ((SubtitlesStream) stream).isAutoGenerated();
|
||||
kind = 's';
|
||||
} else {
|
||||
throw new RuntimeException("Unknown stream kind");
|
||||
}
|
||||
|
||||
format = stream.getFormat();
|
||||
if (format == null) throw new NullPointerException("Stream format cannot be null");
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
String info;
|
||||
StringBuilder str = new StringBuilder();
|
||||
str.append("{type=");
|
||||
switch (kind) {
|
||||
case 'a':
|
||||
str.append("audio");
|
||||
info = "bitrate=" + desiredBitrate;
|
||||
break;
|
||||
case 'v':
|
||||
str.append("video");
|
||||
info = "quality=" + desired + " videoOnly=" + desired2;
|
||||
break;
|
||||
case 's':
|
||||
str.append("subtitles");
|
||||
info = "language=" + desired + " autoGenerated=" + desired2;
|
||||
break;
|
||||
default:
|
||||
info = "";
|
||||
str.append("other");
|
||||
}
|
||||
|
||||
str.append(" format=")
|
||||
.append(format.getName())
|
||||
.append(' ')
|
||||
.append(info)
|
||||
.append('}');
|
||||
|
||||
return str.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel parcel, int flags) {
|
||||
parcel.writeInt(this.format.ordinal());
|
||||
parcel.writeString(this.desired);
|
||||
parcel.writeInt(this.desired2 ? 0x01 : 0x00);
|
||||
parcel.writeInt(this.desiredBitrate);
|
||||
parcel.writeByte(this.kind);
|
||||
parcel.writeString(this.validateCondition);
|
||||
}
|
||||
|
||||
private MissionRecoveryInfo(Parcel parcel) {
|
||||
this.format = MediaFormat.values()[parcel.readInt()];
|
||||
this.desired = parcel.readString();
|
||||
this.desired2 = parcel.readInt() != 0x00;
|
||||
this.desiredBitrate = parcel.readInt();
|
||||
this.kind = parcel.readByte();
|
||||
this.validateCondition = parcel.readString();
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<MissionRecoveryInfo> CREATOR = new Parcelable.Creator<MissionRecoveryInfo>() {
|
||||
@Override
|
||||
public MissionRecoveryInfo createFromParcel(Parcel source) {
|
||||
return new MissionRecoveryInfo(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MissionRecoveryInfo[] newArray(int size) {
|
||||
return new MissionRecoveryInfo[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -5,21 +5,23 @@ import org.schabi.newpipe.streams.io.SharpStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class ChunkFileInputStream extends SharpStream {
|
||||
private static final int REPORT_INTERVAL = 256 * 1024;
|
||||
|
||||
private SharpStream source;
|
||||
private final long offset;
|
||||
private final long length;
|
||||
private long position;
|
||||
|
||||
public ChunkFileInputStream(SharpStream target, long start) throws IOException {
|
||||
this(target, start, target.length());
|
||||
}
|
||||
private long progressReport;
|
||||
private final ProgressReport onProgress;
|
||||
|
||||
public ChunkFileInputStream(SharpStream target, long start, long end) throws IOException {
|
||||
public ChunkFileInputStream(SharpStream target, long start, long end, ProgressReport callback) throws IOException {
|
||||
source = target;
|
||||
offset = start;
|
||||
length = end - start;
|
||||
position = 0;
|
||||
onProgress = callback;
|
||||
progressReport = REPORT_INTERVAL;
|
||||
|
||||
if (length < 1) {
|
||||
source.close();
|
||||
@@ -60,12 +62,12 @@ public class ChunkFileInputStream extends SharpStream {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte b[]) throws IOException {
|
||||
public int read(byte[] b) throws IOException {
|
||||
return read(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte b[], int off, int len) throws IOException {
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
if ((position + len) > length) {
|
||||
len = (int) (length - position);
|
||||
}
|
||||
@@ -76,6 +78,11 @@ public class ChunkFileInputStream extends SharpStream {
|
||||
int res = source.read(b, off, len);
|
||||
position += res;
|
||||
|
||||
if (onProgress != null && position > progressReport) {
|
||||
onProgress.report(position);
|
||||
progressReport = position + REPORT_INTERVAL;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
@@ -174,12 +174,12 @@ public class CircularFileWriter extends SharpStream {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte b[]) throws IOException {
|
||||
public void write(byte[] b) throws IOException {
|
||||
write(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte b[], int off, int len) throws IOException {
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
if (len == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -261,7 +261,7 @@ public class CircularFileWriter extends SharpStream {
|
||||
@Override
|
||||
public void rewind() throws IOException {
|
||||
if (onProgress != null) {
|
||||
onProgress.report(-out.length - aux.length);// rollback the whole progress
|
||||
onProgress.report(0);// rollback the whole progress
|
||||
}
|
||||
|
||||
seek(0);
|
||||
@@ -357,16 +357,6 @@ public class CircularFileWriter extends SharpStream {
|
||||
long check();
|
||||
}
|
||||
|
||||
public interface ProgressReport {
|
||||
|
||||
/**
|
||||
* Report the size of the new file
|
||||
*
|
||||
* @param progress the new size
|
||||
*/
|
||||
void report(long progress);
|
||||
}
|
||||
|
||||
public interface WriteErrorHandle {
|
||||
|
||||
/**
|
||||
@@ -381,10 +371,10 @@ public class CircularFileWriter extends SharpStream {
|
||||
|
||||
class BufferedFile {
|
||||
|
||||
protected final SharpStream target;
|
||||
final SharpStream target;
|
||||
|
||||
private long offset;
|
||||
protected long length;
|
||||
long length;
|
||||
|
||||
private byte[] queue = new byte[QUEUE_BUFFER_SIZE];
|
||||
private int queueSize;
|
||||
@@ -397,16 +387,16 @@ public class CircularFileWriter extends SharpStream {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
protected long getOffset() {
|
||||
long getOffset() {
|
||||
return offset + queueSize;// absolute offset in the file
|
||||
}
|
||||
|
||||
protected void close() {
|
||||
void close() {
|
||||
queue = null;
|
||||
target.close();
|
||||
}
|
||||
|
||||
protected void write(byte b[], int off, int len) throws IOException {
|
||||
void write(byte[] b, int off, int len) throws IOException {
|
||||
while (len > 0) {
|
||||
// if the queue is full, the method available() will flush the queue
|
||||
int read = Math.min(available(), len);
|
||||
@@ -436,7 +426,7 @@ public class CircularFileWriter extends SharpStream {
|
||||
target.seek(0);
|
||||
}
|
||||
|
||||
protected int available() throws IOException {
|
||||
int available() throws IOException {
|
||||
if (queueSize >= queue.length) {
|
||||
flush();
|
||||
return queue.length;
|
||||
@@ -451,7 +441,7 @@ public class CircularFileWriter extends SharpStream {
|
||||
target.seek(0);
|
||||
}
|
||||
|
||||
protected void seek(long absoluteOffset) throws IOException {
|
||||
void seek(long absoluteOffset) throws IOException {
|
||||
if (absoluteOffset == offset) {
|
||||
return;// nothing to do
|
||||
}
|
||||
|
||||
11
app/src/main/java/us/shandian/giga/io/ProgressReport.java
Normal file
11
app/src/main/java/us/shandian/giga/io/ProgressReport.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package us.shandian.giga.io;
|
||||
|
||||
public interface ProgressReport {
|
||||
|
||||
/**
|
||||
* Report the size of the new file
|
||||
*
|
||||
* @param progress the new size
|
||||
*/
|
||||
void report(long progress);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package us.shandian.giga.postprocessing;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.OggFromWebMWriter;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
class OggFromWebmDemuxer extends Postprocessing {
|
||||
|
||||
OggFromWebmDemuxer() {
|
||||
super(true, true, ALGORITHM_OGG_FROM_WEBM_DEMUXER);
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean test(SharpStream... sources) throws IOException {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(4);
|
||||
sources[0].read(buffer.array());
|
||||
|
||||
// youtube uses WebM as container, but the file extension (format suffix) is "*.opus"
|
||||
// check if the file is a webm/mkv file before proceed
|
||||
|
||||
switch (buffer.getInt()) {
|
||||
case 0x1a45dfa3:
|
||||
return true;// webm/mkv
|
||||
case 0x4F676753:
|
||||
return false;// ogg
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException("file not recognized, failed to demux the audio stream");
|
||||
}
|
||||
|
||||
@Override
|
||||
int process(SharpStream out, @NonNull SharpStream... sources) throws IOException {
|
||||
OggFromWebMWriter demuxer = new OggFromWebMWriter(sources[0], out);
|
||||
demuxer.parseSource();
|
||||
demuxer.selectTrack(0);
|
||||
demuxer.build();
|
||||
|
||||
return OK_RESULT;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package us.shandian.giga.postprocessing;
|
||||
|
||||
import android.os.Message;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.File;
|
||||
@@ -14,11 +14,11 @@ import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.io.ChunkFileInputStream;
|
||||
import us.shandian.giga.io.CircularFileWriter;
|
||||
import us.shandian.giga.io.CircularFileWriter.OffsetChecker;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
import us.shandian.giga.io.ProgressReport;
|
||||
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
|
||||
|
||||
public abstract class Postprocessing implements Serializable {
|
||||
|
||||
@@ -28,6 +28,7 @@ public abstract class Postprocessing implements Serializable {
|
||||
public transient static final String ALGORITHM_WEBM_MUXER = "webm";
|
||||
public transient static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4";
|
||||
public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a";
|
||||
public transient static final String ALGORITHM_OGG_FROM_WEBM_DEMUXER = "webm-ogg-d";
|
||||
|
||||
public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args) {
|
||||
Postprocessing instance;
|
||||
@@ -45,6 +46,9 @@ public abstract class Postprocessing implements Serializable {
|
||||
case ALGORITHM_M4A_NO_DASH:
|
||||
instance = new M4aNoDash();
|
||||
break;
|
||||
case ALGORITHM_OGG_FROM_WEBM_DEMUXER:
|
||||
instance = new OggFromWebmDemuxer();
|
||||
break;
|
||||
/*case "example-algorithm":
|
||||
instance = new ExampleAlgorithm();*/
|
||||
default:
|
||||
@@ -59,24 +63,24 @@ public abstract class Postprocessing implements Serializable {
|
||||
* Get a boolean value that indicate if the given algorithm work on the same
|
||||
* file
|
||||
*/
|
||||
public final boolean worksOnSameFile;
|
||||
public boolean worksOnSameFile;
|
||||
|
||||
/**
|
||||
* Indicates whether the selected algorithm needs space reserved at the beginning of the file
|
||||
*/
|
||||
public final boolean reserveSpace;
|
||||
public boolean reserveSpace;
|
||||
|
||||
/**
|
||||
* Gets the given algorithm short name
|
||||
*/
|
||||
private final String name;
|
||||
private String name;
|
||||
|
||||
|
||||
private String[] args;
|
||||
|
||||
protected transient DownloadMission mission;
|
||||
private transient DownloadMission mission;
|
||||
|
||||
private File tempFile;
|
||||
private transient File tempFile;
|
||||
|
||||
Postprocessing(boolean reserveSpace, boolean worksOnSameFile, String algorithmName) {
|
||||
this.reserveSpace = reserveSpace;
|
||||
@@ -91,8 +95,12 @@ public abstract class Postprocessing implements Serializable {
|
||||
|
||||
public void cleanupTemporalDir() {
|
||||
if (tempFile != null && tempFile.exists()) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
tempFile.delete();
|
||||
try {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
tempFile.delete();
|
||||
} catch (Exception e) {
|
||||
// nothing to do
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,16 +113,24 @@ public abstract class Postprocessing implements Serializable {
|
||||
long finalLength = -1;
|
||||
|
||||
mission.done = 0;
|
||||
mission.length = mission.storage.length();
|
||||
|
||||
long length = mission.storage.length() - mission.offsets[0];
|
||||
mission.length = length > mission.nearLength ? length : mission.nearLength;
|
||||
|
||||
final ProgressReport readProgress = (long position) -> {
|
||||
position -= mission.offsets[0];
|
||||
if (position > mission.done) mission.done = position;
|
||||
};
|
||||
|
||||
if (worksOnSameFile) {
|
||||
ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length];
|
||||
try {
|
||||
int i = 0;
|
||||
for (; i < sources.length - 1; i++) {
|
||||
sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i], mission.offsets[i + 1]);
|
||||
for (int i = 0, j = 1; i < sources.length; i++, j++) {
|
||||
SharpStream source = mission.storage.getStream();
|
||||
long end = j < sources.length ? mission.offsets[j] : source.length();
|
||||
|
||||
sources[i] = new ChunkFileInputStream(source, mission.offsets[i], end, readProgress);
|
||||
}
|
||||
sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i]);
|
||||
|
||||
if (test(sources)) {
|
||||
for (SharpStream source : sources) source.rewind();
|
||||
@@ -136,7 +152,7 @@ public abstract class Postprocessing implements Serializable {
|
||||
};
|
||||
|
||||
out = new CircularFileWriter(mission.storage.getStream(), tempFile, checker);
|
||||
out.onProgress = this::progressReport;
|
||||
out.onProgress = (long position) -> mission.done = position;
|
||||
|
||||
out.onWriteError = (err) -> {
|
||||
mission.psState = 3;
|
||||
@@ -183,11 +199,10 @@ public abstract class Postprocessing implements Serializable {
|
||||
|
||||
if (result == OK_RESULT) {
|
||||
if (finalLength != -1) {
|
||||
mission.done = finalLength;
|
||||
mission.length = finalLength;
|
||||
}
|
||||
} else {
|
||||
mission.errCode = ERROR_UNKNOWN_EXCEPTION;
|
||||
mission.errCode = ERROR_POSTPROCESSING;
|
||||
mission.errObject = new RuntimeException("post-processing algorithm returned " + result);
|
||||
}
|
||||
|
||||
@@ -212,7 +227,7 @@ public abstract class Postprocessing implements Serializable {
|
||||
*
|
||||
* @param out output stream
|
||||
* @param sources files to be processed
|
||||
* @return a error code, 0 means the operation was successful
|
||||
* @return an error code, {@code OK_RESULT} means the operation was successful
|
||||
* @throws IOException if an I/O error occurs.
|
||||
*/
|
||||
abstract int process(SharpStream out, SharpStream... sources) throws IOException;
|
||||
@@ -225,23 +240,12 @@ public abstract class Postprocessing implements Serializable {
|
||||
return args[index];
|
||||
}
|
||||
|
||||
private void progressReport(long done) {
|
||||
mission.done = done;
|
||||
if (mission.length < mission.done) mission.length = mission.done;
|
||||
|
||||
Message m = new Message();
|
||||
m.what = DownloadManagerService.MESSAGE_PROGRESS;
|
||||
m.obj = mission;
|
||||
|
||||
mission.mHandler.sendMessage(m);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder str = new StringBuilder();
|
||||
|
||||
str.append("name=").append(name).append('[');
|
||||
str.append("{ name=").append(name).append('[');
|
||||
|
||||
if (args != null) {
|
||||
for (String arg : args) {
|
||||
@@ -251,6 +255,6 @@ public abstract class Postprocessing implements Serializable {
|
||||
str.delete(0, 1);
|
||||
}
|
||||
|
||||
return str.append(']').toString();
|
||||
return str.append("] }").toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,10 @@ package us.shandian.giga.postprocessing;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.streams.SubtitleConverter;
|
||||
import org.schabi.newpipe.streams.SrtFromTtmlWriter;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.ParseException;
|
||||
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.xpath.XPathExpressionException;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
@@ -27,33 +22,16 @@ class TtmlConverter extends Postprocessing {
|
||||
int process(SharpStream out, SharpStream... sources) throws IOException {
|
||||
// check if the subtitle is already in srt and copy, this should never happen
|
||||
String format = getArgumentAt(0, null);
|
||||
boolean ignoreEmptyFrames = getArgumentAt(1, "true").equals("true");
|
||||
|
||||
if (format == null || format.equals("ttml")) {
|
||||
SubtitleConverter ttmlDumper = new SubtitleConverter();
|
||||
SrtFromTtmlWriter writer = new SrtFromTtmlWriter(out, ignoreEmptyFrames);
|
||||
|
||||
try {
|
||||
ttmlDumper.dumpTTML(
|
||||
sources[0],
|
||||
out,
|
||||
getArgumentAt(1, "true").equals("true"),
|
||||
getArgumentAt(2, "true").equals("true")
|
||||
);
|
||||
writer.build(sources[0]);
|
||||
} catch (Exception err) {
|
||||
Log.e(TAG, "subtitle parse failed", err);
|
||||
|
||||
if (err instanceof IOException) {
|
||||
return 1;
|
||||
} else if (err instanceof ParseException) {
|
||||
return 2;
|
||||
} else if (err instanceof SAXException) {
|
||||
return 3;
|
||||
} else if (err instanceof ParserConfigurationException) {
|
||||
return 4;
|
||||
} else if (err instanceof XPathExpressionException) {
|
||||
return 7;
|
||||
}
|
||||
|
||||
return 8;
|
||||
return err instanceof IOException ? 1 : 8;
|
||||
}
|
||||
|
||||
return OK_RESULT;
|
||||
|
||||
@@ -2,13 +2,11 @@ package us.shandian.giga.service;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -37,6 +35,7 @@ public class DownloadManager {
|
||||
|
||||
public static final String TAG_AUDIO = "audio";
|
||||
public static final String TAG_VIDEO = "video";
|
||||
private static final String DOWNLOADS_METADATA_FOLDER = "pending_downloads";
|
||||
|
||||
private final FinishedMissionStore mFinishedMissionStore;
|
||||
|
||||
@@ -74,25 +73,35 @@ public class DownloadManager {
|
||||
mMissionsFinished = loadFinishedMissions();
|
||||
mPendingMissionsDir = getPendingDir(context);
|
||||
|
||||
if (!Utility.mkdir(mPendingMissionsDir, false)) {
|
||||
throw new RuntimeException("failed to create pending_downloads in data directory");
|
||||
}
|
||||
|
||||
loadPendingMissions(context);
|
||||
}
|
||||
|
||||
private static File getPendingDir(@NonNull Context context) {
|
||||
//File dir = new File(ContextCompat.getDataDir(context), "pending_downloads");
|
||||
File dir = context.getExternalFilesDir("pending_downloads");
|
||||
File dir = context.getExternalFilesDir(DOWNLOADS_METADATA_FOLDER);
|
||||
if (testDir(dir)) return dir;
|
||||
|
||||
if (dir == null) {
|
||||
// One of the following paths are not accessible ¿unmounted internal memory?
|
||||
// /storage/emulated/0/Android/data/org.schabi.newpipe[.debug]/pending_downloads
|
||||
// /sdcard/Android/data/org.schabi.newpipe[.debug]/pending_downloads
|
||||
Log.w(TAG, "path to pending downloads are not accessible");
|
||||
dir = new File(context.getFilesDir(), DOWNLOADS_METADATA_FOLDER);
|
||||
if (testDir(dir)) return dir;
|
||||
|
||||
throw new RuntimeException("path to pending downloads are not accessible");
|
||||
}
|
||||
|
||||
private static boolean testDir(@Nullable File dir) {
|
||||
if (dir == null) return false;
|
||||
|
||||
try {
|
||||
if (!Utility.mkdir(dir, false)) {
|
||||
Log.e(TAG, "testDir() cannot create the directory in path: " + dir.getAbsolutePath());
|
||||
return false;
|
||||
}
|
||||
|
||||
File tmp = new File(dir, ".tmp");
|
||||
if (!tmp.createNewFile()) return false;
|
||||
return tmp.delete();// if the file was created, SHOULD BE deleted too
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "testDir() failed: " + dir.getAbsolutePath(), e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return dir;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,8 +139,12 @@ public class DownloadManager {
|
||||
Log.d(TAG, "Loading pending downloads from directory: " + mPendingMissionsDir.getAbsolutePath());
|
||||
}
|
||||
|
||||
File tempDir = pickAvailableTemporalDir(ctx);
|
||||
Log.i(TAG, "using '" + tempDir + "' as temporal directory");
|
||||
|
||||
for (File sub : subs) {
|
||||
if (!sub.isFile()) continue;
|
||||
if (sub.getName().equals(".tmp")) continue;
|
||||
|
||||
DownloadMission mis = Utility.readFromFile(sub);
|
||||
if (mis == null || mis.isFinished()) {
|
||||
@@ -140,6 +153,8 @@ public class DownloadManager {
|
||||
continue;
|
||||
}
|
||||
|
||||
mis.threads = new Thread[0];
|
||||
|
||||
boolean exists;
|
||||
try {
|
||||
mis.storage = StoredFileHelper.deserialize(mis.storage, ctx);
|
||||
@@ -158,8 +173,6 @@ public class DownloadManager {
|
||||
// is Java IO (avoid showing the "Save as..." dialog)
|
||||
if (exists && mis.storage.isDirect() && !mis.storage.delete())
|
||||
Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath());
|
||||
|
||||
exists = true;
|
||||
}
|
||||
|
||||
mis.psState = 0;
|
||||
@@ -174,10 +187,9 @@ public class DownloadManager {
|
||||
|
||||
if (mis.psAlgorithm != null) {
|
||||
mis.psAlgorithm.cleanupTemporalDir();
|
||||
mis.psAlgorithm.setTemporalDir(pickAvailableTemporalDir(ctx));
|
||||
mis.psAlgorithm.setTemporalDir(tempDir);
|
||||
}
|
||||
|
||||
mis.recovered = exists;
|
||||
mis.metadata = sub;
|
||||
mis.maxRetry = mPrefMaxRetry;
|
||||
mis.mHandler = mHandler;
|
||||
@@ -232,7 +244,6 @@ public class DownloadManager {
|
||||
boolean start = !mPrefQueueLimit || getRunningMissionsCount() < 1;
|
||||
|
||||
if (canDownloadInCurrentNetwork() && start) {
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
|
||||
mission.start();
|
||||
}
|
||||
}
|
||||
@@ -241,7 +252,6 @@ public class DownloadManager {
|
||||
|
||||
public void resumeMission(DownloadMission mission) {
|
||||
if (!mission.running) {
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
|
||||
mission.start();
|
||||
}
|
||||
}
|
||||
@@ -250,7 +260,6 @@ public class DownloadManager {
|
||||
if (mission.running) {
|
||||
mission.setEnqueued(false);
|
||||
mission.pause();
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,7 +272,6 @@ public class DownloadManager {
|
||||
mFinishedMissionStore.deleteMission(mission);
|
||||
}
|
||||
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED);
|
||||
mission.delete();
|
||||
}
|
||||
}
|
||||
@@ -280,7 +288,6 @@ public class DownloadManager {
|
||||
mFinishedMissionStore.deleteMission(mission);
|
||||
}
|
||||
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED);
|
||||
mission.storage = null;
|
||||
mission.delete();
|
||||
}
|
||||
@@ -363,35 +370,29 @@ public class DownloadManager {
|
||||
}
|
||||
|
||||
public void pauseAllMissions(boolean force) {
|
||||
boolean flag = false;
|
||||
|
||||
synchronized (this) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (!mission.running || mission.isPsRunning() || mission.isFinished()) continue;
|
||||
|
||||
if (force) mission.threads = null;// avoid waiting for threads
|
||||
if (force) {
|
||||
// avoid waiting for threads
|
||||
mission.init = null;
|
||||
mission.threads = new Thread[0];
|
||||
}
|
||||
|
||||
mission.pause();
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
|
||||
}
|
||||
|
||||
public void startAllMissions() {
|
||||
boolean flag = false;
|
||||
|
||||
synchronized (this) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (mission.running || mission.isCorrupt()) continue;
|
||||
|
||||
flag = true;
|
||||
mission.start();
|
||||
}
|
||||
}
|
||||
|
||||
if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -472,28 +473,18 @@ public class DownloadManager {
|
||||
|
||||
boolean isMetered = mPrefMeteredDownloads && mLastNetworkStatus == NetworkState.MeteredOperating;
|
||||
|
||||
int running = 0;
|
||||
int paused = 0;
|
||||
synchronized (this) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (mission.isCorrupt() || mission.isPsRunning()) continue;
|
||||
|
||||
if (mission.running && isMetered) {
|
||||
paused++;
|
||||
mission.pause();
|
||||
} else if (!mission.running && !isMetered && mission.enqueued) {
|
||||
running++;
|
||||
mission.start();
|
||||
if (mPrefQueueLimit) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (running > 0) {
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
|
||||
return;
|
||||
}
|
||||
if (paused > 0) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
|
||||
}
|
||||
|
||||
void updateMaximumAttempts() {
|
||||
@@ -502,22 +493,6 @@ public class DownloadManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast check for pending downloads. If exists, the user will be notified
|
||||
* TODO: call this method in somewhere
|
||||
*
|
||||
* @param context the application context
|
||||
*/
|
||||
public static void notifyUserPendingDownloads(Context context) {
|
||||
int pending = getPendingDir(context).list().length;
|
||||
if (pending < 1) return;
|
||||
|
||||
Toast.makeText(context, context.getString(
|
||||
R.string.msg_pending_downloads,
|
||||
String.valueOf(pending)
|
||||
), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
public MissionState checkForExistingMission(StoredFileHelper storage) {
|
||||
synchronized (this) {
|
||||
DownloadMission pending = getPendingMission(storage);
|
||||
@@ -541,13 +516,21 @@ public class DownloadManager {
|
||||
}
|
||||
|
||||
static File pickAvailableTemporalDir(@NonNull Context ctx) {
|
||||
if (isDirectoryAvailable(ctx.getExternalFilesDir(null)))
|
||||
return ctx.getExternalFilesDir(null);
|
||||
else if (isDirectoryAvailable(ctx.getFilesDir()))
|
||||
return ctx.getFilesDir();
|
||||
File dir = ctx.getExternalFilesDir(null);
|
||||
if (isDirectoryAvailable(dir)) return dir;
|
||||
|
||||
dir = ctx.getFilesDir();
|
||||
if (isDirectoryAvailable(dir)) return dir;
|
||||
|
||||
// this never should happen
|
||||
return ctx.getDir("tmp", Context.MODE_PRIVATE);
|
||||
dir = ctx.getDir("muxing_tmp", Context.MODE_PRIVATE);
|
||||
if (isDirectoryAvailable(dir)) return dir;
|
||||
|
||||
// fallback to cache dir
|
||||
dir = ctx.getCacheDir();
|
||||
if (isDirectoryAvailable(dir)) return dir;
|
||||
|
||||
throw new RuntimeException("Not temporal directories are available");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -23,15 +23,17 @@ import android.os.Handler;
|
||||
import android.os.Handler.Callback;
|
||||
import android.os.IBinder;
|
||||
import android.os.Message;
|
||||
import android.os.Parcelable;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationCompat.Builder;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.download.DownloadActivity;
|
||||
@@ -42,6 +44,7 @@ import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.get.MissionRecoveryInfo;
|
||||
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
import us.shandian.giga.postprocessing.Postprocessing;
|
||||
@@ -54,11 +57,11 @@ public class DownloadManagerService extends Service {
|
||||
|
||||
private static final String TAG = "DownloadManagerService";
|
||||
|
||||
public static final int MESSAGE_RUNNING = 0;
|
||||
public static final int MESSAGE_PAUSED = 1;
|
||||
public static final int MESSAGE_FINISHED = 2;
|
||||
public static final int MESSAGE_PROGRESS = 3;
|
||||
public static final int MESSAGE_ERROR = 4;
|
||||
public static final int MESSAGE_DELETED = 5;
|
||||
public static final int MESSAGE_ERROR = 3;
|
||||
public static final int MESSAGE_DELETED = 4;
|
||||
|
||||
private static final int FOREGROUND_NOTIFICATION_ID = 1000;
|
||||
private static final int DOWNLOADS_NOTIFICATION_ID = 1001;
|
||||
@@ -73,6 +76,7 @@ public class DownloadManagerService extends Service {
|
||||
private static final String EXTRA_PATH = "DownloadManagerService.extra.storagePath";
|
||||
private static final String EXTRA_PARENT_PATH = "DownloadManagerService.extra.storageParentPath";
|
||||
private static final String EXTRA_STORAGE_TAG = "DownloadManagerService.extra.storageTag";
|
||||
private static final String EXTRA_RECOVERY_INFO = "DownloadManagerService.extra.recoveryInfo";
|
||||
|
||||
private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished";
|
||||
private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished";
|
||||
@@ -212,9 +216,11 @@ public class DownloadManagerService extends Service {
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
);
|
||||
}
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
}
|
||||
return START_NOT_STICKY;
|
||||
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -245,6 +251,7 @@ public class DownloadManagerService extends Service {
|
||||
if (icDownloadFailed != null) icDownloadFailed.recycle();
|
||||
if (icLauncher != null) icLauncher.recycle();
|
||||
|
||||
mHandler = null;
|
||||
mManager.pauseAllMissions(true);
|
||||
}
|
||||
|
||||
@@ -269,6 +276,8 @@ public class DownloadManagerService extends Service {
|
||||
}
|
||||
|
||||
private boolean handleMessage(@NonNull Message msg) {
|
||||
if (mHandler == null) return true;
|
||||
|
||||
DownloadMission mission = (DownloadMission) msg.obj;
|
||||
|
||||
switch (msg.what) {
|
||||
@@ -279,7 +288,7 @@ public class DownloadManagerService extends Service {
|
||||
handleConnectivityState(false);
|
||||
updateForegroundState(mManager.runMissions());
|
||||
break;
|
||||
case MESSAGE_PROGRESS:
|
||||
case MESSAGE_RUNNING:
|
||||
updateForegroundState(true);
|
||||
break;
|
||||
case MESSAGE_ERROR:
|
||||
@@ -295,11 +304,8 @@ public class DownloadManagerService extends Service {
|
||||
if (msg.what != MESSAGE_ERROR)
|
||||
mFailedDownloads.delete(mFailedDownloads.indexOfValue(mission));
|
||||
|
||||
synchronized (mEchoObservers) {
|
||||
for (Callback observer : mEchoObservers) {
|
||||
observer.handleMessage(msg);
|
||||
}
|
||||
}
|
||||
for (Callback observer : mEchoObservers)
|
||||
observer.handleMessage(msg);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -364,18 +370,20 @@ public class DownloadManagerService extends Service {
|
||||
/**
|
||||
* Start a new download mission
|
||||
*
|
||||
* @param context the activity context
|
||||
* @param urls the list of urls to download
|
||||
* @param storage where the file is saved
|
||||
* @param kind type of file (a: audio v: video s: subtitle ?: file-extension defined)
|
||||
* @param threads the number of threads maximal used to download chunks of the file.
|
||||
* @param psName the name of the required post-processing algorithm, or {@code null} to ignore.
|
||||
* @param source source url of the resource
|
||||
* @param psArgs the arguments for the post-processing algorithm.
|
||||
* @param nearLength the approximated final length of the file
|
||||
* @param context the activity context
|
||||
* @param urls array of urls to download
|
||||
* @param storage where the file is saved
|
||||
* @param kind type of file (a: audio v: video s: subtitle ?: file-extension defined)
|
||||
* @param threads the number of threads maximal used to download chunks of the file.
|
||||
* @param psName the name of the required post-processing algorithm, or {@code null} to ignore.
|
||||
* @param source source url of the resource
|
||||
* @param psArgs the arguments for the post-processing algorithm.
|
||||
* @param nearLength the approximated final length of the file
|
||||
* @param recoveryInfo array of MissionRecoveryInfo, in case is required recover the download
|
||||
*/
|
||||
public static void startMission(Context context, String[] urls, StoredFileHelper storage, char kind,
|
||||
int threads, String source, String psName, String[] psArgs, long nearLength) {
|
||||
public static void startMission(Context context, String[] urls, StoredFileHelper storage,
|
||||
char kind, int threads, String source, String psName,
|
||||
String[] psArgs, long nearLength, MissionRecoveryInfo[] recoveryInfo) {
|
||||
Intent intent = new Intent(context, DownloadManagerService.class);
|
||||
intent.setAction(Intent.ACTION_RUN);
|
||||
intent.putExtra(EXTRA_URLS, urls);
|
||||
@@ -385,6 +393,7 @@ public class DownloadManagerService extends Service {
|
||||
intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName);
|
||||
intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs);
|
||||
intent.putExtra(EXTRA_NEAR_LENGTH, nearLength);
|
||||
intent.putExtra(EXTRA_RECOVERY_INFO, recoveryInfo);
|
||||
|
||||
intent.putExtra(EXTRA_PARENT_PATH, storage.getParentUri());
|
||||
intent.putExtra(EXTRA_PATH, storage.getUri());
|
||||
@@ -404,6 +413,7 @@ public class DownloadManagerService extends Service {
|
||||
String source = intent.getStringExtra(EXTRA_SOURCE);
|
||||
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
|
||||
String tag = intent.getStringExtra(EXTRA_STORAGE_TAG);
|
||||
Parcelable[] parcelRecovery = intent.getParcelableArrayExtra(EXTRA_RECOVERY_INFO);
|
||||
|
||||
StoredFileHelper storage;
|
||||
try {
|
||||
@@ -418,10 +428,15 @@ public class DownloadManagerService extends Service {
|
||||
else
|
||||
ps = Postprocessing.getAlgorithm(psName, psArgs);
|
||||
|
||||
MissionRecoveryInfo[] recovery = new MissionRecoveryInfo[parcelRecovery.length];
|
||||
for (int i = 0; i < parcelRecovery.length; i++)
|
||||
recovery[i] = (MissionRecoveryInfo) parcelRecovery[i];
|
||||
|
||||
final DownloadMission mission = new DownloadMission(urls, storage, kind, ps);
|
||||
mission.threadCount = threads;
|
||||
mission.source = source;
|
||||
mission.nearLength = nearLength;
|
||||
mission.recoveryInfo = recovery;
|
||||
|
||||
if (ps != null)
|
||||
ps.setTemporalDir(DownloadManager.pickAvailableTemporalDir(this));
|
||||
@@ -509,16 +524,6 @@ public class DownloadManagerService extends Service {
|
||||
return PendingIntent.getService(this, intent.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
private void manageObservers(Callback handler, boolean add) {
|
||||
synchronized (mEchoObservers) {
|
||||
if (add) {
|
||||
mEchoObservers.add(handler);
|
||||
} else {
|
||||
mEchoObservers.remove(handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void manageLock(boolean acquire) {
|
||||
if (acquire == mLockAcquired) return;
|
||||
|
||||
@@ -591,11 +596,11 @@ public class DownloadManagerService extends Service {
|
||||
}
|
||||
|
||||
public void addMissionEventListener(Callback handler) {
|
||||
manageObservers(handler, true);
|
||||
mEchoObservers.add(handler);
|
||||
}
|
||||
|
||||
public void removeMissionEventListener(Callback handler) {
|
||||
manageObservers(handler, false);
|
||||
mEchoObservers.remove(handler);
|
||||
}
|
||||
|
||||
public void clearDownloadNotifications() {
|
||||
|
||||
@@ -5,21 +5,12 @@ import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.Adapter;
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
@@ -34,8 +25,22 @@ import android.widget.PopupMenu;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.Adapter;
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
@@ -44,11 +49,12 @@ import java.io.File;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.get.FinishedMission;
|
||||
import us.shandian.giga.get.Mission;
|
||||
import us.shandian.giga.get.MissionRecoveryInfo;
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
import us.shandian.giga.service.DownloadManager;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
@@ -62,7 +68,6 @@ import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_CONNECT_HOST;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_FILE_CREATION;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_NO_CONTENT;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_UNSUPPORTED_RANGE;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_INSUFFICIENT_STORAGE;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_PATH_CREATION;
|
||||
@@ -71,6 +76,7 @@ import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_STOPPED;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_PROGRESS_LOST;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_RESOURCE_GONE;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_SSL_EXCEPTION;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_TIMEOUT;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
|
||||
@@ -81,6 +87,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
private static final String TAG = "MissionAdapter";
|
||||
private static final String UNDEFINED_PROGRESS = "--.-%";
|
||||
private static final String DEFAULT_MIME_TYPE = "*/*";
|
||||
private static final String UNDEFINED_ETA = "--:--";
|
||||
|
||||
|
||||
static {
|
||||
@@ -101,11 +108,16 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
private MenuItem mPauseButton;
|
||||
private View mEmptyMessage;
|
||||
private RecoverHelper mRecover;
|
||||
private View mView;
|
||||
private ArrayList<Mission> mHidden;
|
||||
private Snackbar mSnackbar;
|
||||
|
||||
public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage) {
|
||||
private final Runnable rUpdater = this::updater;
|
||||
private final Runnable rDelete = this::deleteFinishedDownloads;
|
||||
|
||||
public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage, View root) {
|
||||
mContext = context;
|
||||
mDownloadManager = downloadManager;
|
||||
mDeleter = null;
|
||||
|
||||
mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
mLayout = R.layout.mission_item;
|
||||
@@ -116,7 +128,14 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
|
||||
mIterator = downloadManager.getIterator();
|
||||
|
||||
mDeleter = new Deleter(root, mContext, this, mDownloadManager, mIterator, mHandler);
|
||||
|
||||
mView = root;
|
||||
|
||||
mHidden = new ArrayList<>();
|
||||
|
||||
checkEmptyMessageVisibility();
|
||||
onResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -141,17 +160,13 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
if (h.item.mission instanceof DownloadMission) {
|
||||
mPendingDownloadsItems.remove(h);
|
||||
if (mPendingDownloadsItems.size() < 1) {
|
||||
setAutoRefresh(false);
|
||||
checkMasterButtonsVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
h.popupMenu.dismiss();
|
||||
h.item = null;
|
||||
h.lastTimeStamp = -1;
|
||||
h.lastDone = -1;
|
||||
h.lastCurrent = -1;
|
||||
h.state = 0;
|
||||
h.resetSpeedMeasure();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -190,7 +205,6 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
|
||||
h.size.setText(length);
|
||||
h.pause.setTitle(mission.unknownLength ? R.string.stop : R.string.pause);
|
||||
h.lastCurrent = mission.current;
|
||||
updateProgress(h);
|
||||
mPendingDownloadsItems.add(h);
|
||||
} else {
|
||||
@@ -215,40 +229,27 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
private void updateProgress(ViewHolderItem h) {
|
||||
if (h == null || h.item == null || h.item.mission instanceof FinishedMission) return;
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
DownloadMission mission = (DownloadMission) h.item.mission;
|
||||
|
||||
if (h.lastCurrent != mission.current) {
|
||||
h.lastCurrent = mission.current;
|
||||
h.lastTimeStamp = now;
|
||||
h.lastDone = 0;
|
||||
} else {
|
||||
if (h.lastTimeStamp == -1) h.lastTimeStamp = now;
|
||||
if (h.lastDone == -1) h.lastDone = mission.done;
|
||||
}
|
||||
|
||||
long deltaTime = now - h.lastTimeStamp;
|
||||
long deltaDone = mission.done - h.lastDone;
|
||||
double done = mission.done;
|
||||
long length = mission.getLength();
|
||||
long now = System.currentTimeMillis();
|
||||
boolean hasError = mission.errCode != ERROR_NOTHING;
|
||||
|
||||
// hide on error
|
||||
// show if current resource length is not fetched
|
||||
// show if length is unknown
|
||||
h.progress.setMarquee(!hasError && (!mission.isInitialized() || mission.unknownLength));
|
||||
h.progress.setMarquee(mission.isRecovering() || !hasError && (!mission.isInitialized() || mission.unknownLength));
|
||||
|
||||
float progress;
|
||||
double progress;
|
||||
if (mission.unknownLength) {
|
||||
progress = Float.NaN;
|
||||
progress = Double.NaN;
|
||||
h.progress.setProgress(0f);
|
||||
} else {
|
||||
progress = (float) ((double) mission.done / mission.length);
|
||||
if (mission.urls.length > 1 && mission.current < mission.urls.length) {
|
||||
progress = (progress / mission.urls.length) + ((float) mission.current / mission.urls.length);
|
||||
}
|
||||
progress = done / length;
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
h.progress.setProgress(isNotFinite(progress) ? 1f : progress);
|
||||
h.progress.setProgress(isNotFinite(progress) ? 1d : progress);
|
||||
h.status.setText(R.string.msg_error);
|
||||
} else if (isNotFinite(progress)) {
|
||||
h.status.setText(UNDEFINED_PROGRESS);
|
||||
@@ -257,59 +258,78 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
h.progress.setProgress(progress);
|
||||
}
|
||||
|
||||
long length = mission.getLength();
|
||||
@StringRes int state;
|
||||
String sizeStr = Utility.formatBytes(length).concat(" ");
|
||||
|
||||
int state;
|
||||
if (mission.isPsFailed() || mission.errCode == ERROR_POSTPROCESSING_HOLD) {
|
||||
state = 0;
|
||||
h.size.setText(sizeStr);
|
||||
return;
|
||||
} else if (!mission.running) {
|
||||
state = mission.enqueued ? 1 : 2;
|
||||
state = mission.enqueued ? R.string.queued : R.string.paused;
|
||||
} else if (mission.isPsRunning()) {
|
||||
state = 3;
|
||||
state = R.string.post_processing;
|
||||
} else if (mission.isRecovering()) {
|
||||
state = R.string.recovering;
|
||||
} else {
|
||||
state = 0;
|
||||
}
|
||||
|
||||
if (state != 0) {
|
||||
// update state without download speed
|
||||
if (h.state != state) {
|
||||
String statusStr;
|
||||
h.state = state;
|
||||
h.size.setText(sizeStr.concat("(").concat(mContext.getString(state)).concat(")"));
|
||||
h.resetSpeedMeasure();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case 1:
|
||||
statusStr = mContext.getString(R.string.queued);
|
||||
break;
|
||||
case 2:
|
||||
statusStr = mContext.getString(R.string.paused);
|
||||
break;
|
||||
case 3:
|
||||
statusStr = mContext.getString(R.string.post_processing);
|
||||
break;
|
||||
default:
|
||||
statusStr = "?";
|
||||
break;
|
||||
}
|
||||
if (h.lastTimestamp < 0) {
|
||||
h.size.setText(sizeStr);
|
||||
h.lastTimestamp = now;
|
||||
h.lastDone = done;
|
||||
return;
|
||||
}
|
||||
|
||||
h.size.setText(Utility.formatBytes(length).concat(" (").concat(statusStr).concat(")"));
|
||||
} else if (deltaDone > 0) {
|
||||
h.lastTimeStamp = now;
|
||||
h.lastDone = mission.done;
|
||||
}
|
||||
long deltaTime = now - h.lastTimestamp;
|
||||
double deltaDone = done - h.lastDone;
|
||||
|
||||
if (h.lastDone > done) {
|
||||
h.lastDone = done;
|
||||
h.size.setText(sizeStr);
|
||||
return;
|
||||
}
|
||||
|
||||
if (deltaDone > 0 && deltaTime > 0) {
|
||||
float speed = (deltaDone * 1000f) / deltaTime;
|
||||
float speed = (float) ((deltaDone * 1000d) / deltaTime);
|
||||
float averageSpeed = speed;
|
||||
|
||||
String speedStr = Utility.formatSpeed(speed);
|
||||
String sizeStr = Utility.formatBytes(length);
|
||||
if (h.lastSpeedIdx < 0) {
|
||||
for (int i = 0; i < h.lastSpeed.length; i++) {
|
||||
h.lastSpeed[i] = speed;
|
||||
}
|
||||
h.lastSpeedIdx = 0;
|
||||
} else {
|
||||
for (int i = 0; i < h.lastSpeed.length; i++) {
|
||||
averageSpeed += h.lastSpeed[i];
|
||||
}
|
||||
averageSpeed /= h.lastSpeed.length + 1f;
|
||||
}
|
||||
|
||||
h.size.setText(sizeStr.concat(" ").concat(speedStr));
|
||||
String speedStr = Utility.formatSpeed(averageSpeed);
|
||||
String etaStr;
|
||||
|
||||
h.lastTimeStamp = now;
|
||||
h.lastDone = mission.done;
|
||||
if (mission.unknownLength) {
|
||||
etaStr = "";
|
||||
} else {
|
||||
long eta = (long) Math.ceil((length - done) / averageSpeed);
|
||||
etaStr = Utility.formatBytes((long) done) + "/" + Utility.stringifySeconds(eta) + " ";
|
||||
}
|
||||
|
||||
h.size.setText(sizeStr.concat(etaStr).concat(speedStr));
|
||||
|
||||
h.lastTimestamp = now;
|
||||
h.lastDone = done;
|
||||
h.lastSpeed[h.lastSpeedIdx++] = speed;
|
||||
|
||||
if (h.lastSpeedIdx >= h.lastSpeed.length) h.lastSpeedIdx = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,6 +408,13 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
return true;
|
||||
}
|
||||
|
||||
private ViewHolderItem getViewHolder(Object mission) {
|
||||
for (ViewHolderItem h : mPendingDownloadsItems) {
|
||||
if (h.item.mission == mission) return h;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleMessage(@NonNull Message msg) {
|
||||
if (mStartButton != null && mPauseButton != null) {
|
||||
@@ -395,33 +422,28 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
}
|
||||
|
||||
switch (msg.what) {
|
||||
case DownloadManagerService.MESSAGE_PROGRESS:
|
||||
case DownloadManagerService.MESSAGE_ERROR:
|
||||
case DownloadManagerService.MESSAGE_FINISHED:
|
||||
case DownloadManagerService.MESSAGE_DELETED:
|
||||
case DownloadManagerService.MESSAGE_PAUSED:
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
if (msg.what == DownloadManagerService.MESSAGE_PROGRESS) {
|
||||
setAutoRefresh(true);
|
||||
return true;
|
||||
}
|
||||
ViewHolderItem h = getViewHolder(msg.obj);
|
||||
if (h == null) return false;
|
||||
|
||||
for (ViewHolderItem h : mPendingDownloadsItems) {
|
||||
if (h.item.mission != msg.obj) continue;
|
||||
|
||||
if (msg.what == DownloadManagerService.MESSAGE_FINISHED) {
|
||||
switch (msg.what) {
|
||||
case DownloadManagerService.MESSAGE_FINISHED:
|
||||
case DownloadManagerService.MESSAGE_DELETED:
|
||||
// DownloadManager should mark the download as finished
|
||||
applyChanges();
|
||||
return true;
|
||||
}
|
||||
|
||||
updateProgress(h);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
updateProgress(h);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void showError(@NonNull DownloadMission mission) {
|
||||
@@ -430,7 +452,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
|
||||
switch (mission.errCode) {
|
||||
case 416:
|
||||
msg = R.string.error_http_requested_range_not_satisfiable;
|
||||
msg = R.string.error_http_unsupported_range;
|
||||
break;
|
||||
case 404:
|
||||
msg = R.string.error_http_not_found;
|
||||
@@ -443,9 +465,6 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
case ERROR_HTTP_NO_CONTENT:
|
||||
msg = R.string.error_http_no_content;
|
||||
break;
|
||||
case ERROR_HTTP_UNSUPPORTED_RANGE:
|
||||
msg = R.string.error_http_unsupported_range;
|
||||
break;
|
||||
case ERROR_PATH_CREATION:
|
||||
msg = R.string.error_path_creation;
|
||||
break;
|
||||
@@ -466,27 +485,35 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
break;
|
||||
case ERROR_POSTPROCESSING:
|
||||
case ERROR_POSTPROCESSING_HOLD:
|
||||
showError(mission.errObject, UserAction.DOWNLOAD_POSTPROCESSING, R.string.error_postprocessing_failed);
|
||||
showError(mission, UserAction.DOWNLOAD_POSTPROCESSING, R.string.error_postprocessing_failed);
|
||||
return;
|
||||
case ERROR_INSUFFICIENT_STORAGE:
|
||||
msg = R.string.error_insufficient_storage;
|
||||
break;
|
||||
case ERROR_UNKNOWN_EXCEPTION:
|
||||
showError(mission.errObject, UserAction.DOWNLOAD_FAILED, R.string.general_error);
|
||||
return;
|
||||
if (mission.errObject != null) {
|
||||
showError(mission, UserAction.DOWNLOAD_FAILED, R.string.general_error);
|
||||
return;
|
||||
} else {
|
||||
msg = R.string.msg_error;
|
||||
break;
|
||||
}
|
||||
case ERROR_PROGRESS_LOST:
|
||||
msg = R.string.error_progress_lost;
|
||||
break;
|
||||
case ERROR_TIMEOUT:
|
||||
msg = R.string.error_timeout;
|
||||
break;
|
||||
case ERROR_RESOURCE_GONE:
|
||||
msg = R.string.error_download_resource_gone;
|
||||
break;
|
||||
default:
|
||||
if (mission.errCode >= 100 && mission.errCode < 600) {
|
||||
msgEx = "HTTP " + mission.errCode;
|
||||
} else if (mission.errObject == null) {
|
||||
msgEx = "(not_decelerated_error_code)";
|
||||
} else {
|
||||
showError(mission.errObject, UserAction.DOWNLOAD_FAILED, msg);
|
||||
showError(mission, UserAction.DOWNLOAD_FAILED, msg);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
@@ -503,29 +530,89 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
if (mission.errObject != null && (mission.errCode < 100 || mission.errCode >= 600)) {
|
||||
@StringRes final int mMsg = msg;
|
||||
builder.setPositiveButton(R.string.error_report_title, (dialog, which) ->
|
||||
showError(mission.errObject, UserAction.DOWNLOAD_FAILED, mMsg)
|
||||
showError(mission, UserAction.DOWNLOAD_FAILED, mMsg)
|
||||
);
|
||||
}
|
||||
|
||||
builder.setNegativeButton(android.R.string.ok, (dialog, which) -> dialog.cancel())
|
||||
builder.setNegativeButton(R.string.finish, (dialog, which) -> dialog.cancel())
|
||||
.setTitle(mission.storage.getName())
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showError(Exception exception, UserAction action, @StringRes int reason) {
|
||||
private void showError(DownloadMission mission, UserAction action, @StringRes int reason) {
|
||||
StringBuilder request = new StringBuilder(256);
|
||||
request.append(mission.source);
|
||||
|
||||
request.append(" [");
|
||||
if (mission.recoveryInfo != null) {
|
||||
for (MissionRecoveryInfo recovery : mission.recoveryInfo)
|
||||
request.append(' ')
|
||||
.append(recovery.toString())
|
||||
.append(' ');
|
||||
}
|
||||
request.append("]");
|
||||
|
||||
String service;
|
||||
try {
|
||||
service = NewPipe.getServiceByUrl(mission.source).getServiceInfo().getName();
|
||||
} catch (Exception e) {
|
||||
service = "-";
|
||||
}
|
||||
|
||||
ErrorActivity.reportError(
|
||||
mContext,
|
||||
Collections.singletonList(exception),
|
||||
mission.errObject,
|
||||
null,
|
||||
null,
|
||||
ErrorActivity.ErrorInfo.make(action, "-", "-", reason)
|
||||
ErrorActivity.ErrorInfo.make(action, service, request.toString(), reason)
|
||||
);
|
||||
}
|
||||
|
||||
public void clearFinishedDownloads() {
|
||||
mDownloadManager.forgetFinishedDownloads();
|
||||
applyChanges();
|
||||
public void clearFinishedDownloads(boolean delete) {
|
||||
if (delete && mIterator.hasFinishedMissions() && mHidden.isEmpty()) {
|
||||
for (int i = 0; i < mIterator.getOldListSize(); i++) {
|
||||
FinishedMission mission = mIterator.getItem(i).mission instanceof FinishedMission ? (FinishedMission) mIterator.getItem(i).mission : null;
|
||||
if (mission != null) {
|
||||
mIterator.hide(mission);
|
||||
mHidden.add(mission);
|
||||
}
|
||||
}
|
||||
applyChanges();
|
||||
|
||||
String msg = String.format(mContext.getString(R.string.deleted_downloads), mHidden.size());
|
||||
mSnackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE);
|
||||
mSnackbar.setAction(R.string.undo, s -> {
|
||||
Iterator<Mission> i = mHidden.iterator();
|
||||
while (i.hasNext()) {
|
||||
mIterator.unHide(i.next());
|
||||
i.remove();
|
||||
}
|
||||
applyChanges();
|
||||
mHandler.removeCallbacks(rDelete);
|
||||
});
|
||||
mSnackbar.setActionTextColor(Color.YELLOW);
|
||||
mSnackbar.show();
|
||||
|
||||
mHandler.postDelayed(rDelete, 5000);
|
||||
} else if (!delete) {
|
||||
mDownloadManager.forgetFinishedDownloads();
|
||||
applyChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteFinishedDownloads() {
|
||||
if (mSnackbar != null) mSnackbar.dismiss();
|
||||
|
||||
Iterator<Mission> i = mHidden.iterator();
|
||||
while (i.hasNext()) {
|
||||
Mission mission = i.next();
|
||||
if (mission != null) {
|
||||
mDownloadManager.deleteMission(mission);
|
||||
mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mission.storage.getUri()));
|
||||
}
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean handlePopupItem(@NonNull ViewHolderItem h, @NonNull MenuItem option) {
|
||||
@@ -538,16 +625,10 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
switch (id) {
|
||||
case R.id.start:
|
||||
h.status.setText(UNDEFINED_PROGRESS);
|
||||
h.state = -1;
|
||||
h.size.setText(Utility.formatBytes(mission.getLength()));
|
||||
mDownloadManager.resumeMission(mission);
|
||||
return true;
|
||||
case R.id.pause:
|
||||
h.state = -1;
|
||||
mDownloadManager.pauseMission(mission);
|
||||
updateProgress(h);
|
||||
h.lastTimeStamp = -1;
|
||||
h.lastDone = -1;
|
||||
return true;
|
||||
case R.id.error_message_view:
|
||||
showError(mission);
|
||||
@@ -580,12 +661,9 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
shareFile(h.item.mission);
|
||||
return true;
|
||||
case R.id.delete:
|
||||
if (mDeleter == null) {
|
||||
mDownloadManager.deleteMission(h.item.mission);
|
||||
} else {
|
||||
mDeleter.append(h.item.mission);
|
||||
}
|
||||
mDeleter.append(h.item.mission);
|
||||
applyChanges();
|
||||
checkMasterButtonsVisibility();
|
||||
return true;
|
||||
case R.id.md5:
|
||||
case R.id.sha1:
|
||||
@@ -621,7 +699,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
mIterator.end();
|
||||
|
||||
for (ViewHolderItem item : mPendingDownloadsItems) {
|
||||
item.lastTimeStamp = -1;
|
||||
item.resetSpeedMeasure();
|
||||
}
|
||||
|
||||
notifyDataSetChanged();
|
||||
@@ -654,6 +732,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
|
||||
public void checkMasterButtonsVisibility() {
|
||||
boolean[] state = mIterator.hasValidPendingMissions();
|
||||
Log.d(TAG, "checkMasterButtonsVisibility() running=" + state[0] + " paused=" + state[1]);
|
||||
setButtonVisible(mPauseButton, state[0]);
|
||||
setButtonVisible(mStartButton, state[1]);
|
||||
}
|
||||
@@ -663,86 +742,57 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
button.setVisible(visible);
|
||||
}
|
||||
|
||||
public void ensurePausedMissions() {
|
||||
public void refreshMissionItems() {
|
||||
for (ViewHolderItem h : mPendingDownloadsItems) {
|
||||
if (((DownloadMission) h.item.mission).running) continue;
|
||||
updateProgress(h);
|
||||
h.lastTimeStamp = -1;
|
||||
h.lastDone = -1;
|
||||
h.resetSpeedMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void deleterDispose(boolean commitChanges) {
|
||||
if (mDeleter != null) mDeleter.dispose(commitChanges);
|
||||
public void onDestroy() {
|
||||
mDeleter.dispose();
|
||||
}
|
||||
|
||||
public void deleterLoad(View view) {
|
||||
if (mDeleter == null)
|
||||
mDeleter = new Deleter(view, mContext, this, mDownloadManager, mIterator, mHandler);
|
||||
public void onResume() {
|
||||
mDeleter.resume();
|
||||
mHandler.post(rUpdater);
|
||||
}
|
||||
|
||||
public void deleterResume() {
|
||||
if (mDeleter != null) mDeleter.resume();
|
||||
}
|
||||
|
||||
public void recoverMission(DownloadMission mission) {
|
||||
for (ViewHolderItem h : mPendingDownloadsItems) {
|
||||
if (mission != h.item.mission) continue;
|
||||
|
||||
mission.errObject = null;
|
||||
mission.resetState(true, false, DownloadMission.ERROR_NOTHING);
|
||||
|
||||
h.status.setText(UNDEFINED_PROGRESS);
|
||||
h.state = -1;
|
||||
h.size.setText(Utility.formatBytes(mission.getLength()));
|
||||
h.progress.setMarquee(true);
|
||||
|
||||
mDownloadManager.resumeMission(mission);
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private boolean mUpdaterRunning = false;
|
||||
private final Runnable rUpdater = this::updater;
|
||||
|
||||
public void onPaused() {
|
||||
setAutoRefresh(false);
|
||||
mDeleter.pause();
|
||||
mHandler.removeCallbacks(rUpdater);
|
||||
}
|
||||
|
||||
private void setAutoRefresh(boolean enabled) {
|
||||
if (enabled && !mUpdaterRunning) {
|
||||
mUpdaterRunning = true;
|
||||
updater();
|
||||
} else if (!enabled && mUpdaterRunning) {
|
||||
mUpdaterRunning = false;
|
||||
mHandler.removeCallbacks(rUpdater);
|
||||
}
|
||||
|
||||
public void recoverMission(DownloadMission mission) {
|
||||
ViewHolderItem h = getViewHolder(mission);
|
||||
if (h == null) return;
|
||||
|
||||
mission.errObject = null;
|
||||
mission.resetState(true, false, DownloadMission.ERROR_NOTHING);
|
||||
|
||||
h.status.setText(UNDEFINED_PROGRESS);
|
||||
h.size.setText(Utility.formatBytes(mission.getLength()));
|
||||
h.progress.setMarquee(true);
|
||||
|
||||
mDownloadManager.resumeMission(mission);
|
||||
}
|
||||
|
||||
private void updater() {
|
||||
if (!mUpdaterRunning) return;
|
||||
|
||||
boolean running = false;
|
||||
for (ViewHolderItem h : mPendingDownloadsItems) {
|
||||
// check if the mission is running first
|
||||
if (!((DownloadMission) h.item.mission).running) continue;
|
||||
|
||||
updateProgress(h);
|
||||
running = true;
|
||||
}
|
||||
|
||||
if (running) {
|
||||
mHandler.postDelayed(rUpdater, 1000);
|
||||
} else {
|
||||
mUpdaterRunning = false;
|
||||
}
|
||||
mHandler.postDelayed(rUpdater, 1000);
|
||||
}
|
||||
|
||||
private boolean isNotFinite(Float value) {
|
||||
return Float.isNaN(value) || Float.isInfinite(value);
|
||||
private boolean isNotFinite(double value) {
|
||||
return Double.isNaN(value) || Double.isInfinite(value);
|
||||
}
|
||||
|
||||
public void setRecover(@NonNull RecoverHelper callback) {
|
||||
@@ -771,10 +821,11 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
MenuItem source;
|
||||
MenuItem checksum;
|
||||
|
||||
long lastTimeStamp = -1;
|
||||
long lastDone = -1;
|
||||
int lastCurrent = -1;
|
||||
int state = 0;
|
||||
long lastTimestamp = -1;
|
||||
double lastDone;
|
||||
int lastSpeedIdx;
|
||||
float[] lastSpeed = new float[3];
|
||||
String estimatedTimeArrival = UNDEFINED_ETA;
|
||||
|
||||
ViewHolderItem(View view) {
|
||||
super(view);
|
||||
@@ -859,7 +910,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
|
||||
delete.setVisible(true);
|
||||
|
||||
boolean flag = !mission.isPsFailed();
|
||||
boolean flag = !mission.isPsFailed() && mission.urls.length > 0;
|
||||
start.setVisible(flag);
|
||||
queue.setVisible(flag);
|
||||
}
|
||||
@@ -884,6 +935,12 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
|
||||
return popup;
|
||||
}
|
||||
|
||||
private void resetSpeedMeasure() {
|
||||
estimatedTimeArrival = UNDEFINED_ETA;
|
||||
lastTimestamp = -1;
|
||||
lastSpeedIdx = -1;
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolderHeader extends RecyclerView.ViewHolder {
|
||||
|
||||
@@ -4,9 +4,10 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.os.Handler;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import android.view.View;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -113,7 +114,7 @@ public class Deleter {
|
||||
show();
|
||||
}
|
||||
|
||||
private void pause() {
|
||||
public void pause() {
|
||||
running = false;
|
||||
mHandler.removeCallbacks(rNext);
|
||||
mHandler.removeCallbacks(rShow);
|
||||
@@ -126,13 +127,11 @@ public class Deleter {
|
||||
mHandler.postDelayed(rShow, DELAY_RESUME);
|
||||
}
|
||||
|
||||
public void dispose(boolean commitChanges) {
|
||||
public void dispose() {
|
||||
if (items.size() < 1) return;
|
||||
|
||||
pause();
|
||||
|
||||
if (!commitChanges) return;
|
||||
|
||||
for (Mission mission : items) mDownloadManager.deleteMission(mission);
|
||||
items = null;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
@@ -35,8 +36,8 @@ public class ProgressDrawable extends Drawable {
|
||||
mForegroundColor = foreground;
|
||||
}
|
||||
|
||||
public void setProgress(float progress) {
|
||||
mProgress = progress;
|
||||
public void setProgress(double progress) {
|
||||
mProgress = (float) progress;
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
|
||||
@@ -12,18 +12,20 @@ import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.nononsenseapps.filepicker.Utils;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
@@ -72,8 +74,7 @@ public class MissionsFragment extends Fragment {
|
||||
mBinder = (DownloadManagerBinder) binder;
|
||||
mBinder.clearDownloadNotifications();
|
||||
|
||||
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty);
|
||||
mAdapter.deleterLoad(getView());
|
||||
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty, getView());
|
||||
|
||||
mAdapter.setRecover(MissionsFragment.this::recoverMission);
|
||||
|
||||
@@ -132,7 +133,7 @@ public class MissionsFragment extends Fragment {
|
||||
* Added in API level 23.
|
||||
*/
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
// Bug: in api< 23 this is never called
|
||||
@@ -147,7 +148,7 @@ public class MissionsFragment extends Fragment {
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
public void onAttach(@NonNull Activity activity) {
|
||||
super.onAttach(activity);
|
||||
|
||||
mContext = activity;
|
||||
@@ -162,7 +163,7 @@ public class MissionsFragment extends Fragment {
|
||||
mBinder.removeMissionEventListener(mAdapter);
|
||||
mBinder.enableNotifications(true);
|
||||
mContext.unbindService(mConnection);
|
||||
mAdapter.deleterDispose(true);
|
||||
mAdapter.onDestroy();
|
||||
|
||||
mBinder = null;
|
||||
mAdapter = null;
|
||||
@@ -189,20 +190,20 @@ public class MissionsFragment extends Fragment {
|
||||
return true;
|
||||
case R.id.clear_list:
|
||||
AlertDialog.Builder prompt = new AlertDialog.Builder(mContext);
|
||||
prompt.setTitle(R.string.clear_finished_download);
|
||||
prompt.setTitle(R.string.clear_download_history);
|
||||
prompt.setMessage(R.string.confirm_prompt);
|
||||
prompt.setPositiveButton(android.R.string.ok, (dialog, which) -> mAdapter.clearFinishedDownloads());
|
||||
prompt.setNegativeButton(R.string.cancel, null);
|
||||
// Intentionally misusing button's purpose in order to achieve good order
|
||||
prompt.setNegativeButton(R.string.clear_download_history, (dialog, which) -> mAdapter.clearFinishedDownloads(false));
|
||||
prompt.setPositiveButton(R.string.delete_downloaded_files, (dialog, which) -> mAdapter.clearFinishedDownloads(true));
|
||||
prompt.setNeutralButton(R.string.cancel, null);
|
||||
prompt.create().show();
|
||||
return true;
|
||||
case R.id.start_downloads:
|
||||
item.setVisible(false);
|
||||
mBinder.getDownloadManager().startAllMissions();
|
||||
return true;
|
||||
case R.id.pause_downloads:
|
||||
item.setVisible(false);
|
||||
mBinder.getDownloadManager().pauseAllMissions(false);
|
||||
mAdapter.ensurePausedMissions();// update items view
|
||||
mAdapter.refreshMissionItems();// update items view
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
@@ -271,23 +272,12 @@ public class MissionsFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
if (mAdapter != null) {
|
||||
mAdapter.deleterDispose(false);
|
||||
mForceUpdate = true;
|
||||
mBinder.removeMissionEventListener(mAdapter);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (mAdapter != null) {
|
||||
mAdapter.deleterResume();
|
||||
mAdapter.onResume();
|
||||
|
||||
if (mForceUpdate) {
|
||||
mForceUpdate = false;
|
||||
@@ -303,7 +293,13 @@ public class MissionsFragment extends Fragment {
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (mAdapter != null) mAdapter.onPaused();
|
||||
|
||||
if (mAdapter != null) {
|
||||
mForceUpdate = true;
|
||||
mBinder.removeMissionEventListener(mAdapter);
|
||||
mAdapter.onPaused();
|
||||
}
|
||||
|
||||
if (mBinder != null) mBinder.enableNotifications(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,14 @@ import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
@@ -26,6 +27,7 @@ import java.io.Serializable;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Locale;
|
||||
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
|
||||
@@ -39,26 +41,28 @@ public class Utility {
|
||||
}
|
||||
|
||||
public static String formatBytes(long bytes) {
|
||||
Locale locale = Locale.getDefault();
|
||||
if (bytes < 1024) {
|
||||
return String.format("%d B", bytes);
|
||||
return String.format(locale, "%d B", bytes);
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
return String.format("%.2f kB", bytes / 1024d);
|
||||
return String.format(locale, "%.2f kB", bytes / 1024d);
|
||||
} else if (bytes < 1024 * 1024 * 1024) {
|
||||
return String.format("%.2f MB", bytes / 1024d / 1024d);
|
||||
return String.format(locale, "%.2f MB", bytes / 1024d / 1024d);
|
||||
} else {
|
||||
return String.format("%.2f GB", bytes / 1024d / 1024d / 1024d);
|
||||
return String.format(locale, "%.2f GB", bytes / 1024d / 1024d / 1024d);
|
||||
}
|
||||
}
|
||||
|
||||
public static String formatSpeed(float speed) {
|
||||
public static String formatSpeed(double speed) {
|
||||
Locale locale = Locale.getDefault();
|
||||
if (speed < 1024) {
|
||||
return String.format("%.2f B/s", speed);
|
||||
return String.format(locale, "%.2f B/s", speed);
|
||||
} else if (speed < 1024 * 1024) {
|
||||
return String.format("%.2f kB/s", speed / 1024);
|
||||
return String.format(locale, "%.2f kB/s", speed / 1024);
|
||||
} else if (speed < 1024 * 1024 * 1024) {
|
||||
return String.format("%.2f MB/s", speed / 1024 / 1024);
|
||||
return String.format(locale, "%.2f MB/s", speed / 1024 / 1024);
|
||||
} else {
|
||||
return String.format("%.2f GB/s", speed / 1024 / 1024 / 1024);
|
||||
return String.format(locale, "%.2f GB/s", speed / 1024 / 1024 / 1024);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,12 +192,11 @@ public class Utility {
|
||||
switch (type) {
|
||||
case MUSIC:
|
||||
return R.drawable.music;
|
||||
default:
|
||||
case VIDEO:
|
||||
return R.drawable.video;
|
||||
case SUBTITLE:
|
||||
return R.drawable.subtitle;
|
||||
default:
|
||||
return R.drawable.video;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,4 +277,25 @@ public class Utility {
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static String pad(int number) {
|
||||
return number < 10 ? ("0" + number) : String.valueOf(number);
|
||||
}
|
||||
|
||||
public static String stringifySeconds(double seconds) {
|
||||
int h = (int) Math.floor(seconds / 3600);
|
||||
int m = (int) Math.floor((seconds - (h * 3600)) / 60);
|
||||
int s = (int) (seconds - (h * 3600) - (m * 60));
|
||||
|
||||
String str = "";
|
||||
|
||||
if (h < 1 && m < 1) {
|
||||
str = "00:";
|
||||
} else {
|
||||
if (h > 0) str = pad(h) + ":";
|
||||
if (m > 0) str += pad(m) + ":";
|
||||
}
|
||||
|
||||
return str + pad(s);
|
||||
}
|
||||
}
|
||||
|
||||
BIN
app/src/main/res/drawable-hdpi/ic_kiosk_local_black_24dp.png
Executable file
BIN
app/src/main/res/drawable-hdpi/ic_kiosk_local_black_24dp.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 208 B |
BIN
app/src/main/res/drawable-hdpi/ic_kiosk_local_white_24dp.png
Executable file
BIN
app/src/main/res/drawable-hdpi/ic_kiosk_local_white_24dp.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 206 B |
BIN
app/src/main/res/drawable-hdpi/ic_kiosk_recent_black_24dp.png
Executable file
BIN
app/src/main/res/drawable-hdpi/ic_kiosk_recent_black_24dp.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 475 B |
BIN
app/src/main/res/drawable-hdpi/ic_kiosk_recent_white_24dp.png
Executable file
BIN
app/src/main/res/drawable-hdpi/ic_kiosk_recent_white_24dp.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 460 B |
BIN
app/src/main/res/drawable-mdpi/ic_kiosk_local_black_24dp.png
Executable file
BIN
app/src/main/res/drawable-mdpi/ic_kiosk_local_black_24dp.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 166 B |
BIN
app/src/main/res/drawable-mdpi/ic_kiosk_local_white_24dp.png
Executable file
BIN
app/src/main/res/drawable-mdpi/ic_kiosk_local_white_24dp.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 166 B |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user